@sabaiway/agent-workflow-kit 3.15.0 → 4.0.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 (64) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/README.md +3 -3
  3. package/SKILL.md +1 -1
  4. package/bridges/antigravity-cli-bridge/SKILL.md +12 -8
  5. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +735 -55
  6. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +899 -51
  7. package/bridges/antigravity-cli-bridge/bin/agy.sh +4 -3
  8. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +23 -0
  9. package/bridges/antigravity-cli-bridge/capability.json +14 -4
  10. package/bridges/antigravity-cli-bridge/references/driving-agy.md +12 -4
  11. package/bridges/antigravity-cli-bridge/references/models-and-flags.md +4 -3
  12. package/bridges/antigravity-cli-bridge/references/review-prompt.md +65 -2
  13. package/bridges/codex-cli-bridge/SKILL.md +1 -1
  14. package/bridges/codex-cli-bridge/bin/codex-exec.sh +2 -1
  15. package/bridges/codex-cli-bridge/bin/codex-review.sh +63 -13
  16. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +38 -0
  17. package/bridges/codex-cli-bridge/capability.json +1 -1
  18. package/capability.json +1 -1
  19. package/package.json +1 -1
  20. package/references/agents/review-lens.md +39 -0
  21. package/references/hooks/gate-approve.mjs +15 -0
  22. package/references/modes/agents.md +11 -2
  23. package/references/modes/autonomy-doctor.md +2 -0
  24. package/references/modes/backends.md +2 -0
  25. package/references/modes/bootstrap.md +2 -0
  26. package/references/modes/bridge-settings.md +4 -1
  27. package/references/modes/commit-guard.md +2 -0
  28. package/references/modes/core-evidence.md +2 -0
  29. package/references/modes/coverage-check.md +2 -0
  30. package/references/modes/doc-parity.md +2 -0
  31. package/references/modes/gates.md +2 -0
  32. package/references/modes/grounding.md +2 -0
  33. package/references/modes/help.md +2 -0
  34. package/references/modes/hook.md +4 -1
  35. package/references/modes/migrate-adr-store.md +2 -0
  36. package/references/modes/procedures.md +2 -0
  37. package/references/modes/recipes.md +2 -0
  38. package/references/modes/recommendations.md +4 -3
  39. package/references/modes/review-state.md +2 -0
  40. package/references/modes/sandbox-masks.md +2 -0
  41. package/references/modes/set-autonomy.md +2 -0
  42. package/references/modes/set-recipe.md +3 -0
  43. package/references/modes/setup.md +2 -0
  44. package/references/modes/state-block-guard.md +2 -0
  45. package/references/modes/status.md +4 -1
  46. package/references/modes/uninstall.md +2 -0
  47. package/references/modes/upgrade.md +2 -0
  48. package/references/modes/velocity.md +7 -0
  49. package/references/modes/worktrees.md +2 -0
  50. package/tools/bridge-settings-read.mjs +40 -10
  51. package/tools/bridge-settings.mjs +22 -7
  52. package/tools/cheap-agents.mjs +15 -5
  53. package/tools/commands.mjs +2 -2
  54. package/tools/core-evidence.mjs +29 -2
  55. package/tools/detect-backends.mjs +1 -1
  56. package/tools/manifest/schema.md +7 -0
  57. package/tools/manifest/validate.mjs +8 -0
  58. package/tools/presentation.mjs +1 -1
  59. package/tools/procedures.mjs +9 -2
  60. package/tools/recipes.mjs +4 -1
  61. package/tools/recommendations.mjs +110 -59
  62. package/tools/renderers.mjs +10 -1
  63. package/tools/review-state.mjs +4 -0
  64. package/tools/view-model.mjs +3 -1
@@ -1,5 +1,7 @@
1
1
  ### Mode: bridge-settings
2
2
 
3
+ <!-- opt-in-capability: codex-fast -->
4
+
3
5
  The reader + consent-gated **writer** for the **host-level** bridge settings file — the answer to *"turn on the codex Fast tier (or another bridge knob) once, predictably, so it survives kit upgrades."* The four bridge wrappers read `${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf` (`KEY=VALUE` lines, **parsed never sourced**); this is the ONLY writer for it. The file lives **outside every kit-managed tree**, so a kit refresh never writes or clobbers it — upgrade-survival is structural (D2). It **previews by default**; `--apply` writes. Hand-editing the file stays fully supported — this is an offered convenience, never a lock.
4
6
 
