tools: prompt-registry check โ SessionStart staleness nudge for the ledger (#959)
The carbon tally stays current because a SessionStart hook nags sessions when it goes stale; the prompt registry had no equivalent, so it sat frozen at its ship-day build while transcripts aged toward pruning. - prompt-registry.mjs gains a `check` mode (mirrors carbon-tally.mjs): reads the last build's generated_at, nags only when it's >3 days old AND this repo's transcripts have been touched since. Machines with no corpus at the default --out stay silent โ publishing is opt-in, the nudge follows the choice. - .claude/settings.json runs it alongside the carbon check (and commits the aikido plugin enablement that was live-but-uncommitted). - Both ledgers' staleness bar tightens 5 โ 3 days (a 3-day lag already reads as stale, and transcripts prune after ~a week). - docs/status.md: document the nudge; make haiku-subagent summaries for new sessions an explicit numbered step before publishing (a keyless build otherwise uploads a summary-less index).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
5 files changed,
+81
−10
.claude/settings.json+8 −1CLAUDE.md+4 −2docs/status.md+28 −4tools/carbon-tally.mjs+1 −1tools/prompt-registry.mjs+40 −2
diff --git a/.claude/settings.json b/.claude/settings.json| index d3c6c30..7e77162 100644 |
| --- a/.claude/settings.json |
| +++ b/.claude/settings.json |
| @@ -1,6 +1,7 @@ |
| { |
| "enabledPlugins": { |
| - "stripe@claude-plugins-official": true |
| + "stripe@claude-plugins-official": true, |
| + "aikido@claude-plugins-official": true |
| }, |
| "hooks": { |
| "SessionStart": [ |
| @@ -11,6 +12,12 @@ |
| "command": "node tools/carbon-tally.mjs check 2>/dev/null || true", |
| "timeout": 15, |
| "statusMessage": "Checking carbon tally freshness" |
| + }, |
| + { |
| + "type": "command", |
| + "command": "node tools/prompt-registry.mjs check 2>/dev/null || true", |
| + "timeout": 15, |
| + "statusMessage": "Checking prompt registry freshness" |
| } |
| ] |
| } |
diff --git a/CLAUDE.md b/CLAUDE.md| index e358459..cb297bb 100644 |
| --- a/CLAUDE.md |
| +++ b/CLAUDE.md |
| @@ -398,9 +398,11 @@ open with the note itself) carry the ๐ค on their first line. |
| status.elevenmessenger.com/carbon.html; methodology |
| `docs/comparison-carbon.md`). A `SessionStart` hook runs |
| `node tools/carbon-tally.mjs check` and tells the session when this |
| - machine's entries are stale (>5 days) โ its raw material, local Claude Code |
| + machine's entries are stale (>3 days) โ its raw material, local Claude Code |
| transcripts, prunes after about a week, so a missed week is lost to the |
| - ledger. When nudged (or before starting long work): run |
| + ledger. (The same hook runs `node tools/prompt-registry.mjs check` for the |
| + prompt registry โ silent on machines that don't publish one; when it nags, |
| + accrue per `docs/status.md ยง The prompt registry`.) When nudged (or before starting long work): run |
| `node tools/carbon-tally.mjs --write` (per-source cutoffs โ re-runs and |
| other machines never double-count), commit the tally as a **tiny standalone |
| PR merged promptly** (two open branches accruing the same window is the one |
diff --git a/docs/status.md b/docs/status.md| index c536992..6961133 100644 |
| --- a/docs/status.md |
| +++ b/docs/status.md |
| @@ -393,7 +393,7 @@ prints yours; override with `LCHAT_CARBON_SOURCE`). |
| **The nudge is automatic.** A `SessionStart` hook in `.claude/settings.json` |
| runs `node tools/carbon-tally.mjs check` (fast โ no transcript sweep): when |
| -this machine's newest entry is older than 5 days (`LCHAT_CARBON_STALE_DAYS`), |
| +this machine's newest entry is older than 3 days (`LCHAT_CARBON_STALE_DAYS`), |
| the session is told to accrue. The accrual itself: |
| 1. `node tools/carbon-tally.mjs --write` (from the repo root) โ sweeps this |
| @@ -458,6 +458,17 @@ everyone), then publish their prefix one of two ways: |
| set +a` for the creds). The operator never edits the corpus โ the |
| contributor's build is what publishes, fail-closed scan and all. |
| +**The nudge is automatic here too.** The same `SessionStart` hook runs |
| +`node tools/prompt-registry.mjs check` (fast โ reads the last build's |
| +`generated_at`, no sweep or redaction): when this machine's corpus is older |
| +than 3 days (`LCHAT_REGISTRY_STALE_DAYS`) *and* transcripts have been touched |
| +since, the session is told to accrue. A machine that has never built a corpus |
| +at the default `--out` (`~/.cache/eleven-registry`) hears nothing โ publishing |
| +is opt-in (see above), so the nudge follows that choice, never precedes it. |
| +The check can't run the accrual itself: the security sweep and the withhold |
| +decisions are judgment calls, so a session acts on the nudge the way it acts |
| +on the carbon one. |
| + |
| Accruing a machine's sessions: |
| 1. `node tools/prompt-registry.mjs --out <dir>` (from the repo root) โ sweeps |
| @@ -467,7 +478,20 @@ Accruing a machine's sessions: |
| the same detectors and the corpus is not emitted on any hit. Publishing is |
| whole-corpus-per-machine for now; per-session accrual can come later if |
| two machines ever need to merge into one index. |
| -2. Upload the corpus straight to the status bucket (it's far too large to |
| +2. **Summarise new sessions before publishing โ dispatch haiku subagents.** |
| + A keyless build carries prior summaries/gists forward but leaves *new* |
| + sessions bare (the index falls back to first prompts), so don't upload |
| + straight from step 1. Fan haiku subagents out over the **redacted** build |
| + output only โ never the raw transcripts: one-line session summaries from |
| + each new session's prompts (โค90 chars, ending with the session's most |
| + relevant GitHub URL โ THE SUMMARY RULE in the tool's header), and โค80-char |
| + reply gists for replies missing from `<id>.sum.json` (keys are |
| + `parsedLine:block`, reconstructable from the redacted JSONL: assistant text |
| + blocks >200 chars). Write the results into the out dir (`index.json` |
| + `summary` fields + merged `<id>.sum.json`), then **re-run step 1**: the |
| + rebuild carries them through today's redaction rules and the fail-closed |
| + verifier, and hashes exactly what publishes. |
| +3. Upload the corpus straight to the status bucket (it's far too large to |
| commit โ a quarter GB and growing). The tool prints the exact commands: |
| `aws s3 sync <dir>/prompts/<slug> s3://eleven-status-<account>/prompts/<slug> |
| --delete --cache-control max-age=300`, then `aws s3 cp` the manifest (run |
| @@ -475,14 +499,14 @@ Accruing a machine's sessions: |
| set +a` โ or anywhere with equivalent PutObject rights). Sync **only your |
| own prefix**; the manifest upload is a `cp`, never a sync with `--delete` |
| at the `prompts/` root, which would erase other contributors. |
| -3. Commit the hash ledger entry (the tool prints the exact command): the |
| +4. Commit the hash ledger entry (the tool prints the exact command): the |
| corpus's `manifest.sha256.json` goes to the **public** |
| `elevenmessenger/releases` repo at `prompt-registry/<slug>.json` via |
| `gh api -X PUT`. Its git history is the registry's tamper-evidence โ a |
| few KB per accrual, and any after-the-fact change to a published |
| transcript shows as a dated hash change. (The manifest also uploads with |
| the corpus, so the bucket is self-describing.) |
| -4. `prompts.html` itself is a committed viewer page: copy it to the box's |
| +5. `prompts.html` itself is a committed viewer page: copy it to the box's |
| `/opt/lchat/status-pages/` like the other pages; the 5-minute publish |
| re-uploads it. |
diff --git a/tools/carbon-tally.mjs b/tools/carbon-tally.mjs| index 8079807..070a6c0 100644 |
| --- a/tools/carbon-tally.mjs |
| +++ b/tools/carbon-tally.mjs |
| @@ -31,7 +31,7 @@ import { join } from "node:path"; |
| import os from "node:os"; |
| const mode = process.argv.includes("check") ? "check" : process.argv.includes("--write") ? "write" : "dry"; |
| -const STALE_DAYS = Number(process.env.LCHAT_CARBON_STALE_DAYS || 5); |
| +const STALE_DAYS = Number(process.env.LCHAT_CARBON_STALE_DAYS || 3); |
| const git = (args) => execSync("git " + args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }); |
diff --git a/tools/prompt-registry.mjs b/tools/prompt-registry.mjs| index 02b3e48..809775e 100644 |
| --- a/tools/prompt-registry.mjs |
| +++ b/tools/prompt-registry.mjs |
| @@ -82,16 +82,54 @@ import os from "node:os"; |
| const args = process.argv.slice(2); |
| const argAll = (flag) => args.flatMap((a, i) => (a === flag && args[i + 1] ? [args[i + 1]] : [])); |
| -const outRoot = argAll("--out")[0] || null; |
| +const checkMode = args.includes("check"); |
| +const outRoot = argAll("--out")[0] || (checkMode ? join(os.homedir(), ".cache", "eleven-registry") : null); |
| const alsoRoots = argAll("--also"); |
| const checkOnly = args.includes("--check-only"); |
| if (!outRoot) { |
| - console.error("usage: node tools/prompt-registry.mjs --out <dir> [--check-only] [--also <path>]โฆ [--contributor <name>]"); |
| + console.error("usage: node tools/prompt-registry.mjs --out <dir> [check] [--check-only] [--also <path>]โฆ [--contributor <name>]"); |
| process.exit(1); |
| } |
| const git = (a) => execSync("git " + a, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }); |
| +// ---- `check` โ the SessionStart staleness nudge ------------------------------ |
| +// Mirrors carbon-tally.mjs `check`: no transcript sweep, no redaction โ read |
| +// the last build's generated_at and print only when this machine is sitting on |
| +// unpublished sessions going stale (stdout lands in the agent's context; the |
| +// raw transcripts prune after ~a week). A machine with no corpus at --out |
| +// (default ~/.cache/eleven-registry) stays silent: publishing is opt-in |
| +// (docs/status.md ยง The prompt registry), so the nudge follows that choice, |
| +// never precedes it. Never fails the session. |
| +const STALE_DAYS = Number(process.env.LCHAT_REGISTRY_STALE_DAYS || 3); |
| +if (checkMode) { |
| + try { |
| + const promptsDir = join(outRoot, "prompts"); |
| + // Newest generated_at across slug corpora (plus the pre-multi flat layout). |
| + const built = Math.max(...readdirSync(promptsDir, { withFileTypes: true }) |
| + .map((d) => (d.isDirectory() ? join(promptsDir, d.name, "index.json") : d.name === "index.json" ? join(promptsDir, d.name) : null)) |
| + .filter((p) => p && existsSync(p)) |
| + .map((p) => Date.parse(JSON.parse(readFileSync(p, "utf8")).generated_at || ""))); |
| + const days = (Date.now() - built) / 86400e3; |
| + if (Number.isFinite(days) && days > STALE_DAYS) { |
| + // Only nag over real material: any of this repo's transcripts touched |
| + // since the last build (mtime scan โ same dir resolution as the build). |
| + const root = join(git("rev-parse --path-format=absolute --git-common-dir").trim(), ".."); |
| + const prefix = root.replace(/[^a-zA-Z0-9]/g, "-"); |
| + const projects = join(os.homedir(), ".claude", "projects"); |
| + const grown = readdirSync(projects).filter((d) => d.startsWith(prefix)) |
| + .flatMap((d) => readdirSync(join(projects, d)).filter((f) => f.endsWith(".jsonl")).map((f) => join(projects, d, f))) |
| + .filter((f) => statSync(f).mtimeMs > built).length; |
| + if (grown) console.log("Prompt registry is stale: this machine's last build is " + Math.floor(days) + |
| + " days old and " + grown + " session transcript(s) have been touched since. Local transcripts prune " + |
| + "after ~a week, so, when convenient this session, accrue it: security sweep, build, haiku-subagent " + |
| + "summaries for new sessions, rebuild, publish โ docs/status.md ยง The prompt registry (this machine's " + |
| + "exact flags are in the operator's notes)."); |
| + } |
| + } catch {} // no corpus yet, or unreadable โ not a contributor machine; stay quiet |
| + process.exit(0); |
| +} |
| + |
| // Same public-safe machine label as the carbon tally (never a hostname). |
| const NAMES = ["ada", "grace", "alan", "edsger", "radia", "hedy", "katherine", "margaret", |
| "linus", "barbara", "donald", "hypatia", "annie", "vint", "niklaus", "frances"]; |