Appearance
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.shCallers: /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
bash
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
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.
bash
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_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
jsonc
{
"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
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
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)
jsonc
{
"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
The state file records what the page was last generated through:
json
{
"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.