5
7
  **The knobs are the bundled bridges' own `settings` blocks** (manifest-as-source, D6) — the tool never invents a key or a value rule, and what it writes always passes the wrappers' own validation. Model/effort are **NOT** settable here (the wrappers' quality-first guard is untouched, D4). Run **`node ${CLAUDE_SKILL_DIR}/tools/bridge-settings.mjs`** to see the live list; today:
@@ -10,7 +12,8 @@ The reader + consent-gated **writer** for the **host-level** bridge settings fil
10
12
  | `CODEX_HARD_TIMEOUT` | codex | integer `1..86400` | hard wall-clock cap (seconds) via `timeout(1)`. |
11
13
  | `CODEX_REVIEW_MAX_TOTAL_BYTES` | codex | integer `1..100000000` | codex-review payload size above which the diff rides a temp file (never truncated). |
12
14
  | `AGY_HARD_TIMEOUT` | agy | duration `5m`/`30m`/`90s` (unit required, nonzero) | hard wall-clock cap via `timeout(1)`. |
13
- | `AGY_REVIEW_ALLOW_ADDDIR` | agy | `0` \| `1` | `1` re-enables the oversized-review `--add-dir` offload (Issue-001 stall risk, bounded by the timeout). |
15
+ | `AGY_REVIEW_MAX_TOTAL_BYTES` | agy | integer `1..100000000` | the ceiling on the SUM of all outgoing prompt bytes an oversized `agy-review code` may feed (default 240000); past it the fed review refuses **before** spending turn 1. |
16
+ | `AGY_REVIEW_ALLOW_ADDDIR` | agy | `0` \| `1` | **RETIRED** — still recognized (an existing line never warns as unknown) but it **arms nothing**; the writer refuses a new `--set` and `--unset` clears it. Headless agy auto-denies its own `read_file`, so the offload it armed could return a confident fabrication; an oversized code review is a **chunked feed with a per-part delivery proof** now. |
14
17
 
15
18
  **Invocations:**
16
19
 
@@ -1,5 +1,7 @@
1
1
  ### Mode: commit-guard
2
2
 
3
+ <!-- opt-in-capability: commit-guard -->
4
+
3
5
  The **read-only pre-commit guard** (strip-the-kit D10) — the last line of the loop: a commit is permitted only against the LATEST completed `run-gates --final` receipt that binds EXACTLY the current tree. It re-runs NO gate or test subprocess — the heavy D3 verification lives in the final run; the guard recomputes the tree fingerprint (read-only git plumbing) and compares file contents.
4
6
 
5
7
  Run `node ${CLAUDE_SKILL_DIR}/tools/commit-guard.mjs --check [--cwd <dir>]` — it refuses, each with a named recovery, on:
@@ -1,5 +1,7 @@
1
1
  ### Mode: core-evidence
2
2
 
3
+ <!-- opt-in-capability: none — an on-demand evidence writer that leaves no persistent opt-in state -->
4
+
3
5
  The ONE **loop-evidence writer** (strip-the-kit D6a/D7) — every core evidence record lands in a single append-only JSONL store **inside the git dir** (`<git dir>/agent-workflow-core-evidence.jsonl` — never committable by construction; `AW_CORE_EVIDENCE` overrides, a test seam). Records carry a versioned schema and a per-kind KEY; the **LATEST record per key is authoritative** (supersession, file order); a byte-identical replayed line is refused as a duplicate; a malformed line fails every reader CLOSED. The store holds four kinds: `red-proof` and `degrade` (written HERE), plus `final-start`/`final` (written only by `run-gates --final` — the same sole-writer module, a different entry point).
4
6
 
5
7
  Run `node ${CLAUDE_SKILL_DIR}/tools/core-evidence.mjs <verb> …`:
@@ -1,5 +1,7 @@
1
1
  ### Mode: coverage-check
2
2
 
3
+ <!-- opt-in-capability: none — a read-only gate entry; declaring the gate matrix is covered by gates-declaration -->
4
+
3
5
  The **final-run checker** (strip-the-kit D3(c)+(d)) — two deterministic arms over ONE fixed artifact path, every refusal naming its locations `file:line`, never a bare count. It reads the lcov file the declared `unit-tests` gate cmd produced at `<git dir>/agent-workflow-lcov.info` (the constant this tool exports; `AW_LCOV_FILE` overrides, a test seam; the git dir is outside the fingerprint domain and never committable by construction) and prints `coverage-check: lcov-sha256=<hex|none>` — the sha of the EXACT bytes it consumed, which the `run-gates --final` receipt binds and re-hashes (exactly ONE such line attests).
4
6
 
5
7
  Run `node ${CLAUDE_SKILL_DIR}/tools/coverage-check.mjs --check [--cwd <dir>]`:
@@ -1,5 +1,7 @@
1
1
  ### Mode: doc-parity
2
2
 
3
+ <!-- opt-in-capability: none — a read-only gate entry; declaring the gate matrix is covered by gates-declaration -->
4
+
3
5
  The DOC-PARITY lint (BUGFREE-3 / AD-049, session-economics item (b)) — the deterministic doc-drift killer. A recurring class of review churn came from a mode-contract doc silently lagging a code constant (a `--check` doc still reading "300" after the diff cap moved to 400, caught only in a later review round). This read-only tool closes that class mechanically: a **closed, exported registry** ties each live code constant to the exact token its `references/modes/*.md` contract must carry, and asserts the CURRENT value renders into every bound file.
4
6
 
5
7
  **Run** — `node ${CLAUDE_SKILL_DIR}/tools/doc-parity.mjs [--check | --json]`:
@@ -1,5 +1,7 @@
1
1
  ### Mode: gates
2
2
 
3
+ <!-- opt-in-capability: gates-declaration -->
4
+
3
5
  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**). **`--final`** is the ONE writing mode: it runs the FULL declared matrix as the D3(a) final verification run and mints the receipt the commit guard consumes (step 4).
4
6
 
5
7
  Run `node ${CLAUDE_SKILL_DIR}/tools/run-gates.mjs [--cwd <project>] [--only <id>]… [--final]`:
@@ -1,5 +1,7 @@
1
1
  ### Mode: grounding
2
2
 
3
+ <!-- opt-in-capability: none — an on-demand assembler that leaves no persistent state the advisor could observe -->
4
+
3
5
  The **grounded-review facts assembler** (AD-038) — an ungrounded `agy` review GUESSES, and while the grounding contract is mechanized (`agy-review code --facts @f`), populating the facts file was a manual chore. This mode emits the **mechanical** facts sources of a payload (constraints / autonomy / plan / ledger digest); the orchestrator still owns any judgment-bearing additions. **Catalogued honestly as a WRITER** — `--out <path>` writes one file — with the invariant: `--out` accepts **only system-temp destinations outside the repo ($TMPDIR / /tmp — rewritable scratch) or a FRESH gitignored in-repo path (create-only, exclusive write)** and REFUSES everything else: a tracked path, an in-repo not-ignored path (a new untracked file would itself move the review fingerprint the facts are about to ground), an EXISTING in-repo file even when gitignored (the `.env` clobber class — this writer is bridge-tier auto-allowable), any other outside-repo destination, and symlink/non-regular leaves; **stdout is the default**. It never commits and never runs a subscription CLI.
4
6
 
5
7
  Run `node ${CLAUDE_SKILL_DIR}/tools/grounding.mjs [--constraints] [--autonomy] [--plan <path>] [--reserve-bytes <n>] [--out <path>]`:
@@ -1,5 +1,7 @@
1
1
  ### Mode: help
2
2
 
3
+ <!-- opt-in-capability: none — an inspection surface that configures nothing -->
4
+
3
5
  Read-only. The single discoverable **command index** — it answers *"what can `/agent-workflow-kit` do, and which commands change things?"* It **never writes, never commits, and never runs a subscription CLI**.
4
6
 
