@sabaiway/agent-workflow-kit 1.39.0 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,78 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
4
4
  is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
5
5
  every `migrations/<version>-<slug>.md` newer than it, in semver order.
6
6
 
7
+ ## 1.41.0 — review-state degraded lane: align the presence gate with the review-ledger (AD-050)
8
+
9
+ A **feature** release (kit-only; deployment-lineage head stays `1.3.0` — no migration;
10
+ memory/engine/bridges untouched). Closes the AD-049 residual: the family shipped two read-only review
11
+ gates that **disagreed** on the same tree when a ready backend genuinely can't review a diff.
12
+ `review-ledger --check` (convergence) already excused a recorded-degraded backend; `review-state
13
+ --check` (presence) had no degraded model, so a legitimately degraded agy (stalled → no receipt) read
14
+ missing/stale → exit 1. On BUGFREE-3 S2 the loop converged codex-only with agy recorded degraded, yet
15
+ review-ledger PASSED and review-state FAILED — a consumer wiring review-state into a pre-commit hook
16
+ was blocked on an honest degrade.
17
+
18
+ - **A neutral read-core (`review-ledger-core.mjs`)** — the validated review-ledger read/schema core
19
+ (path/base resolvers, `validateRecord` + its validators + schema constants, loop/segment filters)
20
+ moves VERBATIM into a new node-built-ins-only module both read-only checkers import; `review-ledger.mjs`
21
+ re-exports every symbol for back-compat. It breaks the `review-ledger ↔ review-state` import cycle so
22
+ review-state can read the ledger without a back-import (the `changed-surface.mjs` precedent). Pure
23
+ mechanical move — no behaviour change, pinned by the full pre-existing suite + import-split pins.
24
+ - **The `review-state` degraded exemption** — `review-state --check` now exempts a recipe-named backend
25
+ without a current grounded receipt when the in-flight segment's LATEST review-ledger round records it
26
+ `degraded:true` at the current tree fingerprint, with ≥1 non-degraded recipe-named backend present in
27
+ that round and grounded, the loop unambiguous, and the ledger clean. It **mirrors review-ledger's
28
+ `decideStop`** exactly (allPresent + presence) and stays **verdict-blind** — presence, not unanimity:
29
+ the two gates now AGREE on an honestly-converged-with-degrade tree, and still intentionally differ on
30
+ a non-converged one. `--await` inherits the exemption.
31
+ - **Fail-closed, exemption-scoped** — an unreadable/malformed ledger DENIES the exemption but never
32
+ fails a tree whose receipts independently satisfy the gate (all-current stays exit 0, the ledger issue
33
+ surfaced). More than one plan in flight suppresses the exemption without adding a fail-closed arm.
34
+
35
+ Contract surfaces moved in lockstep (the tool header, `--help`, the human render, the `--await`
36
+ comment, `references/modes/review-state.md`). Council-converged both segments (S1 codex + agy SHIP; S2
37
+ codex R1 revise → allPresent fold → R2 both SHIP), the two-gate agreement dogfooded live + pinned by a
38
+ detector-independent `two-gate-agreement.test.mjs` + a 17-case matrix; full suite 2969 green.
39
+
40
+ ## 1.40.0 — Universal verification profile + session-loop economics (a)–(h) (BUGFREE-3, AD-049)
41
+
42
+ A **feature** release (deployment-lineage head stays `1.3.0` — no migration; co-released with
43
+ `@sabaiway/agent-workflow-memory` 1.12.0; engine/bridges untouched). BUGFREE-3 closes two residuals
44
+ AD-046/AD-048 left open: the fold-completeness signal was **JS/V8-only** (coverage read solely from
45
+ `NODE_V8_COVERAGE`, the single-test probe parsed node:test TAP from stdout — no consumer on another
46
+ runner could use the gate), and the BUGFREE-2 retro left **eight kit-level cycle-costs** unfolded.
47
+
48
+ - **Verification profile** — a new, optional, versionable `docs/ai/verification-profile.json`
49
+ (`schema:1`; memory-canon template + kit mirror + a read-core kit tool) declares WHERE the suite
50
+ leaves coverage (`coverage.kind ∈ {v8,lcov}` + `lcovPath`), the single-test template
51
+ (`resultFormat ∈ {tap-stdout,tap-file,junit-xml}`), and an optional SARIF findings path. Env knobs
52
+ override; an **absent profile reproduces today's V8 + node:test behaviour exactly**. Every declared
53
+ path is realpath-guarded gitignored/out-of-tree. LCOV branches at `readCoverage` (the V8 path is
54
+ byte-unchanged); the single-test strategy preserves `resolvable = matched>0` across every format;
55
+ SARIF is advisory, never on the blocking path.
56
+ - **(a) one suite run per fingerprint** — fold RESULT schema v3→v4 carries suite-execution evidence,
57
+ and `run-gates --record` **credits** the `unit-tests` gate from it instead of re-spawning (strictly
58
+ fingerprint-bound + tree-unchanged + command-identity + exit-0; no "recent enough" cache).
59
+ **(c) same-segment re-attest** — a recorded `reattest` receipt anchors custody at a new file hash
60
+ for a green-only test append without fabricating a red-observe (the honest replacement for
61
+ mis-using `red-proof`). **(f) `--preflight`** — the cheap set only, actions routed by kind, no
62
+ suite/probe spawn.
63
+ - **(b) doc-parity** — a new read-only lint + mode: a closed, live-imported registry pins each
64
+ mode-contract doc token to its code constant (caps, schema versions, the ledger vocabulary),
65
+ fail-closed on drift. **(d) `review-state --await`** — block until every recipe-named backend has
66
+ a fresh grounded receipt for the current tree (receipts-not-pgrep; deadline-first, bounded sleep).
67
+ **(e) `grounding --ledger-summary`** — a loop/base-scoped review-ledger digest for `--facts`,
68
+ fail-closed on an unreadable/malformed ledger. **(g) `record --from-receipts`** — draft
69
+ `backends[]` from the current-fingerprint receipts; an explicit non-degraded row is a loud STOP.
70
+ - **(h) a rotation regenerates `docs/ai/index.md`** (the memory-canon `archive-decisions.mjs` reuses
71
+ the root-parameterized `check-docs-size.mjs --write-index --report`) — so an ADR rotation never
72
+ leaves the index stale mid-release-matrix; **dogfooded on this very release** (AD-049's rotation
73
+ regenerated the index automatically, zero mid-matrix trip).
74
+
75
+ **Stated residual (Option A).** The (a) credit rides `NODE_V8_COVERAGE`, an observable env var — a
76
+ test reading it could flip; bounded (fails-under-coverage caught by exit-0), documented, and tested.
77
+ The clean closure (Node ≥22 + `--test-reporter=lcov`) is queued.
78
+
7
79
  ## 1.39.0 — Fold boundaries: commit-anchored segments, the diff-size cap, the green-baseline receipt, no-repro-no-fold, and gate telemetry (AD-048)
8
80
 
