@sabaiway/agent-workflow-kit 5.5.0 → 5.7.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/README.md +1 -1
  3. package/SKILL.md +1 -1
  4. package/capability.json +1 -1
  5. package/package.json +1 -1
  6. package/references/hooks/gate-approve.mjs +7 -1
  7. package/references/modes/doc-parity.md +1 -1
  8. package/references/modes/gates.md +20 -4
  9. package/references/modes/procedures.md +2 -0
  10. package/references/modes/recommendations.md +4 -1
  11. package/references/modes/review-state.md +1 -1
  12. package/references/modes/setup.md +18 -2
  13. package/references/modes/upgrade.md +38 -18
  14. package/references/modes/velocity.md +1 -0
  15. package/references/scripts/migrate-gates-branches.test.mjs +146 -1
  16. package/references/scripts/migrate-gates.mjs +295 -60
  17. package/references/scripts/migrate-gates.test.mjs +206 -14
  18. package/references/shared/deploy-tail.md +1 -1
  19. package/references/templates/gates.json +1 -1
  20. package/tools/ack-write.mjs +20 -11
  21. package/tools/atomic-write.mjs +71 -18
  22. package/tools/checker-claim.mjs +100 -0
  23. package/tools/coverage-producer.mjs +43 -6
  24. package/tools/direct-run.mjs +76 -0
  25. package/tools/doc-parity.mjs +34 -3
  26. package/tools/engine-source.mjs +12 -8
  27. package/tools/ensure-configs.mjs +141 -0
  28. package/tools/ensure-ops.mjs +284 -0
  29. package/tools/ensure-vocabulary.mjs +71 -0
  30. package/tools/flow-check-cores.mjs +253 -0
  31. package/tools/flow-check-git-lane.mjs +56 -0
  32. package/tools/flow-check-rungs.mjs +330 -0
  33. package/tools/flow-check.mjs +23 -611
  34. package/tools/gates-declaration.mjs +36 -11
  35. package/tools/gates-init.mjs +140 -25
  36. package/tools/hide-footprint.mjs +21 -3
  37. package/tools/lens-region.mjs +74 -23
  38. package/tools/orchestration-config.mjs +5 -3
  39. package/tools/orchestration-write.mjs +7 -0
  40. package/tools/procedures.mjs +64 -5
  41. package/tools/recommendations.mjs +384 -34
  42. package/tools/refresh-parity.mjs +263 -0
  43. package/tools/run-gates.mjs +8 -5
  44. package/tools/setup-backends.mjs +88 -77
  45. package/tools/source-size-check.mjs +310 -0
  46. package/tools/source-size-config.mjs +244 -0
  47. package/tools/source-size-core.mjs +59 -0
  48. package/tools/source-size-gate-cmd.mjs +27 -0
  49. package/tools/source-size-judge.mjs +114 -0
  50. package/tools/source-size-refusal.mjs +70 -0
  51. package/tools/source-size-report.mjs +254 -0
  52. package/tools/source-size-scope.mjs +145 -0
  53. package/tools/tracked-tree-census.mjs +102 -0
  54. package/tools/upgrade-runlist.mjs +92 -0
  55. package/tools/velocity-profile.mjs +24 -3
