@sabaiway/agent-workflow-kit 14.30.0 → 14.31.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,44 @@ 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
+ ## 14.31.0 — the upgrade's Claude Code hook items get writers: a stale gate hook is reseeded in place and the state-block Stop hook is placed and wired, each on the chat yes (AD-183)
8
+
9
+ **Three `/agent-workflow-kit upgrade` items that used to hand the user a hook to fix by hand are now harness items the
10
+ agent applies on the chat yes, after the danger check: `gate-hook.marker-stale`, `read-lane.stale` and `state-block`.**
11
+ Before, the two stale-hook arms printed `HAND-APPLY: rm <hook>, then gate-hook --apply` (the writer never overwrote a
12
+ placed hook), and `state-block` pointed at a JSON block to paste into `.claude/settings.json` by three merge cases.
13
+
14
+ - **`gate-hook --reseed`.** `node <kit>/tools/gate-hook.mjs --reseed --cwd <root>` previews; `--apply --reseed --cwd
15
+ <root>` writes the bundled hook over a placed hook whose bytes differ, in place, and wires the `PreToolUse` entry only
16
+ while neither settings file has it. A current hook wired in either file prints `nothing to change: the placed hook is
17
+ current` (exit 0); a current unwired one is only wired. It refuses an absent hook (`--apply` places it) and a
18
+ symlinked or non-regular one, and `--reseed` with `--read-lane` is a usage error (exit 2). The read-lane currency
19
+ refusal, the diverged-and-unwired refusal, the report's diverged line, `--help`, `hook.md`, `gates.md` and the README's
20
+ hook row now name this route; none of these recoveries says `rm` any more.
21
+ - **`tools/state-block-hook.mjs`, the Stop-hook writer.** A preview by default, `--apply` writes: it places
22
+ `.claude/hooks/state-block-guard.mjs` from the bundle (created when absent, kept when byte-equal, replaced in place
23
+ when it differs) and merges one `Stop` entry running `node "$CLAUDE_PROJECT_DIR/.claude/hooks/state-block-guard.mjs"`
24
+ (timeout 10, no `--require-block`) into `.claude/settings.json`: a new `hooks` key, a `Stop` key beside other hooks,
25
+ or one element appended to an existing `hooks.Stop` array, every foreign entry kept. The entry is merged only while no
26
+ `Stop` entry of either settings file runs the runtime, so a wiring you made yourself is never duplicated. A
27
+ `hooks.Stop` that is not an array refuses with nothing written; the writer's symlink, target and settings-shape checks
28
+ are `gate-hook.mjs`'s own, imported, and its unsafe-mode and stamp refusals use gate-hook's error codes.
29
+ `state-block-guard.md`'s "No writer yet" section and its two hand steps
30
+ are gone; the catalog lists the mode as a writer.
31
+ - **A hook file swapped for a symlink is never written through.** Both writers create the hook file exclusively and
32
+ replace it with a no-follow open, so a hook file itself swapped for a symlink after the preview refuses with nothing
33
+ written through it. Where Node
34
+ exposes no no-follow open flag (native Windows), replacing a placed hook refuses instead (`fail closed`); placing an
35
+ absent one still works.
36
+ - **The danger check covers the three.** `hooks.Stop` joins the keys it judges; each variant's scope admits only its
37
+ hook file, its one settings entry and the `.claude/` and `.claude/hooks/` directories. A hook file the apply replaces
38
+ prints `writes: <path> sha256:<new> — replaces sha256:<old>`, so the yes is bound to the bytes it overwrites. `init`
39
+ previews and applies the three by its existing rule.
40
+ - **The bare `sandbox-provision` item is the user's own step,** worded as such: the doctor has no install it can run
41
+ for you there, so you run `cd <root> && node <kit>/tools/autonomy-doctor.mjs` in a terminal of your own.
42
+
43
+ The git hooks (`enforcement`, `commit-guard`) keep their current apply texts; their writers come in a later release.
44
+
7
45
  ## 14.30.0 — the upgrade brings the docs navigator back to green past 80 lines: one yes replaces every edited spec-layer script, and a navigator write the project's own checker rejects is put back (AD-182)
8
46
 
9
47
  **`/agent-workflow-kit upgrade` now leaves a project with more docs than the navigator's old cap on a navigator its
package/README.md CHANGED
@@ -263,8 +263,8 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
263
263
  | `/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. |
264
264
  | `/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; every allowlist apply (flagless, `--kit-tools`, `--bridge-tier`) also seeds one `permissions.ask` rule, so all work can stay in auto mode: without it auto mode refuses a harness apply (the `apply:` line of each harness item the upgrade advisor renders) and you must leave auto mode for it; with it the harness shows its usual dialog — the rule allows nothing; 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. |
265
265
  | `/agent-workflow-kit agents` | Claude Code · opt-in | **subagent vehicles** — places the five bundled subagent definitions into `.claude/agents/`: **four read-only vehicles + one full-tool `executor`**. **No read-only vehicle gets `Bash`**, and that is the load-bearing property: a read-only fan-out on a full-tool subagent shells out for facts it could have read, and every shelled command is an approval prompt you never needed to see. Three of the four ride a **cheap model** (haiku, low effort) for mechanical work — extraction sweeps, changelog fact-skeletons, gate-failure triage; the fourth, **`review-lens`**, is an ADDITIONAL independent read-only review opinion on code your configured backends have already seen (never a replacement for your plan or diff review recipe, advisory like every review; an epic is reviewed by the lens alone, whatever `epic.review` resolves to). The fifth, **`executor`** (all tools; model and effort from `docs/ai/vehicles.json`), is the ONE vehicle with a shell and the instrument behind the **Subagent** recipe: dispatched only for a bounded, file-disjoint execution / authoring / routine slice you verify afterwards — never for read-only work, never as a review backend, and it never commits. Its state (`placed` / `customized` / `unusable` / `missing`) is what a `subagent` slot's readiness resolves from. Writing code and running gates stay off the four read-only vehicles; the executor carries a bounded slice you re-verify on your main lane, and every vehicle's output is verified. Preview by default (`--apply` writes); an existing customized read-only vehicle or lens is **preserved, never overwritten**; the executor's body is re-derived from `docs/ai/vehicles.json` (a hand edit is replaced); never touches `settings*.json`, never commits. |