9
81
  A **feature** release (deployment-lineage head stays `1.3.0` — no migration; engine/memory/bridges
package/README.md CHANGED
@@ -234,10 +234,11 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
234
234
  | `/agent-workflow-kit recipes` | any time | **read-only** orchestration advisor: presents four named recipes for composing the bridges into plan → execute → review — **Solo / Reviewed / Council / Delegated** — plans + recommends one for your environment (degrading with a stated reason when a backend isn't ready), and offers the choice. The orchestrator runs it via the bridge skills and **always commits**; the kit never executes a recipe, never runs a subscription CLI, never commits. |
235
235
  | `/agent-workflow-kit procedures <activity>` | any time | **read-only** activity-procedures advisor: prints a named activity's ordered steps (`plan-authoring` / `plan-execution`) read **live** from the engine, plus the **resolved recipe per slot** from your `docs/ai/orchestration.json` (agent-writable via `set-recipe`, or hand-edited) + backend readiness (default Reviewed when a backend is ready, Council on request, slot-aware incl. Delegated) — and, for every dispatched backend, the **full driving contract at the point of use** (exact copy-pasteable invocation, grounding levers like agy's `--facts`/`--decided`, the round-2 `--continue` delta, guarded passthrough), verbatim from the bridge manifests (drift-guarded; each wrapper's `--help` prints the same). `--override <slot>=<recipe>` adjusts one slot per run. Composes with `recipes`; never writes, never commits, never runs a subscription CLI. |
236
236
  | `/agent-workflow-kit set-recipe` | any time | **config writer** for `docs/ai/orchestration.json`: tell the agent your preference in plain language and it maps it to explicit `--set <activity>.<slot>=<recipe>` / `--unset` ops; the kit validates, **previews by default**, and writes only on `--write` (deployment-gated, atomic, symlink/TOCTOU-safe), resolving the effective recipe vs live readiness. Writes **only** that file — **never runs a backend, never commits**; hand-editing stays fully supported. |
237
- | `/agent-workflow-kit review-state` | any time | **read-only review-receipt checker** — makes "reviewed ≠ shipped" detectable: the bridge review wrappers append a receipt per successful review (into a file inside the git dir — never committable); this checks that every backend your configured `plan-execution.review` recipe names holds a **fresh, grounded receipt for the current uncommitted tree** (any later edit moves the fingerprint and stales the receipt; a review continuation never re-attests a folded tree). `--check` gives a gate exit code to declare in `docs/ai/gates.json` **by hand or via the consent-gated seeder** (preview → your explicit yes; never without consent). Never writes, never commits, never runs a subscription CLI; it spawns read-only `git` queries to fingerprint the tree — and `git commit --no-verify` stays possible (discipline, not a sandbox). |
238
- | `/agent-workflow-kit grounding` | any time | **grounded-review facts assembler** — mechanizes populating `agy-review --facts @f`: slices your entry-point's **Hard Constraints** section verbatim (exactly one match, else a loud stop) and/or a plan's decision-bearing sections (`## Approach` + `## Verification` required, `## Decisions (locked)` when present; duplicates stop), under the same byte budget the agy wrapper enforces (minus `--reserve-bytes` for the artifact share), with a loud tail-trim on overflow. Prints to stdout; `--out` writes **one scratch file only** (gitignored / outside the repo — a tracked or not-ignored in-repo path is refused). Never commits, never runs a subscription CLI. |
239
- | `/agent-workflow-kit review-ledger` | any time | **review-round ledger** — turns the review-loop stop rule into a **computed** signal: record each plan-execution review round, its triage, and any recorded **overrides** (`oracle-change` lifts named tampered test files; `red-proof` waives the observed-red proof for exactly one `testId`; v4 adds **`size-cap`** — the exact sanctioned magnitude for a surface over the diff cap, segment-scoped — all loud, durable, auditable waivers, never silent) into a JSONL ledger inside the git dir (never committable), and read the stop decision (`converged > resolved-residual > triage-required > continue`) from the records — never from a remembered rule. **Since v4 every record carries `base` = the HEAD commit and the whole loop is SEGMENT-scoped (AD-048):** round numbering, the caps, and every tooth reset ONLY at a gated commit — a multiphase plan records fully while round 4 within one segment stays refused; `gate-run` records (minted by `gates --record`) make gates-before-review a computed precondition; the triage class **`refuted`** is the honest phantom-finding lane (grounds mandatory); `--telemetry` renders counts-only gate-efficacy data across all loops and both ledgers. `--status` replaces the hand-composed per-round tally; `--check` is a fail-closed gate exit code for `docs/ai/gates.json` (by hand or via the consent-gated seeder). A `fixable-bug` triage **requires its red→green `testId`** (v1..v3 records stay valid on read — old records never enter a segment). The writer refuses a round (past the hard-max, while a triage is pending, or lacking a grounded review receipt) and refuses an override outside its single in-flight loop. Honest residual: records are forgeable — self-discipline, not a security boundary. Never commits, never runs a subscription CLI. |
237
+ | `/agent-workflow-kit review-state` | any time | **read-only review-receipt checker** — makes "reviewed ≠ shipped" detectable: the bridge review wrappers append a receipt per successful review (into a file inside the git dir — never committable); this checks that every backend your configured `plan-execution.review` recipe names holds a **fresh, grounded receipt for the current uncommitted tree** (any later edit moves the fingerprint and stales the receipt; a review continuation never re-attests a folded tree). `--check` gives a gate exit code to declare in `docs/ai/gates.json` **by hand or via the consent-gated seeder** (preview → your explicit yes; never without consent). `--await [--timeout <s>]` (AD-049) BLOCKS until every recipe-named backend has receipted the current tree — the durable completion signal is the receipt, never a process event — so you wait for the bridges instead of hand-polling. Never writes, never commits, never runs a subscription CLI; it spawns read-only `git` queries to fingerprint the tree — and `git commit --no-verify` stays possible (discipline, not a sandbox). |
238
+ | `/agent-workflow-kit grounding` | any time | **grounded-review facts assembler** — mechanizes populating `agy-review --facts @f`: slices your entry-point's **Hard Constraints** section verbatim (exactly one match, else a loud stop) and/or a plan's decision-bearing sections (`## Approach` + `## Verification` required, `## Decisions (locked)` when present; duplicates stop), under the same byte budget the agy wrapper enforces (minus `--reserve-bytes` for the artifact share), with a loud tail-trim on overflow. `--ledger-summary` (AD-049) appends a COMPUTED review-ledger digest for the single in-flight segment (rounds · origins · classifications · verdicts · overrides — unrelated loops excluded), so the reviewer sees the loop's own history without hand-copying. Prints to stdout; `--out` writes **one scratch file only** (gitignored / outside the repo — a tracked or not-ignored in-repo path is refused). Never commits, never runs a subscription CLI. |
239
+ | `/agent-workflow-kit review-ledger` | any time | **review-round ledger** — turns the review-loop stop rule into a **computed** signal: record each plan-execution review round, its triage, and any recorded **overrides** (`oracle-change` lifts named tampered test files; `red-proof` waives the observed-red proof for exactly one `testId`; v4 adds **`size-cap`** — the exact sanctioned magnitude for a surface over the diff cap, segment-scoped — all loud, durable, auditable waivers, never silent) into a JSONL ledger inside the git dir (never committable), and read the stop decision (`converged > resolved-residual > triage-required > continue`) from the records — never from a remembered rule. **Since v4 every record carries `base` = the HEAD commit and the whole loop is SEGMENT-scoped (AD-048):** round numbering, the caps, and every tooth reset ONLY at a gated commit — a multiphase plan records fully while round 4 within one segment stays refused; `gate-run` records (minted by `gates --record`) make gates-before-review a computed precondition; the triage class **`refuted`** is the honest phantom-finding lane (grounds mandatory); `--telemetry` renders counts-only gate-efficacy data across all loops and both ledgers. `--status` replaces the hand-composed per-round tally; `record --from-receipts` (AD-049) DRAFTS the `backends[]` from the current-fingerprint receipts (verdict per backend, counts from the supplied findings) instead of hand-composing them — a recipe-named backend with no receipt is a loud stop; `--check` is a fail-closed gate exit code for `docs/ai/gates.json` (by hand or via the consent-gated seeder). A `fixable-bug` triage **requires its red→green `testId`** (v1..v3 records stay valid on read — old records never enter a segment). The writer refuses a round (past the hard-max, while a triage is pending, or lacking a grounded review receipt) and refuses an override outside its single in-flight loop. Honest residual: records are forgeable — self-discipline, not a security boundary. Never commits, never runs a subscription CLI. |
240
240
  | `/agent-workflow-kit fold-completeness` | any time | **fold-completeness gate** — attests the review loop's folded fixes were proven the HONEST way (no fix theater): the runner executes your suite ONCE under `NODE_V8_COVERAGE`, checks every changed executable line is **executed**, probes every bound `testId` N times (`AW_FOLD_RERUNS`), records content hashes, and scans the test surface for tamper; the `--red` verb observes a test FAILING on the pre-fold tree — **BEFORE the fix** — and mints an **observed-red receipt**. The read-only checker (`--status` / `--check`) then requires, per bound test: the receipt, receipt-precedes-run order, N/N-green probes, and content **custody** (the green test is byte-identical to the one seen failing) — the receipt+custody half waivable for exactly one `testId` by a recorded `red-proof` override; mixed/timed-out probes are QUARANTINE (never converted, no override); **v3 records carry `base` and the proof is segment-scoped (AD-048)** — a committed phase's custody obligations close with its commit, and a receipt never crosses a commit boundary; tampered pre-existing test files fail closed unless covered by a recorded `oracle-change` override. Honest residuals: coverage proves **execution, not assertion**, and records are forgeable — self-discipline, not a security boundary. v1 scope is JS/V8 (pluggable via `AW_FOLD_BOUND_CMD`): changed TS/JSX fails closed, docs/config changes are listed but never block, and **no mutation testing ships**. Wire the gate by hand — the consent-gated seeder deliberately does not offer it yet (JS-only hold). Never commits, never runs a subscription CLI. |
241
+ | `/agent-workflow-kit doc-parity` | any time | **read-only doc-parity lint** (AD-049) — kills the doc-drift class where a mode-contract doc silently lags a code constant (a `--check` doc still reading `300` after the diff cap moved to `400`): a **closed, exported registry** binds each live constant (review caps, schema versions, and the ledger's own class/scope vocabulary) to the exact token its `references/modes/*.md` contract must carry, and asserts the CURRENT value renders into every bound file — a drifted doc, an unreadable file, or an absent token **fails closed**. The values are sourced from the live imports (never re-typed), so the lint can't itself go stale; adding a binding is adding a checked entry (closed-world, edit-safe). `--check` is a gate exit code for `docs/ai/gates.json`. Never writes, never commits, never runs a subscription CLI. |
241
242
  | `/agent-workflow-kit uninstall` | opt-in, any time | **guarded teardown** — the inverse of `init` / `setup`. Removes only what's **provably ours** (managed skill dirs + bridge wrappers; in a project, the hidden-mode git-ignore block it added + the pre-commit hook it installed); **never deletes** your `docs/ai` / `AGENTS.md` (prints the exact `rm` to run by hand) or your `.claude/settings.json` (prints an **edit** — remove the attribution key, review any velocity `permissions.*` — never an `rm`). Always `--dry-run` first; preflight-then-mutate; never commits. |
242
243
  | `/agent-workflow-kit velocity` | Claude Code · opt-in | **onboarding velocity profile** — seeds a fixed, audited **read-only** allowlist into `.claude/settings.json` so routine read-only commands stop idling on approval prompts while you're away; opt-in `acceptEdits`; plus a **read-only advisory** of likely project gate commands to add by hand. Writes **only** `.claude/settings.json` — **never** allowlists commit/push/publish, never writes `settings.local.json`, never commits. A seeded entry is a **trust posture, not a sandbox** (a runtime residual remains at the settings level — its guard ships as the opt-in `hook` command); a direct commit/push/publish still asks. `--dry-run` first. |
243
244
  | `/agent-workflow-kit agents` | Claude Code · opt-in | **cheap-lane subagents** — places bundled subagent definitions (`.claude/agents/`) pinned to a **cheap model** (haiku, low effort, read-only tools) for mechanical work: extraction sweeps, changelog fact-skeletons, gate-failure triage. Judgment, review, and real code stay on your main lane — these vehicles only extract and draft, and their output is verified. Preview by default (`--apply` writes); an existing customized file is **preserved, never overwritten**; never touches `settings*.json`, never commits. |
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: agent-workflow-kit
3
3
  description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow".
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '1.39.0'
6
+ version: '1.41.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
@@ -22,8 +22,8 @@ This kit is the **composition root** of the `agent-workflow` family. The memory
22
22
  when it is present and healthy, and otherwise uses its **own bundled copy** (`references/`,
23
23
  `migrations/`) — so the existing one-command install keeps working with **no new dependency on the
24
24
  memory substrate**. (The methodology slot is a separate axis: its fragment is read **live from the
25
- installed `agent-workflow-engine`**, which `npx @sabaiway/agent-workflow-kit@latest init` installs — a
26
- runtime dependency read live; see `${CLAUDE_SKILL_DIR}/references/shared/composition-handoff.md`.)
25
+ installed `agent-workflow-engine`**, which `npx @sabaiway/agent-workflow-kit@latest init` installs;
26
+ see `${CLAUDE_SKILL_DIR}/references/shared/composition-handoff.md`.)
27
27
  `init` also **refreshes the installed memory substrate** (best-effort — a miss is a loud DEGRADED
28
28
  success: warn + the exact recovery command + exit 0, never silent, never the engine's hard STOP;
29
29
  `--no-memory` skips it), so a returning `init` leaves **no stale core member**. The
@@ -163,6 +163,10 @@ writer — read `${CLAUDE_SKILL_DIR}/references/modes/review-ledger.md` before a
163
163
 
164
164
  writer — read `${CLAUDE_SKILL_DIR}/references/modes/fold-completeness.md` before acting.
165
165
 
166
+ ### Mode: doc-parity
167
+
168
+ read-only — read `${CLAUDE_SKILL_DIR}/references/modes/doc-parity.md` before acting.
169
+
166
170
  ---
167
171
 
168
172
  ## References
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "1.39.0",
6
+ "version": "1.41.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "1.39.0",
3
+ "version": "1.41.0",
4
4
  "description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -18,7 +18,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
18
18
  3. **Choose conversational language — answered in the step-2 batch.** Which language should the agent *talk to them* in — questions, explanations, summaries, status updates? Offer the language they're already writing in as the default. Carry the answer into the `{{COMM_LANGUAGE}}` slot of the *Communication language* block when `AGENTS.md` is created (step 5). See [Communication contract](${CLAUDE_SKILL_DIR}/references/contracts.md#communication-contract). This sets the **dialogue** language only — never the files.
19
19
  4. **Choose agent attribution — answered in the step-2 batch.** May the agent attribute work to itself / to AI — `Co-Authored-By` trailers, "Generated with …" footers, "AI"/agent/model mentions in code, comments, commit messages, PR titles/bodies, or docs? **Default to `off`** (no agent/AI mention anywhere) unless they opt in — people are routinely surprised to find an AI listed as a repo contributor. Carry the answer into the `{{AGENT_ATTRIBUTION}}` slot of the *Attribution* block when `AGENTS.md` is created (step 5). **If `off` and the project uses Claude Code**, also set `"includeCoAuthoredBy": false` in the project's `.claude/settings.json` (create it if absent) — the trailer is added by the harness, so a doc directive alone won't stop it. See [Attribution contract](${CLAUDE_SKILL_DIR}/references/contracts.md#attribution-contract).
20
20
  5. **Entry-point doc.** If `AGENTS.md` / `CLAUDE.md` already exist (step-1 recon), do **not** overwrite — show the user and ask whether to merge or replace. Otherwise create `AGENTS.md` (the cross-agent standard — Codex / Cursor / Devin Desktop / Copilot read it natively) from `${CLAUDE_SKILL_DIR}/references/templates/AGENTS.md`, and symlink `CLAUDE.md -> AGENTS.md` (`ln -s AGENTS.md CLAUDE.md`) for Claude Code — single source, no duplication. For nested context, add a subdir `AGENTS.md` (+ a `CLAUDE.md` symlink beside it for Claude Code).
21
- 6. **Deploy `docs/ai/`.** Create every `docs/ai/` file + `pages/` from `${CLAUDE_SKILL_DIR}/references/templates/` (the template loop deploys each non-`AGENTS.md` template — the `.md` docs **and** the two seeded, user-editable strict-JSON configs: **`docs/ai/orchestration.json`** (the per-project recipe defaults the `procedures` advisor reads) and **`docs/ai/gates.json`** (the project's gate declaration — an empty list to fill with its own verification commands, consumed by `${CLAUDE_SKILL_DIR}/references/modes/gates.md`)). Keep each `.md` file's frontmatter (`type / lastUpdated / scope / staleAfter / owner / maxLines`); the `.json` seeds carry no frontmatter (the docs cap-validator globs `*.md` only, so they are inherently skipped).
21
+ 6. **Deploy `docs/ai/`.** Create every `docs/ai/` file + `pages/` from `${CLAUDE_SKILL_DIR}/references/templates/` (the template loop deploys each non-`AGENTS.md` template — the `.md` docs **and** the three seeded, user-editable strict-JSON configs: **`docs/ai/orchestration.json`** (the per-project recipe defaults the `procedures` advisor reads), **`docs/ai/gates.json`** (the project's gate declaration — an empty list to fill with its own verification commands, consumed by `${CLAUDE_SKILL_DIR}/references/modes/gates.md`), and **`docs/ai/verification-profile.json`** (the OPTIONAL fold-completeness verification profile — its seeded default reproduces the composition root's V8 + node:test behaviour, so a project deletes it to fall back to exactly that, or edits it for another language/runner)). Keep each `.md` file's frontmatter (`type / lastUpdated / scope / staleAfter / owner / maxLines`); the `.json` seeds carry no frontmatter (the docs cap-validator globs `*.md` only, so they are inherently skipped).
22
22
  7. **Fill templates** per the table below.
23
23
  8. **Install enforcement (Node projects).** Copy `${CLAUDE_SKILL_DIR}/references/scripts/*.mjs` (+ `*.test.mjs`) into the project's `scripts/`. They self-configure (project name from `package.json`, hierarchical/on-demand sections auto-discovered). **If the project has no Node runtime** (step-1 recon), skip this step and the hook in step 9 — follow the cap/archive/index policy manually, or port the scripts to the project's language.
24
24
  9. **Wire / hide** per visibility (see [Visibility contract](${CLAUDE_SKILL_DIR}/references/contracts.md#visibility-contract)). Install the pre-commit hook (Node projects): `node scripts/install-git-hooks.mjs`. If the installer reports a pre-existing non-marker hook, stop and ask the user to merge it manually rather than overwriting.
@@ -0,0 +1,18 @@
1
+ ### Mode: doc-parity
2
+
3
+ The DOC-PARITY lint (BUGFREE-3 / AD-049, session-economics item (b)) — the deterministic doc-drift killer. A recurring class of review churn came from a mode-contract doc silently lagging a code constant (a `--check` doc still reading "300" after the diff cap moved to 400, caught only in a later review round). This read-only tool closes that class mechanically: a **closed, exported registry** ties each live code constant to the exact token its `references/modes/*.md` contract must carry, and asserts the CURRENT value renders into every bound file.
4
+
5
+ **Run** — `node ${CLAUDE_SKILL_DIR}/tools/doc-parity.mjs [--check | --json]`:
6
+ - default → the per-binding report (each `constant → file` marked ✓/✗).
7
+ - **`--check`** → the gate exit code (0 consistent, 1 drift). Declare it as a project gate by hand in `docs/ai/gates.json`.
8
+ - `--json` → the structured result.
9
+
10
+ **What it checks (the closed registry).** The numeric/version tokens are IMPORTED live from the tools — never re-typed in the lint — so the registry itself can never go stale:
11
+ - `references/modes/review-ledger.md` carries `SCHEMA_VERSION` (schema v4), `HARD_MAX` (the hard-max ceiling of 3), `DEFAULT_DIFF_CAP` (the default 400 diff cap), `REVIEW_CAP` (cap ≤2), and the ledger vocabulary sourced from the schema's own `V4_CLASSES` / `V4_OVERRIDE_SCOPES` Sets plus the `gate-run` kind (`size-cap`, `refuted`, `gate-run`, `red-proof`, `oracle-change`, `fixable-bug`, `inherent-layer-residual`, `escalate`);
12
+ - `references/modes/fold-completeness.md` carries `RESULT_SCHEMA_VERSION` (fold RESULT schema v4).
13
+
14
+ **Why the modes/*.md docs and NOT the tool HELP strings.** Every tool's HELP INTERPOLATES the same constant (`the ${DEFAULT_DIFF_CAP}-line diff cap`), so it can never drift from the code — there is nothing to check there. The hand-authored contract prose in `references/modes/*.md` is the surface that DOES drift, so that is exactly what this lint pins. Change a constant and the current-value token stops appearing in the lagging doc → the gate fails, forcing the doc update **in the same edit as the code** (the §2.6 "contract docs change in the same edit as code" rule, mechanized).
15
+
16
+ **Edit-safe (the closed-world discipline).** Adding a binding ADDS a checked entry; it never widens a blocklist. A token that stops appearing, a bound file that cannot be read, or an unknown binding all FAIL CLOSED — never a silent pass.
17
+
18
+ **Invariants:** read-only · never writes · never commits · never runs a subscription CLI · spawns nothing · the value source is the live imported constant (and the schema's exported vocab Sets), never a re-typed literal.
@@ -1,6 +1,6 @@
1
1
  ### Mode: fold-completeness
2
2
 
3
- The FOLD-COMPLETENESS gate (M3a / AD-046, honest red→green BUGFREE-1 / AD-047) — the sibling of the review-round ledger: the ledger computes WHEN the plan-execution review loop stops; this tool pair verifies that the tests the loop's folds bind to ACTUALLY pin the changed code, and that each bound test was SEEN FAILING before its fix (no fix theater). One recorded run proves (a) every changed executable line is executed by the suite and (b) every `fixable-bug` bound testId is N/N green with an observed-red receipt behind it; a read-only checker turns the recorded evidence into a gate exit code. **Since schema v3 (BUGFREE-2 / AD-048, D7) every record carries `base` and the whole proof is SEGMENT-scoped** — bound testIds, receipts, custody chains, and tamper obligations all filter to (loop, `base` = the current HEAD): a committed phase's custody obligations **close with its commit** (editing a custody file in a LATER segment no longer re-demands the closed segment's proof — the cross-phase churn class is dead), while a fold whose red was observed in a PREVIOUS segment still needs the `red-proof` lane (a receipt never crosses a commit boundary — the stated residual). Pre-v3 records never enter a segment; a dirty loop holding only pre-v3 records fails with a reason naming the schema upgrade.
3
+ The FOLD-COMPLETENESS gate (M3a / AD-046, honest red→green BUGFREE-1 / AD-047) — the sibling of the review-round ledger: the ledger computes WHEN the plan-execution review loop stops; this tool pair verifies that the tests the loop's folds bind to ACTUALLY pin the changed code, and that each bound test was SEEN FAILING before its fix (no fix theater). One recorded run proves (a) every changed executable line is executed by the suite and (b) every `fixable-bug` bound testId is N/N green with an observed-red receipt behind it; a read-only checker turns the recorded evidence into a gate exit code. **Since schema v3 (BUGFREE-2 / AD-048, D7) every record carries `base` and the whole proof is SEGMENT-scoped** — bound testIds, receipts, custody chains, and tamper obligations all filter to (loop, `base` = the current HEAD): a committed phase's custody obligations **close with its commit** (editing a custody file in a LATER segment no longer re-demands the closed segment's proof — the cross-phase churn class is dead), while a fold whose red was observed in a PREVIOUS segment still needs the `red-proof` lane (a receipt never crosses a commit boundary — the stated residual). Pre-v3 records never enter a segment; a dirty loop holding only pre-v3 records fails with a reason naming the schema upgrade. **Since schema v4 (BUGFREE-3 / AD-049)** a run record additionally carries the **suite-execution evidence** ((a): `cmd` + exit + pre/post fingerprints — so `run-gates --record` can CREDIT the `unit-tests` gate instead of re-spawning it), and a new record kind **`reattest`** joins `run`/`red-probe` ((c): an operator-minted, recorded custody re-attestation — see the runner's `--reattest`). A placed pre-v4 checker rejects a v4 record outright (the version-skew guard); v1/v2/v3 rules are untouched.
4
4
 
5
5
  **The fold-time order (the operational contract — red is observed BEFORE the fix):** classify the fixable-bug with its testId → write the failing test → `--red "<testId>"` observes it FAIL on the real pre-fold tree and mints the receipt → fold the fix → the normal run observes green → `--check`. Folding first is not recoverable without reverting the fix to re-observe red — observe red when it actually happens.
6
6
 
@@ -9,15 +9,19 @@ The FOLD-COMPLETENESS gate (M3a / AD-046, honest red→green BUGFREE-1 / AD-047)
9
9
  1. **Runner** — `node ${CLAUDE_SKILL_DIR}/tools/fold-completeness-run.mjs [--suite "<cmd>"] [--cwd <dir>]`, plus the red verb `--red "<test-file>#<test-name-pattern>"` — the SOLE tree-toucher and the SOLE result writer. The default run, over the in-flight plan-execution loop's dirty tree: resolve the loop (the single in-flight plan stem — 0 or >1 plans is a typed refusal), classify the changed surface by a CLOSED extension rule (assessable JS / unsupported TS-JSX / out-of-domain), run the suite ONCE under `NODE_V8_COVERAGE` (the coverage dir lives OUTSIDE the work tree — no repo artifacts), map every changed executable line to covered/uncovered from the V8 report itself, probe each bound testId N times (`AW_FOLD_RERUNS`, default 3; a shell-free spawn of the resolver's canonical path; each run under its own per-run `AW_FOLD_PROBE_TIMEOUT_S`, default 120 — probes only, never the suite) recording rerun counts + the test file's content hash (custody), and append ONE machine-only v3 run record — segment-framed (`base`) and bound to BOTH the tree fingerprint AND the segment's sorted `fixable-bug` testId set — to `<git dir>/agent-workflow-fold-completeness.jsonl` (never committable by construction; `AW_FOLD_RESULTS` overrides). **`--red` observes a testId on the CURRENT (pre-fold) tree**: failing on N/N runs → it appends the observed-red receipt (testId, counts, content hash, fingerprint); observed-green / unresolvable / mixed / timed-out are DISTINGUISHED typed refusals and nothing is written. The verdict algebra is strict N/N: RED = N/N fail, GREEN = N/N pass, anything mixed or timed out is **QUARANTINE** — it never converts and has NO override lane (a flaky pin proves nothing — replace the test). A test that cannot even LOAD pre-fold (it imports an export the fix introduces) is authored with a dynamic `import()` so it loads and fails pre-fold.
10
10
  2. **Read-only checker** — `node ${CLAUDE_SKILL_DIR}/tools/fold-completeness.mjs [--check | --status | --json]` — never runs tests, never touches the tree, never imports the runner (an import-split test pins the structural split). Per bound testId (of the CURRENT segment) the gate requires: an N/N-green probe, an observed-red receipt in this segment, that receipt PRECEDING the loop's latest run in ledger order (a post-hoc red proves nothing — a late receipt forces one fresh run), and content **custody** — the run's recorded test-file hash equals the latest custody-eligible receipt hash on that file (eligible = the receipt's own testId is bound AND it precedes the run), so the test that is green now is byte-identical to a test that provably failed; any edit after the receipt requires re-observing red, and appending the NEXT fold's red test to the same file re-attests it without ceremony. `--status` (default) is the human report; **`--check` is the gate exit code — the normative exit contract lives in the CHECKER's tool header (the single home — do not re-enumerate it elsewhere)**; `--json` is the structured state + decision.
11
11
 
12
+ **The verification PROFILE — language independence (BUGFREE-3 / AD-049).** An OPTIONAL per-project `docs/ai/verification-profile.json` (seeded by the memory bootstrap; a memory-canon template with a kit mirror) generalizes the fold signal beyond JS/V8 so a consumer on another language/runner drives the SAME gate. It declares three inputs — **`coverage.kind`** `"v8"` (default) or `"lcov"` (+ `lcovPath`: your suite leaves an LCOV file there, parsed by a dependency-free reader — the diff-cover pattern); **`singleTest`** an argv template (`{file}`/`{pattern}`, plus `{resultPath}` for a file-based format) + `resultFormat` `"tap-stdout"` (default) / `"tap-file"` / `"junit-xml"` (the "0 tests matched" → unresolvable guard holds for EVERY format — a zero-match is never read green); and an optional **`findings.sarifPath`** for ADVISORY findings (`--findings` prints them — never recorded, never gate-blocking; a malformed SARIF fails the advisory read loudly but leaves `--check` unaffected). An ABSENT profile reproduces today's exact behaviour (V8 + node:test TAP on stdout); env knobs (`AW_FOLD_SUITE_CMD`/`AW_FOLD_BOUND_CMD`) still override. Every DECLARED path (`lcovPath`, `sarifPath`) MUST be gitignored or outside the repo (a symlink is refused) — else the suite writing there would move the review fingerprint. The suite COMMAND is NOT a profile field: it stays the `gates.json` `unit-tests` cmd (command-identity — the (a) credit).
13
+
14
+ **The operator verbs (recorded, self-discipline — never auto-detection):** besides `--red`, the runner offers **`--reattest "<testId>"`** ((c)): re-anchor a bound test FILE's custody at its CURRENT bytes after a GREEN-ONLY append — the honest replacement for a `red-proof` waiver (there is no red to observe); it never converts a red baseline or waives the observed-red proof, and the guard still fails closed on any un-reattested change. **`--preflight`** ((f)): the CHEAP half — from the git diff + ledgers (seconds, no suite run, no probe, no coverage) it prints the overrides/re-attests to RECORD BEFORE the expensive pass, routed by kind (`oracle-change` for a tampered file, `--reattest` for a green-only custody delta, `--red`/`red-proof` for a bound testId with no observed-red receipt). **`--findings`**: the SARIF advisory above.
15
+
12
16
  **The oracle-tamper guard + the recorded override lanes (review-ledger schema v3).** The runner also records a **tamper surface** over the tracked working-vs-HEAD diff — the union of test-classified paths and the bound-testId file halves, restricted to files existing at HEAD — classified by hunk line polarity: any removed/modified line (or a deleted file) is TAMPER; pure additions and brand-new test files never trip it. Note two edges: a **rename** reads as delete+add under the pinned `--no-renames` diff, so renaming a test file trips the guard (record the override or split the rename from the fix); and a run record written by a **pre-tamper runner** (no recorded surface) fails the gate with a re-run reason — after an upgrade, one fresh run refreshes the evidence. The gate fails closed on any tampered file not named by a recorded **`oracle-change`** override (`review-ledger-write override --json '{"loop":…,"round":…,"scope":"oracle-change","files":[…],"reason":…}'` — a durable, auditable ledger entry, never a silent waiver; one override covers the named files for the rest of the loop). The second lane, scope **`red-proof`**, names ONE testId whose red is genuinely unestablishable pre-fold (or whose custody file was legitimately edited post-fix) and waives the receipt + custody proof for exactly that testId — never the N/N-green requirement, never QUARANTINE. Overrides are loop + payload scoped, minted only inside the single in-flight loop.
13
17
 
14
- **Wire the gate BY HAND, once, BEFORE the round's runner run.** The candidate line for your own `docs/ai/gates.json`: `{ "id": "fold-completeness", "title": "Fold-completeness — every changed executable line executed by tests (M3a)", "cmd": "node \"<path-to-this-skill>/tools/fold-completeness.mjs\" --check" }` — the path your project reaches the kit by, kept inside the escaped quotes so a path with spaces survives. Wiring comes FIRST because where `docs/ai` is tracked, the gate-file edit itself moves the tree fingerprint — wiring after a run stales the record you just wrote. The consent-gated seeder does NOT offer this entry yet — a deliberate v1 hold: the signal is JS/V8-only, and offering it to a project whose source the runner cannot assess would seed a gate that can only fail closed; seeder offering arrives with a language-aware verification profile.
18
+ **Wire the gate BY HAND, once, BEFORE the round's runner run.** The candidate line for your own `docs/ai/gates.json`: `{ "id": "fold-completeness", "title": "Fold-completeness — every changed executable line executed by tests (M3a)", "cmd": "node \"<path-to-this-skill>/tools/fold-completeness.mjs\" --check" }` — the path your project reaches the kit by, kept inside the escaped quotes so a path with spaces survives. Wiring comes FIRST because where `docs/ai` is tracked, the gate-file edit itself moves the tree fingerprint — wiring after a run stales the record you just wrote. The consent-gated seeder does NOT offer this entry yet — a deliberate hold: the language-aware verification profile now EXISTS (BUGFREE-3 / AD-049 coverage via LCOV, single-test via TAP-file/JUnit-XML), so the JS/V8-only blocker is lifted, but consumer-seeding of this gate is a SEPARATE follow-up the profile merely unblocks.
15
19
 
16
20
  **Run-then-check usage (each plan-execution round):** run the runner AFTER the round's LAST edit — any later tree edit (a source fix, a doc touch, a gate-file tweak) moves the fingerprint and requires a re-run — then the wired `--check` attests it. Staleness is by BOTH bindings: a tree edit moves the fingerprint, and a new `fixable-bug` triage moves the bound-testId set — either makes the previous record STALE, so a green check always attests the CURRENT tree against the CURRENT fold set.
17
21
 
18
- **Suite + bound-test commands:** the suite command defaults to the `unit-tests` gate `cmd` in the project's `docs/ai/gates.json`; absent that, `--suite "<cmd>"` (or `AW_FOLD_SUITE_CMD`) is REQUIRED — refused loudly otherwise. Bound-test probes default to the `node --test --test-name-pattern` shape over the testId halves (`<test-file>#<test-name-pattern>`); a consumer on another runner sets `AW_FOLD_BOUND_CMD` — a JSON ARRAY of argv strings with `{file}` / `{pattern}` placeholder substitution, spawned WITHOUT a shell (testId content never reaches a shell). Note: the suite's own child processes must inherit `NODE_V8_COVERAGE` for their lines to count (node child spawns inherit it by default; a test runner that scrubs its child env needs consumer configuration).
22
+ **Suite + bound-test commands:** the suite command defaults to the `unit-tests` gate `cmd` in the project's `docs/ai/gates.json`; absent that, `--suite "<cmd>"` (or `AW_FOLD_SUITE_CMD`) is REQUIRED — refused loudly otherwise. Bound-test probes default to the `node --test --test-name-pattern` shape over the testId halves (`<test-file>#<test-name-pattern>`); a consumer on another runner sets the profile's `singleTest.argv` (or the `AW_FOLD_BOUND_CMD` env override, which wins) — a JSON ARRAY of argv strings with `{file}` / `{pattern}` substitution (plus `{resultPath}` for a file-based `resultFormat`), spawned WITHOUT a shell (testId content never reaches a shell). Coverage comes from V8 by default, or from an LCOV file the suite writes (`coverage.kind:"lcov"` + `lcovPath`) — either source feeds the same uncovered-changed decision. Note: the suite's own child processes must inherit `NODE_V8_COVERAGE` for their lines to count (node child spawns inherit it by default; a test runner that scrubs its child env needs consumer configuration).
19
23
 
20
- **v1 scope (stated):** assessable = `.mjs` / `.cjs` / `.js`, excluding test files (`*.test.*` / `*.spec.*`); changed `.ts` / `.tsx` / `.jsx` / `.mts` / `.cts` files are recorded as unsupported and FAIL the gate CLOSED — the signal never vouches for JS-family source it cannot assess; everything else (docs/config the suite does not execute) is recorded out-of-domain and LOUDLY listed, never gate-blocking. Executability is V8-derived: a changed assessable file entirely absent from the coverage report is RED at file level ("never imported" must not read as "nothing executable").
24
+ **v1 scope (stated):** assessable = `.mjs` / `.cjs` / `.js`, excluding test files (`*.test.*` / `*.spec.*`); changed `.ts` / `.tsx` / `.jsx` / `.mts` / `.cts` files are recorded as unsupported and FAIL the gate CLOSED — the signal never vouches for JS-family source it cannot assess; everything else (docs/config the suite does not execute) is recorded out-of-domain and LOUDLY listed, never gate-blocking. Executability is coverage-derived (V8 by default, or LCOV via the profile): a changed assessable file entirely absent from the coverage report is RED at file level ("never imported" must not read as "nothing executable"). The LANGUAGE-INDEPENDENCE contract (BUGFREE-3) generalizes the coverage source + single-test result format without changing this closed JS assessable rule — a consumer whose suite emits LCOV + JUnit-XML drives the same gate.
21
25
 
22
26
  **Mutation is NOT shipped (v1).** The result record carries a reserved, always-empty `mutation` shape; the shipped runner performs NO mutation and never modifies a source file, and the checker FAILS CLOSED on a record carrying ANY mutation data (such a record was not produced by this runner version). The researched mutation half was shelved: a bounded operator set tests local expression boundaries, not the cross-module interactions that produce real fold bugs, and it is not language-independent. The `AW_FOLD_MUTANTS_MAX` / `AW_FOLD_HUNK_MUTANTS_MAX` / `AW_FOLD_TIME_BUDGET_S` budgets are parsed and recorded but inert in v1 (reserved).
23
27
 
@@ -2,11 +2,12 @@
2
2
 
3
3
  The **grounded-review facts assembler** (AD-038) — an ungrounded `agy` review GUESSES, and while the grounding contract is mechanized (`agy-review code --facts @f`), populating the facts file was a manual chore. This mode emits the two **mechanical** halves of a facts payload; the orchestrator still owns any judgment-bearing additions. **Catalogued honestly as a WRITER** — `--out <path>` writes one file — with the invariant: `--out` accepts **only gitignored / out-of-repo scratch destinations** and REFUSES a tracked path AND an in-repo not-ignored path (a new untracked file would itself move the review fingerprint the facts are about to ground); **stdout is the default**. It never commits and never runs a subscription CLI.
4
4
 
5
- Run `node ${CLAUDE_SKILL_DIR}/tools/grounding.mjs [--constraints] [--plan <path>] [--reserve-bytes <n>] [--out <path>]`:
5
+ Run `node ${CLAUDE_SKILL_DIR}/tools/grounding.mjs [--constraints] [--plan <path>] [--ledger-summary] [--reserve-bytes <n>] [--out <path>]`:
6
6
 
7
7
  1. **`--constraints`** — slice the root `AGENTS.md` **Hard Constraints** section, verbatim and whole (exactly ONE matching heading; zero or several is a loud STOP, never a guess — the marker-slot discipline).
8
8
  2. **`--plan <path>`** — extract the plan's decision-bearing canonical sections, verbatim + whole: `## Approach` (REQUIRED — its "What we are NOT doing" text rides inside; it is not a heading in canon) and `## Verification` (REQUIRED — STOP if missing), plus `## Decisions (locked)` when present (the optional engine §7 heading); a DUPLICATE heading is always a STOP.
9
- 3. **Byte budget**the output honors the same `AGY_MAX_PROMPT_BYTES` contract the agy wrapper enforces (override may only tighten; the OS argv ceiling is rejected), MINUS **`--reserve-bytes <n>`** — the artifact share the caller expects `agy-review` to add around these facts. Overflow is trimmed tail-first with a loud in-band marker + stderr report never a silent cut.
10
- 4. Feed the result to the wrapper: `agy-review code --facts @<out>`. The `procedures` advisor renders this invocation as a concrete pre-step whenever the resolved review dispatch includes agy populated with the in-flight plan path when exactly one plan is in flight.
9
+ 3. **`--ledger-summary`** (BUGFREE-3 / AD-049) append a COMPUTED review-ledger digest for the SINGLE in-flight `plan-execution` segment: the round/origin/classification/verdict/override counts (filtered to that loop + base unrelated loops excluded) plus a terse per-round render. "Computed, not remembered" facts — the reviewer sees the loop's own history without the orchestrator hand-copying it. A loud STOP unless exactly one plan is in flight; read-only (the ledger is never written here).
10
+ 4. **Byte budget** — the output honors the same `AGY_MAX_PROMPT_BYTES` contract the agy wrapper enforces (override may only tighten; the OS argv ceiling is rejected), MINUS **`--reserve-bytes <n>`** the artifact share the caller expects `agy-review` to add around these facts. Overflow is trimmed tail-first with a loud in-band marker + stderr report never a silent cut.
11
+ 5. Feed the result to the wrapper: `agy-review code --facts @<out>`. The `procedures` advisor renders this invocation as a concrete pre-step whenever the resolved review dispatch includes agy — populated with the in-flight plan path when exactly one plan is in flight.
11
12
 
12
13
  **Invariants:** writer (writes at most the ONE `--out` scratch file — gitignored / out-of-repo only) · never commits · never runs a subscription CLI · verbatim slices only (assembly is mechanical; facts judgment stays with the orchestrator).
@@ -13,7 +13,7 @@ The review-round **LEDGER** (DEBT-REVIEW-CAP / AD-045) — it turns the prose re
13
13
  - **`--telemetry`** → read-only COUNTS across ALL loops and BOTH ledgers (never combined with the other flags — a mixed-mode gate cmd would silently pass): rounds/segments per loop, finding-origin totals, classification distribution (incl. `refuted`), per-backend verdict + divergence-round counts, override usage by scope, gate-run counts (quality-green / red results by gate id), fold runs, observed-red receipts, quarantined probes. Counts only — which gates earn their keep stays the maintainer's judgment.
14
14
 
15
15
  2. **Writer** — `node ${CLAUDE_SKILL_DIR}/tools/review-ledger-write.mjs record|classify|override --json '<payload>'` (the SOLE writer, over the shared `atomic-write` core):
16
- - `record` appends one round — `{ loop, round, origins, backends, findings }` (activity defaults to `plan-execution`). **The teeth (Decision 5 + AD-048, all per SEGMENT):** it REFUSES (typed STOP) to append a round WHILE `decideStop` on the segment's records is `triage-required` (an UNCLASSIFIED surviving blocking finding at/after the cap); refuses ANY round beyond the **hard-max ceiling of 3 within one segment** unconditionally (the counter reopens only at the next gated commit); refuses while a blocking finding of the segment's previous round **VANISHED unclassified** (present means present-as-blocking — a downgrade to minor does not survive; a pending `escalate` never clears; `refuted` is the honest phantom lane); refuses while the changed source surface exceeds the **diff-size cap** (`AW_REVIEW_DIFF_CAP`, default 400 new-side lines of assessable + unsupported SOURCE — tests and out-of-domain never count, pure deletions are free) without a recorded segment `size-cap` override; and refuses without a **quality-green gate-run** at the current fingerprint (`run-gates --record` — gates-before-review is computed, not remembered; a `--only` subset or a tree-changed run never satisfies; red PROCESS gates — the kit's own `--check` loop gates — never block). **Integrity binding (Decision 7):** each NON-degraded backend needs a grounded **code** receipt (from `codex-review code` / `agy-review code --facts @f`) for the current tree — a round cannot be recorded for a tree no bridge reviewed.
16
+ - `record` appends one round — `{ loop, round, origins, backends, findings }` (activity defaults to `plan-execution`). **The teeth (Decision 5 + AD-048, all per SEGMENT):** it REFUSES (typed STOP) to append a round WHILE `decideStop` on the segment's records is `triage-required` (an UNCLASSIFIED surviving blocking finding at/after the cap); refuses ANY round beyond the **hard-max ceiling of 3 within one segment** unconditionally (the counter reopens only at the next gated commit); refuses while a blocking finding of the segment's previous round **VANISHED unclassified** (present means present-as-blocking — a downgrade to minor does not survive; a pending `escalate` never clears; `refuted` is the honest phantom lane); refuses while the changed source surface exceeds the **diff-size cap** (`AW_REVIEW_DIFF_CAP`, default 400 new-side lines of assessable + unsupported SOURCE — tests and out-of-domain never count, pure deletions are free) without a recorded segment `size-cap` override; and refuses without a **quality-green gate-run** at the current fingerprint (`run-gates --record` — gates-before-review is computed, not remembered; a `--only` subset or a tree-changed run never satisfies; red PROCESS gates — the kit's own `--check` loop gates — never block). **Integrity binding (Decision 7):** each NON-degraded backend needs a grounded **code** receipt (from `codex-review code` / `agy-review code --facts @f`) for the current tree — a round cannot be recorded for a tree no bridge reviewed. **`--from-receipts`** (BUGFREE-3 / AD-049) DRAFTS the `backends[]` from those receipts instead of hand-composing them: each recipe-named backend's verdict comes from its fresh grounded code receipt and its counts are computed from the supplied `findings` (`origins` / `findings` stay explicit input); a recipe-named backend with no receipt is a LOUD stop — the draft never invents one (supply it explicitly as a degraded backend if the bridge really is down).
17
17
  - `classify` appends one triage — each surviving blocking finding of a SEGMENT round classified `fixable-bug` / `inherent-layer-residual` / `escalate` / **`refuted`** (v4 — the honest lane for a phantom finding, refuted against code: a MANDATORY non-empty `note` cites the grounds; never silently dropped, never folded). **A `fixable-bug` REQUIRES a `testId`** (M2/AD-046, schema v2) — the red→green test that pins the fold, formatted `<test-file>#<test-name-pattern>` (a `#` separator, both halves non-empty; no file-suffix rule); `inherent-layer-residual` / `escalate` may omit it. The writer validates the FORMAT only. **This is what BREAKS the deadlock:** once every surviving blocking finding is classified, `record` permits the next round (a `fixable-bug` classification lets the fix round run — no deadlock).
18
18
  - `override` appends one override record (schema v3, BUGFREE-1/AD-047) — the LOUD, durable waiver the **fold-completeness** gate consumes, `{ loop, round, scope, reason }` plus the per-scope payload (EXACT — a stray key is refused): scope **`oracle-change`** → `files[]`, the repo-relative pre-existing test files whose tamper flag it lifts (a fix diff that deliberately rewrote test expectations); scope **`red-proof`** → `testId`, the ONE bound test whose observed-red receipt + custody it waives (a red genuinely unestablishable pre-fold, or a custody file legitimately edited post-fix); scope **`size-cap`** (v4) → `sanctionedLines`, the EXACT changed-surface magnitude the waiver sanctions — **segment-scoped** (it dies at the next commit; a grown surface needs a fresh recorded sanction). Teeth: schema validation, fail-closed ledger read, round-sequence integrity, and the loop must be the SINGLE in-flight plan. QUARANTINE (a flaky/timed-out probe) has NO override lane. Overrides never enter `decideStop` — the two v3 scopes are fold-completeness inputs; `size-cap` is a writer-tooth input.
19
19
  - **`gate-run` records (v4, kind `gate-run`)** are minted by `run-gates --record` through the writer's `recordGateRun` API (the runner never opens the ledger itself) — the D5 green-baseline receipt: the FULL declaration + exactly what ran + the tree fingerprint BEFORE and AFTER the run, segment-framed with NO round number, minted only inside the single in-flight loop. A red run records honestly (telemetry fuel); **quality-green** (every declared NON-process gate green, tree unchanged under the run) is judged at read time. Revert-first beyond this is not mechanically observable at this layer — it ships as protocol + telemetry visibility (consecutive red gate-runs), stated plainly.
@@ -5,8 +5,9 @@ Read-only **review-receipt checker** (AD-038) — it makes *"reviewed ≠ shippe
5
5
  Run `node ${CLAUDE_SKILL_DIR}/tools/review-state.mjs [--check] [--json]`:
6
6
 
7
7
  1. Plain run → the human report: resolved recipe + source, plan-in-flight, tree fingerprint, per-backend receipt state (current / stale / ungrounded / missing) with verdict + grounding + timestamp.
8
- 2. **`--check`** → the gate exit code. The **normative exit contract lives in the tool header** (the single home — do not re-enumerate it elsewhere): exit 0 for a solo-resolved recipe (configured, or degraded there — no ready reviewer), no plan in flight (the `docs/plans` naming convention: `queue.md` and `EXECUTE-`/`FEEDBACK-`-prefixed or `PROMPT`/`prompt`/`handoff`-carrying names are scratch), a clean tree, a non-git cwd, or every recipe-named backend receipted **current + grounded**; exit 1 when a backend is missing, **stale** (ANY edit after its review moves the fingerprint), or grounded:false under reviewed/council. **Presence, not unanimity:** verdict adjudication (ship/revise, the divergence crossover) stays orchestrator judgment — the gate only proves the configured backends really reviewed THIS tree. Plan/diff receipts and continuations (`agy-review --continue`) are **informational-only**: after a fold, only a **fresh grounded re-run** (`codex-review code`; `agy-review code --facts @f`) restores green.
8
+ 2. **`--check`** → the gate exit code. The **normative exit contract lives in the tool header** (the single home — do not re-enumerate it elsewhere): exit 0 for a solo-resolved recipe (configured, or degraded there — no ready reviewer), no plan in flight (the `docs/plans` naming convention: `queue.md` and `EXECUTE-`/`FEEDBACK-`-prefixed or `PROMPT`/`prompt`/`handoff`-carrying names are scratch), a clean tree, a non-git cwd, or every recipe-named backend receipted **current + grounded** **or degraded-exempt**: the current segment's latest **review-ledger** round records that backend `degraded:true` at the current tree fingerprint, with ≥1 non-degraded recipe-named backend present + grounded and the ledger reading clean (AD-050; a corrupt ledger DENIES the exemption fail-closed but never fails an otherwise-satisfied tree — the tool header is the single home). Exit 1 when a backend is missing, **stale** (ANY edit after its review moves the fingerprint), or grounded:false under reviewed/council, and is not degraded-exempt. **Presence, not unanimity:** verdict adjudication (ship/revise, the divergence crossover) stays orchestrator judgment — the gate only proves the configured backends really reviewed THIS tree (a recorded degrade counts as "reviewed", never as "converged" — that stays `review-ledger`'s job). Plan/diff receipts and continuations (`agy-review --continue`) are **informational-only**: after a fold, only a **fresh grounded re-run** (`codex-review code`; `agy-review code --facts @f`) restores green.
9
9
  3. **Wire it as a gate by hand OR via the explicit-consent seeder — never without consent (AD-021/AD-042).** The candidate line for your own `docs/ai/gates.json`: `{ "id": "review-state", "title": "Review receipts current for the uncommitted tree", "cmd": "node <path-to-this-skill>/tools/review-state.mjs --check" }` — with the path your project actually reaches the kit by, QUOTED so a path with spaces survives, executable from the project root. The consent-gated seeder (`${CLAUDE_SKILL_DIR}/references/modes/gates.md`, consent-seed section) offers exactly this entry — path resolved and quoted — ONLY when your `docs/ai/orchestration.json` declares `reviewed`/`council` on `plan-execution.review` (the slot this checker enforces); it writes nothing without your explicit yes. Once declared, the opt-in `${CLAUDE_SKILL_DIR}/references/modes/hook.md` auto-approves it like any other declared gate.
10
+ 4. **`--await [--timeout <s>]`** (BUGFREE-3 / AD-049) → BLOCK until every recipe-named backend is SATISFIED for the current tree (i.e. until `--check` would PASS), or the bounded timeout elapses (a loud exit 1; default 900s). Run it after dispatching the review bridges to WAIT for their receipts to land instead of hand-polling a pid: the durable completion signal is the **receipt**, never a process event (a harness "completed" notification fires early; a bridge's output late-flushes). It waits until each backend has a fresh grounded receipt **OR is degraded-exempt**: once a current-tree degrade is recorded in the review-ledger, `--await` stops waiting for that backend and returns READY (AD-050 — it inherits the `--check` exemption via the shared decision), so you no longer hand-`--await` around a known degrade. Still read-only (it only re-reads state, now the ledger too); solo / no-plan / clean-tree resolve instantly.
10
11
 
11
12
  **Human residual (stated, accepted):** `git commit --no-verify` and receipt-file deletion/forgery remain possible — this is a self-discipline mechanism against silent process drift, not a security boundary.
12
13
 
@@ -22,6 +22,8 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
22
22
 
23
23
  **Gate-declaration ensure (seed-if-missing) — stamp-independent, same gate, BEFORE the equal-head short-circuit.** Ensure `docs/ai/gates.json` exists: **create it from `${CLAUDE_SKILL_DIR}/references/templates/gates.json` if missing** — the kit's OWN template twin, so this works even when the installed memory substrate predates the gates feature (a stale memory never silently loses it); **an existing file is preserved byte-for-byte** (a project's declared gate matrix is authored content — never clobbered, never refreshed in place; unlike the orchestration `_README` there is no note-refresh here). Report it in the step 4 / step 8 success report (*seeded* / *already present*). Like the config ensure, this reaches an equal-head deployment without a lineage-head bump or a migration file (a `.json`, inherently outside the docs cap-validator).
24
24
 
25
+ **Verification-profile ensure (seed-if-missing) — stamp-independent, same gate, BEFORE the equal-head short-circuit.** Ensure `docs/ai/verification-profile.json` exists: **create it from `${CLAUDE_SKILL_DIR}/references/templates/verification-profile.json` if missing** (the kit's OWN template twin, so the fallback/stale-memory path still gains the OPTIONAL fold-completeness profile); **an existing file is preserved byte-for-byte** (a project may have edited it for its language/runner). An ABSENT profile reproduces the default V8 + node:test behaviour, so this ensure only ADDS the opt-in default. Report it in the step 4 / step 8 report (*seeded* / *already present*). Like the config + gate ensures, no lineage-head bump or migration file (a `.json`, outside the docs cap-validator).
26
+
25
27
  **Enforcement-script ensure (seed-if-missing) — stamp-independent, same gate, BEFORE the equal-head short-circuit.** A deployment older than the ADR-cascade feature has no `scripts/archive-decisions.mjs`, and an equal-head exit would otherwise never deliver it. Ensure the pair exists in the project's `scripts/`: **copy `archive-decisions.mjs` + `archive-decisions.test.mjs` from `${CLAUDE_SKILL_DIR}/references/scripts/` if missing** (the kit's own fallback copies — byte-identical to the memory canon by the mirror guard); **an existing file is preserved, never overwritten** (drift repair belongs to a lineage migration, not this ensure). The deployed pre-commit hook gains the `archive-decisions.mjs --check` line only when the hook itself is next refreshed (re-run `node scripts/install-git-hooks.mjs` after the ensure and it will refuse a non-marker hook as always); an OLD hook without the line stays consistent-safe — the decisions gate is simply not enforced yet, never a broken hook. Skip this ensure on a No-Node project (the scripts are Node enforcement). Report it in the step 4 / step 8 success report (*added* / *already present*).
26
28
 
27
29
  **Placed-bridge refresh — stamp-independent, same gate, BEFORE the equal-head short-circuit.** Run
@@ -52,7 +54,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
52
54
  **NEVER writes** it (the file lives outside every kit tree — D2), so an unknown/retired key is
53
55
  flagged + preserved, never edited. Runs on **every** upgrade; exit 0 covers every outcome.
54
56
  4. **Equal-head exit — a real successful-exit report, not a bare stop.** If the stamp **equals** the head, the lineage is up to date — but step 3 (the stamp-independent reconciles) ran first and may have changed things, so this is a proper exit report, not a no-op:
55
- - **Report step 3's outcome in plain language** — for **each** pointer (workflow-methodology and orchestration-recipes) whether it was *added*, was *already present* (nothing changed), or was *skipped because the entry point is over its line limit* (the cap-refusal soft-skip from step 3, with its reason); whether the `docs/ai/orchestration.json` config was *seeded* (created from the template), had its onboarding note *refreshed*, was *already current*, or carried a *customized note that was preserved* (a user edit is never clobbered); whether the `docs/ai/gates.json` gate declaration was *seeded* or was *already present* (preserved byte-for-byte); whether the enforcement-script ensure *added* the `archive-decisions` pair to `scripts/` or found it *already present*; the **placed-bridge refresh** outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / *could not refresh* + recovery); the **agent-rules lens** outcome (*refreshed* / *already current* / *custom edit preserved + note* / *file absent* / *engine too old* / *over the line cap*); the **bridge-settings reconcile** outcome (paste the tool's line verbatim); and, for a hidden deployment, whether the hidden-mode footprint was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility / a leftover machine-wide block). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
57
+ - **Report step 3's outcome in plain language** — for **each** pointer (workflow-methodology and orchestration-recipes) whether it was *added*, was *already present* (nothing changed), or was *skipped because the entry point is over its line limit* (the cap-refusal soft-skip from step 3, with its reason); whether the `docs/ai/orchestration.json` config was *seeded* (created from the template), had its onboarding note *refreshed*, was *already current*, or carried a *customized note that was preserved* (a user edit is never clobbered); whether the `docs/ai/gates.json` gate declaration was *seeded* or was *already present* (preserved byte-for-byte); whether the `docs/ai/verification-profile.json` profile was *seeded* or was *already present* (preserved byte-for-byte); whether the enforcement-script ensure *added* the `archive-decisions` pair to `scripts/` or found it *already present*; the **placed-bridge refresh** outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / *could not refresh* + recovery); the **agent-rules lens** outcome (*refreshed* / *already current* / *custom edit preserved + note* / *file absent* / *engine too old* / *over the line cap*); the **bridge-settings reconcile** outcome (paste the tool's line verbatim); and, for a hidden deployment, whether the hidden-mode footprint was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility / a leftover machine-wide block). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
56
58
  - **Never surface the structure number on this exit.** Whatever step 3 did, do **not** recite the `docs/ai` structure version, the internal versioning vocabulary, or the two-axes note here — the number is inert on an equal-head exit; it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md` (shown at the never-downgrade STOP, the explicit status view, or on an explicit ask). Frame the success itself per the final bullet: if step 3 changed anything, say **what changed** in plain human terms; only a pure zero-diff no-op is *settings already current — no update needed*.
57
59
  - **Print the report footer** in the canonical order (version block → one-line backend-status line → welcome mat — the shared contracts in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`; rendered from the helpers, same host-can't-run skip-with-reason). The welcome mat closes on **one** caveat-aware next step (a behind member first, else `setup` / `recipes` / `velocity` / `agents` / `hook`).
58
60
  - **Then ask before committing — never auto-commit.** If step 3 added the slot (or anything else changed), report it and ask. If step 3 was a pure zero-diff no-op and nothing else changed, give the plain **settings already current — no update needed** message (the *Success state* contract in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`) and still print the read-only version block (installed package versions) + backend line — but **no `docs/ai` structure version and no two-axes note** (nothing changed, so the number is inert here).
@@ -10,7 +10,8 @@
10
10
  // oldest (lowest AD id, top of file) first; an entry's lines move verbatim.
11
11
  //
12
12
  // Modes:
13
- // (default) rotate, mutate files in place (only when something is over cap)
13
+ // (default) rotate, mutate files in place (only when something is over cap), then regenerate
14
+ // docs/ai/index.md so the rotation never leaves the index stale (item (h))
14
15
  // --dry-run print the planned move-set, change nothing
15
16
  // --check report per-tier lines/cap; exit 1 if any tier is over its cap
16
17
  // --today=YYYY-MM-DD pin the lastUpdated stamp (tests / reproducible runs)
@@ -41,10 +42,40 @@
41
42
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
42
43
  import { dirname, resolve } from 'node:path';
43
44
  import { fileURLToPath, pathToFileURL } from 'node:url';
45
+ import { spawnSync } from 'node:child_process';
44
46
 
45
47
  const __dirname = dirname(fileURLToPath(import.meta.url));
46
48
  const DEFAULT_ROOT = resolve(__dirname, '..');
47
49
 
50
+ // (h) — after a rotation write, docs/ai/index.md would silently go stale (a moved ADR, or just the
51
+ // bumped lastUpdated), tripping the SEPARATE `--check-index` gate mid-release-matrix. So the
52
+ // rotation regenerates the index by REUSING the ONE generator in the sibling check-docs-size.mjs
53
+ // (spawned with --write-index --root=<root>). The subprocess bridges that script's ASYNC generator,
54
+ // so this runCli stays SYNCHRONOUS (spawnSync) — the existing sync callers/tests never ripple. It
55
+ // DEGRADES LOUDLY to an instruct (never a silent failure) when the sibling is absent or the
56
+ // regeneration fails; the --check-index gate still catches a stale index. Injectable for tests.
57
+ const CHECK_DOCS_SIBLING = resolve(__dirname, 'check-docs-size.mjs');
58
+ const INDEX_INSTRUCT = 'run `node scripts/check-docs-size.mjs --write-index` to refresh docs/ai/index.md';
59
+ // Exported + its filesystem edges injectable (deps) so BOTH degrade branches are unit-testable.
60
+ export const defaultRegenerateIndex = (root, today, deps = {}) => {
61
+ const exists = deps.existsSync ?? existsSync;
62
+ const spawn = deps.spawnSync ?? spawnSync;
63
+ const sibling = deps.sibling ?? CHECK_DOCS_SIBLING;
64
+ if (!exists(sibling)) {
65
+ return { ok: false, detail: `the index generator is not beside this script — ${INDEX_INSTRUCT}` };
66
+ }
67
+ // `--report` ISOLATES the index-WRITE outcome from the docs-cap-CHECK outcome: check-docs-size
68
+ // --write-index still WRITES the index (and still exits 2 on an empty write / rejects a genuine
69
+ // throw), but --report suppresses its exit-1 on an unrelated over-cap co-located doc — otherwise a
70
+ // benign over-cap sibling would read as an index-regeneration FAILURE (a cry-wolf on this very
71
+ // loud-degrade channel).
72
+ const r = spawn(process.execPath, [sibling, '--write-index', '--report', `--root=${root}`, `--today=${today}`], { encoding: 'utf8' });
73
+ if (r.error || r.status !== 0) {
74
+ return { ok: false, detail: `index regeneration failed (${(r.error && r.error.message) || `exit ${r.status}`}) — ${INDEX_INSTRUCT}` };
75
+ }
76
+ return { ok: true, detail: (r.stdout || '').trim() };
77
+ };
78
+
48
79
  export const HOT_REL = 'docs/ai/decisions.md';
49
80
  export const WARM_REL = 'docs/ai/history/decisions-archive.md';
50
81
  export const COLD_REL = 'docs/ai/history/decisions-archive-early.md';
@@ -295,7 +326,7 @@ const parseArgs = (argv) => {
295
326
  };
296
327
 
297
328
  export const runCli = (argv, deps = {}) => {
298
- const { root = DEFAULT_ROOT, log = console.log, logError = console.error } = deps;
329
+ const { root = DEFAULT_ROOT, log = console.log, logError = console.error, regenerateIndex = defaultRegenerateIndex } = deps;
299
330
  try {
300
331
  const { flags, today: todayOpt } = parseArgs(argv);
301
332
  if (flags.help) {
@@ -406,6 +437,10 @@ export const runCli = (argv, deps = {}) => {
406
437
  mkdirSync(dirname(tier.path), { recursive: true });
407
438
  writeFileSync(tier.path, renderTier(updated, entries), 'utf8');
408
439
  }
440
+ // (h) — the write loop completed (a full successful rotation OR a normalize-only rewrite): the
441
+ // docs index is now stale, so regenerate it here (never on --check / --dry-run / the
442
+ // nothing-to-rotate no-op / a pre-write refusal — those return before this point).
443
+ const regen = regenerateIndex(root, today);
409
444
  log('[archive-decisions] rotated:');
410
445
  log(` HOT→WARM: ${summary.hotToWarm.join(', ') || '(none)'}`);
411
446
  log(` WARM→COLD: ${summary.warmToCold.join(', ') || '(none)'}`);
@@ -413,6 +448,8 @@ export const runCli = (argv, deps = {}) => {
413
448
  log(` normalize-only rewrite (over cap on raw lines, no entry moves): ${summary.normalizeOnly.join(', ')}`);
414
449
  }
415
450
  log(` now: HOT ${summary.after.hot} · WARM ${summary.after.warm} · COLD ${summary.after.cold}`);
451
+ if (regen.ok) log(' regenerated docs/ai/index.md (the rotation kept the index fresh)');
452
+ else logError(`[archive-decisions] docs/ai/index.md NOT regenerated — ${regen.detail}`);
416
453
  return 0;
417
454
  } catch (err) {
418
455
  logError(`[archive-decisions] ${err.message}`);
@@ -14,6 +14,7 @@ import {
14
14
  planRotation,
15
15
  updateRangeTokens,
16
16
  runCli,
17
+ defaultRegenerateIndex,
17
18
  } from './archive-decisions.mjs';
18
19
 
19
20
  // Hermetic by design: this test ships as deploy payload and runs inside CONSUMER repos via the
@@ -67,10 +68,14 @@ const seedProject = (root, { hotIds, warmIds, coldIds, hotCapDelta = 100, warmCa
67
68
  };
68
69
  };
69
70
 
71
+ // The existing rotation tests are about ROTATION, not the index-regen hook (item (h)) — inject a
72
+ // no-op regenerator so they stay hermetic + fast (no real check-docs-size subprocess). The hook's
73
+ // own firing/degrade behavior is pinned by the dedicated (h) describe below; the real end-to-end
74
+ // spawn is isolated to one integration test there.
70
75
  const run = (argv, root) => {
71
76
  const out = [];
72
77
  const err = [];
73
- const code = runCli(argv, { root, log: (l) => out.push(l), logError: (l) => err.push(l) });
78
+ const code = runCli(argv, { root, log: (l) => out.push(l), logError: (l) => err.push(l), regenerateIndex: () => ({ ok: true, detail: '' }) });
74
79
  return { code, out, err, text: out.join('\n'), errText: err.join('\n') };
75
80
  };
76
81
 
@@ -363,3 +368,115 @@ describe('created tiers (a consumer with no history files yet)', () => {
363
368
  assert.match(errText, /Unknown argument/);
364
369
  });
365
370
  });
371
+
372
+ // ── (h) — a rotation regenerates docs/ai/index.md (BUGFREE-3 / AD-049) ─────────────────
373
+ describe('(h) index regeneration after rotation', () => {
374
+ const spyRun = (argv, root, regen) => {
375
+ const out = [];
376
+ const err = [];
377
+ const code = runCli(argv, { root, log: (l) => out.push(l), logError: (l) => err.push(l), regenerateIndex: regen });
378
+ return { code, out, err, text: out.join('\n'), errText: err.join('\n') };
379
+ };
380
+
381
+ it('calls the regenerator once with (root, today) AFTER a successful rotation write', () => {
382
+ const root = makeRoot();
383
+ seedProject(root, { hotIds: ['005', '006', '007', '008'], warmIds: ['003', '004'], coldIds: ['001', '002'], hotCapDelta: -1 });
384
+ const calls = [];
385
+ const { code } = spyRun(['--today=2026-01-02'], root, (r, t) => { calls.push([r, t]); return { ok: true, detail: '' }; });
386
+ assert.equal(code, 0);
387
+ assert.deepEqual(calls, [[root, '2026-01-02']], 'exactly one regen, with the rotation root + today');
388
+ });
389
+
390
+ it('does NOT regenerate on a no-op, --check, --dry-run, or a pre-write refusal', () => {
391
+ let called = 0;
392
+ const spy = () => { called += 1; return { ok: true, detail: '' }; };
393
+ // no-op (under cap)
394
+ const r1 = makeRoot(); seedProject(r1, { hotIds: ['005'], warmIds: ['003'], coldIds: ['001'] });
395
+ spyRun([], r1, spy);
396
+ // --check
397
+ spyRun(['--check'], r1, spy);
398
+ // --dry-run over a real rotation plan
399
+ const r2 = makeRoot(); seedProject(r2, { hotIds: ['005', '006'], warmIds: [], coldIds: [], hotCapDelta: -1 });
400
+ spyRun(['--dry-run'], r2, spy);
401
+ // a pre-write refusal (COLD exhausted)
402
+ const r3 = makeRoot(); seedProject(r3, { hotIds: ['005', '006', '007'], warmIds: ['003', '004'], coldIds: ['001', '002'], hotCapDelta: -1, warmCapDelta: 3, coldCapDelta: 3 });
403
+ spyRun(['--today=2026-01-02'], r3, spy);
404
+ assert.equal(called, 0, 'the hook fires only AFTER a real write — never on no-op/check/dry-run/refusal');
405
+ });
406
+
407
+ it('a NORMALIZE-ONLY rewrite (stampLastUpdated bumps lastUpdated, zero moves) still triggers regeneration', () => {
408
+ const root = makeRoot();
409
+ mkdirSync(join(root, 'docs', 'ai', 'history'), { recursive: true });
410
+ const blocks = [entryBlock('001'), entryBlock('002'), entryBlock('003')];
411
+ const body = blocks.join('\n\n---\n\n'); // the template separator shape: raw > cap, normalized fits
412
+ const probe = `${fm(999)}\n# ADRs\n\n${body}\n`;
413
+ writeFileSync(join(root, HOT_REL), `${fm(lineCountOf(probe) - 2)}\n# ADRs\n\n${body}\n`);
414
+ let called = 0;
415
+ const { code, text } = spyRun(['--today=2026-01-02'], root, () => { called += 1; return { ok: true, detail: '' }; });
416
+ assert.equal(code, 0, text);
417
+ assert.match(text, /normalize-only rewrite/);
418
+ assert.equal(called, 1, 'a normalize-only rewrite still leaves the index stale → regenerate');
419
+ });
420
+
421
+ it('degrades LOUDLY to an instruct when regeneration fails — the rotation still succeeds (exit 0)', () => {
422
+ const root = makeRoot();
423
+ seedProject(root, { hotIds: ['005', '006'], warmIds: [], coldIds: [], hotCapDelta: -1 });
424
+ const { code, errText } = spyRun(['--today=2026-01-02'], root, () => ({ ok: false, detail: 'the index generator is not beside this script — run `node scripts/check-docs-size.mjs --write-index` to refresh docs/ai/index.md' }));
425
+ assert.equal(code, 0, 'the rotation itself succeeded — regen is best-effort');
426
+ assert.match(errText, /NOT regenerated/);
427
+ assert.match(errText, /check-docs-size\.mjs --write-index/, 'the loud instruct names the recovery command');
428
+ });
429
+
430
+ it('the REAL default regenerator writes docs/ai/index.md end-to-end (no injection)', () => {
431
+ const root = makeRoot();
432
+ seedProject(root, { hotIds: ['005', '006', '007', '008'], warmIds: ['003', '004'], coldIds: ['001', '002'], hotCapDelta: -1 });
433
+ const out = [];
434
+ const code = runCli(['--today=2026-01-02'], { root, log: (l) => out.push(l), logError: (l) => out.push(l) });
435
+ assert.equal(code, 0, out.join('\n'));
436
+ assert.ok(existsSync(join(root, 'docs', 'ai', 'index.md')), 'the index was regenerated by the sibling check-docs-size.mjs');
437
+ const idx = readFileSync(join(root, 'docs', 'ai', 'index.md'), 'utf8');
438
+ assert.match(idx, /decisions\.md/, 'the regenerated index lists the rotated ADR file');
439
+ assert.match(out.join('\n'), /regenerated docs\/ai\/index\.md/, 'the success log line fires');
440
+ });
441
+
442
+ // The index-write outcome must be ISOLATED from the docs-cap-CHECK outcome: check-docs-size
443
+ // --write-index exits 1 on ANY over-cap co-located doc even after writing the index correctly, so
444
+ // a benign over-cap sibling must NEVER read as an index-regeneration failure (cry-wolf on the very
445
+ // loud-degrade channel item (h) is built around). The regen spawns --write-index --report.
446
+ it('the REAL regenerator succeeds even when an UNRELATED co-located doc is over its cap (no cry-wolf)', () => {
447
+ const root = makeRoot();
448
+ seedProject(root, { hotIds: ['005', '006', '007', '008'], warmIds: ['003', '004'], coldIds: ['001', '002'], hotCapDelta: -1 });
449
+ // An unrelated doc over its OWN maxLines cap: --write-index writes the index fine, but without
450
+ // --report it would exit 1 on this file → a false "NOT regenerated" instruct.
451
+ writeFileSync(join(root, 'docs', 'ai', 'handover.md'), `---\ntype: state\nlastUpdated: 2026-01-02\nscope: session\nstaleAfter: 7d\nowner: none\nmaxLines: 3\n---\n\n# H\nl1\nl2\nl3\nl4\nl5\n`);
452
+ const out = [];
453
+ const err = [];
454
+ const code = runCli(['--today=2026-01-02'], { root, log: (l) => out.push(l), logError: (l) => err.push(l) });
455
+ assert.equal(code, 0);
456
+ assert.match(out.join('\n'), /regenerated docs\/ai\/index\.md/, 'a benign over-cap sibling must NOT read as a regen failure');
457
+ assert.ok(!err.join('\n').includes('NOT regenerated'), 'no false cry-wolf on the loud-degrade channel');
458
+ assert.ok(existsSync(join(root, 'docs', 'ai', 'index.md')), 'the index was actually written');
459
+ });
460
+
461
+ describe('defaultRegenerateIndex — the loud-degrade branches', () => {
462
+ it('an absent index generator sibling → a loud instruct (ok:false)', () => {
463
+ const r = defaultRegenerateIndex('/tmp/anyroot', '2026-01-02', { sibling: '/nonexistent/check-docs-size.mjs' });
464
+ assert.equal(r.ok, false);
465
+ assert.match(r.detail, /not beside this script/);
466
+ assert.match(r.detail, /check-docs-size\.mjs --write-index/);
467
+ });
468
+
469
+ it('a regeneration subprocess that exits nonzero → a loud instruct (ok:false)', () => {
470
+ const r = defaultRegenerateIndex('/tmp/anyroot', '2026-01-02', { existsSync: () => true, spawnSync: () => ({ status: 1 }) });
471
+ assert.equal(r.ok, false);
472
+ assert.match(r.detail, /index regeneration failed/);
473
+ assert.match(r.detail, /check-docs-size\.mjs --write-index/);
474
+ });
475
+
476
+ it('a successful subprocess (status 0) → ok:true with the trimmed stdout', () => {
477
+ const r = defaultRegenerateIndex('/tmp/anyroot', '2026-01-02', { existsSync: () => true, spawnSync: () => ({ status: 0, stdout: 'Wrote docs/ai/index.md\n' }) });
478
+ assert.equal(r.ok, true);
479
+ assert.match(r.detail, /Wrote docs\/ai\/index\.md/);
480
+ });
481
+ });
482
+ });