package/CHANGELOG.md CHANGED
@@ -4,6 +4,128 @@ 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
+ ## 5.7.0 — what the kit emits is now something a test executed, a claim it proved, or a command you can run (AD-092 + AD-093 + AD-094; memory 4.3.0)
8
+
9
+ **Three feedback plans, one release.** A live upgrade session on a deployed TypeScript project
10
+ returned seven items of feedback, and every one reduced to the same shape: the kit EMITTED something
11
+ — a shell command, a status line, a prescribed operation, an optimality verdict — and nothing
12
+ downstream ever ran it, proved it, or performed it. This release closes that shape end to end, and
13
+ closes it for future releases too: a kit candidate can no longer be dispatched without proving
14
+ itself against the kit that is ALREADY published.
15
+
16
+ - **The canonical coverage destination refuses by name instead of writing to the filesystem root.**
17
+ The emitted cmd becomes `"${AW_GIT_DIR:?exported by run-gates}/agent-workflow-lcov.info"`:
18
+ byte-identical where the runner injects the variable, a loud named bash refusal where a human
19
+ pastes it into a bare shell — the old form expanded an unset variable to empty and sent the lcov
20
+ to `/`. Recognition is APPEND-ONLY (`KNOWN_COVERAGE_FLAG_SETS`): every form the kit ever emitted
21
+ stays recognized, so a deployed old-form gate is never reclassified as customized. `run-gates` now
22
+ reads `${VAR:?…}` as a producer reference and refuses before spawning uninjected. And the emitted
23
+ command is a FIXTURE the tests execute — asserted by running it, unset and injected both, never by
24
+ admiring the string.
25
+ - **"Flow optimal" became a claim the advisor must be able to SEE.** Binary probes gained NAMED third
26
+ outcomes: `gates-inert.producer-unrecognized` (a checker no recognized producer feeds, where the
27
+ tracked-tree census says most of the tree is outside the assessable domain — the remedy is "mark
28
+ the real producer or drop the checker", never a `node --test` prescription) and
29
+ `gates-inert.coverage-domain-narrow` (a live producer certifying an assessable minority — converges
30
+ only on an explicit acknowledgment, fingerprint-bound to the fact, re-firing when the fact
31
+ changes). The census is the predicate: no census, no optimality claim. A vendored copy of a core
32
+ check no longer stops the upgrade: the migration preserves it as a zero-diff `keep` row plus
33
+ separately rendered verify metadata, and only a genuine id collision on the canonical checker id
34
+ still hard-stops. On the advisor side, the source-size tool copy gets its own named outcomes —
35
+ `source-size.adopted-elsewhere` (optional) and `source-size.id-squatter` (attention, a
36
+ hand-apply recovery, never a stop).
37
+ - **The `lcovProducer` marker lets a declaration CLAIM a producer the closed cmd-world cannot name**
38
+ — vitest, pnpm wrappers, any runner that writes the lcov. Only the literal `true` counts, the
39
+ marker never widens what a RUN may certify (a marker-claimed producer that writes no lcov still
40
+ ends `skipped-no-lcov`), and it is FORWARD-ONLY by decision: the published 5.6.0 kit rejects a
41
+ marker-carrying `gates.json` loudly at validation (exit 5, naming the key) rather than degrading
42
+ to a false advisory — upgrade the kit before marking (Issue-016 states all three cross-version
43
+ axes).
44
+ - **Upgrade step 3 is a registry-owned run-list, not prose.** `tools/upgrade-runlist.mjs` owns the
45
+ seven operation identities (`pointers` · `footprint` · `configs` · `gates-migration` · `bridges` ·
46
+ `lens` · `bridge-settings`); the checklist in `references/modes/upgrade.md` renders from it, a
47
+ structure test holds doc ↔ registry in both directions, and the four config "ensures" the doc used
48
+ to prescribe as prose are ONE runnable command — `ensure-configs.mjs --reconcile [--dry-run]`,
49
+ fixed order, create-only seeds, a CLOSED outcome vocabulary that throws on any token outside it.
50
+ A library module invoked as a command now says so and exits 2 instead of silently exiting 0.
51
+ - **A tool-composed line a user reads is user-grade language.** Machine tokens and tool self-labels
52
+ ride machine lines (`[tool] key=value`); alarm words render only under a DETECTED abnormal
53
+ condition; the read-only refresh degrade states only what its re-scan PROVED (one scanner, closed
54
+ `clean-parity`/`drifted`/`unverifiable` vocabulary) instead of an unconditional "may be PARTIALLY
55
+ updated". The guard test enumerates every composer's outcome variants from its own closed branch
56
+ set — a new line joins the contract by construction. The hide-footprint report now states its
57
+ delta against the current managed block: `+N added` / `−N removed`, sets listed, `+0/−0` explicit.
58
+ - **A kit-carrying dispatch needs TWO receipts, dry-run included.** Beside the candidate smoke
59
+ (packs the candidate, installs it into a foreign fixture, asserts the advisor sees what this
60
+ repo's own suite cannot show), `scripts/release/cross-version-gate.mjs` installs the PUBLISHED kit
61
+ `@latest` and asserts the three Issue-016 axes BY NAME — `schema-accept` / `execution` /
62
+ `producer-recognition` — with the conditional arms decided by comparing the probed published
63
+ version against `MARKER_AWARE_SINCE` (fixed forever at `5.7.0`, the first marker-aware kit), never
64
+ inferred from the accept itself. Both receipts are HEAD-bound and field-validated; an unreachable
65
+ registry refuses loudly with no receipt.
66
+
67
+ ## 5.6.0 — a source-size practice your project declares, and a record that is debt rather than permission (AD-091; engine 2.1.0)
68
+
69
+ **A big module is expensive long before anyone calls it a problem, and nothing in the kit ever said
70
+ so.** This release adds the practice: a checker your project declares scope for, a cap it can hold
71
+ you to, and — for the files already over it — a recorded baseline that can shrink for free but can
72
+ only GROW with a written reason. Nothing is guessed and nothing is imposed: a project that declares
73
+ no scope gets no cap, and the one manual step is a scope file you author from the starter template
74
+ the tool prints — its `roots`/`extensions` placeholders are deliberately invalid until you replace
75
+ them. With a valid, judgeable scope, `--adopt` then mints the record and attempts the gate
76
+ declaration, reporting the second half if it is refused.
77
+
78
+ - **`source-size-check.mjs` — the checker, and it refuses rather than assumes.** In scope is what
79
+ you DECLARE: git-tracked files under your roots, with your extensions, minus your excluded
80
+ prefixes. New files stay within 400 lines and 1000 bytes per line by default (both overridable,
81
+ because a multi-KB single line hides from a line count). There is no default root list, no default
82
+ file-type list and no content sniffing — a fixed extension list would silently exempt every
83
+ language it forgot. Run it with no config and it tells you the exact path to author and prints a
84
+ template; the placeholders in that template are rejected by the validator until you replace them,
85
+ so the printed starting point can never be pasted into a scope that matches nothing and passes.
86
+ - **A recorded size is DEBT, not permission — that is the whole design.** Adopting on a real
87
+ codebase records today's oversized files instead of demanding a refactor you did not plan. From
88
+ then on the record only moves under rules: it may not grow without `--reason "<text>"` (recorded
89
+ verbatim in the entry it raised), it may not sit ABOVE what the tree now measures (a stale record
90
+ is headroom nobody earned, so the checker tells you to tighten it), and it disappears when the
91
+ file finally comes back under the cap. A record whose file is GONE is an error — that is what
92
+ makes a split or a rename visible rather than silent. **Every declared root also carries a line
93
+ budget**, so splitting 3000 lines across six modules buys exactly zero headroom.
94
+ - **Every ratchet refusal hands you the next step, and it is a step this build can actually
95
+ perform.** Shrunk below the record? You get the regenerator command exactly as it should be pasted
96
+ — no reason asked, because shrinking is progress. Growing something? You get the same command as a
97
+ template with the reason placeholder and a plain statement that it is required: the tool cannot
98
+ invent your reason. On a project path that would not survive quoting, no command is printed at all
99
+ — the file, the actual, the allowed and the manual lane are stated instead, because a rendered
100
+ command that runs somewhere else is worse than none. Inputs the checker cannot judge at all — a
101
+ malformed config, a failed git enumeration — exit 2 stating what is unusable, with no rendered
102
+ recovery command.
103
+ - **Adoption is one consented line.** `--adopt --reason "<text>"` records the baseline AND declares
104
+ the gate in `docs/ai/gates.json` in a single step, is idempotent once adopted, and reports exactly
105
+ what it did and did not do if the declaration is refused. Existing projects meet it as a
106
+ Recommendations item at upgrade; new ones meet it the same way on their first upgrade.
107
+ - **The practice arrives BEFORE the code, not after it.** The plan-authoring and plan-execution
108
+ procedure renders now print your declared caps, your recorded debt and the reason the caps exist;
109
+ the checker's green line says the same thing on the enforced path. The point is a layout decided
110
+ while a plan is written, with the gate as the backstop rather than the teacher. A project that
111
+ declares no practice gets no invented
112
+ limits: the render's declared-practice block stays empty and `gates-init` offers no candidate. The
113
+ canon rule it does still print is conditional by construction — no declared cap, no limit to
114
+ state.
115
+ - **The gate declaration learned where to place things.** A consented non-checker gate is now written
116
+ BEFORE a trailing canonical coverage checker instead of after it, which is what makes adopting on a
117
+ final-capable declaration work at all; entries you already declared are never reordered. The fill's
118
+ result gained `placed`, and `appended` remains as an alias of it.
119
+ - **Scope, counting and the gate matcher live in a pure read core**, so the surfaces that ask about
120
+ the practice — the advisor, the gate declaration, the procedure render — never reach the writer.
121
+ Counting is pinned by literal fixtures: LF, and the CR of a CRLF, never count; a file with no final
122
+ newline still counts its last line.
123
+ - **Housekeeping shipped alongside:** `flow-check.mjs` became a 254-line facade over two pure
124
+ decision modules and a git-I/O leaf (the decision cores and the evidence rungs, plus the git lane
125
+ that is deliberately the only one allowed to spawn `git`) with its behaviour and its declared gate
126
+ command unchanged — the first tranche of the cleanup this practice arms, and the first proof that
127
+ the record notices a split.
128
+
7
129
  ## 5.5.0 — delegating a sub-task stops being a feeling and becomes a record (AD-090; codex-cli-bridge 3.5.0, antigravity-cli-bridge 5.1.1)
8
130
 
9
131
  **"How much does handing this off actually buy?" had no answer, because nothing wrote the answer
package/README.md CHANGED
@@ -229,7 +229,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
229
229
  | `/agent-workflow-kit upgrade` | existing deployment | reads `docs/ai/.workflow-version`, shows the changelog diff, preserves your authored memory, applies migrations, re-stamps — then prints a **read-only** one-line backend-status line (what's set up vs missing); refreshes the already-placed bridges from the kit's bundled copies (never installs a new one — set one up with `/agent-workflow-kit setup`) |
230
230
  | `/agent-workflow-kit help` | any time | **read-only command index** — every command, grouped (Inspect / Configure / Orchestrate / Lifecycle) and tagged read-only / writer / guarded. The discoverable entry point, and where any unrecognized invocation lands (always read-only — a garbage invocation never writes). Never writes, never commits, never runs a subscription CLI. |
231
231
  | `/agent-workflow-kit backends` | any time | **read-only** check of the optional execution-backends (the `codex` / `agy` bridges): what's set up vs missing and the next step. Never writes, never commits, never runs a subscription CLI (credentials = marker-file presence, not a live login). |
232
- | `/agent-workflow-kit gates` | any time | **project gate runner** — runs the verification commands **your project itself declares** in `docs/ai/gates.json` (seeded at deploy; hand-editable `{ id, title, cmd }` entries, each `cmd` one bash line) as one batch: a per-gate **PASS/FAIL table** + one machine-readable summary line, exit 0 iff all green; a failing gate's own output is shown verbatim; `--only <id>` re-runs one. Honest distinct outcomes for a missing / empty / malformed declaration — never a silent green. The runner writes nothing **by default** and never commits — opt-in `--final` runs the FULL declared matrix and mints ONE final-run receipt into the git-dir core-evidence store (status green/red, pre/post tree fingerprints, the declaration, the consumed lcov's sha; the receipt the commit-guard binds); it executes only your own declared commands (a batching convenience, not a sandbox). The velocity tier auto-approves only the exact no-`--final` form — the recording run stays explicit. |
232
+ | `/agent-workflow-kit gates` | any time | **project gate runner** — runs the verification commands **your project itself declares** in `docs/ai/gates.json` (seeded at deploy; hand-editable `{ id, title, cmd }` entries plus the optional boolean `lcovProducer`, each `cmd` one bash line) as one batch: a per-gate **PASS/FAIL table** + one machine-readable summary line, exit 0 iff all green; a failing gate's own output is shown verbatim; `--only <id>` re-runs one. Honest distinct outcomes for a missing / empty / malformed declaration — never a silent green. The runner writes nothing **by default** and never commits — opt-in `--final` runs the FULL declared matrix and mints ONE final-run receipt into the git-dir core-evidence store (status green/red, pre/post tree fingerprints, the declaration, the consumed lcov's sha; the receipt the commit-guard binds); it executes only your own declared commands (a batching convenience, not a sandbox). The velocity tier auto-approves only the exact no-`--final` form — the recording run stays explicit. |
233
233
  | `/agent-workflow-kit setup [backend]` | opt-in, any time | **link-only** auto-setup of a bridge: places the bundled bridge skill (only into an absent / empty / managed dir — never overwrites an unmanaged one) + links its wrappers onto `PATH` via managed symlinks (idempotent; refuses to clobber a non-symlink; try `--dry-run` to preview). The binary install + the one-time subscription login stay **manual**: it prints the exact **login** command and points the binary install at each bridge's `setup/README.md`. POSIX wrappers — on Windows use WSL. Never commits, never runs a subscription CLI. |
234
234
  | `/agent-workflow-kit status` | any time | **read-only** single view of **versions + deployment + settings + bridges**: which members (kit / memory / engine / the two bridges) are installed and at what version (with an honest "installed on this machine" note when one is behind) and — in a project — what's deployed (`docs/ai`, the version stamps, and the **visibility**: visible / hidden / unclear), plus your settings (orchestration recipes, attribution, velocity) and the bridges' readiness. The two version axes (package number vs deployment-structure head) stay decoupled. Never writes, never commits, never runs a subscription CLI. |
235
235
  | `/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. |
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: '5.5.0'
6
+ version: '5.7.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
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": "5.5.0",
6
+ "version": "5.7.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": "5.5.0",
3
+ "version": "5.7.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",
@@ -175,6 +175,11 @@ const WHITESPACE_PATTERN = /\s+/u;
175
175
  const GATE_ID_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
176
176
  const GATE_CMD_NEWLINE_PATTERN = /[\r\n]/u;
177
177
  const GATE_KEYS = Object.freeze(['id', 'title', 'cmd']);
178
+ // The ONE optional gate key (the coverage-producer marker), accepted here for the same reason every
179
+ // other shape rule is restated: a declaration the RUNNER accepts must never be rejected here, or a
180
+ // marker-carrying project would silently lose auto-approval on gates the runner runs happily.
181
+ const LCOV_PRODUCER_KEY = 'lcovProducer';
182
+ const ALLOWED_GATE_KEYS = Object.freeze([...GATE_KEYS, LCOV_PRODUCER_KEY]);
178
183
  const README_KEY = '_README';
179
184
  const GATES_KEY = 'gates';
180
185
 
@@ -196,11 +201,12 @@ export const validateDeclarationShape = (parsed) => {
196
201
  for (const gate of parsed[GATES_KEY]) {
197
202
  if (gate === null || typeof gate !== 'object' || Array.isArray(gate)) return invalid;
198
203
  for (const key of Object.keys(gate)) {
199
- if (!GATE_KEYS.includes(key)) return invalid;
204
+ if (!ALLOWED_GATE_KEYS.includes(key)) return invalid;
200
205
  }
201
206
  for (const key of GATE_KEYS) {
202
207
  if (typeof gate[key] !== 'string' || gate[key].trim() === '') return invalid;
203
208
  }
209
+ if (gate[LCOV_PRODUCER_KEY] !== undefined && typeof gate[LCOV_PRODUCER_KEY] !== 'boolean') return invalid;
204
210
  if (GATE_CMD_NEWLINE_PATTERN.test(gate.cmd)) return invalid;
205
211
  if (!GATE_ID_PATTERN.test(gate.id)) return invalid;
206
212
  if (seenIds.has(gate.id)) return invalid;
@@ -13,7 +13,7 @@ The DOC-PARITY lint (BUGFREE-3 / AD-049, session-economics item (b)) — the det
13
13
  - `references/modes/autonomy-doctor.md` carries the doctor's frozen D7 contract (AD-044 Plan 2): every live EXIT-table phrase (`` `0` ready `` … `` `6` unsupported / untrusted ``), every status token (sourced from the exported `STATUS`), and the trusted-dir allowlist (`TRUSTED_DIRS`);
14
14
  - `references/modes/recommendations.md` + `references/modes/upgrade.md` carry the frozen upgrade-Recommendations presentation contract (the section header, the exact empty-state line, the verdict templates);
15
15
  - `references/modes/recommendations.md` + `references/modes/velocity.md` carry the family-owned ack-store path (`docs/ai/acks.json`);
16
- - `references/modes/setup.md` + `references/modes/upgrade.md` carry the `skipped-readonly` refresh degrade token.
16
+ - `references/modes/setup.md` + `references/modes/upgrade.md` carry the `skipped-readonly` refresh degrade token **and all three parity verdicts that outcome reports** — `clean-parity`, `drifted`, `unverifiable` (one binding per value, backticked so a bare word in prose cannot pass for the token).
17
17
 
18
18
  **Why the modes/*.md docs and NOT the tool HELP strings.** Every tool's HELP INTERPOLATES the same constant, 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).
19
19
 
@@ -2,12 +2,13 @@
2
2
 
3
3
  <!-- opt-in-capability: gates-declaration -->
4
4
  <!-- opt-in-capability: gates-verification -->
5
+ <!-- opt-in-capability: source-size -->
5
6
 
6
7
  The **generic project gate runner** — it batches the project's OWN declared verification commands into one run. The runner itself **writes nothing on a plain run, never commits, and never runs a subscription CLI**; what it EXECUTES is the project's own declaration, with the caller's privileges (trust posture: a batching convenience over commands the project already runs by hand — **not a sandbox**). Two modes write state: **`--final`** runs the FULL declared matrix as the D3(a) final verification run and mints the receipt the commit guard consumes (step 4), and an ARMED **`--pre-review`** records its subset attempt in the flow store (step 3; unarmed repos byte-unchanged).
7
8
 
8
9
  Run `node ${CLAUDE_SKILL_DIR}/tools/run-gates.mjs [--cwd <project>] [--only <id>]… [--final]`:
9
10
 
10
- 1. **Reads `docs/ai/gates.json`** (strict JSON, hand-editable; seeded from `references/templates/gates.json`). Each gate is `{ id, title, cmd }` — `id` a unique kebab handle, `cmd` **ONE bash command line** (brace/glob expansion works; a host without bash gets a loud preflight error, exit 6 — never a silent reinterpretation under another shell). The declaration names **WHAT to check, never who executes it** — the schema has no lane/model/routing fields and rejects unknown keys loudly.
11
+ 1. **Reads `docs/ai/gates.json`** (strict JSON, hand-editable; seeded from `references/templates/gates.json`). Each gate is `{ id, title, cmd }` plus the optional boolean **`lcovProducer`** (the producer paragraph below) — `id` a unique kebab handle, `cmd` **ONE bash command line** (brace/glob expansion works; a host without bash gets a loud preflight error, exit 6 — never a silent reinterpretation under another shell). The declaration names **WHAT to check, never who executes it** — the schema has no lane/model/routing fields and rejects unknown keys loudly.
11
12
  2. **Runs each gate from the project root** and prints a per-gate **PASS/FAIL table** plus **one machine-readable summary line** as the last line (`[run-gates] status=… gates=… passed=… failed=… failed_ids=… coverage=…`). A failing gate's own output is preserved **verbatim** (triage without re-running); a green gate's output is not echoed; gates after a failure still run. **Exit 0 iff all selected gates are green.**
12
13
  3. **Honest outcomes, each distinct — never a silent green:** a **missing** declaration (exit 3 — the report names the recovery: create `docs/ai/gates.json` from the template; `upgrade` re-seeds a missing one), an **empty** `gates` list (exit 4), a **malformed/invalid** declaration (exit 5, loud `path: reason`). Repeatable **`--only <id>`** re-runs a subset; an unknown id is a loud usage error (exit 2). **`--pre-review`** runs the DERIVED mechanical subset (#66): the full matrix minus every gate whose cmd is a canonical kit checker invocation — derivation **matches canonical checker paths in the cmd strings** (realpath-resolved `--check` forms of review-state / commit-guard / coverage-check / flow-check, never a project-authored id), so a project abstracting a checker behind its own script declares it in `flow.pregateExclude` (an unknown id refuses loudly, exit 5). A failing subset gate gets the review-dependent diagnosis, naming the mechanical reset (a declared exclude changes the `subsetDigest`). **Under an ARMED flow (exactly one open adopted chain owned by this worktree) every subset run is RECORDED** as a `subset-attempt` via the flow store's locked append factory — the context keys `{planId, cycle, stepId, foldBatch, subsetDigest}`; index + hard-stop state are computed under the lock against the pre-run identity. **Hard stop (Decision 7/8):** the SECOND red records and exits red; past two reds every attempt needs `--diagnosis "<non-empty, byte-distinct from the prior>"` (recorded, self-servable); the THIRD red EXHAUSTS the context — further solo runs refuse, and only a recorded fresh-eyes consult verdict (a grounded bridge consult-attestation at this round context) reopens ONE further attempt. Armed-but-unrecordable (zero/several open chains, broken store) refuses loudly; a spawn failure records NO attempt; unarmed repos stay byte-unchanged. Mutually exclusive with `--only`/`--final` (exit 2); plain and `--final` runs never load the config.
13
14
  4. **`--final`** — the D3(a) final verification run: it REFUSES `--only` (a subset never attests) and a declaration lacking the canonical core checks (ONE plain invocation each of the kit's OWN `review-state.mjs --check` and `coverage-check.mjs --check`, the checker declared LAST — a masked form, a compound, or a lookalike path never counts); deletes the stale git-dir lcov before the suite; exports `AW_GIT_DIR` + `AW_LCOV_FILE` to every gate cmd; records EVERY attempt (start + completed green/red) in the core-evidence store via its sole writer; and binds the receipt to { fingerprint before/after · the full declaration · per-gate results · the canonical red-proof + degrade evidence hashes · the sha of the lcov the checker actually read (exactly ONE `lcov-sha256` machine line, end-re-hashed) · **`evidenceHashes.flow`** when a flow store exists (D10: the sha of the OWNER-SCOPED flow projection — foreign worktrees never move it, except same-fingerprint planId-less globals, which share this tree's decision context; absent store → absent field; a broken store refuses up front) }. An artifact moving UNDER the run — the flow projection included — is a named `integrityFailure`; the receipt lands red. Stated residual: the movement arm is best-effort — an append racing the receipt write is refused at commit by the guard. A receipt that cannot be written is its own distinct outcome (exit 8): green gates never read as success without it. `${CLAUDE_SKILL_DIR}/references/modes/commit-guard.md` consumes the receipt at commit time (the guard re-hashes the live projection against it — a post-final append, or the store vanishing, refuses the commit).
@@ -20,10 +21,25 @@ Declared gates can also be **auto-approved** (no permission prompt on a byte-exa
20
21
 
21
22
  **Candidate line — the review-receipt gate (opt-in, never auto-seeded; AD-021).** Projects that configure a reviewed/council `plan-execution.review` recipe can declare the review-state check as one more gate — the exact candidate `{ id, title, cmd }` line and its contract live under `${CLAUDE_SKILL_DIR}/references/modes/review-state.md` (step 3).
22
23
 
23
- **Consent-gated filling — the init preview, not part of the runner (D9).** The template `gates.json` is seeded EMPTY; FILLING it is a consented preview at init (`node ${CLAUDE_SKILL_DIR}/tools/gates-init.mjs --cwd <project>`, dry-run by default prints the derived entries and **writes NOTHING**; `--apply [--only <id>]…` appends exactly the consented entries on your explicit yes; append-only, id collisions refused). The offer derivation is **closed-world** (AD-052): only a terminating-class script NAME (test / lint / type-check / build — never dev/watch/serve, never a write-mode or release/publish/deploy variant) whose BODY is a member of the literal runner allowlist is offered membership, never blocklist screening: the worst case is a legit command not offered, never a dangerous one offered. The offered cmd is the uniform hook-free **`COREPACK_ENABLE_NETWORK=0 <pm> exec -- <allowlisted-body>`** — `exec` runs a command, not a named script, so no pre/post hook can fire (npm/pnpm/yarn alike; never `<pm> run <name>`, which re-exposes hooks), and the Corepack env prefix blocks a hostile `packageManager` pin from fetching the PM binary before exec. npm is pinned `--offline --script-shell /bin/sh`; pnpm/yarn refuse an absent runner without network (a user-installed cache/global/PATH runner executing is user machine state — part of the disclosed residual); a family without a verified fail-closed exec contract is WITHHELD loudly. A screened-out body is always named — and when nothing but kit checkers remains, the preview says so in plain words: the offer carries **no project-verification gate at all**. **Disclose before the yes** (the preview prints it): gates.json is a PRIVILEGED file the wired hook auto-approves byte-exact declared commandsand a script gate runs project-controlled tooling the preview does not sandbox (safe-by-construction = the OFFER DERIVATION). At upgrade the only gates.json writer is the consented legacy migration (`${CLAUDE_SKILL_DIR}/references/modes/upgrade.md`).
24
+ **Candidate line — the source-size gate (opt-in, never auto-seeded).** A project declaring the source-size practice (`docs/ai/source-size.json`) can declare its checker as one more gate, offered **only over a MINTED practice** an absent config has nothing to offer, an authored-but-unminted one is withheld by name rather than declaring a gate that would refuse. Adoption is ONE consented verb, `node ${CLAUDE_SKILL_DIR}/tools/source-size-check.mjs --adopt --reason "<text>" [--cwd <project>]`: it mints the record and declares this gate and NOTHING else, is idempotent once adopted, and with no config refuses with the exact file to author (the practice's one manual step). The advisor surfaces it as the `source-size` item, reading a declared cmd as one of THREE claims rather than a yes/no **canonical** (this copy answered), **tool-elsewhere** (the same shape resolving to a DIFFERENT real copy a vendored deployment, converging on a `source-size-copy` acknowledgement, never on `--adopt`, which would collide on the declared id), **not-the-tool** (a gate merely CARRYING the id is a squatter: rename it, or repoint its cmd at this `--check` form) in that precedence. An unminted record stays the same attention outcome for either copy, and no acknowledgement silences it.
24
25
 
25
- **The coverage PRODUCER and the canonical checker are declared together or not at all.** `coverage-check` READS an lcov; something must WRITE it, and a checker with no producer PASSES (`skipped-no-lcov`) certifying nothing. A **producer** is a CLOSED set of full command forms, never a substring probe: the suite body `node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination="$AW_GIT_DIR/agent-workflow-lcov.info" --test-reporter=spec --test-reporter-destination=stdout` (optionally + the project's own test paths), bare or behind ONE verified per-PM `exec` wrapper. A cmd that merely MENTIONS the destination (in an `echo`, as a bare substring) or carries a PARTIAL flag set is **not** one. The tail must be path-shaped path/glob/quoting bytes only, nothing that could sequence, redirect or substitute a command, no plain leading `-`. **Residual:** the screen judges SOURCE bytes brace *sequence* expansion can produce others (`{Y..a}`), though bash never re-scans an expansion result as syntax, so such a byte is literal argument data, not an operator; the leading-`-` rule is first-order only (`'--flag'`, `{path,--flag}` pass); neither proves the lcov SURVIVES "producer" means *configured with the reporters*, and a run producing none is caught at runtime as `skipped-no-lcov`. The destination rides `AW_GIT_DIR`, exported to every gate child on plain and `--final` runs alike (`AW_LCOV_FILE` is `--final`-only), so one cmd survives the unmet-producer preflight in both modes. On BOTH declaration paths: the fill preview WIRES those reporters onto a `node --test` body the one allowlist member producing lcov unaided, every other body is emitted unchanged WITHHOLDS the `coverage-check` candidate, with a named note, when neither the offer nor the declaration carries a producer; and `--apply` REFUSES a checker with no producer, a checker that is not LAST (an ORDERING refusalreorder by hand; the fill is append-only), or a SECOND canonical checker. The legacy migration likewise never ADDS the checker into a producer-less declaration, and reports an already-declared one as INERT. A producer declared AFTER the checker leaves it just as inert (it reads nothing, or stale bytes) ORDER is the rule. An already-declared inert pair, and a matrix of nothing but kit checkers, are surfaced by the advisor's `gates-inert` item (`${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`); its cause-A remedy is HAND-APPLY because the fill cannot reorder.
26
+ **Consent-gated filling the init preview, not part of the runner (D9).** The template `gates.json` is seeded EMPTY; FILLING it is a consented preview at init (`node ${CLAUDE_SKILL_DIR}/tools/gates-init.mjs --cwd <project>`, dry-run by default prints the derived entries and **writes NOTHING**; `--apply [--only <id>]…` writes exactly the consented entries on your explicit yes; add-only, id collisions refused). **Placement, not blind appending:** a consented entry lands at the END, except that a non-checker entry goes **BEFORE** a trailing canonical coverage checker that checker must stay last, so appending after it would write a declaration the validator refuses, leaving a hand edit as the only lane on any final-capable declaration. Existing entries are never modified, removed or reordered. The offer derivation is **closed-world** (AD-052): only a terminating-class script NAME (test / lint / type-check / build never dev/watch/serve, never a write-mode or release/publish/deploy variant) whose BODY is a member of the literal runner allowlist is offeredmembership, never blocklist screening: the worst case is a legit command not offered, never a dangerous one offered. The offered cmd is the uniform hook-free **`COREPACK_ENABLE_NETWORK=0 <pm> exec -- <allowlisted-body>`** `exec` runs a command, not a named script, so no pre/post hook can fire (npm/pnpm/yarn alike; never `<pm> run <name>`, which re-exposes hooks), and the Corepack env prefix blocks a hostile `packageManager` pin from fetching the PM binary before exec. npm is pinned `--offline --script-shell /bin/sh`; pnpm/yarn refuse an absent runner without network (a user-installed cache/global/PATH runner executing is user machine state part of the disclosed residual); a family without a verified fail-closed exec contract is WITHHELD loudly. A screened-out body is always namedand when nothing but kit checkers remains, the preview says so in plain words: the offer carries **no project-verification gate at all**. **Disclose before the yes** (the preview prints it): gates.json is a PRIVILEGED file the wired hook auto-approves byte-exact declared commands and a script gate runs project-controlled tooling the preview does not sandbox (safe-by-construction = the OFFER DERIVATION). At upgrade the only gates.json writer is the consented legacy migration (`${CLAUDE_SKILL_DIR}/references/modes/upgrade.md`).
27
+
28
+ **The coverage PRODUCER and the canonical checker are declared together or not at all.** `coverage-check` READS an lcov; something must WRITE it, and a checker with no producer PASSES (`skipped-no-lcov`) certifying nothing. A **producer** is a CLOSED set of full command forms, never a substring probe: the suite body `node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination="${AW_GIT_DIR:?exported by run-gates}/agent-workflow-lcov.info" --test-reporter=spec --test-reporter-destination=stdout` (optionally + the project's own test paths), bare or behind ONE verified per-PM `exec` wrapper. A cmd that merely MENTIONS the destination (in an `echo`, as a bare substring) or carries a PARTIAL flag set is **not** one. The tail must be path-shaped — path/glob/quoting bytes only, nothing that could sequence, redirect or substitute a command, no plain leading `-`. **Residual:** the screen judges SOURCE bytes — brace *sequence* expansion can produce others (`{Y..a}`), though bash never re-scans an expansion result as syntax, so such a byte is literal argument data, not an operator; the leading-`-` rule is first-order only (`'--flag'`, `{path,--flag}` pass); neither proves the lcov SURVIVES — "producer" means *configured with the reporters*, and a run producing none is caught at runtime as `skipped-no-lcov`. The destination rides `AW_GIT_DIR`, exported to every gate child on plain and `--final` runs alike (`AW_LCOV_FILE` is `--final`-only), so one cmd survives the unmet-producer preflight in both modes; the `:?` is the required-parameter form, so a hand-run with `AW_GIT_DIR` unset or empty fails naming it instead of writing the lcov to the filesystem ROOT (a STALE exported value is not caught), and the preflight counts it as a reference (a `${VAR:-…}` fallback stays unmatched). Recognition is APPEND-ONLY — every destination form the kit has emitted stays a producer, so an earlier kit's declaration never re-reads as customized; emission uses the newest alone. Recognition never WIDENS: a suite the closed set cannot express declares itself with **`"lcovProducer": true`** — the ONE other way to be a producer. Only the literal `true` claims (a string is a loud refusal); the claim binds the DECLARATION, never the run (a marked gate producing no lcov still ends `skipped-no-lcov` / `attested=no`); and it is FORWARD-ONLY — an older kit rejects a marker-carrying declaration by name (exit 5) instead of reading a claim it cannot honor. On BOTH declaration paths: the fill preview WIRES those reporters onto a `node --test` body — the one allowlist member producing lcov unaided, every other body is emitted unchanged — WITHHOLDS the `coverage-check` candidate, with a named note, when neither the offer nor the declaration carries a producer; and `--apply` REFUSES a checker with no producer, a checker that is not LAST (an ORDERING refusal about entries that are ALREADY declared — reorder by hand; the fill places new entries before a trailing checker but never reorders what it did not write), or a SECOND canonical checker. The legacy migration likewise never ADDS the checker into a producer-less declaration, and reports an already-declared one as INERT. A producer declared AFTER the checker leaves it just as inert (it reads nothing, or stale bytes) — ORDER is the rule. An already-declared inert pair, and a matrix of nothing but kit checkers, are surfaced by the advisor's `gates-inert` item (`${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`); its cause-A remedy is the fill preview when the checker is LAST and the project's own scripts yield an offerable producer (the placement rule lands it before the checker), and HAND-APPLY otherwise.
29
+
30
+ **The marker as it appears in a declaration** — the three required keys plus the boolean, nothing else:
31
+
32
+ ```json
33
+ { "gates": [
34
+ { "id": "suite", "title": "Test suite", "cmd": "vitest run --coverage", "lcovProducer": true },
35
+ { "id": "coverage-check", "title": "Coverage", "cmd": "node \"${CLAUDE_SKILL_DIR}/tools/coverage-check.mjs\" --check" }
36
+ ] }
37
+ ```
38
+
39
+ **A declared checker is read in THREE outcomes, not two.** `gates-inert.producer-unrecognized` — no producer anywhere in the declaration, over a tracked tree the changed-line domain cannot reach: the remedies are the marker above or dropping the checker, never a `node --test` prescription and never an acknowledgement (a dead pair is broken, not narrow). `gates-inert.coverage-domain-narrow` — the pair IS live and the tree is still dominated by what the domain excludes: certification covers the assessable minority, a FACT that converges on a `coverage-domain` acknowledgement. The census is the TRACKED tree (`git ls-files`) in this same closed vocabulary; dominance is STRICT (a tie, or a lone `.d.ts` beside real JS, never fires), and a tree it cannot read is a stated skip — no census, no optimality claim. A producer declared AFTER the checker is neither outcome: it keeps the ORDERING remedy above. Each outcome's convergence and posture note live in `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`.
40
+
41
+ **A marker-carrying declaration needs a CURRENT placed hook.** The gate-approval hook restates this schema in its own baked copy and goes dark on any key it does not know (`${CLAUDE_SKILL_DIR}/references/modes/hook.md`), so a declaration carrying the key under a hook that predates it silently switches auto-approval off — every gate prompting again, no error anywhere. The condition is the key's PRESENCE: an older hook rejects a key it does not know whatever the value, so `"lcovProducer": false` darkens it exactly as `true` does. `gate-hook --apply` places only an ABSENT target, so the recovery is remove-then-reseed by hand: `rm <project>/.claude/hooks/agent-workflow-gates.mjs`, then `node ${CLAUDE_SKILL_DIR}/tools/gate-hook.mjs --apply --cwd <project>`.
26
42
 
27
43
  **`coverage=` states what the run can honestly say about coverage.** A CLOSED four-value set, always present, DETAIL only — exit code, `status=`, the receipt status, `--final` acceptance and the commit-guard disposition are untouched. `coverage=certified` — the checker consumed an lcov and ISSUED a verdict, pass **or** fail (one listing uncovered lines still reads certified and still exits 1). `coverage=not-run` — the checker ran and issued NO verdict (no lcov bytes were read, or the run holds no attestation context). `coverage=none` — no canonical checker ran here (an `--only` subset, the `--pre-review` subset). `coverage=unknown` — the run ended before the gates produced a signal (missing / empty / malformed declaration, no bash, a pre-spend refusal), or that signal is unreadable: the checker could not spawn, or its two anchored lines are missing, duplicated, or CONTRADICTORY (`attested=yes` over `lcov-sha256=none` certifies nothing) — fail closed. The value is DERIVED by cross-reading both anchored machine lines, the bytes the `--final` receipt binds, so it can never disagree with what the checker printed; the checker's table row names a withheld verdict in the same words, and a `--final` receipt RECORDS the token (`none` never rides one — a final run always selects the checker).
28
44
 
29
- **Invariants:** the runner writes nothing on a plain run; `--final`'s ONE evidence write rides the core-evidence sole writer (the runner never opens the store itself) · an ARMED `--pre-review`'s ONE flow write rides the flow store's locked append factory (unarmed: byte-unchanged) · never commits · never runs a subscription CLI · executes only the project's OWN declared commands (never a kit-invented one) · the bash contract fails loud, never reinterprets · gates-init is a separate consent-per-run preview — append-only, never pre-approved by any velocity tier.
45
+ **Invariants:** the runner writes nothing on a plain run; `--final`'s ONE evidence write rides the core-evidence sole writer (the runner never opens the store itself) · an ARMED `--pre-review`'s ONE flow write rides the flow store's locked append factory (unarmed: byte-unchanged) · never commits · never runs a subscription CLI · executes only the project's OWN declared commands (never a kit-invented one) · the bash contract fails loud, never reinterprets · gates-init is a separate consent-per-run preview — add-only (it never modifies, removes or reorders an existing entry), never pre-approved by any velocity tier.
@@ -17,6 +17,8 @@ Run **`node ${CLAUDE_SKILL_DIR}/tools/procedures.mjs <activity> [--override <slo
17
17
 
18
18
  **Flow armed-halves block (session-start read side).** When the config carries a `flow` block, the advisor also renders `Flow (schema 1) — armed halves (config · chain · bookkeeping):` — the **config half** (preset · councilRounds · kitMinVersion), the **chain half** (a light read-only probe of the flow store on the checker's fixed path: ARMED at an adoption record, UNARMED for an absent or unadopted store, fail-closed BROKEN wording for a malformed one), and the **bookkeeping half** (each declared path: declared non-excluded — the tracked-file floor verifies on the `set-flow` arming path — vs loudly DECLARED-EXCLUDED). A config with no `flow` block renders byte-identically to before and pays no store probe.
19
19
 
20
+ **Declared source-size practice block (upfront knowledge, D-17 U1).** When the project declares a source-size practice in `docs/ai/source-size.json`, the advisor also renders `Declared source-size practice (docs/ai/source-size.json) — known BEFORE the code is written:` — the caps, what is recorded, the canonical WHY sentence and the plan-time rung (every Step that creates a file names the file and its single responsibility, and the planned layout fits the caps). `--json` carries the SAME lines under **`declaredPractice`** (an empty array when nothing is declared), so the human and structured renders never diverge. The four config states each read as themselves: **ABSENT** renders nothing at all (a project declaring no practice is handed no invented limits), **AUTHORED** and **INCOMPLETE** are pre-mint (the second names the half-written machine key), **MINTED** carries the recorded counts. A declaration that cannot be read (malformed, unknown-keyed, a dangling symlink) renders ONE loud `UNREADABLE` line and the advisor **still exits 0** — the practice's own checker owns the exit code for its config (exit 2 there) and its declared gate reds the gate matrix. Read through the practice's pure read core, so this advisor never reaches its writer half.
21
+
20
22
  **Cap-soft-skip degradation (the feature's only AUTO route).** The activity procedures are auto-discoverable only through the one-line **`workflow:methodology`** pointer (this kit + the engine carry `disable-model-invocation:true`, so NL like "write a plan" does **not** auto-load this skill). On a deployment whose methodology pointer was cap-soft-skipped — or whose pre-existing customized pointer lacks the procedures clause — the procedures are still reachable by **explicitly** invoking `/agent-workflow-kit procedures`; surface that plainly rather than treating it as a gap.
21
23
 
22
24
  **Invariants:** read-only · never writes · never commits · never runs a subscription CLI · the deterministic resolution is the kit's, the recipe execution is the orchestrator's.
@@ -27,8 +27,11 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
27
27
  - `read-lane` — enabling the opt-in read-only compound lane auto-approves *compounds* (and singles) of the seeded read-only core that carry ZERO shell metaprogramming: an UNATTENDED trust extension, bounded by the audited read-only core (never a command outside it; prompt-bypass only, never a sandbox bypass) and applied regardless of which of those core commands you seeded as individual settings rules. It is a PROJECT-PERSISTENT declaration in `docs/ai/lanes.json` — every future session, subagents' Bash too where the host fires hooks on subagent Bash, and (committed) every checkout. The apply depends on state: when the lane is OFF, it is the `gate-hook --read-lane` preview (whose own currency check refuses a stale hook — a pre-1.48 hook never reads `lanes.json`); when the placed hook is STALE (an enabled lane over an old hook) or MISSING, the item instead surfaces a **delete-to-reseed** / re-place recovery (a destructive `rm` + `--apply`, an attention item — never the safe preview). Risk profile: a bounded read-only trust-posture extension — no write/exec exposure beyond the audited core.
28
28
  - `worktrees-dir` — on a settings-native host that honors the key, the HAND-APPLY line widens the OS-sandbox WRITE surface to the whole worktrees parent dir: every sibling path under it (other repositories included) becomes agent-writable, and the widening persists for every later session. A harness-managed host may ignore that project setting; grant the narrow parent through host/session controls or use the provision terminal fallback instead. When that scope is wider than you want, narrow it FIRST: create a dedicated dir yourself (outside the agent's write surface), point `docs/ai/worktrees.json` `parentDir` at it, then re-run recommendations — the item re-renders with the narrowed dir. The kit never writes sandbox filesystem allowances itself; the line is always yours to paste. **Convergence** is two-path: a declared `sandbox.filesystem.allowWrite` entry covering the probed dir (either settings scope — `~` and `~/…` resolve against home, and coverage is path-segment-aware, so a grant on a sibling or on a child never counts), or — for a host that ignores that key — the neutral dir-bound acknowledgement recorded by this item's consent-gated apply one-liner (`ack-write --lane worktrees-dir` — a dry-run preview that prints the exact `--apply`; recorded as `worktreesDirAck` in the family-owned `docs/ai/acks.json`, never a security key), while the grant advice itself rides the `recipe:` line as the labeled FIRST step; against a trusted host NO the apply stays the HAND-APPLY grant advice and no ack is offered. Neither is proof of write CAPABILITY: the provision preflight's real create+delete probe stays the runtime truth, and the fingerprint is bound to the **resolved probe dir**, so the item re-fires only when that resolved dir changes (two absent `parentDir` values sharing an existing ancestor resolve to the same dir and keep the same ack). Risk profile: a real write-surface widening where honored — scope it deliberately.
29
29
 
30
- - `gates-inert` — the gate matrix is DECLARED but verifies nothing, and the two causes have different remedies, so the item renders a different apply for each. **Cause A** — a canonical `coverage-check` gate with no producer gate declared BEFORE it (none at all, or one declared after it, which writes the lcov too late): the run certifies no coverage of its own — `coverage=not-run` when nothing wrote an lcov, or a verdict over STALE bytes an earlier run left in the git dir, which is worse because it reads as `coverage=certified`. This item is what surfaces that state at upgrade instead of leaving it to be noticed. The remedy needs the producer to run BEFORE the checker — declared or MOVED there — and the `gates-init` fill is append-only, so it cannot reorder an existing declaration; this arm is therefore **HAND-APPLY**: the maintainer edits `docs/ai/gates.json` (the exact producer form lives in `${CLAUDE_SKILL_DIR}/references/modes/gates.md`), never you, never the kit. **Cause B** — every declared gate is one of the kit's own canonical checkers, so the matrix runs no project-verification command at all: the apply is the `gates-init` dry-run preview, the same consent-gated seeder the `gates-declaration` item renders, and after the SAME confirmation you run the `--apply` line it prints. Neither cause changes any gate result, exit code or receipt — the item adds no enforcement, only the offer. Risk profile: `docs/ai/gates.json` is a PRIVILEGED file (a declared gate is auto-approvable by the wired hook), so cause A stays maintainer-only editing and cause B appends a project-controlled command on your explicit yes.
30
+ - `gates-inert` — the gate matrix is DECLARED but verifies nothing, and the two causes have different remedies, so the item renders a different apply for each. **Cause A** — a canonical `coverage-check` gate with no producer gate declared BEFORE it (none at all, or one declared after it, which writes the lcov too late): the run certifies no coverage of its own — `coverage=not-run` when nothing wrote an lcov, or a verdict over STALE bytes an earlier run left in the git dir, which is worse because it reads as `coverage=certified`. This item is what surfaces that state at upgrade instead of leaving it to be noticed. The remedy needs the producer to run BEFORE the checker — declared or MOVED there — and what the fill can do about that is exactly what the apply says. When the checker is the LAST declared gate and the project's own scripts yield an offerable producer, the fill PLACES that producer before it, so the apply is the ordinary `gates-init` dry-run preview. Otherwise — no offerable producer, or a checker that is not last this arm stays **HAND-APPLY**: the maintainer edits `docs/ai/gates.json` (the exact producer form lives in `${CLAUDE_SKILL_DIR}/references/modes/gates.md`), never you, never the kit, because the fill never reorders entries it did not write. **Cause B** — every declared gate is one of the kit's own canonical checkers, so the matrix runs no project-verification command at all: the apply is the `gates-init` dry-run preview, the same consent-gated seeder the `gates-declaration` item renders, and after the SAME confirmation you run the `--apply` line it prints. Neither cause changes any gate result, exit code or receipt — the item adds no enforcement, only the offer. Risk profile: `docs/ai/gates.json` is a PRIVILEGED file (a declared gate is auto-approvable by the wired hook), so both causes land in the same place where the fill can help, it declares a project-controlled command on your explicit yes; where it cannot, the edit stays maintainer-only.
31
31
 
32
+ - `source-size` — the practice's arms differ by what the declared gate actually IS. **`adopted-elsewhere`** — the gate runs a DIFFERENT real copy of this checker (a vendored deployment): the practice is running, the advisor simply cannot resolve it as its own sibling, and the realpath anchor deliberately does not widen (it is what stops a lookalike from certifying). So the convergence is the ack writer's preview (`--lane source-size-copy` → `sourceSizeCopyAck`), never `--adopt`, which would mint the record and then be refused by the fill on the id already declared. The fingerprint binds the declared claims AS AUTHORED, sorted — not their resolved paths, which are machine-specific and would churn a committed ack between machines — so a second external copy re-fires it. **`id-squatter`** — a gate carries the `source-size` id and is not this checker under any reading: the id claims the practice while nothing measures module size, so it is **HAND-APPLY** (rename the id, or repoint the cmd at this checker's `--check` form). **`unminted`** stays what it was for both copies: a gate certain to refuse on every run, attention, and NO acknowledgement silences it — only the rendered verb differs (with the gate already declared elsewhere the missing half is the record alone, so the line is `--write-baseline`). Risk profile: `docs/ai/gates.json` is a PRIVILEGED file, so every arm here either records an acknowledgement or hands you an edit — the kit declares nothing on its own.
33
+ - `gate-hook` — the base arm is the ordinary opt-in wiring offer. The **`marker-stale`** arm is not: the placed hook validates your declaration through its OWN baked copy and goes dark on any key it does not know, so a declaration carrying the `lcovProducer` key under a hook that predates it silently turns auto-approval OFF, and every gate prompts again with no error anywhere. The condition is the key's PRESENCE, not its value — an older hook rejects a key it does not know whatever that key says, so `"lcovProducer": false` darkens it exactly as `true` does. The arm is deliberately marker-scoped — a stale hook is otherwise harmless — and its recovery is the writer's own: `gate-hook --apply` places only an ABSENT target, so converging means deleting the placed hook and re-placing it. That makes it a destructive **HAND-APPLY** (`rm` + `--apply`, absolute path so it can only delete this project's hook), never something the consent flow runs for you. When the read-lane is also enabled, this arm and `read-lane.stale` would report the same file with the same recovery, so exactly one renders — **this one**, because its cause is the true one: a hook that postdates the read-lane and merely predates the marker key reads `lanes.json` perfectly well, and the read-lane arm's wording would be false over it. Risk profile: deleting one placed hook file, then re-placing it from the bundle; the declaration is never touched.
34
+ - ADDITIONAL `gates-inert` arms (the third outcomes) — two further arms, and they differ in whether anything is BROKEN. **`producer-unrecognized`** — a checker with no producer anywhere in the declaration, on a tracked tree the changed-line coverage domain cannot reach (`.ts`/`.tsx`/`.jsx`/`.mts`/`.cts` strictly outnumber `.mjs`/`.cjs`/`.js`): the dead pair is real, so this arm is **HAND-APPLY** and the two remedies are marking the real producer with `"lcovProducer": true` or dropping the checker. Never a `node --test` prescription over a project that has no such suite, never the fill preview, and never an acknowledgement — a dead pair is broken, not narrow, and removing a producer after an acknowledgement lands right back in this arm. **`coverage-domain-narrow`** — the producer/checker pair IS live and the tree is still dominated by what the domain excludes: nothing is broken, and the honest sentence is that certification covers the assessable minority. Its apply is the consent-gated **ack writer** preview (a NEUTRAL fingerprint into `docs/ai/acks.json` as `coverageDomainAck`, never a security key); after the SAME confirmation you run the `--apply` it prints. The fingerprint binds the FACT — the verdict plus the unsupported extensions present, never the file counts — so an acknowledged project stays quiet as it grows and re-fires when a new unsupported language arrives or the verdict flips. The census reads the TRACKED tree with a read-only `git ls-files`; a tree it cannot read (a non-git deployment) becomes a stated skip, so optimality is withheld rather than assumed. Risk profile: no enforcement change of any kind — one acknowledgement recorded in a family-owned file, and one hand edit that stays the maintainer's.
32
35
  - `adr-store-migration` — other items write project files too; what is unique here is that the crossing **overwrites and deletes files the project already has**: it replaces the deployed enforcement scripts in `scripts/` (the directional subset — only basenames the project already has; a locally-edited copy is snapshotted first, never silently clobbered) and, where a retired archive file exists, DELETES it once conservation has been proven. That is why it is **HAND-APPLY** and why the command shown in the apply slot is a **`--dry-run`** — it writes nothing and prints the whole plan. `--apply` is a SEPARATE step, run only after that plan has been shown and **fresh consent** obtained for it; the consent flow executes only the apply slot, so an item that needs consent AFTER its preview cannot use that lane at all. Every write is idempotent and the run is re-runnable to completion after any interruption, so a re-run repairs rather than double-applies. It never commits. Risk profile: overwrite + delete of existing project files, gated on a preview you have actually read.
33
36
 
34
37
  **Sandbox lanes (what to DO with the `sandbox-lane` recipe, per host class):**
@@ -8,7 +8,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/review-state.mjs [--check] [--json]`:
8
8
 
9
9
  1. Plain run → the human report: resolved recipe + source, plan-in-flight, tree fingerprint, per-backend receipt state (current / stale / ungrounded / probe / rejected / missing) with verdict + grounding + timestamp.
10
10
  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 CONFIGURED solo recipe (a computed readiness-degrade NEVER silently becomes solo — it needs the explicit degrade record below), 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 SATISFIED for the current tree. **A clean-tree PASS under a NON-SOLO review obligation is never silent about a latent arm:** when a `reviewed` or `council` recipe is configured, it NAMES every plan in flight and states that this gate arms as soon as the tree is dirty — the condition is discoverable BEFORE it blocks, instead of surfacing at the worst moment (a pending commit, or the landing of a feature worktree when main first turns dirty). Nothing is announced under a configured `solo` recipe or a non-git cwd, where the gate can never arm. **Satisfaction is ship-class-only on the LATEST NORMAL receipt (D3(b)):** per backend, the latest probe-free current-fingerprint receipt is selected FIRST and THEN verdict-checked — only the recognized ship-class vocabulary (`ship` / `ship with nits`) satisfies; a recognized NEGATIVE (`revise` / `rethink` / `rework`) is an authoritative VETO (an earlier ship never survives a later revise); an UNRECOGNIZED verdict (e.g. `unknown` from a dead run) fails CLOSED unconditionally — a later `unknown` never lets an earlier SHIP stand, and a fresh normal re-run supersedes it. **The ONLY escape is an explicit degrade RECORD** (`core-evidence.mjs degrade --backend <name> --reason "…"`, fingerprint-bound to the current tree) — and never all backends: ≥1 non-degraded ship-class receipt is required whenever ≥1 backend is configured; a malformed evidence store denies the escape fail-closed but never fails an independently-satisfied tree. Exit 1 otherwise — missing, **stale** (ANY edit after a review moves the fingerprint), ungrounded, vetoed, or unrecognized. 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. **Probe receipts never attest either:** a `CODEX_PROBE=1` / `AGY_PROBE=1` review runs with the frontier-model/max-effort guard OFF, so the wrapper stamps `probe:true` and this checker excludes it — per receipt, so a real review at the same fingerprint still satisfies. **Silence is not a declaration:** a malformed *or* absent probe marker is rejected fail-closed and stated in the check line. Honest bound: receipts are **not authenticated** (a forger could write `probe:false` as easily as any other field) — like the rest of the receipt this is a self-discipline mechanism, not a security boundary.
11
- 3. **Wire it as a gate by hand OR via the explicit-consent init preview — never without consent (AD-021/D9).** 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 gates-init preview (`${CLAUDE_SKILL_DIR}/references/modes/gates.md`, consent-fill 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 — and `run-gates --final` requires this check among its canonical core gates.
11
+ 3. **Wire it as a gate by hand OR via the explicit-consent init preview — never without consent (AD-021/D9).** 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 gates-init preview — `node ${CLAUDE_SKILL_DIR}/tools/gates-init.mjs --cwd <project>` (dry-run: it prints the derived entries and writes NOTHING; `--apply --only review-state` writes exactly this one on your explicit yes) 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). The full consent-fill contract is in `${CLAUDE_SKILL_DIR}/references/modes/gates.md`. Once declared, the opt-in `${CLAUDE_SKILL_DIR}/references/modes/hook.md` auto-approves it like any other declared gate — and `run-gates --final` requires this check among its canonical core gates.
12
12
  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 inherits the `--check` decision whole — a backend with a current-tree degrade RECORD stops being waited on (the shared decideCheck), so you never hand-`--await` around a known degrade. **An AUTHORITATIVE veto terminates the wait loudly BEFORE the deadline** (flow-orchestration Decision 4/#50): a landed recognized-negative verdict for the current tree is the dispatched review's *answer*, so `--await` exits 1 with `VETO — …` immediately instead of misclassifying it as a timeout — only a fresh review can move a landed negative, never waiting. Still read-only (it re-reads the receipts + the evidence store); solo / no-plan / clean-tree resolve instantly. For waiting on **one dispatch's arrival** (never obligation satisfaction) use the receipt-arrival deadline runner, `${CLAUDE_SKILL_DIR}/references/modes/receipt-deadline.md`.
13
13
 
14
14
  **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.
@@ -16,8 +16,24 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/setup-backends.mjs [<backend>] [--bindir <pa
16
16
  downgraded), and every outcome line is composed by the tool — paste verbatim. When the skills dir
17
17
  is **read-only this session** and the placed bridge is already at the bundled version, the
18
18
  equal-version re-sync it would run cannot write: that outcome is `skipped-readonly` — a **stated
19
- skip** (exit 0, not a failure) naming the current version, the skipped/incomplete re-sync, and the
20
- read-only cause; it never claims a re-sync ran, and any local drift persists until a writable rerun.
19
+ skip** (exit 0, not a failure) naming the current version and the read-only cause; it never claims
20
+ a re-sync ran. What it says about the tree is **never assumed** it runs a read-only **re-scan** of
21
+ the bundle-owned files **and** the wrapper links (the degrade returns before the re-link step, so
22
+ that axis is genuinely unreconciled) and reports exactly one **proven** verdict: `clean-parity` —
23
+ nothing the refresh manages differs — every file it would overwrite already matches, every node it
24
+ would add is present, every wrapper link and source mode is in place; `drifted` — something is
25
+ provably wrong, and it **names every item** with the recovery that actually applies to it. A
26
+ writable rerun **repairs** what it can bring to the required state (a differing file, an absent
27
+ bundled node, a wrapper it can re-link or chmod). It **REFUSES** anything else, by one rule: *a node
28
+ in the reconcile set that the rerun cannot be guaranteed to converge under the refresh's no-follow
29
+ and ownership policy* — for instance (a **non-exhaustive** illustration, never the contract) a
30
+ symlink standing where the refresh must write, a node of an incompatible kind, or a wrapper target
31
+ that is foreign or not a symlink at all. Refusals are named apart as resolve-by-hand-then-rerun; one
32
+ blanket "re-run to repair" would promise a repair that never happens. The **outcome line is the
33
+ authority on the exact cause** — each item carries its own; `unverifiable` — it **names every item
34
+ it could not compare** (a read or stat error, or a re-scan that could not run at all) and says the
35
+ remaining need for repair is unknown. Could-not-verify is never rendered as clean, and every verdict
36
+ keeps the exit code at 0.
21
37
  (A version-**behind** refresh blocked by the same read-only dir stays a loud `could not refresh`,
22
38
  its recovery pointing at a writable rerun.) Does not combine with `--dry-run`.
23
39
  - `--help`, `-h` — usage.