266
- | `/agent-workflow-kit hook` | Claude Code · opt-in | **gate-approval hook** — places a self-contained PreToolUse hook (`.claude/hooks/`) and wires it into `.claude/settings.json`: a Bash command **byte-identical** to a gate you declared in `docs/ai/gates.json` (run from the project root) is auto-approved — no prompt, no idle; a seeded read-only command carrying a runtime residual (output redirection, command substitution, `--output` writes) now **asks** even where an allow rule would have silently passed it (proven live — hook `ask` overrides an allow rule). Detection is a conservative string scan, so it over-asks on a byte that is ordinary text — searching for `=>`, or a read wearing `2>/dev/null`, prompts. That is a documented open limit: 4.1.0 built three mechanisms to narrow it and removed all three in review, each counterexample kept as a test (see Mode: hook). Exact matches only, never patterns; never `deny`; a broken `gates.json` only disables auto-approval, never the guard. An opt-in **read-lane** (`--read-lane` → `docs/ai/lanes.json`, a **separate** file from `gates.json`) additionally auto-approves *compounds* of seeded read-only commands carrying no shell metaprogramming; `--apply --read-lane` verifies the placed hook is current first (delete-to-reseed otherwise). Preview by default; never writes `settings.local.json`; never commits. |
267
- | `/agent-workflow-kit state-block-guard` | Claude Code · opt-in | **closing-block detector** — a `Stop` hook that reads the turn's final assistant message and warns when the closing state block is defective: the «what I need from you» slot answering *nothing* (a turn that ENDS always needs a resume, so the answer is false, not merely unhelpful), or a first-person promise of imminent work in a turn that is over. Judged on the slot's first clause, so a real ask followed by a clause break and "nothing else" passes, and a promise gated on something named passes; matching is word-bounded and Unicode-aware, quoted and fenced examples are stripped, and the last STARTED block decides — an incomplete tail counts as no block rather than falling back to an earlier one. A message carrying no block at all is silent unless you pass `--require-block` — this kit does not mandate the block, and a hook that runs every turn must not warn every turn. The judgement is lexical and the mode doc names every residual it leaves. **Detection, never prevention** — a `Stop` hook cannot un-send the message it judges; what it buys is that a silent recurrence becomes a visible one. Warns via `systemMessage` and **exits 0 on every path**: never `deny`, never blocks the stop, never re-enters the model. Reads nothing but the message, makes no network call, approves nothing. This mode is **read-only and has no writer** — it ships the self-contained hook and a paste-ready wiring block you merge into `.claude/settings.json` yourself. |
266
+ | `/agent-workflow-kit hook` | Claude Code · opt-in | **gate-approval hook** — places a self-contained PreToolUse hook (`.claude/hooks/`) and wires it into `.claude/settings.json`: a Bash command **byte-identical** to a gate you declared in `docs/ai/gates.json` (run from the project root) is auto-approved — no prompt, no idle; a seeded read-only command carrying a runtime residual (output redirection, command substitution, `--output` writes) now **asks** even where an allow rule would have silently passed it (proven live — hook `ask` overrides an allow rule). Detection is a conservative string scan, so it over-asks on a byte that is ordinary text — searching for `=>`, or a read wearing `2>/dev/null`, prompts. That is a documented open limit: 4.1.0 built three mechanisms to narrow it and removed all three in review, each counterexample kept as a test (see Mode: hook). Exact matches only, never patterns; never `deny`; a broken `gates.json` only disables auto-approval, never the guard. An opt-in **read-lane** (`--read-lane` → `docs/ai/lanes.json`, a **separate** file from `gates.json`) additionally auto-approves *compounds* of seeded read-only commands carrying no shell metaprogramming; `--apply --read-lane` verifies the placed hook is current first; a stale or diverged placed hook is replaced by the reseed route (`--apply --reseed`, the bundled bytes in place). Preview by default; never writes `settings.local.json`; never commits. |
267
+ | `/agent-workflow-kit state-block-guard` | Claude Code · opt-in | **closing-block detector** — a `Stop` hook that reads the turn's final assistant message and warns when the closing state block is defective: the «what I need from you» slot answering *nothing* (a turn that ENDS always needs a resume, so the answer is false, not merely unhelpful), or a first-person promise of imminent work in a turn that is over. Judged on the slot's first clause, so a real ask followed by a clause break and "nothing else" passes, and a promise gated on something named passes; matching is word-bounded and Unicode-aware, quoted and fenced examples are stripped, and the last STARTED block decides — an incomplete tail counts as no block rather than falling back to an earlier one. A message carrying no block at all is silent unless you pass `--require-block` — this kit does not mandate the block, and a hook that runs every turn must not warn every turn. The judgement is lexical and the mode doc names every residual it leaves. **Detection, never prevention** — a `Stop` hook cannot un-send the message it judges; what it buys is that a silent recurrence becomes a visible one. Warns via `systemMessage` and **exits 0 on every path**: never `deny`, never blocks the stop, never re-enters the model. Reads nothing but the message, makes no network call, approves nothing. Its writer is `tools/state-block-hook.mjs`: it places the self-contained hook and merges its `Stop` entry into `.claude/settings.json` by the three merge cases, keeping every foreign hook — the agent applies it on the chat yes after the danger check. |
268
268
  | `/agent-workflow-kit bridge-settings` | opt-in, any time | **host-level bridge settings** — read or change the bridges' `KEY=VALUE` config file (`${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf`), the **one place a knob survives kit upgrades** (it lives outside every kit tree; a refresh never touches it, and now states loudly if it ever overwrote a local edit). The **model each bridge runs**: `CODEX_MODEL` + `CODEX_EFFORT` (default `gpt-6.1-sol` at `high`) and `AGY_MODEL` (default `Gemini 3.8 Flash (High)`) — the read-out shows the models each installed CLI offers, and a set one is checked against that list before anything is written. Other knobs: the **codex Fast tier** (`CODEX_SERVICE_TIER=priority` — "2x speed, **increased usage**" in the catalog's own words, quality-neutral, default off — a consented spend), the codex/agy hard timeouts, the codex review byte cap, and `AGY_REVIEW_MAX_TOTAL_BYTES` (the ceiling on what an oversized agy code review may feed before it refuses pre-spend). `AGY_REVIEW_ALLOW_ADDDIR` is a **retired** compatibility key: still recognized so an existing line never warns as unknown, but it arms nothing — the writer refuses to set it and `--unset` clears it. Allowed keys + value rules come from the bundled bridge manifests. **Previews by default**; refuses an unknown key, an out-of-range value, a model or effort the catalog does not offer, or a duplicate-carrying file; writes atomically (symlink/TOCTOU-safe). Never commits, never runs a model (only the read-only catalog commands). |
269
269
  | `/agent-workflow-kit autonomy-doctor` | opt-in, any time | **guarded sandbox provisioner "doctor"** — can this machine run the Claude sandbox, and (only with your consent) fix it: macOS Seatbelt built-in / Linux+WSL2 needs `bwrap`+`socat` / native Windows → WSL2. Flagless = **FS-only preview** (the diagnosis, the exact absolute-path command it WOULD run, the exact `--apply <pm>:<pkgs>` consent tuple — runs nothing, never claims ready); `--verify` runs the unprivileged proof (a `bwrap` user-namespace smoke + `socat -V`) — the **only** source of a Linux "ready (verified)" claim; `--apply <pm>:<pkgs>` is the consent-gated privileged install (the tuple must equal the previewed plan; everything executed resolves to absolute paths inside `/usr/bin:/bin:/usr/sbin:/sbin`; the exact command is re-printed immediately before execution), then verifies. Honest loud degrades (unknown PM, untrusted location, nested-sandbox INDETERMINATE, root-unproven) and a stated restart step. Sits **outside every velocity auto-approve tier**; never auto-runs, never writes repo files, never commits. |
270
270
 
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". New to the epic, story and task tier? Run `/agent-workflow-kit tier`. A typed decision with a confidence from Jev? Run `/agent-workflow-kit jev`.
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '14.30.0'
6
+ version: '14.31.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
@@ -185,7 +185,7 @@ writer — read `${CLAUDE_SKILL_DIR}/references/modes/hook.md` before acting.
185
185
 
186
186
  ### Mode: state-block-guard
187
187
 
188
- read-only — read `${CLAUDE_SKILL_DIR}/references/modes/state-block-guard.md` before acting.
188
+ writer — read `${CLAUDE_SKILL_DIR}/references/modes/state-block-guard.md` before acting.
189
189
 
190
190
  ### Mode: bridge-settings