5
7
  Run `node ${CLAUDE_SKILL_DIR}/tools/commands.mjs` (add `--json` for the machine-readable catalog) and present its grouped index — **Inspect / Configure / Orchestrate / Lifecycle**, each command tagged **read-only / writer / guarded / runs-project-commands** (the last: the kit writes nothing, but the mode executes the project's OWN declared commands — the `gates` runner) — in the user's conversational language. That catalog is the **single source of truth** for the command surface (the same one the bootstrap / upgrade report footers point at); `routeInvocation(token)` in the same file is the executable contract for which invocation maps to which mode.
@@ -1,11 +1,14 @@
1
1
  ### Mode: hook
2
2
 
3
+ <!-- opt-in-capability: gate-hook -->
4
+ <!-- opt-in-capability: read-lane -->
5
+
3
6
  The opt-in **gate-approval PreToolUse hook** — the family's third `.claude/` writer (velocity discipline), and the shipped closure of the velocity trust-posture residual (**AD-021 scope C, probe-proven in AD-037**). It places a **self-contained** hook runtime at `.claude/hooks/agent-workflow-gates.mjs` (no kit imports — it keeps working if the kit is uninstalled) and wires ONE `PreToolUse` `"Bash"` entry into `.claude/settings.json`. Per Bash call the hook then walks a decision ladder, first match wins:
4
7
 
5
8
  - **Auto-approve** a command **byte-identical** (leading/trailing trim only — no whitespace collapsing, no quote/glob/variable interpretation, no prefix or pattern matching, ever: patterns are what made AD-021 auto-seeding rejected) to a gate `cmd` declared in `docs/ai/gates.json` — read **LIVE on every call** (editing gates.json never needs re-wiring; one declaration, two consumers with `${CLAUDE_SKILL_DIR}/references/modes/gates.md`) — invoked **from the project root** (gates run from the root by contract; the same bytes from a subdirectory are NOT approved) and under `default`/`acceptEdits` permission mode (an approval never loosens `plan`/`bypassPermissions`).
6
9
  - **Ask** on a command whose leading tokens match the velocity **seeded read-only core** when it carries the documented runtime residual — output redirection, command substitution, or the bounded `--output` write-flag family — surfacing a human prompt even where a seeded allow rule would have silently approved (**hook `ask` overrides an allow rule — proven live**: on Claude Code 2.1.185 a seeded `Bash(git log:*)` silently wrote a file via `git log --output=…`; with the hook wired the same call prompts). Detection is string-level and conservative: a quoted metacharacter may over-ask, never under-allow.
7
10
  - **Auto-approve a read-only COMPOUND** (opt-in — dark unless enabled): when `docs/ai/lanes.json` sets `{ "readLane": true }` (read **LIVE on every call**), a command whose EVERY separator-split segment is a plain seeded read-only core command, carrying **zero shell metaprogramming** anywhere (no `$`/expansion, quoting, backslash, brace, glob, redirection, substitution, `--output`, env-assignment prefix, or backgrounding), is auto-approved — a conservative **closed-world** allow (any doubt falls through, never a widening). Mode-fenced like gate auto-approval; **cwd-agnostic** (a read is a read from any directory). It runs AFTER the residual ask, so a core command that carries a residual still prompts (most-restrictive-wins). The lane is **bounded by the frozen audited read-only core** (the set velocity seeds) — a **standalone opt-in grant**, never a command OUTSIDE that audited core; enabling it auto-approves compounds (and singles) of that audited core **regardless of which of those commands you seeded** as individual settings rules (that is the trust the opt-in consent covers — not strictly a subset of your current settings).
8
- - **Stay silent otherwise** — the normal permission flow proceeds unchanged. The gate-approval hook **never emits `deny`**; nothing is hard-blocked.
11
+ - **Stay silent otherwise** — the normal permission flow proceeds unchanged. The gate-approval hook **never emits `deny`**; nothing is hard-blocked. A deny rung was built for kit 4.0.0 and REMOVED before release: it refused only a seeded read that provably discarded its output (`2>/dev/null`), and three council rounds found five shell constructs that defeated the byte-level discard PROOF (`1<&2`, a quoted literal `>/dev/null `, leading-token-only segment matching, a bare `&`, a `#` comment) — each one a FALSE REFUSAL. On an ask rung an incomplete scan merely over-asks; on a deny rung the same incompleteness refuses real work, and telling an operator from text needs a shell lexer this hook deliberately does not carry. Design record + all five counterexamples: the family's queue, `BARE-LANE-DENY-RUNG`.
9
12
 
10
13
  **Honest residual status (AD-037):** current engine builds already intercept `>` redirection and `$()` substitution upstream (observed headless on 2.1.185); the **`--output` family was proven open** and is the seam this hook demonstrably closes. The guard still covers all three documented classes (defense-in-depth — engine behavior may vary across surfaces/versions). **Fail-safe, decoupled:** a missing/broken/invalid `gates.json` disables ONLY gate auto-approval — the residual guard keeps running; every anomaly path exits 0 (the hook is never the blocker or the noise — the `gates` runner reports a broken declaration at its own point of use). **Not a sandbox:** it closes the named residual for the seeded core and auto-approves declared gates; it does not police arbitrary commands or user-added rules.
11
14
 
@@ -1,5 +1,7 @@
1
1
  ### Mode: migrate-adr-store
2
2
 
3
+ <!-- opt-in-capability: none — the old-layout condition is already reported by status and upgrade; the advisor does not duplicate a report the same run prints -->
4
+
3
5
  The **guarded ADR-store migration** — a one-time, opt-in move of an existing project's `docs/ai` from the retired 3-tier ADR cascade (HOT `decisions.md` → the WARM/COLD `decisions-archive*.md` monoliths) to the durable **one-file-per-ADR store** (HOT `decisions.md` + `docs/ai/adr/AD-NNN-slug.md` records + the `docs/ai/adr/log.md` navigator). **In-agent, opt-in**, and reached ONLY here: a normal `upgrade` never installs the new-scheme rotator into an un-migrated project — the new rotator arrives ONLY through this mode, which migrates in the same step (AD-051). Run **`--dry-run` first, always**, show the user the plan in plain language, get explicit consent, then re-run with `--apply`. It **never commits**.
4
6
 
5
7
  When to run it: `status` (or `upgrade`) reports an *old ADR layout* (a `decisions-archive*.md` monolith is still on disk). A project already on the one-file-per-ADR store — or a fresh deployment seeded with it — needs nothing here (the mode is a stated no-op).
@@ -1,5 +1,7 @@
1
1
  ### Mode: procedures
2
2
 
3
+ <!-- opt-in-capability: none — an inspection surface that configures nothing -->
4
+
3
5
  Read-only **activity-procedures advisor**. Answers *"what are the steps of this named activity, and which recipe applies at each slot here?"* It composes the orchestration recipes (`${CLAUDE_SKILL_DIR}/references/modes/recipes.md`) into **named activities** with **typed recipe slots**. It **never writes, never commits, never runs a subscription CLI** — the deterministic resolution lives in the kit; the orchestrator runs the resolved recipe via the bridge skills and **owns any commit when the activity has a commit boundary** (a backend never commits). Not every activity commits: `plan-authoring` ends at approval and produces no commit (plans are ephemeral, never committed); `plan-execution` commits per Step.
4
6
 
5
7
  The two v1 activities (canon in the **installed engine**, `references/procedures.md`):
@@ -1,5 +1,7 @@
1
1
  ### Mode: recipes
2
2
 
3
+ <!-- opt-in-capability: none — an inspection surface that configures nothing -->
4
+
3
5
  Read-only **orchestration advisor**. Answers *"how should I compose the optional execution-backends into plan → execute → review here, and which recipe fits?"* It **never writes, never commits, never runs a subscription CLI, and never executes a recipe** — the orchestrator (you) runs the chosen recipe through the bridge skills and makes the single commit; a backend is advisory or delegated, never autonomous.
4
6
 
5
7
  The four recipes (defined over each bridge's `provides` roles — `codex`: execute + review; `agy`: review + probe), canonical narrative in the **installed engine** (`references/orchestration.md`):
@@ -1,8 +1,10 @@
1
1
  ### Mode: recommendations
2
2
 
3
+ <!-- opt-in-capability: none — this IS the advisor; an offer to enable it would be circular -->
4
+
3
5
  Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md
4
6
 
5
- The **read-only deployment advisor** — the deterministic section every `upgrade` run ends with, also invocable on its own. It computes what in THIS deployment is configured sub-optimally (allowlist not seeded, autonomy render drifted, sandbox unavailable, gates undeclared, bridge friction, sandbox-mask clutter, an unacknowledged sandbox recipe) and renders **verdict-first**: one composed verdict line, then each item as **{severity · what is sub-optimal · the benefit in ONE plain line · an optional `recipe:` line (the `sandbox-lane` live recipe — egress hosts + resolved writable dirs — or the `worktrees-dir` hand-apply-first grant advice) · the exact consent-gated apply one-liner}**. The tool computes deterministic English DATA; **you PRESENT the section in the user's conversational language** — every fact, count and item from the tool, nothing added or dropped; commands, paths, hosts and rule strings stay **byte-exact**; show the raw tool block on request (the AD-032 report-contract lane — the tool cannot know the dialogue language, so the language rendering is your presentation layer).
7
+ The **read-only deployment advisor** — the deterministic section every `upgrade` run ends with, also invocable on its own. It computes what in THIS deployment is configured sub-optimally (allowlist not seeded, autonomy render drifted, sandbox unavailable, gates undeclared, bridge friction, sandbox-mask clutter, an unacknowledged sandbox recipe) and renders **verdict-first**: one composed verdict line, then each item as **{severity · what is sub-optimal · the benefit in ONE plain line · an optional `recipe:` line (the `sandbox-lane` live recipe — egress hosts + resolved writable dirs — the `worktrees-dir` hand-apply-first grant advice, or the `agents` hidden-mode reconcile follow-up) · the exact consent-gated apply one-liner}**. The tool computes deterministic English DATA; **you PRESENT the section in the user's conversational language** — every fact, count and item from the tool, nothing added or dropped; commands, paths, hosts and rule strings stay **byte-exact**; show the raw tool block on request (the AD-032 report-contract lane — the tool cannot know the dialogue language, so the language rendering is your presentation layer).
6
8
 
7
9
  **Live host/session facts are tool-composed only.** Every fact this section states about the current
8
10
  host or session — prompts fired, sandbox scope, whether a bypass was needed, network reachability,
@@ -15,12 +17,11 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
15
17
 
16
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).
17
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.
18
- 3. **The apply-through-agent lane — an explicit informed-consent checkpoint:** 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 `sandbox-lane` the note INCLUDES the sandbox-lanes ladder — present the whole ladder inline at the consent moment, never as a bare pointer). 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 items' rendered command IS the mutation and completes on that one run, **including a no-`--apply` mutation** such as `family-freshness`'s `npx … init`. 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. `sandbox-lane`'s ack-write — a NEUTRAL recipe fingerprint into the family-owned `docs/ai/acks.json`, never a security key — and 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. `agy-adddir`) is **never run by you and never written by the kit** — 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:** 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 `sandbox-lane` the note INCLUDES the sandbox-lanes ladder — present the whole ladder inline at the consent moment, never as a bare pointer). 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 items' rendered command IS the mutation and completes on that one run, **including a no-`--apply` mutation** such as `family-freshness`'s `npx … init`. 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. `sandbox-lane`'s ack-write — a NEUTRAL recipe fingerprint into the family-owned `docs/ai/acks.json`, never a security key — and 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. the `read-lane` stale/missing arms) is **never run by you and never written by the kit** — 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.
19
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**.
20
22
 
