@sabaiway/agent-workflow-kit 3.6.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,41 @@ 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
+ ## 3.7.0 — the worktrees-dir advisor item can finally converge (AD-068)
8
+
9
+ The `recommendations` advisor's `worktrees-dir` item used to fire forever: its only convergence
10
+ signal was a host write-capability callback that production never supplies, so the item kept
11
+ re-rendering even once its own advice had been applied. It now converges on either of two
12
+ maintainer-visible signals — while a genuinely supplied host signal still overrides both, in
13
+ EITHER direction (a trusted no keeps the item firing however the project is configured):
14
+
15
+ - **a declared grant** — a `sandbox.filesystem.allowWrite` entry covering the probed worktrees
16
+ parent dir, in either settings scope; `~`/`~/…` resolve against home, and coverage is
17
+ path-segment-aware (a grant on a sibling or on a child never counts). A declaration is proof the
18
+ maintainer applied THIS item's advice, never proof of write capability — the provision
19
+ preflight's real create+delete probe stays the runtime truth. The declaration read is guarded
20
+ no-follow end to end (an out-of-project store reached through a symlink cannot silence the item;
21
+ a non-regular target is a stated skip), and the `allowWrite` shape check is fail-closed over the
22
+ whole array — one malformed entry invalidates the list rather than hiding beside a good one;
23
+ - **a dir-bound ack** — for hosts that ignore the settings key: the item's consent-gated APPLY
24
+ one-liner is `ack-write --lane worktrees-dir` (a dry-run preview that prints the exact `--apply`
25
+ which records the ack), while the HAND-APPLY grant advice rides the `recipe:` line as the
26
+ labeled first step — the consent flow waits for the grant (or the terminal-fallback choice)
27
+ before the ack runs, because the ack RECORDS a choice, never makes one. The recorded
28
+ `worktreesDirAck` fingerprint lives in the family-owned `docs/ai/acks.json` and binds to the
29
+ RESOLVED probe dir, so the item re-fires exactly when that resolved dir changes — never on a
30
+ no-op re-render, never a security key. Against a trusted host NO the apply stays the HAND-APPLY
31
+ grant advice and no ack surfaces (it could never converge there).
32
+
33
+ `ack-write` accordingly grows a closed-world `--lane` registry (`sandbox-lane` — the default, so
34
+ every existing invocation and rendered one-liner stays byte-identical — and `worktrees-dir`); one
35
+ lane sets exactly ONE store key, merge-preserving the rest, and an unknown lane is a usage refusal
36
+ at the writer, never an invented key in the shared store.
37
+
38
+ Honesty fix riding along: every shipped surface that promised the read-lane hook «fires on
39
+ subagent Bash too» now says «where the host fires hooks on subagent Bash» — that behavior has
40
+ never been measured, and a shipped surface must not assert it.
41
+
7
42
  ## 3.6.0 — the dependency-free install posture: a proof, never a default (AD-067)
8
43
 
9
44
  On a provably dependency-free project the provision record and the default-lane report no longer
package/README.md CHANGED
@@ -241,7 +241,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
241
241
  | `/agent-workflow-kit core-evidence` | any time | **the ONE loop-evidence writer** (strip-the-kit) — every core evidence record lands in a single append-only JSONL store inside the git dir (never committable; versioned schema, latest-per-key supersession, byte-identical duplicates refused, malformed lines fail every reader closed). `red-proof "<file>#<pattern>"` declares an observed-red **BEFORE a bugfix** (N/N red runs + content custody + base + the pre-fix fingerprint; green/mixed/timeout are DISTINGUISHED refusals — nothing written); `degrade --backend --reason` is the ONLY escape for an unavailable review backend (per-tree, never all backends); `summary` renders the whole loop state statelessly (gate result · per-backend verdicts · red-proofs · degrades) — no ledger, no rounds, nothing remembered. Honest residual: records are forgeable — self-discipline, not a security boundary. Never commits, never runs a subscription CLI. |
242
242
  | `/agent-workflow-kit coverage-check` | any time | **the final-run checker** (D3(c)+(d)) — reads the lcov the declared `unit-tests` gate produced at the FIXED git-dir path and fails on any uncovered CHANGED executable Node line (listed `file:line`; a changed file absent from the map is a file-level red; out-of-domain/unsupported files are LISTED — the claim narrowed honestly); VERIFIES every current-base red-proof declaration (bound test exists · custody hash unchanged · green N/N now · pre-fix fingerprint differs); prints `lcov-sha256=<hex|none>` of the exact bytes it consumed — the sha the `--final` receipt binds and re-hashes. An absent lcov is a LOUD `skipped-no-lcov`; a symlinked path is a refusal. `--check` is the gate exit code — declare it as the LAST gate (`run-gates --final` refuses otherwise). Read-only. |
243
243
  | `/agent-workflow-kit commit-guard` | any time | **the read-only pre-commit guard** (D10) — binds the LATEST completed `run-gates --final` receipt to the EXACT current tree: refuses on a missing/red/stale receipt, fingerprint drift under the run, a dangling later attempt, declaration content drift, evidence-hash or lcov drift, or unsatisfied review obligations (the same review-state decision, recomputed over a sanitized env — forged out-of-repo stores never satisfy). Re-runs NO gate or test. Wire it into `.git/hooks/pre-commit` (the installer writes the RESOLVED invocation). `git commit --no-verify` stays the stated residual. |