191
191
 
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": "14.30.0",
6
+ "version": "14.31.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": "14.30.0",
3
+ "version": "14.31.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",
@@ -40,7 +40,7 @@ Declared gates can also be **auto-approved** (no permission prompt on a byte-exa
40
40
 
41
41
  **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`.
42
42
 
43
- **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>`.
43
+ **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. The recovery is the reseed route: `node ${CLAUDE_SKILL_DIR}/tools/gate-hook.mjs --apply --reseed --cwd <project>` replaces the placed hook with the bundled bytes in place — the advisor's `gate-hook.marker-stale` item, which the agent applies on the chat yes after the danger check.
44
44
 
45
45
  **`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).
46
46
 
@@ -22,15 +22,17 @@ The opt-in **gate-approval PreToolUse hook** — the family's third `.claude/` w
22
22
 
23
23
  **Trust posture (state it plainly when asking consent):** the hook removes the PROMPT only for commands the human already declared in `docs/ai/gates.json` — the same trust boundary as the `gates` runner, which executes them with the caller's privileges. **gates.json thereby becomes a privileged file**: whoever can edit it can get its commands auto-approved. An invalid declaration approves NOTHING (strict parse, exact validation parity with the runner).
24
24
 
25
- **The read-lane (opt-in, AD-055 Part II).** The toggle lives in `docs/ai/lanes.json` — a **SEPARATE kit-owned strict-JSON file** (`{ "readLane": true }`); `docs/ai/gates.json`, both its validators and the byte-mirrored template are **untouched** (the gates schema has no lane/model/routing fields — that claim stays true). The hook reads `lanes.json` LIVE per call, **fail-closed**: an absent / malformed / non-object file, or a `readLane` that is not the boolean `true`, leaves the lane dark (gate auto-approval and the residual ask are unaffected). `lanes.json` becomes a **privileged file** exactly like `gates.json` — an auto-approved read chain runs **unattended** and can read any file you can (the same trust boundary as the audited read-only core velocity seeds, extended to compounds (and singles) of that core; **prompt-bypass only, never a sandbox bypass**). Enable it with `--read-lane` (below), which runs a **currency check** first: it refuses unless the PLACED hook is byte-identical to the current bundle — a **pre-1.48 hook never reads `lanes.json`**, so enabling the lane against a stale placed copy would be a silent no-op the user paid consent for. The stale-hook silent-blackout class is dead **by construction** (an old hook merely lacks the rung; nothing existing changes behavior); the currency refusal names the **delete-to-reseed** recovery (`rm .claude/hooks/agent-workflow-gates.mjs` + the `--apply` one-liner) — there is **no refresh-only hook lane** this release (a diverged wired copy stays preserved, unchanged).
25
+ **The read-lane (opt-in, AD-055 Part II).** The toggle lives in `docs/ai/lanes.json` — a **SEPARATE kit-owned strict-JSON file** (`{ "readLane": true }`); `docs/ai/gates.json`, both its validators and the byte-mirrored template are **untouched** (the gates schema has no lane/model/routing fields — that claim stays true). The hook reads `lanes.json` LIVE per call, **fail-closed**: an absent / malformed / non-object file, or a `readLane` that is not the boolean `true`, leaves the lane dark (gate auto-approval and the residual ask are unaffected). `lanes.json` becomes a **privileged file** exactly like `gates.json` — an auto-approved read chain runs **unattended** and can read any file you can (the same trust boundary as the audited read-only core velocity seeds, extended to compounds (and singles) of that core; **prompt-bypass only, never a sandbox bypass**). Enable it with `--read-lane` (below), which runs a **currency check** first: it refuses unless the PLACED hook is byte-identical to the current bundle — a **pre-1.48 hook never reads `lanes.json`**, so enabling the lane against a stale placed copy would be a silent no-op the user paid consent for. The stale-hook silent-blackout class is dead **by construction** (an old hook merely lacks the rung; nothing existing changes behavior); the currency refusal names the **reseed** route (`gate-hook --apply --reseed --cwd <root>`, which replaces the placed hook with the bundled bytes in place).
26
26
 
27
27
  **Version-status routing** like the other writer modes (stamp head `4.0.0`; `--apply` enforces it in code).
28
28
 
29
29
  Run `node ${CLAUDE_SKILL_DIR}/tools/gate-hook.mjs [--dry-run | --apply] [--cwd <dir>]`:
30
30
 
31
- 1. **`--dry-run` first, always** (the default — changes nothing): previews the placement and the exact settings entry it would merge. It STOPs (zero writes) on: a symlinked `.claude` / `.claude/hooks` / target file / non-regular `settings.json`; malformed settings JSON; a **malformed existing `hooks` shape** (never a merge-through-clobber); an unsafe `permissions.defaultMode` in **either** settings file; or a target hook file with **different** content while our entry is not wired (**it refuses to wire an unknown script as a PreToolUse hook** — the recovery is named: delete the file to reseed from the bundle).
31
+ 1. **`--dry-run` first, always** (the default — changes nothing): previews the placement and the exact settings entry it would merge. It STOPs (zero writes) on: a symlinked `.claude` / `.claude/hooks` / target file / non-regular `settings.json`; malformed settings JSON; a **malformed existing `hooks` shape** (never a merge-through-clobber); an unsafe `permissions.defaultMode` in **either** settings file; or a target hook file with **different** content while our entry is not wired (**it refuses to wire an unknown script as a PreToolUse hook** — the recovery is named: the reseed route, below).
32
32
  2. **Ask consent** via **`AskUserQuestion` where supported**, the no-change option first: keep prompting for gate commands, or place + wire the hook — presenting the trust posture above in plain language.
33
- 3. **Only on an explicit yes**, re-run with `--apply`. It places the file FIRST, then wires settings (a wired-but-missing entry would error on every Bash call); merge-don't-clobber (foreign hooks/matchers/keys and existing permissions preserved; re-apply never duplicates); **settings hot-reload — the hook is active for new Bash calls, no session restart needed**. An identical existing file is *already current*; a diverged-but-already-wired file is reported, never clobbered or unwired.
33
+ 3. **Only on an explicit yes**, re-run with `--apply`. It places the file FIRST, then wires settings (a wired-but-missing entry would error on every Bash call); merge-don't-clobber (foreign hooks/matchers/keys and existing permissions preserved; re-apply never duplicates); **settings hot-reload — the hook is active for new Bash calls, no session restart needed**. An identical existing file is *already current*; a diverged-but-already-wired file is reported, never clobbered or unwired by `--apply`.
34
+
35
+ **The reseed route — `--reseed`, the one consented replacement.** `gate-hook --reseed` previews and `--apply --reseed --cwd <root>` replaces a PLACED hook whose bytes differ from the bundle with the bundled bytes, in place, and wires the entry only while it is wired in neither settings file; a current hook wired in either file is `nothing to change: the placed hook is current`, and a current unwired one is only wired. It refuses an absent hook (`--apply` places it), a symlinked or non-regular target, a hook file swapped for a symlink before the write (nothing is written through it), and `--read-lane` beside it; where Node exposes no no-follow open flag (native Windows) it refuses the replace instead of risking a write through a link. It is what the advisor's `gate-hook.marker-stale` and `read-lane.stale` items apply: the agent runs it on the chat yes after the danger check, whose result names the replaced file with the old and the new digest; `init` applies it as the route the user may take instead.
34
36
  4. **Hidden-mode deployments:** after apply, run the hide-footprint reconcile (`node ${CLAUDE_SKILL_DIR}/tools/hide-footprint.mjs --dir <project> --reconcile`) — `/.claude/hooks/` is in the known-footprint registry; the apply report reminds you.
35
37
 
36
38
  **Enabling the read-lane:** `node ${CLAUDE_SKILL_DIR}/tools/gate-hook.mjs --read-lane [--dry-run | --apply] [--cwd <dir>]`. `--dry-run` (the default) previews the `lanes.json` edit, the hook-currency status, and the trust posture; `--apply --read-lane` runs the **currency check** (above), then writes `{ "readLane": true }` into `docs/ai/lanes.json` — create-if-absent, merge-preserve every key, symlink / non-regular refusal, malformed-JSON fail-closed, atomic. It **never touches** `.claude/settings.json` or `docs/ai/gates.json`. The upgrade Recommendations advisor surfaces this as the `read-lane` offer once the hook is placed **and** wired.
@@ -39,4 +41,4 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/gate-hook.mjs [--dry-run | --apply] [--cwd <
39
41
 
40
42
  **Exit codes:** `0` done / dry-run (incl. the reported diverged-but-wired state); `1` a precondition STOP; `2` bad arguments.
41
43
 
42
- **The kit's other hook.** `state-block-guard` (see `${CLAUDE_SKILL_DIR}/references/modes/state-block-guard.md`) is a separate, unrelated `Stop` hook that judges the closing state block of a turn. It has no writer (you paste its wiring), it never approves or denies anything, and nothing here places or wires it — the two hooks share only the `.claude/` directory they live in.
44
+ **The kit's other hook.** `state-block-guard` (see `${CLAUDE_SKILL_DIR}/references/modes/state-block-guard.md`) is a separate, unrelated `Stop` hook that judges the closing state block of a turn. Its writer is `tools/state-block-hook.mjs` (the advisor's `state-block` item), it never approves or denies anything, and this mode neither places nor wires it — the two hooks share only the `.claude/` directory they live in and the hardened placement discipline the state-block writer imports from `gate-hook.mjs`.
@@ -17,7 +17,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
17
17
 
18
18
  1. **`--cwd` is REQUIRED** — the target project is explicit, never inferred from the shell's current directory (a subdirectory invocation still advises on the named root).
19
19
  2. **Verdict first (present-even-when-empty, opening at the `## Recommendations (agent-workflow)` header).** With everything optimal the body is exactly `no recommendations — flow optimal.` — the empty-state line ALONE is the verdict, zero added lines. Every other state opens the body with ONE verdict line composed from the frozen templates: `{K} item(s) need attention` leads when any item's severity is `attention` (a CONFIGURED declaration that is broken, drifted, degrading or invalid — the deployment needs review); `{N} optional recommendation(s), apply any you want` covers the `optional` class (offers to enable an unconfigured capability), led by `nothing is broken` ONLY when no item needs attention AND no probe check was skipped (a skipped probe could hide an attention-class problem — the claim never overreaches); `optimality NOT attested — {M} probe check(s) skipped` is appended last whenever probe checks were skipped. (The `(s)` invariant form IS the pinned pluralization — no singular/plural branching.) Items render attention-first, each tagged `needs attention:` / `optional:`. A failed probe renders as a stated `⚠ skipped item …` line — never a crash, never a fabricated item.
20
- 3. **The apply-through-agent lane — an explicit informed-consent checkpoint:** the agent applies chat and harness items on the chat yes, harness items by the consent route below. A chat item is never retried outside the sandbox and every failure of a chat step is a STOP. A user item, and any apply slot that opens with HAND-APPLY whatever its lane, is the user's own step, handed over as worded. After presenting, OFFER to apply; the user selects items in plain language; for each selected item you **surface its posture note inline** where one exists (the per-item notes below cover exactly the risk-marked keys). For a chat item: the user **explicitly confirms**, and **only then** — **no command runs before confirmation** — do you run the rendered command. Do NOT infer safety from the presence or absence of an `--apply` flag: most chat items' rendered command IS the mutation and completes on that one run, **including a no-`--apply` mutation** such as `spec-adoption`'s `ensure-configs --reconcile --only specs` (behind an edited file it prints a replace line that takes its own yes). Some items instead render a **dry-run preview** that changes nothing and prints an explicit follow-up `--apply`/mutating command to run NEXT (e.g. the `gates-declaration` seeder): after the SAME confirmation you run that printed follow-up command — no second ask, no improvisation (each writer keeps its own consent semantics: previews, `--apply` flags, refusals). An item marked **HAND-APPLY** (e.g. `enforcement` or `read-lane.stale`) is **the user's own step** — hand the user its rendered line together with its posture note; HAND-APPLY is **maintainer territory wherever it renders** — the apply slot or the `recipe:` line. For `worktrees-dir` the two classes COMPOSE in one item and the ORDER is part of the contract: its `recipe:` line is the HAND-APPLY grant advice, its apply one-liner is the dry-run ack preview, and you **wait for the maintainer to confirm the grant is applied (or the terminal fallback chosen)** before running that preview and the exact `--apply` command it prints — the ack RECORDS a choice that must already have been made, and against a trusted host NO the item renders no ack at all.
20
+ 3. **The apply-through-agent lane — an explicit informed-consent checkpoint:** the agent applies chat and harness items on the chat yes, harness items by the consent route below. A chat item is never retried outside the sandbox and every failure of a chat step is a STOP. A user item, and any apply slot that opens with HAND-APPLY whatever its lane, is the user's own step, handed over as worded. After presenting, OFFER to apply; the user selects items in plain language; for each selected item you **surface its posture note inline** where one exists (the per-item notes below cover exactly the risk-marked keys). For a chat item: the user **explicitly confirms**, and **only then** — **no command runs before confirmation** — do you run the rendered command. Do NOT infer safety from the presence or absence of an `--apply` flag: most chat items' rendered command IS the mutation and completes on that one run, **including a no-`--apply` mutation** such as `spec-adoption`'s `ensure-configs --reconcile --only specs` (behind an edited file it prints a replace line that takes its own yes). Some items instead render a **dry-run preview** that changes nothing and prints an explicit follow-up `--apply`/mutating command to run NEXT (e.g. the `gates-declaration` seeder): after the SAME confirmation you run that printed follow-up command — no second ask, no improvisation (each writer keeps its own consent semantics: previews, `--apply` flags, refusals). An item marked **HAND-APPLY** (e.g. `enforcement` or `source-size.id-squatter`) is **the user's own step** — hand the user its rendered line together with its posture note; HAND-APPLY is **maintainer territory wherever it renders** — the apply slot or the `recipe:` line. For `worktrees-dir` the two classes COMPOSE in one item and the ORDER is part of the contract: its `recipe:` line is the HAND-APPLY grant advice, its apply one-liner is the dry-run ack preview, and you **wait for the maintainer to confirm the grant is applied (or the terminal fallback chosen)** before running that preview and the exact `--apply` command it prints — the ack RECORDS a choice that must already have been made, and against a trusted host NO the item renders no ack at all.
21
21
  4. Registry strings (benefits + item texts) are frozen tool data, fact-true, ONE line under the pinned shape cap — posture/risk detail lives in the notes below at the consent moment, never inline in the overview. The dual velocity+security wording (`safer — blast radius bounded by the OS sandbox, not human attention`) rides ONLY the items with a real security delta (the autonomy render, the sandbox provisioning); the bridge-wrappers item claims **velocity only**.
22
22
 
23
23
  The **consent route** for a harness item: run its `check:` line in the sandbox and show its result. On the check's `masked:` line, say so and run that same check line once more outside the sandbox, so it re-judges in the apply's own environment; the yes is taken on the outside result. Take the user's yes on that result and run its `apply:` line with that result's digest once, outside the sandbox at once, so the harness asks at the point of action (with the apply ask rule seeded, its own dialog); never another line, never unseen. When velocity-core, kit-tools-tier or bridge-tier is among the user's picks (velocity-core preferred), check, confirm and apply it, or leave it pending, before any other pick's check runs, so its apply seeds the ask rule before the other harness applies. When the harness's auto mode refuses the check or the apply, say so and give the one plain step: leave auto mode for this one action (Shift+Tab in the terminal, the mode indicator in an editor) and say yes, or run `npx @sabaiway/agent-workflow-kit@latest init` in a terminal of your own. On the user's word, run the same line once more when they left auto mode, never another, never unseen; when they ran `init` instead, run the advisor again and offer only what is still pending, each through this route from its check (after a restart `init` asked for, the next session's upgrade does it). When that refusal hits the velocity-core, kit-tools-tier or bridge-tier pick taken first, run no other pick's check until the user answers the plain step. A check that refuses (exit 1) leaves the item unapplied and pending, its result shown; a changed digest (exit 5) applies nothing, shows the new result and asks a new yes. A no, a harness refusal and any other failure of a harness apply leave the item pending with its output shown, and the agent continues. `init`, run in a console from the project's folder, is the route the user may take instead.
@@ -30,13 +30,13 @@ A variant's posture note, where its key has one, is printed by `init` from this
30
30
 
31
31
  - `bridge-tier` — the lane is harness and env-dependent: the agent applies the item on the chat yes after the check, writing the project `.claude/settings.json`; `init` previews `velocity-profile --bridge-tier` and applies it on the terminal y as the route the user may take instead. It wires the USED bridges from the project's effective recipes and host readiness: each used review wrapper's `code`-mode allow rule (a wrapper then runs UNATTENDED: it reads any repo file it is pointed at and sends the assembled payload to its subscription backend), the quoted grounding pre-step rule when `agy-review` is used, `codex-review *` / `agy-review *` in `sandbox.excludedCommands`, and `codex-exec *` where an execute slot delegates (no execution allow rule: it keeps its prompt). It also seeds the used bridges' declared hosts (`networkHosts`) in `sandbox.network.allowedDomains` and resolved state dirs (`writableDirs`), home-relative where under home, in `sandbox.filesystem.allowWrite`. Where the host honours the settings sandbox keys, an excluded wrapper runs outside the sandbox, every sandboxed command of the project can reach the seeded hosts, and every sandboxed command can write the seeded state dirs. The item fires while an allow rule, a `*` exclusion, a host or a state dir of a used bridge is missing, so a project wired before this revision is offered the one re-run. Its `recipe:` line names each missing entry by kind (allow rules, exclusions, hosts, state dirs), then what the execution wrapper `codex-exec` needs where an execute slot delegates, then, when the project settings enable the sandbox, "once added, wired where the host honors the settings sandbox keys", or "host condition unchecked" with the reason.
32
32
 
33
- - `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.
33
+ - `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 MISSING, the item instead surfaces the re-place offer; when it is STALE (an enabled lane over an old hook), the **stale** arm is an attention item on the harness lane: its writer is `gate-hook --apply --reseed`, which replaces the placed hook with the bundled bytes in place, and the agent applies it on the chat yes after the check, whose result names the replaced file with both digests; `init` applies it on the terminal y as the route the user may take instead. Risk profile: a bounded read-only trust-posture extension — no write/exec exposure beyond the audited core.
34
34
  - `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 does not write this worktrees grant 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.
35
35
 
36
36
  - `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's own step is to edit `docs/ai/gates.json` (the exact producer form lives in `${CLAUDE_SKILL_DIR}/references/modes/gates.md`), 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.
37
37
 
38
38
  - `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.
39
- - `gate-hook` — the base arm is the ordinary opt-in wiring offer on the harness lane: the agent applies it on the chat yes after the check; `init` previews and applies it on the terminal yes as the route the user may take instead. 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), the user's own step. 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.
39
+ - `gate-hook` — the base arm is the ordinary opt-in wiring offer on the harness lane: the agent applies it on the chat yes after the check; `init` previews and applies it on the terminal yes as the route the user may take instead. 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 it is a harness item: its writer is `gate-hook --apply --reseed`, which replaces the placed hook with the bundled bytes in place (and wires the entry only while it is unwired), so the agent applies it on the chat yes after the check, whose result names the replaced file with the old and the new digest; `init` applies it on the terminal y as the route the user may take instead. 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: one placed hook file replaced by the bundled runtime; the declaration is never touched.
40
40
  - `enforcement` — copies the bundled `references/scripts/*.mjs` runnable scripts and their tests, then installs the pre-commit hook. It is HAND-APPLY, the user's own step; the offer renders only when `scripts/` is absent or a plain directory contains no bundled basename, so the copy never clobbers an existing file and never follows a link. The hook invokes `node` directly, so a committer without it on PATH gets a loud failure. The installer refuses to overwrite a pre-commit hook it did not write — on that refusal stop and merge the hook by hand (bootstrap step 9); the copied scripts stay in place. Risk profile: local tooling and a Git hook are added only when the maintainer applies the shown command.
41
41
  - `mcp-channel` — the offer is to REGISTER the kit's read-only stdio MCP server in this project on the harness lane: a registration is a command your MCP client will RUN. The agent applies it on the chat yes after the check; `init` previews the exact entry and applies it on the terminal y as the route the user may take instead. What the registration buys: path questions and literal searches become typed tool calls whose arguments are named JSON fields instead of a string handed to a shell, so a pipe, a redirect or a quote inside a pattern or a path stays DATA and is never interpreted — they are perfectly legal bytes to search for, there is simply no shell to read them as operators. Posture: the server is a **read-only child of your client** (path/type/size/line facts and literal search over this project root; no write and no exec API), and like the client itself it runs **outside the Bash sandbox** — the sandbox is not what bounds it, the server's own root containment is. The two allow rules it adds make those two tools promptless and nothing else. The **`.differing`** arm is also a harness item: an `agent-workflow` entry already stands in `.mcp.json` and **structurally differs** from what this kit copy would write (another kit copy, a hand-edited path, an added `env`; the comparison ignores key order, so re-serialized identical bytes are the same registration). The check shows the entry standing in `.mcp.json` and the entry it would write; the chat yes confirms the replacement. `init` previews `mcp --replace` and replaces only on the terminal y. Stated limit: this item does **not** detect a `disabledMcpjsonServers` veto, so a converged `mcp-channel` means *what the mode writes is in place*, not *the client will load it* — see `${CLAUDE_SKILL_DIR}/references/modes/mcp.md` for why that check was subtracted rather than half-built. Risk profile: a new read-only channel your client will launch; no write or exec exposure; a differing registration is replaced only on the checked chat yes or the terminal y and every other declaration is kept.
42
42
  - 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.
@@ -48,7 +48,7 @@ A variant's posture note, where its key has one, is printed by `init` from this
48
48
 
49
49
  **Sandbox lanes (what to DO with the bridge surfaces, per host class):**
50
50
 
51
- The sandbox items state: on native Windows Claude Code runs no sandbox: this item takes no effect there. `sandbox-provision.installable` is env-dependent and carries the user's own step for a sudo install; the bare `sandbox-provision` keeps its current text.
51
+ The sandbox items state: on native Windows Claude Code runs no sandbox: this item takes no effect there. `sandbox-provision.installable` is env-dependent and carries the user's own step for a sudo install; the bare `sandbox-provision` is the user's own step too: the doctor has no install it can run here, so the user runs it in a terminal of their own.
52
52
  Only Claude Code on Linux/WSL2 is measured; Codex, Devin and agy as hosts and macOS are unmeasured.
53
53
  A first session may meet ordinary prompts until the autonomy render lands; a chat apply is promptless only where the host auto-allows sandboxed commands.
54
54
 
@@ -56,6 +56,6 @@ A first session may meet ordinary prompts until the autonomy render lands; a cha
56
56
  - **Harness-managed sandbox** (an IDE/session-imposed sandbox that ignores the settings keys) — a NARROWEST-SCOPE ladder: (1) prefer wrapper/command-SCOPED sandbox rules where the harness offers them (only the review wrappers gain the egress + state-dir writes); (2) a SESSION-scoped allowance (the manifest hosts + the resolved state dirs for the whole session) is an INFORMED WIDENING — it carries the same blast-radius class the `--bridge-tier` keys carry where a host honours them (every sandboxed command in the session gains that egress and write access to those state dirs), acceptable only as the maintainer's deliberate choice for a review-heavy session. (3) Else the per-run consented bypass. The kit writes no shape for this sandbox — the harness's own controls (its scoped rules, a session allowance, its consent at the point of action) govern there; an IDE-managed session sandbox was live-observed (2026-07-11/12) ignoring the settings security keys in BOTH scopes, and whether a session's sandbox honours them is not knowable from the advisor.
57
57
  - Either way the recipe is **observed-minimal, honestly incomplete** — a blocked host names itself at run time; the manifests record what was observed.
58
58
 
59
- **Invariants:** read-only (never writes, never commits, never runs a subscription CLI) · `--cwd` required · present-even-when-empty · verdict-first from frozen templates · probe failures degrade to stated skip lines · chat apply one-liners are cwd-independent (absolute tool paths + a pinned `--cwd`; the ONE exception is the `set-autonomy` item, a conversational skill invocation explicitly labeled *run IN the target project*) · the bare `sandbox-provision` item pins via a `cd <root> &&` prefix — the doctor reads its cwd · harness items carry a check and an apply bound to its digest; user items, including `family-freshness`, carry the user's own step, and no-writer variants keep their text · the used bridges' `sandbox.filesystem.allowWrite` and `sandbox.network.allowedDomains` are seeded only by the consented `velocity-profile --bridge-tier`; the worktrees grant keeps its hand-apply step · the `worktrees-dir` item converges on a declared `allowWrite` entry covering its probed dir, or on its own `worktreesDirAck` lane in `docs/ai/acks.json` (recorded by its apply one-liner, never by the `recipe:` line).
59
+ **Invariants:** read-only (never writes, never commits, never runs a subscription CLI) · `--cwd` required · present-even-when-empty · verdict-first from frozen templates · probe failures degrade to stated skip lines · chat apply one-liners are cwd-independent (absolute tool paths + a pinned `--cwd`; the ONE exception is the `set-autonomy` item, a conversational skill invocation explicitly labeled *run IN the target project*) · the bare `sandbox-provision` item is the user's own step, pinned via a `cd <root> &&` prefix — the doctor reads its cwd · harness items carry a check and an apply bound to its digest; user items, including `family-freshness`, carry the user's own step, and no-writer variants keep their text · the used bridges' `sandbox.filesystem.allowWrite` and `sandbox.network.allowedDomains` are seeded only by the consented `velocity-profile --bridge-tier`; the worktrees grant keeps its hand-apply step · the `worktrees-dir` item converges on a declared `allowWrite` entry covering its probed dir, or on its own `worktreesDirAck` lane in `docs/ai/acks.json` (recorded by its apply one-liner, never by the `recipe:` line).
60
60
 
61
61
  **Exit codes:** `0` report rendered (items or the empty state); `1` error (e.g. `--cwd` is not a directory); `2` usage.
@@ -4,8 +4,9 @@
4
4
 
5
5
  The opt-in **closing-state-block detector**, wired as a Claude Code `Stop` hook. It ships as a
6
6
  **self-contained** runtime at `references/hooks/state-block-guard.mjs` (no kit imports — a placed
7
- copy keeps working if the kit is uninstalled) and it **writes nothing**: this mode is read-only, and
8
- the wiring is yours to paste (see *No writer yet*, below).
7
+ copy keeps working if the kit is uninstalled) and the runtime itself **writes nothing**. Its writer is
8
+ `tools/state-block-hook.mjs`: it places the runtime and wires the `Stop` entry running
9
+ `node "$CLAUDE_PROJECT_DIR/.claude/hooks/state-block-guard.mjs"` (see *The writer*, below).
9
10
 
10
11
  **Read this first — what it is and is not.** A `Stop` hook runs when a turn has already ENDED, so it
11
12
  **cannot un-send the message it judges**. This is **DETECTION, not prevention**. What it buys is
@@ -83,78 +84,58 @@ self-contained, so that edit is local and survives an uninstalled kit. Until it
83
84
  deployment gets **no detection**, silently. That is a real limit, stated rather than hidden, and it is
84
85
  not a configuration you can set today.
85
86
 
86
- **No writer yet — and why, plainly.** Every other placed thing in this kit arrives through a
87
- consent-gated writer. This one does not, deliberately:
88
-
89
- - extending the gate-approval writer (`tools/gate-hook.mjs`) would grow a second placement path
90
- through the family's **highest-blast-radius** component — the one that wires command
91
- auto-approval — for the benefit of a detector;
92
- - a second dedicated writer would duplicate that writer's hardened placement discipline (symlink
93
- refusal, malformed-settings refusal, unknown-script refusal, merge-don't-clobber), which this
94
- project's own DRY rule forbids.
95
-
96
- The wiring is eight lines of JSON, and hand-editing `settings.json` from a paste-ready block is the
97
- form this family already sanctions. When the detector has earned a writer in real use, it gets one.
98
-
99
- **Wiring it (paste-ready, two steps).**
100
-
101
- 1. Copy the runtime into your project (it is self-contained; the copy keeps working without the kit).
102
- **Check before you copy** — this is a plain copy, not a guarded writer, so the safety is yours:
103
-
104
- ```
105
- ls -ld .claude .claude/hooks
106
- ls -l .claude/hooks/state-block-guard.mjs
107
- ```
108
-
109
- Stop if `.claude` or `.claude/hooks` is a **symlink** (a copy would write through it to somewhere
110
- you did not choose), and stop if the target already exists — including a dangling symlink. Only
111
- then:
112
-
113
- ```
114
- mkdir -p .claude/hooks
115
- cp "${CLAUDE_SKILL_DIR}/references/hooks/state-block-guard.mjs" .claude/hooks/state-block-guard.mjs
116
- ```
117
-
118
- To UPDATE an existing copy, `diff` it against the bundled file first and replace it deliberately.
119
- No `cp` flag makes this safe for you: `-n` is not portable in the same way everywhere, and none of
120
- them refuse a symlinked parent directory.
121
-
122
- 2. Merge this into `.claude/settings.json` — **merge, do not clobber.** Three cases, and getting the
123
- third wrong silently disables someone else's hook:
124
- - no `hooks` key → add the whole object below;
125
- - a `hooks` key WITHOUT `Stop` (for example only the gate-approval `PreToolUse` entry) → add the
126
- `Stop` key beside it;
127
- - a `hooks.Stop` array that ALREADY EXISTS → **append one element to that array**, never replace
128
- it and never write a second `Stop` key. If `hooks.Stop` is present but is not an array, stop and
129
- fix that by hand first — pasting into it would destroy whatever is there.
130
-
131
- ```json
132
- {
133
- "hooks": {
134
- "Stop": [
135
- {
136
- "hooks": [
137
- {
138
- "type": "command",
139
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/state-block-guard.mjs\"",
140
- "timeout": 10
141
- }
142
- ]
143
- }
144
- ]
145
- }
146
- }
147
- ```
148
-
149
- **Add `--require-block` to that command IF your project requires the three-part block on every
150
- message** — then a turn that drops the block entirely is reported too. It is left OFF above on
151
- purpose: this kit does not mandate the block, so switching it on for a project that never adopted it
87
+ **The writer — `tools/state-block-hook.mjs`.** The `state-block` advisor item is a harness item:
88
+ the agent applies it on the chat yes after the danger check, and `init`, run in a console from the
89
+ project's folder, applies it on the terminal y as the route the user may take instead. Without
90
+ `--apply` the writer is a preview that writes nothing; `--apply` writes. It imports the gate-approval
91
+ writer's symlink, target and settings-shape checks from `tools/gate-hook.mjs`; its unsafe-mode and
92
+ stamp refusals use that writer's error codes:
93
+
94
+ - the runtime goes to `.claude/hooks/state-block-guard.mjs` — created when absent, kept when
95
+ byte-equal, replaced in place when it differs (the preview says so, and the check's result names
96
+ the replaced bytes' digest beside the new one);
97
+ - the `Stop` entry is merged into `.claude/settings.json` by the three cases — no `hooks` key: the
98
+ whole object below is added; a `hooks` key without `Stop`: the `Stop` key is added beside it; a
99
+ `hooks.Stop` array: one element is appended, every foreign entry kept. A `hooks.Stop` that is
100
+ present and not an array is a refusal with nothing written;
101
+ - the entry is merged only while no `Stop` entry of either settings file runs the runtime, so a
102
+ wiring you made yourself (another path, `--require-block`) counts and is never duplicated;
103
+ - a symlinked `.claude`, `.claude/hooks`, target or settings file, malformed settings and an unsafe
104
+ permission mode each refuse with nothing written; a deployment stamp behind the kit refuses
105
+ `--apply` (the preview still answers and notes it); the runtime is written through gate-hook's
106
+ `writeHookFile` (an exclusive create, a no-follow replace), so a runtime file swapped for a symlink
107
+ before the write refuses with nothing written through it, and where Node exposes no no-follow open flag
108
+ (native Windows) a replace refuses; the placed file is re-read, no-follow, before the settings write.
109
+
110
+ The entry it writes:
111
+
112
+ ```json
113
+ {
114
+ "hooks": {
115
+ "Stop": [
116
+ {
117
+ "hooks": [
118
+ {
119
+ "type": "command",
120
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/state-block-guard.mjs\"",
121
+ "timeout": 10
122
+ }
123
+ ]
124
+ }
125
+ ]
126
+ }
127
+ }
128
+ ```
129
+
130
+ **The writer offers that command without `--require-block`; add it to the command in your
131
+ `.claude/settings.json` IF your project requires the three-part block on every message** — then a
132
+ turn that drops the block entirely is reported too. It is left OFF on purpose: this kit does not mandate the block, so switching it on for a project that never adopted it
152
133
  would warn after nearly every turn. `--require-block` is the **only** argument accepted: an
153
134
  unrecognised one makes the guard refuse to judge the turn and say so, rather than quietly running in
154
135
  the weaker mode you did not choose.
155
136
 
156
- A `Stop` hook is read at session start, so it becomes live in the **next** session, not the current
157
- one.
137
+ Claude Code reloads its settings files when they change, so a wired `Stop` hook runs from the next
138
+ turn end — no session restart needed.
158
139
 
159
140
  **Trust posture (state it plainly when asking consent):** the guard reads the closing assistant
160
141
  message of each turn and writes one warning line. It runs a Node process on every turn end, it does
@@ -15,6 +15,7 @@ import { KIT_GROUNDING_TOOL, bundledSandboxRecipe, usedBridges } from './bridge-
15
15
  import { mergeAllowWrite, stateDirsOf } from './bridge-state-dirs.mjs';
16
16
  import { loadConfig } from './orchestration-config.mjs';
17
17
  import { HOOK_FILE_REL, HOOKS_DIR, buildHookSettingsEntry, mergeHookEntry, writeGateHook } from './gate-hook.mjs';
18
+ import { STATE_BLOCK_HOOK_FILE_REL, buildStopHookEntry, mergeStopEntry, writeStateBlockHook } from './state-block-hook.mjs';
18
19
  import { writeMcp } from './mcp.mjs';
19
20
  import { ENABLED_KEY, MCP_JSON_REL, SERVERS_KEY, SERVER_NAME, allowRulesFor, buildServerEntry } from './mcp-registration.mjs';
20
21
  import { AGENTS_DIR, writeCheapAgents } from './cheap-agents.mjs';
@@ -28,10 +29,14 @@ import { dirCovers as covers } from './declared-paths.mjs';
28
29
  const TOOLS = dirname(fileURLToPath(import.meta.url));
29
30
  const SKILL_VARIANTS = ['jev-skill', 'jev-skill.earlier'];
30
31
  const VELOCITY_VARIANTS = ['velocity-core', 'kit-tools-tier', 'bridge-tier'];
31
- const HOOK_VARIANTS = ['gate-hook', 'read-lane.missing'];
32
+ const RESEED_VARIANTS = ['gate-hook.marker-stale', 'read-lane.stale'];
33
+ const HOOK_VARIANTS = ['gate-hook', 'read-lane.missing', ...RESEED_VARIANTS];
34
+ const STOP = 'state-block';
32
35
  const MCP_VARIANTS = ['mcp-channel', 'mcp-channel.differing'];
33
36
  const AGENT_VARIANTS = ['agents', 'executor-vehicle'];
34
- const VARIANTS = [...VELOCITY_VARIANTS, 'autonomy-render', ...HOOK_VARIANTS, ...MCP_VARIANTS, ...AGENT_VARIANTS, ...SKILL_VARIANTS];
37
+ const VARIANTS = [...VELOCITY_VARIANTS, 'autonomy-render', ...HOOK_VARIANTS, STOP, ...MCP_VARIANTS, ...AGENT_VARIANTS,
38
+ ...SKILL_VARIANTS];
39
+ const HOOK_KEYS = ['hooks.PreToolUse', 'hooks.Stop'];
35
40
  const CREDENTIALS = 'sandbox.credentials.envVars';
36
41
  const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
37
42
  const stable = (value) => object(value)
@@ -56,6 +61,7 @@ export const KEY_TABLE = Object.freeze({
56
61
  'permissions.deny': 3,
57
62
  'sandbox.enabled': 3,
58
63
  'hooks.PreToolUse': 4,
64
+ 'hooks.Stop': 4,
59
65
  [ENABLED_KEY]: 4,
60
66
  [SERVERS_KEY]: 4,
61
67
  [CREDENTIALS]: 5,
@@ -67,7 +73,7 @@ const valid = (key, value, element = false) => {
67
73
  if (SCALARS.includes(key)) return typeof value === 'boolean';
68
74
  if (!element) return Array.isArray(value) && value.every((entry) => valid(key, entry, true));
69
75
  if (key === CREDENTIALS) return object(value) && typeof value.name === 'string' && typeof value.mode === 'string';
70
- return key === 'hooks.PreToolUse' ? object(value) : typeof value === 'string';
76
+ return HOOK_KEYS.includes(key) ? object(value) : typeof value === 'string';
71
77
  };
72
78
  const keyOf = (key) => key.startsWith(`${SERVERS_KEY}.`) ? SERVERS_KEY : key;
73
79
 
@@ -174,6 +180,7 @@ export const scopeOf = (variant, facts, deps = {}) => {
174
180
  set('permissions.deny', RENDER_OWNED_REDLINE_RULES);
175
181
  allowed[CREDENTIALS] = (value) => value.mode === 'deny';
176
182
  } else if (HOOK_VARIANTS.includes(variant)) set('hooks.PreToolUse', [buildHookSettingsEntry()]);
183
+ else if (variant === STOP) set('hooks.Stop', [buildStopHookEntry()]);
177
184
  else if (MCP_VARIANTS.includes(variant)) {
178
185
  set(`${SERVERS_KEY}.${SERVER_NAME}`, [buildServerEntry(join(facts.toolsDir, 'mcp-server.mjs'))]);
179
186
  set(ENABLED_KEY, [SERVER_NAME]);
@@ -181,11 +188,10 @@ export const scopeOf = (variant, facts, deps = {}) => {
181
188
  }
182
189
  const targets = SKILL_VARIANTS.includes(variant) ? skillTargets({ home: facts.home,
183
190
  claudeDir: facts.claudeDir ?? claudeDirOf(facts.env, facts.home, facts.platform).dir }) : [];
184
- const filePaths = HOOK_VARIANTS.includes(variant) ? [join(facts.root, HOOK_FILE_REL)] : [];
185
- const directories = VELOCITY_VARIANTS.includes(variant) || variant === 'autonomy-render'
186
- || HOOK_VARIANTS.includes(variant) || MCP_VARIANTS.includes(variant) || AGENT_VARIANTS.includes(variant)
187
- ? [join(facts.root, CLAUDE_DIR)] : [];
188
- if (HOOK_VARIANTS.includes(variant)) directories.push(join(facts.root, HOOKS_DIR));
191
+ const filePaths = HOOK_VARIANTS.includes(variant) ? [join(facts.root, HOOK_FILE_REL)]
192
+ : variant === STOP ? [join(facts.root, STATE_BLOCK_HOOK_FILE_REL)] : [];
193
+ const directories = SKILL_VARIANTS.includes(variant) ? [] : [join(facts.root, CLAUDE_DIR)];
194
+ if (HOOK_VARIANTS.includes(variant) || variant === STOP) directories.push(join(facts.root, HOOKS_DIR));
189
195
  if (AGENT_VARIANTS.includes(variant)) directories.push(join(facts.root, AGENTS_DIR));
190
196
  for (const target of targets) {
191
197
  for (let at = target.root; at !== target.base && covers(target.base, at); at = dirname(at)) directories.push(at);
@@ -194,7 +200,7 @@ export const scopeOf = (variant, facts, deps = {}) => {
194
200
  return {
195
201
  admits: ({ path, key, change, value }) => {
196
202
  const correctFile = key.startsWith(`${SERVERS_KEY}.`) ? path === MCP_JSON_REL : path === SETTINGS_FILE;
197
- const gains = variant === 'bridge-tier' || key === 'hooks.PreToolUse' || key === ENABLED_KEY || key === CREDENTIALS
203
+ const gains = variant === 'bridge-tier' || HOOK_KEYS.includes(key) || key === ENABLED_KEY || key === CREDENTIALS
198
204
  || (VELOCITY_VARIANTS.includes(variant) && key === 'permissions.ask');
199
205
  return correctFile && (!gains || change === 'added') && Boolean(allowed[key]?.(value));
200
206
  },
@@ -207,7 +213,7 @@ const jsonAt = (path) => existsSync(path) ? JSON.parse(readFileSync(path, 'utf8'
207
213
  const halted = (lines) => Object.assign(new Error(lines.join('\n')), { code: 'CHECK_HALTED', lines });
208
214
  const maskedFiles = (variant, facts) => {
209
215
  const paths = VELOCITY_VARIANTS.includes(variant) || variant === 'autonomy-render' || HOOK_VARIANTS.includes(variant)
210
- ? [SETTINGS_FILE, SETTINGS_LOCAL_FILE] : MCP_VARIANTS.includes(variant) ? [MCP_JSON_REL, SETTINGS_FILE] : [];
216
+ || variant === STOP ? [SETTINGS_FILE, SETTINGS_LOCAL_FILE] : MCP_VARIANTS.includes(variant) ? [MCP_JSON_REL, SETTINGS_FILE] : [];
211
217
  return paths.flatMap((path) => lstatSync(join(facts.root, path), { throwIfNoEntry: false })?.isCharacterDevice()
212
218
  ? [`masked: ${path}`] : []);
213
219
  };
@@ -243,9 +249,17 @@ export const plannedChange = (variant, facts, deps = {}) => {
243
249
  const before = jsonAt(join(facts.root, SETTINGS_FILE));
244
250
  addFile(SETTINGS_FILE, before, mergeAutonomySettings(before, render));
245
251
  } else if (HOOK_VARIANTS.includes(variant)) {
246
- const plan = writeGateHook({ cwd: facts.root, dryRun: true }, writerDeps);
252
+ const plan = writeGateHook({ cwd: facts.root, dryRun: true, reseed: RESEED_VARIANTS.includes(variant) }, writerDeps);
247
253
  if (plan.wirePlanned) addFile(SETTINGS_FILE, plan.projectSettings.data, mergeHookEntry(plan.projectSettings.data));
248
- if (plan.placePlanned) writes.push({ path: join(facts.root, HOOK_FILE_REL), bytes: plan.bundleContent });
254
+ const path = join(facts.root, HOOK_FILE_REL);
255
+ if (plan.placePlanned) writes.push({ path, bytes: plan.bundleContent });
256
+ if (plan.reseedPlanned) writes.push({ path, bytes: plan.bundleContent, replaces: readFileSync(path) });
257
+ } else if (variant === STOP) {
258
+ const plan = writeStateBlockHook({ cwd: facts.root, dryRun: true }, writerDeps);
259
+ if (plan.mergePlanned) addFile(SETTINGS_FILE, plan.projectSettings.data, mergeStopEntry(plan.projectSettings.data));
260
+ const path = join(facts.root, STATE_BLOCK_HOOK_FILE_REL);
261
+ if (plan.placement === 'create') writes.push({ path, bytes: plan.bundleContent });
262
+ if (plan.placement === 'replace') writes.push({ path, bytes: plan.bundleContent, replaces: readFileSync(path) });
249
263
  } else if (MCP_VARIANTS.includes(variant)) {
250
264
  const result = writeMcp({ cwd: facts.root, dryRun: true, replace: variant === 'mcp-channel.differing' }, writerDeps);
251
265
  const { plan, registration } = result;
@@ -298,7 +312,10 @@ export const checkApply = (variant, facts, deps = {}) => {
298
312
  if (!scope.admitsPath(path, directory)) refused.push({ path: display(path), key: display(path),
299
313
  reason: `beyond the declared scope of ${variant}` });
300
314
  };
301
- for (const write of plan.writes) pathLine(write.path, false, `writes: ${display(write.path)} sha256:${sha(write.bytes)}`);
315
+ for (const write of plan.writes) {
316
+ const replaces = write.replaces === undefined ? '' : ` — replaces sha256:${sha(write.replaces)}`;
317
+ pathLine(write.path, false, `writes: ${display(write.path)} sha256:${sha(write.bytes)}${replaces}`);
318
+ }
302
319
  for (const path of plan.directories) pathLine(path, true, `writes: ${display(path)}/ created`);
303
320
  lines.push(...plan.temporaries);
304
321
  if (refused.length) {
@@ -165,8 +165,8 @@ const CATALOG = [
165
165
  key: 'state-block-guard',
166
166
  invocation: invocationOf('state-block-guard'),
167
167
  group: 'Configure',
168
- kind: READ_ONLY,
169
- oneLine: 'Warn when a turn ends on a defective closing state block — a "nothing needed from you" answer or a promise of work that never started (Claude Code Stop hook; detection, never prevention; you paste the wiring).',
168
+ kind: WRITER,
169
+ oneLine: 'Warn when a turn ends on a defective closing state block — a "nothing needed from you" answer or a promise of work that never started (Claude Code Stop hook; detection, never prevention; its writer places and wires it on your chat yes).',
170
170
  },
171
171
  {
172
172
  key: 'bridge-settings',