eleven / messenger Public

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>
Paul Campbell 6ef8a374c6ba35125b018b78c2e31c03e678ea48 parent 1d4a2c1
5 files changed, +81 −10
  • .claude/settings.json +8 −1
  • CLAUDE.md +4 −2
  • docs/status.md +28 −4
  • tools/carbon-tally.mjs +1 −1
  • tools/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"];