244
- | `/agent-workflow-kit recommendations` | any time (every `upgrade` ends with it) | **read-only deployment advisor** (AD-044) — computes what in THIS deployment is configured sub-optimally (allowlist not seeded, autonomy render drifted, OS sandbox unavailable, gates undeclared, bridge friction, sandbox-mask clutter, an unacknowledged sandbox recipe) and renders **verdict-first**: one composed verdict line (does anything need attention?), then each item as **{severity · what · one-line benefit · an optional `recipe:` line (sandbox-lane only) · the exact consent-gated apply one-liner}**. The agent PRESENTS the section in the user's conversational language — every fact and count, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; raw tool block on request — and runs EXACTLY the rendered one-liners only on your yes, surfacing each item's posture note first. Renders **present-even-when-empty** (`no recommendations — flow optimal.`); a failed probe degrades to a stated skip line. Registry strings are fact-true frozen one-line data (posture/risk notes live in the mode doc at the consent moment); the kit never seeds `sandbox.network.allowedDomains` / `filesystem.allowWrite` (**HAND-APPLY** territory), and the sandbox-lane item's convergence is a neutral fingerprint acknowledgement recorded by a consent-gated ack writer into `docs/ai/acks.json` — never a security key (the recipe is documented per bridge in `capability.json` `networkHosts` + `writableDirs`). `--cwd` is required (the target project is explicit); never writes, never commits, never runs a subscription CLI. |
244
+ | `/agent-workflow-kit recommendations` | any time (every `upgrade` ends with it) | **read-only deployment advisor** (AD-044) — computes what in THIS deployment is configured sub-optimally (allowlist not seeded, autonomy render drifted, OS sandbox unavailable, gates undeclared, bridge friction, sandbox-mask clutter, an unacknowledged sandbox recipe) and renders **verdict-first**: one composed verdict line (does anything need attention?), then each item as **{severity · what · one-line benefit · an optional `recipe:` line (the sandbox-lane live recipe, or the worktrees-dir hand-apply-first grant advice) · the exact consent-gated apply one-liner}**. The agent PRESENTS the section in the user's conversational language — every fact and count, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; raw tool block on request — and runs EXACTLY the rendered one-liners only on your yes, surfacing each item's posture note first. Renders **present-even-when-empty** (`no recommendations — flow optimal.`); a failed probe degrades to a stated skip line. Registry strings are fact-true frozen one-line data (posture/risk notes live in the mode doc at the consent moment); the kit never seeds `sandbox.network.allowedDomains` / `filesystem.allowWrite` (**HAND-APPLY** territory), and the sandbox-lane item's convergence is a neutral fingerprint acknowledgement recorded by a consent-gated ack writer into `docs/ai/acks.json` — never a security key (the recipe is documented per bridge in `capability.json` `networkHosts` + `writableDirs`). `--cwd` is required (the target project is explicit); never writes, never commits, never runs a subscription CLI. |
245
245
  | `/agent-workflow-kit doc-parity` | any time | **read-only doc-parity lint** (AD-049) — kills the doc-drift class where a mode-contract doc silently lags a code constant (a `--check` doc still reading `300` after the diff cap moved to `400`): a **closed, exported registry** binds each live constant (review caps, schema versions, the ledger's own class/scope vocabulary, and the autonomy-doctor EXIT/status/trusted-dir contract) to the exact token its `references/modes/*.md` contract must carry, and asserts the CURRENT value renders into every bound file — a drifted doc, an unreadable file, or an absent token **fails closed**. The values are sourced from the live imports (never re-typed), so the lint can't itself go stale; adding a binding is adding a checked entry (closed-world, edit-safe). `--check` is a gate exit code for `docs/ai/gates.json`. Never writes, never commits, never runs a subscription CLI. |
246
246
  | `/agent-workflow-kit worktrees` | any time | **parallel feature worktrees** — run several features in DIFFERENT agent sessions on one repo, zero interference on working-tree files (the ONE exception is the dependency cache, below): `provision <slug> --plan <file>` creates a sibling git worktree on branch `aw/<slug>` and populates it (registry-derived footprint copy-if-missing — a tracked file is never overwritten; EXACTLY ONE seeded feature plan; the `handoff-<slug>.md` record from minute zero; `node_modules` symlinked where the link stays ignored — a shared MUTABLE dependency cache: writes through it hit MAIN's node_modules; for isolation run the printed isolated-install command (`--install` only PRINTS it; on `--resume` run the printed unlink-first recovery first); absolute root-pinned gate commands rebased on untracked copies only, and only while their bytes equal the MAIN source or its rebased form — user-modified copies stay untouched); `list` is read-only (slug, branch, base, dirty, handoff); `land <slug> --prepare` locks the common git dir, fail-closes on divergence or incomplete satellite state, transfers the complete accepted satellite diff onto a CLEAN main, runs sync plus the declared gates, and reports HEAD/TRANSFER/PREPARED OIDs — the commit ALWAYS stays a dialogue ask; `cleanup <slug>` takes the same lock and removes a LANDED worktree only after live landed-verification against main HEAD, while `--abandon` is the ONE destructive arm (destroys unlanded work; **no preview step** on any writer). The parent dir is the `docs/ai/worktrees.json` `parentDir` setting (default: the repo's sibling parent); an unwritable parent degrades to printed maintainer-pasted commands, and the one-time host consent that makes it promptless surfaces via `recommendations`. Never commits, never pushes, never runs a subscription CLI. |
247
247
  | `/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. |
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: agent-workflow-kit
3
3
  description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow".
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '3.6.0'
6
+ version: '3.7.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "3.6.0",
6
+ "version": "3.7.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -21,4 +21,4 @@ Rules:
21
21
  - Mark anything uncertain as `VERIFY:` rather than asserting it.
22
22
  - No superlatives, no persuasion, no summary paragraph — that is the frontier's red-line work.
23
23
  - Read-only: you never modify files.
24
- - Tooling note: this vehicle grants `Read`/`Grep`/`Glob` only — **no `Bash`**. If a harness omits `Grep`/`Glob`, fall back to the `Read` tool (whole-file reads), never a shelled-out command. Should a harness nonetheless route your reads through `Bash`, keep each one a **plain single read-only command** (`grep …`, `ls …`, `cat …`) — never a `;`/`&&`/`|` chain, never `node -e`; where the maintainer enabled the opt-in **read-lane** (`docs/ai/lanes.json`), the gate hook keeps those seeded-read-only Bash reads promptless (it fires on subagent Bash too).
24
+ - Tooling note: this vehicle grants `Read`/`Grep`/`Glob` only — **no `Bash`**. If a harness omits `Grep`/`Glob`, fall back to the `Read` tool (whole-file reads), never a shelled-out command. Should a harness nonetheless route your reads through `Bash`, keep each one a **plain single read-only command** (`grep …`, `ls …`, `cat …`) — never a `;`/`&&`/`|` chain, never `node -e`; where the maintainer enabled the opt-in **read-lane** (`docs/ai/lanes.json`), the gate hook keeps those seeded-read-only Bash reads promptless (subagent Bash included, where the host fires hooks on subagent Bash).
@@ -21,4 +21,4 @@ Rules:
21
21
  - Output: one section per failing gate, ordered as the run reported them. No fix suggestions,
22
22
  no root-cause speculation beyond the failure class, no code.
23
23
  - Read-only: you never modify files, never re-run commands.
24
- - Tooling note: this vehicle grants `Read`/`Grep`/`Glob` only — **no `Bash`**. If a harness omits `Grep`/`Glob`, fall back to the `Read` tool (whole-file reads), never a shelled-out command. Should a harness nonetheless route your reads through `Bash`, keep each one a **plain single read-only command** (`grep …`, `ls …`, `cat …`) — never a `;`/`&&`/`|` chain, never `node -e`; where the maintainer enabled the opt-in **read-lane** (`docs/ai/lanes.json`), the gate hook keeps those seeded-read-only Bash reads promptless (it fires on subagent Bash too).
24
+ - Tooling note: this vehicle grants `Read`/`Grep`/`Glob` only — **no `Bash`**. If a harness omits `Grep`/`Glob`, fall back to the `Read` tool (whole-file reads), never a shelled-out command. Should a harness nonetheless route your reads through `Bash`, keep each one a **plain single read-only command** (`grep …`, `ls …`, `cat …`) — never a `;`/`&&`/`|` chain, never `node -e`; where the maintainer enabled the opt-in **read-lane** (`docs/ai/lanes.json`), the gate hook keeps those seeded-read-only Bash reads promptless (subagent Bash included, where the host fires hooks on subagent Bash).
@@ -18,4 +18,4 @@ Rules:
18
18
  - Output format: the structure the prompt asks for (list / table / JSON). No preamble, no
19
19
  commentary, no recommendations — the orchestrator applies judgment, you supply facts.
20
20
  - Read-only: you never modify files, never run commands, never propose edits.
21
- - Tooling note: this vehicle grants `Read`/`Grep`/`Glob` only — **no `Bash`**. If a harness omits `Grep`/`Glob`, fall back to the `Read` tool (whole-file reads), never a shelled-out command. Should a harness nonetheless route your reads through `Bash`, keep each one a **plain single read-only command** (`grep …`, `ls …`, `cat …`) — never a `;`/`&&`/`|` chain, never `node -e`; where the maintainer enabled the opt-in **read-lane** (`docs/ai/lanes.json`), the gate hook keeps those seeded-read-only Bash reads promptless (it fires on subagent Bash too).
21
+ - Tooling note: this vehicle grants `Read`/`Grep`/`Glob` only — **no `Bash`**. If a harness omits `Grep`/`Glob`, fall back to the `Read` tool (whole-file reads), never a shelled-out command. Should a harness nonetheless route your reads through `Bash`, keep each one a **plain single read-only command** (`grep …`, `ls …`, `cat …`) — never a `;`/`&&`/`|` chain, never `node -e`; where the maintainer enabled the opt-in **read-lane** (`docs/ai/lanes.json`), the gate hook keeps those seeded-read-only Bash reads promptless (subagent Bash included, where the host fires hooks on subagent Bash).
@@ -2,7 +2,7 @@
2
2
 
3
3
  Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md
4
4
 
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 — only) · 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).
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).
6
6
 
7
7
  **Live host/session facts are tool-composed only.** Every fact this section states about the current
8
8
  host or session — prompts fired, sandbox scope, whether a bypass was needed, network reachability,
@@ -15,7 +15,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
15
15
 
16
16
  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
17
  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.
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.
19
19
  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
20
 
21
21
  **Per-item posture notes (the consent moment — surface BEFORE running or handing over the apply):**
@@ -24,7 +24,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
24
24
  - `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
25
 
26
26
  - `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.
27
- - `worktrees-dir` — on a settings-native host that honors the key, the HAND-APPLY line widens the OS-sandbox WRITE surface to the whole worktrees parent dir: every sibling path under it (other repositories included) becomes agent-writable, and the widening persists for every later session. A harness-managed host may ignore that project setting; grant the narrow parent through host/session controls or use the provision terminal fallback instead. When that scope is wider than you want, narrow it FIRST: create a dedicated dir yourself (outside the agent's write surface), point `docs/ai/worktrees.json` `parentDir` at it, then re-run recommendations — the item re-renders with the narrowed dir. The kit never writes sandbox filesystem allowances itself; the line is always yours to paste. Risk profile: a real write-surface widening where honored — scope it deliberately.
27
+ - `worktrees-dir` — on a settings-native host that honors the key, the HAND-APPLY line widens the OS-sandbox WRITE surface to the whole worktrees parent dir: every sibling path under it (other repositories included) becomes agent-writable, and the widening persists for every later session. A harness-managed host may ignore that project setting; grant the narrow parent through host/session controls or use the provision terminal fallback instead. When that scope is wider than you want, narrow it FIRST: create a dedicated dir yourself (outside the agent's write surface), point `docs/ai/worktrees.json` `parentDir` at it, then re-run recommendations — the item re-renders with the narrowed dir. The kit never writes sandbox filesystem allowances itself; the line is always yours to paste. **Convergence** is two-path: a declared `sandbox.filesystem.allowWrite` entry covering the probed dir (either settings scope — `~` and `~/…` resolve against home, and coverage is path-segment-aware, so a grant on a sibling or on a child never counts), or — for a host that ignores that key — the neutral dir-bound acknowledgement recorded by this item's consent-gated apply one-liner (`ack-write --lane worktrees-dir` — a dry-run preview that prints the exact `--apply`; recorded as `worktreesDirAck` in the family-owned `docs/ai/acks.json`, never a security key), while the grant advice itself rides the `recipe:` line as the labeled FIRST step; against a trusted host NO the apply stays the HAND-APPLY grant advice and no ack is offered. Neither is proof of write CAPABILITY: the provision preflight's real create+delete probe stays the runtime truth, and the fingerprint is bound to the **resolved probe dir**, so the item re-fires only when that resolved dir changes (two absent `parentDir` values sharing an existing ancestor resolve to the same dir and keep the same ack). Risk profile: a real write-surface widening where honored — scope it deliberately.
28
28
 
29
29
  **Sandbox lanes (what to DO with the `sandbox-lane` recipe, per host class):**
30
30
 
@@ -32,6 +32,6 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
32
32
  - **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 settings security keys were rejected for (every sandboxed command in the session gains that egress and those writable credential dirs), acceptable only as the maintainer's deliberate choice for a review-heavy session. Its **paste-ready hand-apply shape** (the kit never writes these keys) is, in `.claude/settings.json`, `"sandbox": { "network": { "allowedDomains": ["<egress host>", …] }, "filesystem": { "allowWrite": ["<writable state dir>", …] } }` — fill `<egress host>` / `<writable state dir>` from THIS recipe (the wired bridges' `capability.json` `networkHosts` ∪ resolved `writableDirs`); one consent then lands durably in config. (3) Else the per-run consented bypass. Record the ack (the ack-write one-liner) once a lane is chosen — choosing the bypass, or consciously declining, also counts.
33
33
  - Either way the recipe is **observed-minimal, honestly incomplete** — a blocked host names itself at run time; extend by hand and re-ack (the fingerprint moves with the recipe, not with your extensions).
34
34
 
35
- **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 · apply one-liners are cwd-independent (absolute tool paths + a pinned `--cwd`; the sandbox-provision item pins via a `cd <root> &&` prefix — the doctor reads its cwd; the ONE exception is the `set-autonomy` item, a conversational skill invocation explicitly labeled *run IN the target project*) · the kit never seeds `sandbox.network.allowedDomains` / `sandbox.filesystem.allowWrite` (hand-apply territory) · the sandbox-lane convergence is a neutral acknowledgement recorded into `docs/ai/acks.json` by the consent-gated ack writer (the legacy settings-scope key read for one deprecation window), never a security key.
35
+ **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 · apply one-liners are cwd-independent (absolute tool paths + a pinned `--cwd`; the sandbox-provision item pins via a `cd <root> &&` prefix — the doctor reads its cwd; the ONE exception is the `set-autonomy` item, a conversational skill invocation explicitly labeled *run IN the target project*) · the kit never seeds `sandbox.network.allowedDomains` / `sandbox.filesystem.allowWrite` (hand-apply territory) · the sandbox-lane convergence is a neutral acknowledgement recorded into `docs/ai/acks.json` by the consent-gated ack writer (the legacy settings-scope key read for one deprecation window), never a security key · the `worktrees-dir` item converges on a declared `allowWrite` entry covering its probed dir, or on its own `worktreesDirAck` lane in that same store (recorded by its apply one-liner, never by the `recipe:` line).
36
36
 
37
37
  **Exit codes:** `0` report rendered (items or the empty state); `1` error (e.g. `--cwd` is not a directory); `2` usage.
@@ -91,8 +91,13 @@ settings-native line (`sandbox.filesystem.allowWrite` in `.claude/settings.json`
91
91
  terminal command. On a settings-native host that honors the key, adding the parent can make later
92
92
  provision/cleanup promptless. On a harness-managed host that ignores project settings, grant the
93
93
  narrow parent through the host/session controls; if that is unavailable, use the printed terminal
94
- command for each operation. The `recommendations` mode surfaces this lane but treats write access
95
- as unverified without a trusted host-capability signal.
94
+ command for each operation. The `recommendations` mode surfaces this lane and converges it two ways:
95
+ a declared `sandbox.filesystem.allowWrite` entry covering the probed parent (either settings
96
+ scope), or — on a host that ignores that key — the neutral dir-bound acknowledgement its
97
+ consent-gated apply one-liner records into `docs/ai/acks.json` (`worktreesDirAck`; the dry-run
98
+ preview prints the exact `--apply`). Neither is proof of write
99
+ capability: the create+delete probe above stays the runtime truth, and the ack binds to the
100
+ resolved probe dir, so the item re-fires only when that resolved dir changes.
96
101
 
97
102
  **Landing flow:** provision → work → handoff → land → re-attest → commit → cleanup. Satellite
98
103
  commits are outside v1: graph divergence stops land and prints cherry-pick/rebase recovery. A gate
@@ -9,8 +9,8 @@
9
9
  // • preview-then-mutate — `--dry-run` is the DEFAULT and writes nothing; `--apply` writes;
10
10
  // • deployment-gated — REFUSES an absent docs/ai with a named recovery pointer (run init/bootstrap);
11
11
  // • creates docs/ai/acks.json (and nothing else) if absent;
12
- // • merge-preserve — every existing key (a hand-authored `_README`, future sibling acks) is kept;
13
- // only the `sandboxLaneAck` key is set;
12
+ // • merge-preserve — every existing key (a hand-authored `_README`, the OTHER lanes' acks) is
13
+ // kept; only the selected lane's ONE key is set (--lane, closed-world over ACK_LANES);
14
14
  // • symlink / non-regular target → STOP (never write through a link or clobber a device/dir);
15
15
  // • fail-closed on malformed existing JSON (never overwrite an unparseable store);
16
16
  // • atomic — exclusive-create *.tmp + rename (no partial-write state); last-writer-wins;
@@ -22,7 +22,7 @@
22
22
  import { lstatSync, readFileSync } from 'node:fs';
23
23
  import { dirname, join, resolve } from 'node:path';
24
24
  import { fileURLToPath, pathToFileURL } from 'node:url';
25
- import { ACKS_FILE, ACKS_LANE_KEY } from './recommendations.mjs';
25
+ import { ACKS_FILE, ACK_LANES } from './recommendations.mjs';
26
26
  import { assertDocsAiDeployment, writeDocsAiFileAtomic, lstatNoFollow } from './atomic-write.mjs';
27
27
  import { shellQuoteArg } from './review-state.mjs';
28
28
 
@@ -37,6 +37,9 @@ const JSON_INDENT = 2;
37
37
  // The recipeFingerprint shape (recommendations.mjs: sha256 hex sliced to 16) — a fail-closed guard so
38
38
  // the store never records a malformed or injected value.
39
39
  export const FINGERPRINT_PATTERN = /^[0-9a-f]{16}$/u;
40
+ // The lane this writer records when none is named — the original single-lane contract, so every
41
+ // pre-existing sandbox-lane invocation and its rendered one-liner stay byte-identical.
42
+ export const DEFAULT_ACK_LANE = 'sandbox-lane';
40
43
 
41
44
  export const ACK_WRITE_STOP = 'ACK_WRITE_STOP';
42
45
  export const stop = (message) =>
@@ -82,34 +85,41 @@ const preflightTarget = (absPath, deps) => {
82
85
 
83
86
  // Pure preflight (both dry-run and apply). Validates the fingerprint, refuses an absent deployment,
84
87
  // gates the target, and computes the merge — no writes.
85
- export const planAckWrite = ({ cwd, fingerprint }, deps = {}) => {
88
+ export const planAckWrite = ({ cwd, fingerprint, lane = DEFAULT_ACK_LANE }, deps = {}) => {
86
89
  // typeof BEFORE RegExp.test — .test() coerces its arg to a string, so a number or a single-element
87
90
  // array of 16 hex chars would otherwise pass the guard and be written as a non-string ack the
88
91
  // reader then ignores.
89
92
  if (typeof fingerprint !== 'string' || !FINGERPRINT_PATTERN.test(fingerprint)) {
90
93
  throw usageFail(`--fingerprint must be a 16-char lowercase hex fingerprint (got ${JSON.stringify(fingerprint ?? null)})`);
91
94
  }
95
+ // hasOwn, never a bare lookup: an inherited name ("constructor", "toString") would otherwise
96
+ // resolve to a function and be written into the shared store as an invented key.
97
+ if (typeof lane !== 'string' || !Object.hasOwn(ACK_LANES, lane)) {
98
+ throw usageFail(`--lane must name a known ack lane (${Object.keys(ACK_LANES).join(', ')}); got ${JSON.stringify(lane ?? null)}`);
99
+ }
100
+ const ackKey = ACK_LANES[lane];
101
+ const noun = `the neutral ${lane} ack`;
92
102
  const root = resolve(cwd);
93
- assertDocsAiDeployment(root, deps, { stop, noun: 'the neutral sandbox-lane ack', rel: ACKS_FILE });
103
+ assertDocsAiDeployment(root, deps, { stop, noun, rel: ACKS_FILE });
94
104
  const absPath = join(root, ACKS_FILE);
95
105
  const { existed, existing } = preflightTarget(absPath, deps);
96
- const alreadyAcked = existing[ACKS_LANE_KEY] === fingerprint;
97
- const merged = { ...existing, [ACKS_LANE_KEY]: fingerprint };
98
- const otherKeys = Object.keys(existing).filter((k) => k !== ACKS_LANE_KEY);
99
- return { root, absPath, fingerprint, existed, existing, merged, alreadyAcked, otherKeys };
106
+ const alreadyAcked = existing[ackKey] === fingerprint;
107
+ const merged = { ...existing, [ackKey]: fingerprint };
108
+ const otherKeys = Object.keys(existing).filter((k) => k !== ackKey);
109
+ return { root, absPath, fingerprint, lane, ackKey, noun, existed, existing, merged, alreadyAcked, otherKeys };
100
110
  };
101
111
 
102
- export const writeAck = ({ cwd, fingerprint, dryRun = true } = {}, deps = {}) => {
103
- const plan = planAckWrite({ cwd, fingerprint }, deps);
112
+ export const writeAck = ({ cwd, fingerprint, lane = DEFAULT_ACK_LANE, dryRun = true } = {}, deps = {}) => {
113
+ const plan = planAckWrite({ cwd, fingerprint, lane }, deps);
104
114
  if (dryRun) return { wrote: false, dryRun: true, ...plan };
105
115
  const body = `${JSON.stringify(plan.merged, null, JSON_INDENT)}\n`;
106
- writeDocsAiFileAtomic(plan.root, ACKS_FILE, body, deps, { stop, noun: 'the neutral sandbox-lane ack' });
116
+ writeDocsAiFileAtomic(plan.root, ACKS_FILE, body, deps, { stop, noun: plan.noun });
107
117
  return { wrote: true, dryRun: false, ...plan };
108
118
  };
109
119
 
110
120
  // ── report ──────────────────────────────────────────────────────────────────────────────
111
- export const applyCommand = (root, fingerprint) =>
112
- `node ${q(ACK_WRITE_TOOL)} --fingerprint ${fingerprint} --cwd ${q(root)} --apply`;
121
+ export const applyCommand = (root, fingerprint, lane = DEFAULT_ACK_LANE) =>
122
+ `node ${q(ACK_WRITE_TOOL)}${lane === DEFAULT_ACK_LANE ? '' : ` --lane ${lane}`} --fingerprint ${fingerprint} --cwd ${q(root)} --apply`;
113
123
 
114
124
  export const formatResult = (result) => {
115
125
  const merge = result.otherKeys.length > 0 ? ` (merge-preserving ${result.otherKeys.length} existing key(s))` : '';
@@ -121,27 +131,29 @@ export const formatResult = (result) => {
121
131
  }
122
132
  return [
123
133
  `agent-workflow ack — DRY RUN (no changes; re-run with --apply)`,
124
- ` - would ${result.existed ? 'set' : 'create'} ${ACKS_FILE} "${ACKS_LANE_KEY}" = "${result.fingerprint}"${merge}`,
134
+ ` - would ${result.existed ? 'set' : 'create'} ${ACKS_FILE} "${result.ackKey}" = "${result.fingerprint}"${merge}`,
125
135
  ` - this is a NEUTRAL recipe acknowledgement, never a security key.`,
126
- ` to apply: ${applyCommand(result.root, result.fingerprint)}`,
136
+ ` to apply: ${applyCommand(result.root, result.fingerprint, result.lane)}`,
127
137
  ].join('\n');
128
138
  }
129
139
  return [
130
140
  `agent-workflow ack — APPLY`,
131
- ` - ${ACKS_FILE}: "${ACKS_LANE_KEY}" = "${result.fingerprint}"${merge}`,
141
+ ` - ${ACKS_FILE}: "${result.ackKey}" = "${result.fingerprint}"${merge}`,
132
142
  ].join('\n');
133
143
  };
134
144
 
135
145
  // ── CLI ─────────────────────────────────────────────────────────────────────────────────
136
- const USAGE = `usage: ack-write --fingerprint <16-hex> [--dry-run | --apply] [--cwd <dir>] [--help]
146
+ const USAGE = `usage: ack-write --fingerprint <16-hex> [--lane <${Object.keys(ACK_LANES).join('|')}>] [--dry-run | --apply] [--cwd <dir>] [--help]
137
147
 
138
- Records the NEUTRAL sandbox-lane recipe acknowledgement into ${ACKS_FILE} (the family-owned ack
139
- store — no host settings validator guards it). Default is --dry-run (a preview; writes nothing) and
140
- prints the exact --apply command. --apply merges "${ACKS_LANE_KEY}" into ${ACKS_FILE}, preserving
141
- every existing key. Refuses an absent docs/ai deployment; never a security key; never commits.`;
148
+ Records a NEUTRAL recipe acknowledgement into ${ACKS_FILE} (the family-owned ack store — no host
149
+ settings validator guards it). --lane names which advisor item is being acknowledged (default
150
+ "${DEFAULT_ACK_LANE}"); each lane owns one top-level key. Default is --dry-run (a preview; writes
151
+ nothing) and prints the exact --apply command. --apply merges that ONE key into ${ACKS_FILE},
152
+ preserving every existing key. Refuses an absent docs/ai deployment; never a security key; never
153
+ commits.`;
142
154
 
143
155
  export const parseArgs = (argv) => {
144
- const opts = { fingerprint: undefined, dryRunFlag: false, apply: false, cwd: undefined, help: false };
156
+ const opts = { fingerprint: undefined, lane: DEFAULT_ACK_LANE, dryRunFlag: false, apply: false, cwd: undefined, help: false };
145
157
  for (let i = 0; i < argv.length; i += 1) {
146
158
  const arg = argv[i];
147
159
  if (arg === '--help' || arg === '-h') opts.help = true;
@@ -151,6 +163,10 @@ export const parseArgs = (argv) => {
151
163
  i += 1;
152
164
  if (argv[i] === undefined || argv[i].startsWith('-')) throw usageFail('--fingerprint needs a value');
153
165
  opts.fingerprint = argv[i];
166
+ } else if (arg === '--lane') {
167
+ i += 1;
168
+ if (argv[i] === undefined || argv[i].startsWith('-')) throw usageFail('--lane needs a lane name');
169
+ opts.lane = argv[i];
154
170
  } else if (arg === '--cwd') {
155
171
  i += 1;
156
172
  if (argv[i] === undefined || argv[i].startsWith('-')) throw usageFail('--cwd needs a directory argument');
@@ -160,7 +176,7 @@ export const parseArgs = (argv) => {
160
176
  }
161
177
  }
162
178
  if (opts.dryRunFlag && opts.apply) throw usageFail('--dry-run and --apply cannot be used together');
163
- return { help: opts.help, fingerprint: opts.fingerprint, dryRun: !opts.apply, cwd: opts.cwd };
179
+ return { help: opts.help, fingerprint: opts.fingerprint, lane: opts.lane, dryRun: !opts.apply, cwd: opts.cwd };
164
180
  };
165
181
 
166
182
  export const main = (argv = process.argv.slice(2), deps = {}) => {
@@ -172,7 +188,7 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
172
188
  log(USAGE);
173
189
  return EXIT_OK;
174
190
  }
175
- const result = writeAck({ cwd: args.cwd ?? deps.cwd ?? process.cwd(), fingerprint: args.fingerprint, dryRun: args.dryRun }, deps);
191
+ const result = writeAck({ cwd: args.cwd ?? deps.cwd ?? process.cwd(), fingerprint: args.fingerprint, lane: args.lane, dryRun: args.dryRun }, deps);
176
192
  log(formatResult(result));
177
193
  return EXIT_OK;
178
194
  } catch (err) {
@@ -454,7 +454,7 @@ export const writeReadLane = ({ cwd, dryRun = true } = {}, deps = {}) => {
454
454
  };
455
455
 
456
456
  const READ_LANE_POSTURE =
457
- "posture: an auto-allowed 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. The subset invariant holds against that AUDITED CORE (every segment is an audited-core read), so the lane is a standalone opt-in grant BOUNDED BY the audited core — never a command outside it; it auto-approves those core commands REGARDLESS of which you seeded as individual settings rules (not strictly a subset of your current settings). This consent is a PROJECT-PERSISTENT declaration in docs/ai/lanes.json — it applies to every future session and to subagents' Bash, and where docs/ai is committed, to every checkout. It bypasses the PROMPT only, never the OS sandbox; on engine builds where a hook allow supersedes an ask rule, the opt-in covers EXACTLY the audited read-only core — nothing else.";
457
+ "posture: an auto-allowed 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. The subset invariant holds against that AUDITED CORE (every segment is an audited-core read), so the lane is a standalone opt-in grant BOUNDED BY the audited core — never a command outside it; it auto-approves those core commands REGARDLESS of which you seeded as individual settings rules (not strictly a subset of your current settings). This consent is a PROJECT-PERSISTENT declaration in docs/ai/lanes.json — it applies to every future session, to subagents' Bash where the host fires hooks on subagent Bash, and where docs/ai is committed, to every checkout. It bypasses the PROMPT only, never the OS sandbox; on engine builds where a hook allow supersedes an ask rule, the opt-in covers EXACTLY the audited read-only core — nothing else.";
458
458
 
459
459
  export const formatReadLaneResult = (result) => {
460
460
  const merge = result.otherKeys.length > 0 ? ` (merge-preserving ${result.otherKeys.length} existing key(s))` : '';
@@ -33,7 +33,7 @@
33
33
  import { readFileSync, readdirSync, lstatSync, existsSync } from 'node:fs';
34
34
  import { createHash } from 'node:crypto';
35
35
  import { homedir } from 'node:os';
36
- import { dirname, join, resolve } from 'node:path';
36
+ import { dirname, join, resolve, sep } from 'node:path';
37
37
  import { fileURLToPath, pathToFileURL } from 'node:url';
38
38
  import {
39
39
  preflightVelocityProfile,
@@ -597,6 +597,14 @@ export const recipeFingerprint = ({ hosts, dirs, home }) => {
597
597
  // consulted as an ack.
598
598
  export const ACKS_FILE = 'docs/ai/acks.json';
599
599
  export const ACKS_LANE_KEY = 'sandboxLaneAck';
600
+ export const ACKS_WORKTREES_DIR_KEY = 'worktreesDirAck';
601
+ // The CLOSED-WORLD ack-lane registry: the lane name an advisor item renders on the writer's
602
+ // command line → the store key that writer sets. A lane the registry does not name is a usage
603
+ // refusal at the writer, never a newly-invented key in the shared store.
604
+ export const ACK_LANES = Object.freeze({
605
+ 'sandbox-lane': ACKS_LANE_KEY,
606
+ 'worktrees-dir': ACKS_WORKTREES_DIR_KEY,
607
+ });
600
608
 
601
609
  // The opt-in read-lane toggle file (AD-055 Part II) — the SAME kit-owned docs/ai/lanes.json the
602
610
  // placed hook reads live. The read-lane item offers to enable it once the hook is placed+wired.
@@ -632,7 +640,7 @@ export const SANDBOX_LANE_ACK_KEY = 'sandboxLaneAck';
632
640
  // writer refuses such a deployment — the reader must too), a symlinked/dangling LEAF must not read as
633
641
  // not-yet-acked, and a non-regular target (FIFO/dir/device) is a fail-closed SKIP — never read it (a
634
642
  // FIFO would BLOCK the advisor). ENOENT-safe: an absent file/dir is the NORMAL not-yet-acked null.
635
- const readAcksLane = (root, deps) => {
643
+ const readAckValue = (root, deps, ackKey) => {
636
644
  const readFile = deps.readFile ?? readFileSync;
637
645
  const lstat = deps.lstat ?? lstatSync;
638
646
  const absPath = join(root, ACKS_FILE);
@@ -651,7 +659,7 @@ const readAcksLane = (root, deps) => {
651
659
  if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
652
660
  throw new Error(`${ACKS_FILE}: expected a JSON object`);
653
661
  }
654
- const value = parsed[ACKS_LANE_KEY];
662
+ const value = parsed[ackKey];
655
663
  return typeof value === 'string' ? value : null;
656
664
  };
657
665
 
@@ -719,7 +727,7 @@ const probeSandboxLane = ({ root, deps, add, skip }) => {
719
727
  // then the legacy settings scopes; a stale value in one store is ignored when another matches
720
728
  // (Decisions 2). A changed recipe (hosts, dirs, or an env override) re-fires the item (D4).
721
729
  const acks = [
722
- readAcksLane(root, deps),
730
+ readAckValue(root, deps, ACKS_LANE_KEY),
723
731
  settings.data?.[SANDBOX_LANE_ACK_PARENT]?.[SANDBOX_LANE_ACK_KEY],
724
732
  localSettings.data?.[SANDBOX_LANE_ACK_PARENT]?.[SANDBOX_LANE_ACK_KEY],
725
733
  ];
@@ -742,9 +750,86 @@ const probeSandboxLane = ({ root, deps, add, skip }) => {
742
750
  }
743
751
  };
744
752
 
745
- // The worktrees-dir arming item (parallel feature worktrees): fires when the resolved worktrees
746
- // parent dir (docs/ai/worktrees.json parentDir, else the repo's own parent) is not confirmed
747
- // writable by a trusted host signal. The provision preflight remains the real create+delete test.
753
+ // A declared `sandbox.filesystem.allowWrite` entry, resolved the way a host that honors the key
754
+ // resolves it: `~` and `~/…` against the resolved home, every other form against the project root.
755
+ const resolveDeclaredDir = (entry, { home, root }) => {
756
+ if (entry === '~') return resolve(home);
757
+ if (entry.startsWith('~/')) return resolve(home, entry.slice(2));
758
+ return resolve(root, entry);
759
+ };
760
+
761
+ // Ancestor-or-equal containment on PATH SEGMENTS, never a raw string prefix — a grant on
762
+ // `<p>/farm` must never read as a grant on the sibling `<p>/farmhouse`. A grant on a DESCENDANT
763
+ // never covers its parent: the parent dir is the one provision writes into.
764
+ const dirCovers = (declaredDir, probeDir) => {
765
+ const base = declaredDir.endsWith(sep) ? declaredDir.slice(0, -sep.length) : declaredDir;
766
+ return probeDir === base || probeDir.startsWith(`${base}${sep}`);
767
+ };
768
+
769
+ // D7 lane 1 — the DECLARATION confirmation. A settings entry is not proof of writable CAPABILITY
770
+ // (runtime truth stays with the provision preflight's real create+delete probe); it is proof the
771
+ // maintainer applied this item's own advice, which is what the item may converge on. Both scopes
772
+ // are consulted, as everywhere else here; a malformed store THROWS → the probe's stated skip.
773
+ // readSettingsFile only filters ENOENT — it rejects neither a symlink nor a non-regular target, and
774
+ // then reads THROUGH the link. A declaration is a CONVERGENCE signal here, so an unguarded read
775
+ // would let a store outside the project silence the item (and a FIFO would block the advisor).
776
+ // Guard the WHOLE path chain no-follow and require a regular file BEFORE the read; ENOENT stays the
777
+ // normal not-declared fall-through, everything else becomes the probe's stated skip.
778
+ const readDeclarationScope = (root, rel, deps) => {
779
+ const lstat = deps.lstat ?? lstatSync;
780
+ const absPath = join(root, rel);
781
+ let st;
782
+ try {
783
+ assertContainedRealPath(root, absPath, { lstat });
784
+ st = lstat(absPath);
785
+ } catch (err) {
786
+ if (err?.code === 'ENOENT') return null;
787
+ throw err;
788
+ }
789
+ if (!st.isFile()) throw new Error(`${rel} is not a regular file — refusing to read it`);
790
+ return readSettingsFile(absPath, { ...deps, cwd: root });
791
+ };
792
+
793
+ // Fail-closed shape check BEFORE any coverage test — UNIFORMLY, over the whole array: a declaration
794
+ // that suppresses a recommendation is never PARTIALLY trusted, so one bad entry invalidates the
795
+ // whole list rather than being filtered away beside a good one. An empty or whitespace-only entry is
796
+ // invalid too: it is meaningless to a host, but would resolve to the project root here and silence
797
+ // the item for any probe dir inside the repo. An ABSENT allowWrite is simply nothing declared.
798
+ const declaredWritableDirs = (scope, rel) => {
799
+ const filesystem = scope?.data?.sandbox?.filesystem;
800
+ if (filesystem === null || typeof filesystem !== 'object' || Array.isArray(filesystem)) return [];
801
+ const { allowWrite } = filesystem;
802
+ if (allowWrite === undefined) return [];
803
+ if (!Array.isArray(allowWrite) || !allowWrite.every((entry) => typeof entry === 'string' && entry.trim() !== '')) {
804
+ throw new Error(`${rel}: sandbox.filesystem.allowWrite must be an array of non-empty strings`);
805
+ }
806
+ return allowWrite;
807
+ };
808
+
809
+ const declaresWritableDir = (root, probeDir, deps) => {
810
+ const home = deps.home ?? homedir();
811
+ // Both scopes are READ and SHAPE-CHECKED (never short-circuited) so the guards screen each one —
812
+ // a malformed second scope must not hide behind a converging first.
813
+ const declared = [SETTINGS_FILE, SETTINGS_LOCAL_FILE].map((rel) =>
814
+ declaredWritableDirs(readDeclarationScope(root, rel, deps), rel),
815
+ );
816
+ return declared.some((entries) =>
817
+ entries.some((entry) => dirCovers(resolveDeclaredDir(entry, { home, root }), probeDir)),
818
+ );
819
+ };
820
+
821
+ // D7 lane 2 — the ack fallback for a host that ignores the settings key, bound to the PROBED DIR
822
+ // through the shared neutral fingerprint (the same home-symbolic canonicalization the sandbox-lane
823
+ // ack uses, so a home-anchored dir acks portably). The binding is to the RESOLVED probe dir, so the
824
+ // item re-fires only when that resolved dir changes — two ABSENT parentDir values sharing an
825
+ // existing ancestor resolve to the same dir and keep the same ack.
826
+ export const worktreesDirFingerprint = (probeDir, home) => recipeFingerprint({ hosts: [], dirs: [probeDir], home });
827
+
828
+ // The worktrees-dir arming item (parallel feature worktrees): fires when write access to the
829
+ // resolved worktrees parent dir (docs/ai/worktrees.json parentDir, else the repo's own parent) is
830
+ // neither confirmed by a trusted host signal nor declared/acknowledged by the maintainer. Before
831
+ // D7 the host signal was the ONLY convergence lane — injectable from tests but never supplied in
832
+ // production, so the item fired forever even once its own advice had been applied.
748
833
  const probeWorktreesDir = ({ root, deps, add, skip }) => {
749
834
  try {
750
835
  const config = loadWorktreesConfig(root, deps);
@@ -752,13 +837,34 @@ const probeWorktreesDir = ({ root, deps, add, skip }) => {
752
837
  // the SAME canonical derivation the provision preflight probes (worktrees.mjs) — an absent
753
838
  // configured dir resolves to its nearest existing ancestor, never a false denial
754
839
  const probeDir = resolveProbeDir(parent, deps);
755
- if (deps.canWriteDir?.(probeDir) === true) return;
840
+ // A supplied host signal is runtime truth and OVERRIDES both declaration lanes in EITHER
841
+ // direction: a trusted yes converges, a trusted no fires however the project is configured.
842
+ const hostSignal = deps.canWriteDir?.(probeDir);
843
+ if (hostSignal === true) return;
844
+ const home = deps.home ?? homedir();
845
+ const fingerprint = worktreesDirFingerprint(probeDir, home);
846
+ if (hostSignal !== false) {
847
+ if (declaresWritableDir(root, probeDir, deps)) return;
848
+ if (readAckValue(root, deps, ACKS_WORKTREES_DIR_KEY) === fingerprint) return;
849
+ }
756
850
  const dir = truncatedTo(oneLineOf(probeDir), templateBudget(WHATS['worktrees-dir']));
757
- add(
758
- 'worktrees-dir',
759
- fillTemplate(WHATS['worktrees-dir'], { dir }),
760
- `HAND-APPLY: add ${JSON.stringify(probeDir)} to sandbox.filesystem.allowWrite in .claude/settings.json on settings-native hosts; on harness-managed hosts grant this dir for the session or use the provision terminal fallback`,
761
- );
851
+ const grantAdvice = `add ${JSON.stringify(probeDir)} to sandbox.filesystem.allowWrite in .claude/settings.json on settings-native hosts; on harness-managed hosts grant this dir for the session or use the provision terminal fallback`;
852
+ // The consent-flow executes only the APPLY slot; a HAND-APPLY line is maintainer territory and
853
+ // a recipe: detail is informational. So when the ack lane is open, the runnable ack preview IS
854
+ // the apply (dry-run; it prints the exact --apply the flow then runs) and the grant advice
855
+ // rides the detail, labeled as the first step. Against a trusted NO the ack could never
856
+ // converge, so the grant advice is the apply and no ack surfaces.
857
+ if (hostSignal === false) {
858
+ add('worktrees-dir', fillTemplate(WHATS['worktrees-dir'], { dir }), `HAND-APPLY: ${grantAdvice}`);
859
+ } else {
860
+ add(
861
+ 'worktrees-dir',
862
+ fillTemplate(WHATS['worktrees-dir'], { dir }),
863
+ `node ${q(toolPath('ack-write.mjs'))} --lane worktrees-dir --fingerprint ${fingerprint} --cwd ${q(root)}`,
864
+ 'worktrees-dir',
865
+ `HAND-APPLY FIRST: ${grantAdvice}; THEN this item's apply one-liner previews the dir-bound ack and prints the exact --apply that records it`,
866
+ );
867
+ }
762
868
  } catch (err) {
763
869
  skip('worktrees-dir', err);
764
870
  }
@@ -852,7 +958,7 @@ Usage:
852
958
  Computes the deterministic Recommendations section every kit upgrade ends with — VERDICT-FIRST:
853
959
  one composed verdict line opens every non-optimal render, then per item {severity · what is
854
960
  sub-optimal · the benefit in one plain line · an optional \`recipe:\` line (the sandbox-lane live
855
- recipe only) · the exact consent-gated apply one-liner}. --cwd is
961
+ recipe, or the worktrees-dir hand-apply-first grant advice) · the exact consent-gated apply one-liner}. --cwd is
856
962
  REQUIRED (the target project is explicit, never inferred from the shell's current directory). The
857
963
  section renders present-even-when-empty ("${RECOMMENDATIONS_EMPTY_LINE}"); a probe failure is a
858
964
  stated skipped-item line. Apply lines are cwd-independent (absolute tool paths, a pinned --cwd;