Narrative-from-Git Engine
Narrative-from-Git Engine
Section titled “Narrative-from-Git Engine”The shared deterministic core behind narrative pages that stay current with the repo (#847). It recomputes live counts, restamps the generated date (via timelord), and detects new feature commits and newly-added surface files since the last marker — everything mechanical about a refresh. The narrative judgement (whether recent work is a new chapter, and writing it in the page’s voice) always stays with the caller.
Engine (single source of truth): scripts/narrative/engine.sh
Callers: /update-story (scripts/story/update-story.sh, config
scripts/story/story.config.json) and /update-journey
(scripts/journey/update-journey.sh, config
scripts/journey/journey.config.json), which refreshes the
workday scenarios page — same core,
different config.
Invocation
Section titled “Invocation”engine.sh --config <config.json> [PROJECT_ROOT] [--check] [--json]| Argument | Meaning |
|---|---|
--config <path> |
Required. The caller config (schema below). Relative paths inside it resolve against PROJECT_ROOT. |
PROJECT_ROOT |
Defaults to the current git toplevel. |
--check |
Dry run — report whether an update is needed, write nothing. |
--json |
Emit the machine-readable summary on stdout. |
Exit codes: 0 = success (whether or not an update was applied), 1 = error.
Writing a new caller
Section titled “Writing a new caller”A caller is a thin wrapper: a config file plus a two-line shell script that
execs the engine with that config. Nothing page-specific lives in the engine —
if you need to hard-code a path or key in engine.sh, the config schema is
missing a field and should grow one instead.
#!/usr/bin/env bashset -euo pipefailSCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"exec "$SCRIPT_DIR/../narrative/engine.sh" --config "$SCRIPT_DIR/<caller>.config.json" "$@"The page being refreshed must mark every value the engine restamps with a
stable data-attribute hook — <span data-story="commits">1230</span> — and the
engine replaces only the hook’s inner text. Positional/regex edits against the
page’s prose are never used, which is what makes the refresh idempotent and
markup-safe.
Config schema
Section titled “Config schema”{ "page": "story/index.html", // HTML page updated in place "state": "story/story-state.json", // marker + last-written counts "data_attr": "data-story", // hook attribute used in the page "date_key": "gendate", // hook key that receives the generated date "feat_pattern": "\\|feat", // ERE matched against "%h|%ad|%s" log lines "counts": { // key order = order in the state file "commits": { "type": "git-commits" }, "releases": { "type": "git-tags", "pattern": "v*" }, "skills": { "type": "dirs", "paths": ["skills", "registry/skills"] }, "commands": { "type": "glob", "patterns": ["commands/*.md"] } }, "surfaces": { // optional: new-surface detection groups "commands": ["commands/*.md", "registry/commands/*.md"] }}Count types
Section titled “Count types”type |
Counts | Extra fields |
|---|---|---|
git-commits |
git rev-list --count HEAD |
— |
git-tags |
tags matching pattern (default v*) |
pattern |
dirs |
subdirectories across each of paths |
paths: [] |
glob |
files matching each of patterns (shell glob, repo-relative) |
patterns: [] |
Every key in counts must have a matching data_attr hook in the page; the
engine also writes each key into the state file’s counts block, which is how
the no-op fast path decides nothing changed.
Surfaces
Section titled “Surfaces”Each surfaces group is a list of globs; the engine reports files added
(git diff --diff-filter=A) in the marker..HEAD range that match. This is
the “genuinely new commands/skills/agents” signal a caller narrates from. With
no marker yet (genesis), groups are empty — everything being “new” is noise,
not signal.
Output contract (--json)
Section titled “Output contract (--json)”{ "changed": true, // counts changed, new feats exist, or no marker yet "check": false, // was this a --check run "head": "f6ac767", "gendate": "2026-07-17", "prev_commit": "b215b91", "prev_date": "2026-06-20", "new_feat_count": 348, "counts": { "commits": 1230, "...": 0 }, "new_feats": [ { "sha": "5ac87ff", "date": "2026-06-20", "subject": "feat(...): ..." } ], "new_surfaces": { "commands": ["registry/commands/foo.md"] } // only when "surfaces" configured}new_feats (feature commits since the marker, oldest first) and
new_surfaces are the caller’s raw material for narration. changed: false
means the page is already current — the engine wrote nothing and the caller
should stop.
State marker
Section titled “State marker”The state file records what the page was last generated through:
{ "generated_through": { "commit": "f6ac767", "date": "2026-07-17" }, "counts": { "commits": 1230 }}The engine owns this file entirely — callers never edit it. Re-running with no new history is a clean no-op (idempotent), so callers are safe to run on every session end, in CI, or ad hoc.