21
23
  **Per-item posture notes (the consent moment — surface BEFORE running or handing over the apply):**
22
24
 
23
- - `agy-adddir` — enabling the add-dir offload re-enables the Issue-001 stall risk (the wrapper's hard timeout bounds it); with the knob off an oversized agy code review refuses instead. Risk profile: availability only, no security delta.
24
25
  - `sandbox-lane` — surface this note TOGETHER with the sandbox-lanes ladder below (the ladder IS the practical half of the note — inline, never a pointer). Pure DISCOVERABILITY: it surfaces the manifest-declared observed session-sandbox recipe (egress hosts ∪ resolved writable state dirs — `networkHosts` ∪ `writableDirs` of the wired bridges' `capability.json`, the single documentation source) and converges on a NEUTRAL fingerprint acknowledgement recorded by the consent-gated **ack writer** into the family-owned `docs/ai/acks.json` (`sandboxLaneAck`; a changed recipe re-fires the item). The store is family-owned so no host settings validator guards it (AD-055 relocated the ack off the Claude Code settings schema, which rejected the unknown key); the legacy `"agentWorkflow": { "sandboxLaneAck": … }` settings-scope key is still READ for one deprecation window (until the next kit MAJOR). It never claims the settings security keys take effect on any host class, never recommends writing them, and the kit never seeds `sandbox.network.allowedDomains` / `sandbox.filesystem.allowWrite` (bridge council 2026-07-11, both backends concur: a network pre-allow widens egress for EVERY sandboxed command; a write allowance on CLI state dirs would expose credential dirs). Posture history: an IDE-managed session sandbox was live-observed (2026-07-11/12) ignoring hand-applied settings security keys in BOTH scopes, and codex needs a writable HOME (EROFS `~/.codex` in-sandbox); whether a session's sandbox honors the settings keys is runtime-unknowable from the advisor (a denial-only signal) — which is exactly why the item states only detectable facts and no zero-prompt promise on any host class.
25
26
 
26
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.
@@ -1,5 +1,7 @@
1
1
  ### Mode: review-state
2
2
 
3
+ <!-- opt-in-capability: none — a read-only gate entry; declaring the gate matrix is covered by gates-declaration -->
4
+
3
5
  Read-only **review-receipt checker** (AD-038) — it makes *"reviewed ≠ shipped"* mechanically detectable. The bridge review wrappers (`codex-review` / `agy-review`, from bridge version 2.2.0) append one receipt line per **successful** review to a file **inside the git dir** (`<git dir>/agent-workflow-review-receipts.jsonl` — never committable by construction; `AW_REVIEW_RECEIPTS` overrides). This mode resolves the effective `plan-execution.review` recipe (the SAME config + read-only detector the `procedures` advisor reads), recomputes the canonical **uncommitted-state fingerprint** (staged + unstaged + untracked-not-ignored contents — exactly the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, so sandbox-injected device masks can never move the fingerprint; the prose definition lives in each bridge's `capability.json` review contract, implementations cross-checked by the kit's fingerprint-parity test), and reports, per recipe-named backend, whether a **fresh, grounded, current-fingerprint** receipt exists. It **never writes, never commits, never runs a subscription CLI**; it does spawn **read-only `git` queries** to compute the fingerprint — stated honestly (that is still read-only, but it is a subprocess).
4
6
 
5
7
  Run `node ${CLAUDE_SKILL_DIR}/tools/review-state.mjs [--check] [--json]`:
@@ -1,5 +1,7 @@
1
1
  ### Mode: sandbox-masks
2
2
 
3
+ <!-- opt-in-capability: sandbox-masks -->
4
+
3
5
  **Cosmetic exclude lane for sandbox-injected device masks** (AD-044). An OS sandbox (Claude Code) injects character-device masks into the work tree (`.bashrc`, `.gitconfig`, `.vscode`, …); git lists them as untracked noise. The **review domain already ignores them by construction** — never-committable untracked classes (character/block devices, FIFOs, sockets) are excluded from the fingerprint, the assembled review payload, and the clean checks in both bridge wrappers and the kit checker. This mode is the optional **cosmetic** half: it hides the masks from `git status` (and every other `--exclude-standard` untracked walk) via **one managed fenced block** in the file `git rev-parse --git-path info/exclude` names.
4
6
 
5
7
  Run `node ${CLAUDE_SKILL_DIR}/tools/sandbox-masks.mjs [--cwd <project-root>]`:
@@ -1,5 +1,7 @@
1
1
  ### Mode: set-autonomy
2
2
 
3
+ <!-- opt-in-capability: autonomy-policy -->
4
+
3
5
  The **policy writer** for `docs/ai/autonomy.json` — the answer to *"set my autonomy policy without hand-editing JSON."* **Division of labor:** YOU turn the user's plain language into explicit ops; the KIT does the deterministic validate → merge → preview → write. It **previews by default** (writes nothing); `--write` applies. It **never renders enforcement** (that is the separate velocity autonomy mode — this writer touches only the policy file), **never runs a backend, and never commits**. Hand-editing `docs/ai/autonomy.json` stays fully supported — an offered convenience, never a lock.
4
6
 
5
7
  **The policy** has two parts: **red-lines** (always hold, segment-independent) + a **per-activity autonomy level**.
@@ -1,5 +1,8 @@
1
1
  ### Mode: set-recipe
2
2
 
3
+ <!-- opt-in-capability: review-recipe -->
4
+ <!-- opt-in-capability: delegated-execution -->
5
+
3
6
  The **config writer** for `docs/ai/orchestration.json` — the answer to *"set my standing recipe preference without hand-editing JSON."* **Division of labor (AD-025):** YOU turn the user's plain language into explicit ops; the KIT does the deterministic validate → merge → preview → write. It **previews by default** (writes nothing); `--write` applies. It **never runs a backend and never commits**. Hand-editing `docs/ai/orchestration.json` stays fully supported — this is an offered convenience, never a lock.
4
7
 
5
8
  **Map the user's plain language → explicit ops** (the kit ships no NL parser; it performs no `all`-magic, so you expand scope explicitly, asking when unclear):
@@ -1,5 +1,7 @@
1
1
  ### Mode: setup
2
2
 
3
+ <!-- opt-in-capability: none — placing a third-party subscription CLI is the user purchase decision; the advisor surfaces it only when a CONFIGURED recipe degrades (review-recipe) -->
4
+
3
5
  The **only writer** among the backend modes, and **opt-in / in-agent only** — **placement** is **never** part of `init`. The npx installer deploys the *kit* and bundles the bridge skills in its tarball, but **does not place** them (that honesty claim is load-bearing — see `decisions.md` AD-009 / AD-011); **once placed** by `setup`, `init` and `${CLAUDE_SKILL_DIR}/references/modes/upgrade.md` keep the placed copy fresh via the refresh-only `--refresh-placed` (below) — **never a first placement, never a downgrade**. `setup` owns exactly the two deterministic, secret-free steps and **guides** the rest. It **never commits and never runs a subscription CLI**.
4
6
 
5
7
  Run `node ${CLAUDE_SKILL_DIR}/tools/setup-backends.mjs [<backend>] [--bindir <path>] [--dry-run]`:
@@ -1,5 +1,7 @@
1
1
  ### Mode: state-block-guard
2
2
 
3
+ <!-- opt-in-capability: state-block -->
4
+
3
5
  The opt-in **closing-state-block detector**, wired as a Claude Code `Stop` hook. It ships as a
4
6
  **self-contained** runtime at `references/hooks/state-block-guard.mjs` (no kit imports — a placed
5
7
  copy keeps working if the kit is uninstalled) and it **writes nothing**: this mode is read-only, and
@@ -1,5 +1,7 @@
1
1
  ### Mode: status
2
2
 
3
+ <!-- opt-in-capability: none — an inspection surface that configures nothing -->
4
+
3
5
  Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md
4
6
 
5
7
  Read-only. The **single answer to "versions + deployment + settings + bridges"** across the whole family — `tools/family-registry.mjs` aggregates every member's `capability.json` and surveys the project. It **never writes, never commits, and never runs a subscription CLI**.
@@ -14,8 +16,9 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/family-registry.mjs --json [--dir <project>]
14
16
  - **recipes** — the effective recipe per slot (detail → `/agent-workflow-kit procedures` / `recipes`); a `recipes.detectError` → say the backends couldn't be checked, so recipes floored at solo.
15
17
  - **attribution** — `includeCoAuthoredBy` effective; call out a **local override** only when `local` is non-null **and** differs from `project` (a `null` `local` means the key is absent there, so the project value stands — that is not an override).
16
18
  - **velocity** — the effective `permissions.defaultMode` + whether an allowlist is seeded (detail → `/agent-workflow-kit velocity`).
17
- - **cheap agents** — how many of the kit's cheap-lane subagent vehicles are placed (`agents.placed` of `agents.bundled`; zero placed → the optional `/agent-workflow-kit agents` opt-in).
19
+ - **subagents** — how many of the kit's read-only subagent vehicles are placed (`agents.placed` of `agents.bundled`; zero placed → the optional `/agent-workflow-kit agents` opt-in). None of them grants a shell; three ride a cheap model for mechanical work and one is a read-only review lens.
18
20
  - **gate hook** — wired / hook file placed / declaration present, plus **`hook.declaredGates`** (0 = absent or an empty list; `null` = present but unreadable → say *couldn't be counted*, never a number; detail → `/agent-workflow-kit hook` and the gates guide).
21
+ - **bridge knobs** — a settings entry carrying **`retired`** is a key that is CONFIGURED but arms nothing: say so plainly (the reason is in the field) and point at `bridge-settings --unset <KEY> --apply` to clear it. Never render a retired key as an active setting.
19
22
  - Any area's **`error`** field → surface it **loudly** in plain language; the rest of `status` still renders (never a crash).
20
23
  4. **Bridges (host, one line)** (from `bridges[]`): per bridge — readiness + wrapper PATH-presence; render each wrapper's `state` as *on PATH* (`present`) / *not on PATH* (`missing`) / *couldn't check* (`unknown`) (detail → `/agent-workflow-kit backends` / `setup`). **No default-model claim.** "credentials present" means a marker file exists, not a live login.
21
24
 
@@ -1,5 +1,7 @@
1
1
  ### Mode: uninstall
2
2
 
3
+ <!-- opt-in-capability: none — removal of what setup placed, never a capability a user could enable -->
4
+
3
5
  The **guarded teardown** — the inverse of `init` (the kit + engine skills) + `setup` (the bridges) + a hidden deploy. **In-agent, opt-in**, and built around one hard rule: **it never deletes user-authored content.** Run **`--dry-run` first, always**, show the user the classified plan in plain language, get explicit consent, then re-run with `--yes`. It **never commits**.
4
6
 
5
7
  Run `node ${CLAUDE_SKILL_DIR}/tools/uninstall.mjs [<member>] [--dir <project>] [--bindir <path>] [--dry-run | --yes]`:
@@ -1,5 +1,7 @@
1
1
  ### Mode: upgrade
2
2
 
3
+ <!-- opt-in-capability: family-freshness -->
4
+
3
5
  Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKILL_DIR}/references/shared/composition-handoff.md · ${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md · ${CLAUDE_SKILL_DIR}/references/shared/command-shapes.md
4
6
 
5
7
  1. Read `docs/ai/.workflow-version` (the project's stamped lineage). If missing, treat as a pre-versioned deployment and offer to re-bootstrap conservatively.
@@ -1,5 +1,12 @@
1
1
  ### Mode: velocity
2
2
 
3
+ <!-- opt-in-capability: velocity-core -->
4
+ <!-- opt-in-capability: kit-tools-tier -->
5
+ <!-- opt-in-capability: bridge-tier -->
6
+ <!-- opt-in-capability: autonomy-render -->
7
+ <!-- opt-in-capability: sandbox-lane -->
8
+ <!-- opt-in-capability: accept-edits -->
9
+
3
10
  Requires: ${CLAUDE_SKILL_DIR}/references/shared/command-shapes.md
4
11
 
5
12
  The opt-in onboarding **velocity profile** — it seeds a fixed, audited **read-only** Claude Code allowlist into `.claude/settings.json` so an agent stops idling on approval prompts for routine read-only commands while the maintainer is away. It is the family's **first programmatic `.claude/settings.json` writer** (attribution stayed an agent-driven prose merge). **In-agent, opt-in, writes only `.claude/settings.json`**, on one hard rule: **it never allowlists `commit`/`push`/`publish`** — so a direct commit/push/publish still ASKs; the only caveat is the trust-posture residual (below) — its closure is **shipped, opt-in: `${CLAUDE_SKILL_DIR}/references/modes/hook.md`**.
@@ -1,5 +1,7 @@
1
1
  ### Mode: worktrees
2
2
 
3
+ <!-- opt-in-capability: worktrees-dir -->
4
+
3
5
  Parallel feature worktrees (v1) — several features implemented simultaneously in DIFFERENT agent
4
6
  sessions on one machine/repo, zero interference on working-tree files (the ONE exception: the
5
7
  default `node_modules` symlink is a shared MUTABLE dependency cache — see below), unambiguous
@@ -151,15 +151,27 @@ export const duplicateKeys = (parsed) => [...parsed.byKey.entries()].filter(([,
151
151
  // The env keys the wrappers resolver-validate AFTER precedence (AD-061 aw_effective_timeout) —
152
152
  // exactly these mirror the wrapper's invalid-env → warn + built-in-default lane.
153
153
  const RESOLVER_VALIDATED_ENV = new Set(['CODEX_HARD_TIMEOUT', 'AGY_HARD_TIMEOUT']);
154
+ // Keys whose wrapper REFUSES the run pre-spend on an invalid explicit env value instead of falling
155
+ // back to a default. Reporting such an override as an active raw value would show a dead setting as
156
+ // live — worse here than for a timeout, because this key exists to bound SPENDING. The wrapper also
157
+ // canonicalizes a digits-only value (leading zeros stripped) before judging it, so the advisor must
158
+ // canonicalize identically or it would disagree with the run it is describing.
159
+ const ENV_REFUSING_KEYS = new Set(['AGY_REVIEW_MAX_TOTAL_BYTES']);
160
+ const canonicalizeDigits = (v) => (/^[0-9]+$/.test(v) ? String(Number(v)) : v);
154
161
  // A control byte (C0 range + DEL) in a screened knob makes the wrapper EXIT 2 pre-spend — the
155
162
  // advisor must report that refusal, never a benign fallback, and never echo the raw byte into
156
163
  // the report (a newline could otherwise forge a `review posture:`/status line). safeShow escapes
157
164
  // every note value: JSON.stringify handles C0, an explicit pass handles DEL (JSON leaves it raw).
158
165
  const hasControlByte = (s) => /[\x01-\x1f\x7f]/.test(s);
159
166
  const safeShow = (s) => JSON.stringify(s).slice(1, -1).replace(/\x7f/g, '\\u007f');
160
- const REFUSED = (key, v) => ({ value: null, source: 'default', note: `env value "${safeShow(v)}" carries control bytes for ${key} — the wrapper REFUSES the run pre-spend` });
167
+ const REFUSED = (key, v) => ({ value: null, source: 'default', configuredIn: 'env', note: `env value "${safeShow(v)}" carries control bytes for ${key} — the wrapper REFUSES the run pre-spend` });
161
168
 
162
169
  // The effective value of one knob + where it comes from. Fact-only; never a model claim.
170
+ // `source` is where the EFFECTIVE value comes from; `configuredIn` is where the operator PUT the key,
171
+ // which is NOT the same fact. An explicitly-set but INVALID or EMPTY env value resolves to the
172
+ // built-in default (`source: 'default'`) while still being an explicit line the operator wrote — and
173
+ // for a RETIRED knob that is exactly the line the retirement asks them to clear, so it must not
174
+ // vanish from the surfaces (council fold: `AGY_REVIEW_ALLOW_ADDDIR=2` and `=` both disappeared).
163
175
  export const effectiveOf = (entry, parsed, getenv) => {
164
176
  const key = entry.key;
165
177
  if (Object.prototype.hasOwnProperty.call(getenv, key)) {
@@ -169,7 +181,7 @@ export const effectiveOf = (entry, parsed, getenv) => {
169
181
  // NOT "flag absent" (only the tier's built-in default happens to be "no flag"; the timeout / bytes /
170
182
  // add-dir knobs fall to their real built-in default). Report the manifest default (null ⇒ "wrapper
171
183
  // built-in applies"), not a misleading null-for-everything.
172
- if (v === '') return { value: entry.default, source: 'default', note: 'the env KEY= suppresses the file override — the wrapper built-in applies' };
184
+ if (v === '') return { value: entry.default, source: 'default', configuredIn: 'env', note: 'the env KEY= suppresses the file override — the wrapper built-in applies' };
173
185
  // A control byte in a SCREENED knob (the tier + the resolver-validated timeouts) is a pre-spend
174
186
  // refusal in the wrapper — report it as such (the default value is inert; the run never happens).
175
187
  if (hasControlByte(v) && (entry.kind === 'enum' || RESOLVER_VALIDATED_ENV.has(key))) {
@@ -182,20 +194,27 @@ export const effectiveOf = (entry, parsed, getenv) => {
182
194
  // the advisor must never display a dead override as active. Every OTHER non-enum env value stays
183
195
  // the operator's RAW override (the wrappers run no resolver on it), shown as-is.
184
196
  if (entry.kind === 'enum' && !settingValueValid(entry, v)) {
185
- return { value: entry.default, source: 'default', note: `env value "${safeShow(v)}" is not a supported ${key} — the wrapper runs the built-in default` };
197
+ return { value: entry.default, source: 'default', configuredIn: 'env', note: `env value "${safeShow(v)}" is not a supported ${key} — the wrapper runs the built-in default` };
186
198
  }
187
199
  if (RESOLVER_VALIDATED_ENV.has(key) && (!settingValueValid(entry, v) || v.match(/^\d*/)[0].length > 7)) {
188
- return { value: entry.default, source: 'default', note: `env value "${safeShow(v)}" is invalid for ${key} — the wrapper falls back to the built-in default` };
200
+ return { value: entry.default, source: 'default', configuredIn: 'env', note: `env value "${safeShow(v)}" is invalid for ${key} — the wrapper falls back to the built-in default` };
189
201
  }
190
- return { value: v, source: 'env' };
202
+ if (ENV_REFUSING_KEYS.has(key)) {
203
+ const canonical = canonicalizeDigits(v);
204
+ if (!settingValueValid(entry, canonical)) {
205
+ return { value: entry.default, source: 'default', configuredIn: 'env', note: `env value "${safeShow(v)}" is invalid for ${key} — the wrapper REFUSES the run pre-spend rather than ignoring a ceiling you set` };
206
+ }
207
+ return { value: canonical, source: 'env', configuredIn: 'env' };
208
+ }
209
+ return { value: v, source: 'env', configuredIn: 'env' };
191
210
  }
192
211
  const fileEntries = parsed.byKey.get(key);
193
212
  if (fileEntries && fileEntries.length) {
194
213
  const v = fileEntries[fileEntries.length - 1].value; // last occurrence wins
195
- if (settingValueValid(entry, v)) return { value: v, source: 'file' };
196
- return { value: entry.default, source: 'default', note: `file value "${safeShow(v)}" is invalid — falls back to the built-in default` };
214
+ if (settingValueValid(entry, v)) return { value: v, source: 'file', configuredIn: 'file' };
215
+ return { value: entry.default, source: 'default', configuredIn: 'file', note: `file value "${safeShow(v)}" is invalid — falls back to the built-in default` };
197
216
  }
198
- return { value: entry.default, source: 'default' };
217
+ return { value: entry.default, source: 'default', configuredIn: null };
199
218
  };
200
219
 
201
220
  export const displayValue = (v) => (v == null ? '(unset — wrapper built-in applies)' : v);
@@ -224,8 +243,19 @@ export const settingsSnapshot = (ctx = {}) => {
224
243
  // ACTIVE = a non-default value is genuinely in effect (source env/file AND differs from the
225
244
  // built-in default). A value that merely equals the default keeps every surface byte-identical to
226
245
  // "nothing set" — the "status line unchanged unless a knob is active" contract.
227
- if ((eff.source === 'env' || eff.source === 'file') && eff.value != null && eff.value !== entry.default) {
228
- active.push({ key: entry.key, value: eff.value, source: eff.source, bridge: entry.bridge });
246
+ // A RETIRED knob surfaces whenever it is EXPLICITLY CONFIGURED, independently of whether its
247
+ // value differs from the default: `AGY_REVIEW_ALLOW_ADDDIR=0` is still a dead line in the
248
+ // user's file that the retirement asks them to clear, and the "retired keys still show"
249
+ // contract would be broken by silently filtering it out for equalling the default.
250
+ // A retired knob is surfaced on EXPLICIT CONFIGURATION, which is `configuredIn` — not `source`.
251
+ // An invalid or empty env value resolves to the built-in default, so keying off `source` hid the
252
+ // very lines the retirement exists to get cleared (council fold).
253
+ const worthShowing = eff.value != null && eff.value !== entry.default;
254
+ const surfaced = entry.retired
255
+ ? eff.configuredIn !== null
256
+ : (eff.source === 'env' || eff.source === 'file') && worthShowing;
257
+ if (surfaced) {
258
+ active.push({ key: entry.key, value: eff.value, source: eff.source, bridge: entry.bridge, retired: entry.retired ?? null });
229
259
  }
230
260
  }
231
261
  return {
@@ -42,8 +42,10 @@ const renderReader = (registry, parsed, fileState, ctx, path) => {
42
42
  for (const entry of registry.values()) {
43
43
  const eff = effectiveOf(entry, parsed, getenv);
44
44
  const note = eff.note ? ` — ${eff.note}` : '';
45
- lines.push(` ${entry.key} = ${displayValue(eff.value)} [${eff.source}]${note}`);
45
+ const retired = entry.retired ? ' ⚠ RETIRED' : '';
46
+ lines.push(` ${entry.key} = ${displayValue(eff.value)} [${eff.source}]${retired}${note}`);
46
47
  lines.push(` ${entry.bridge} · ${allowedLabel(entry)} · ${entry.effect}`);
48
+ if (entry.retired) lines.push(` RETIRED — ${entry.retired}`);
47
49
  }
48
50
  const dups = duplicateKeys(parsed);
49
51
  const unknown = [...parsed.byKey.keys()].filter((k) => !registry.has(k));
@@ -61,7 +63,7 @@ const buildReaderJson = (registry, parsed, fileState, ctx, path) => {
61
63
  fileState: fileState.state,
62
64
  knobs: [...registry.values()].map((entry) => {
63
65
  const eff = effectiveOf(entry, parsed, getenv);
64
- return { key: entry.key, bridge: entry.bridge, kind: entry.kind, effective: eff.value, source: eff.source, note: eff.note ?? null, default: entry.default };
66
+ return { key: entry.key, bridge: entry.bridge, kind: entry.kind, effective: eff.value, source: eff.source, note: eff.note ?? null, default: entry.default, retired: entry.retired ?? null };
65
67
  }),
66
68
  duplicateKeys: duplicateKeys(parsed),
67
69
  unknownKeys: [...parsed.byKey.keys()].filter((k) => !registry.has(k)),
@@ -85,14 +87,21 @@ export const reconcileSettings = (ctx = {}) => {
85
87
  if (fileState.state === 'absent') return { outcome: 'absent', lines: [` bridge-settings: no settings file — skipped (${path})`] };
86
88
  if (fileState.state === 'unusable') return { outcome: 'unusable', lines: [` bridge-settings: settings file is a symlink / not a regular file — skipped, never edited (${path})`] };
87
89
  const parsed = parseSettings(fileState.text);
88
- const known = [...parsed.byKey.keys()].filter((k) => registry.has(k));
89
- const unknown = [...parsed.byKey.keys()].filter((k) => !registry.has(k));
90
+ // RECOGNIZED and CURRENT are not the same thing: a manifest-RETIRED key is still in the registry
91
+ // (so an existing line never warns as unknown) but it arms nothing, and filing it under "all
92
+ // current" would be the survival check contradicting the retirement contract on the one surface
93
+ // init/upgrade actually shows.
94
+ const fileKeys = [...parsed.byKey.keys()];
95
+ const retired = fileKeys.filter((k) => registry.get(k)?.retired);
96
+ const known = fileKeys.filter((k) => registry.has(k) && !registry.get(k).retired);
97
+ const unknown = fileKeys.filter((k) => !registry.has(k));
90
98
  const dups = duplicateKeys(parsed);
91
99
  const lines = [];
92
- if (unknown.length) lines.push(` bridge-settings: ${unknown.length} unknown/retired key(s) preserved verbatim (never edited) — compare with the current knobs when convenient: ${unknown.join(', ')}`);
100
+ if (unknown.length) lines.push(` bridge-settings: ${unknown.length} unknown key(s) preserved verbatim (never edited) — compare with the current knobs when convenient: ${unknown.join(', ')}`);
101
+ for (const key of retired) lines.push(` bridge-settings: ${key} is RETIRED and arms nothing — preserved verbatim (never edited); clear it with --unset ${key} --apply. ${registry.get(key).retired}`);
93
102
  if (dups.length) lines.push(` bridge-settings: duplicate key(s) — the reader takes the last; fix by hand before the writer will edit: ${dups.join(', ')}`);
94
- if (!unknown.length && !dups.length) lines.push(` bridge-settings: ${known.length} key(s) recognized, all current`);
95
- return { outcome: unknown.length ? 'flagged' : dups.length ? 'duplicates' : 'ok', lines };
103
+ if (!unknown.length && !retired.length && !dups.length) lines.push(` bridge-settings: ${known.length} key(s) recognized, all current`);
104
+ return { outcome: unknown.length || retired.length ? 'flagged' : dups.length ? 'duplicates' : 'ok', lines };
96
105
  };
97
106
 
98
107
  // ── writer ─────────────────────────────────────────────────────────────────────────
@@ -143,6 +152,12 @@ const validateOps = (ops, registry) => {
143
152
  for (const op of ops) {
144
153
  const entry = registry.get(op.key);
145
154
  if (!entry) throw fail(2, `unknown key "${op.key}" — not a settings knob of any bundled bridge. Known keys: ${[...registry.keys()].join(', ')}`);
155
+ // A RETIRED key (D3) is recognized so an existing line never warns as unknown — but writing a NEW
156
+ // one would arm nothing while reading as configuration. `--unset` stays allowed on purpose: it is
157
+ // the recovery the retirement notice tells the user to run.
158
+ if (op.kind === 'set' && entry.retired) {
159
+ throw fail(2, `${op.key} is RETIRED — ${entry.retired} Setting it would arm nothing; use --unset ${op.key} to clear an existing line.`);
160
+ }
146
161
  if (op.kind === 'set' && !settingValueValid(entry, op.value)) {
147
162
  throw fail(2, `invalid value "${op.value}" for ${op.key} — allowed: ${allowedLabel(entry)}`);
148
163
  }
@@ -28,6 +28,7 @@
28
28
  import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
29
29
  import { join, resolve, dirname } from 'node:path';
30
30
  import { fileURLToPath, pathToFileURL } from 'node:url';
31
+ import { shellQuoteArg } from './review-state.mjs';
31
32
 
32
33
  const HERE = dirname(fileURLToPath(import.meta.url));
33
34
 
@@ -49,8 +50,10 @@ export const CHEAP_AGENTS_BUNDLE = 'CHEAP_AGENTS_BUNDLE';
49
50
 
50
51
  const USAGE = `usage: cheap-agents [--dry-run | --apply] [--cwd <dir>] [--help]
51
52
 
52
- Places the bundled cheap-lane subagent definitions (haiku/low, read-only tools) into the
53
- project's ${AGENTS_DIR}/. Default is --dry-run (a preview; writes nothing). --apply writes.
53
+ Places the bundled READ-ONLY subagent definitions into the project's ${AGENTS_DIR}/. No vehicle
54
+ grants a shell: three ride a cheap model (haiku/low) for mechanical work — extraction sweeps,
55
+ changelog fact-skeletons, gate triage — and review-lens is a read-only REVIEW vehicle on a
56
+ review-capable model. Default is --dry-run (a preview; writes nothing). --apply writes.
54
57
  An existing file with DIFFERENT content is preserved and reported, never overwritten.`;
55
58
 
56
59
  export const fail = (exitCode, message) => Object.assign(new Error(message), { exitCode });
@@ -176,8 +179,8 @@ const ACTION_LABEL = {
176
179
  export const formatResult = (result) => {
177
180
  const lines = [
178
181
  result.dryRun
179
- ? 'agent-workflow cheap-lane agents — DRY RUN (no changes; re-run with --apply)'
180
- : 'agent-workflow cheap-lane agents — APPLY',
182
+ ? 'agent-workflow read-only subagents — DRY RUN (no changes)'
183
+ : 'agent-workflow read-only subagents — APPLY',
181
184
  ];
182
185
  for (const item of result.plan) {
183
186
  const verb = result.dryRun && item.action === 'place' ? 'would place' : ACTION_LABEL[item.action];
@@ -187,8 +190,15 @@ export const formatResult = (result) => {
187
190
  lines.push(`note: no current deployment stamp found (${result.stamp ?? 'none'}) — --apply will refuse until init/upgrade runs.`);
188
191
  }
189
192
  lines.push(
190
- 'the vehicles are Claude Code subagents (model: haiku, effort: low, read-only tools) for mechanical work only judgment, review, and real code stay on your main lane.',
193
+ 'the vehicles are Claude Code subagents with READ-ONLY tools and NO shellso a fan-out can never turn into a wave of approval prompts.',
194
+ 'three ride the cheap lane (model: haiku, effort: low) for mechanical work; review-lens is an ADDITIONAL read-only review opinion, not a replacement for your configured review recipe. Writing code and running gates stay on your main lane.',
191
195
  );
196
+ // A preview must print the EXACT command that applies it. The advisor renders this dry-run as an
197
+ // item's one-liner, and that flow's contract is "run the printed command, no improvisation" — a
198
+ // bare "re-run with --apply" would leave the caller to reconstruct --cwd and its quoting.
199
+ if (result.dryRun && result.plan.some((item) => item.action === 'place')) {
200
+ lines.push(`to apply, run exactly: node ${shellQuoteArg(fileURLToPath(import.meta.url))} --apply --cwd ${shellQuoteArg(result.projectDir)}`);
201
+ }
192
202
  if (!result.dryRun && result.wrote) {
193
203
  lines.push('hidden-mode note: if this deployment is hidden, run the hide-footprint reconcile so the placed files stay out of `git status`.');
194
204
  }
@@ -131,7 +131,7 @@ const CATALOG = [
131
131
  invocation: invocationOf('agents'),
132
132
  group: 'Configure',
133
133
  kind: WRITER,
134
- oneLine: 'Place bundled cheap-model subagent definitions for mechanical work sweeps, changelog skeletons, gate triage (Claude Code; opt-in; preview first).',
134
+ oneLine: 'Place bundled READ-ONLY subagent definitions, none of them granted a shell — three cheap-model ones for mechanical work (sweeps, changelog skeletons, gate triage) and a review lens (Claude Code; opt-in; preview first).',
135
135
  },
136
136
  {
137
137
  key: 'hook',
@@ -305,7 +305,7 @@ const TUNE_TAIL = Object.freeze([
305
305
  '',
306
306
  'Tune — opt-in accelerators (consent-first: every writer previews before writing; nothing runs without your yes)',
307
307
  ` ${BARE_INVOCATION} velocity routine read-only commands stop prompting (incl. the --kit-tools tier for the kit's own read-only tools)`,
308
- ` ${BARE_INVOCATION} agents cheap-model subagents take the mechanical work (sweeps, changelog skeletons, gate triage)`,
308
+ ` ${BARE_INVOCATION} agents shell-free subagents: cheap ones take the mechanical work, a review lens gives an extra read-only opinion`,
309
309
  ` ${BARE_INVOCATION} gates run your declared gates (docs/ai/gates.json) as one batch; its guide also offers the consent-gated seeding preview — writes only on your yes`,
310
310
  ` ${BARE_INVOCATION} hook auto-approve exactly your declared gate commands (byte-exact matches only)`,
311
311
  ` ${BARE_INVOCATION} set-recipe put a ready review backend to work on plans and diffs`,