@sabaiway/agent-workflow-kit 14.26.0 → 14.27.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,40 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
4
4
  is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
5
5
  every `migrations/<version>-<slug>.md` newer than it, in semver order.
6
6
 
7
+ ## 14.27.0 — the bridge tier wires the bridges your recipes use: `<wrapper> *` exclusions, their hosts and their state dirs (AD-177)
8
+
9
+ **`velocity-profile --bridge-tier` now wires every used bridge's exclusions, hosts and state dirs.** Before, it
10
+ seeded each bridge whose review wrapper was placed on PATH, wrote the bare review wrapper name into
11
+ `sandbox.excludedCommands`, gave `codex-exec` no entry, and left the bridges' state dirs for you to add by hand. A bare
12
+ name matches only the command run with no arguments, so a real `codex-review code` call did not match it; and a bridge
13
+ that cannot write its state dir cannot run.
14
+
15
+ - **The used bridges.** The tier reads `docs/ai/orchestration.json` and the host's readiness and takes every bridge a
16
+ review or execute slot resolves to (the computed default when the file is absent). A placed bridge no slot uses gets
17
+ nothing.
18
+ - **Per used bridge:** for a review role, the wrapper's `code`-mode allow rule (and the quoted grounding rule with
19
+ `agy-review`) and `<wrapper> *` in `sandbox.excludedCommands`; for every used bridge, the manifest's `networkHosts`
20
+ in `sandbox.network.allowedDomains` and its `writableDirs` in `sandbox.filesystem.allowWrite`, home-relative
21
+ (`~/.codex`, `~/.gemini/antigravity-cli`), an env override honoured. Where an execute slot delegates to codex,
22
+ `codex-exec *` is excluded, with no allow rule: a delegated run keeps its prompt, as the review `plan` and `diff`
23
+ modes keep theirs.
24
+ - **Safe by construction.** Foreign entries and sibling keys stay, a second apply writes the same bytes, an entry an
25
+ earlier version wrote stays, and an entry already covering a dir (`~/.gemini` covers `~/.gemini/antigravity-cli`)
26
+ counts as present. A malformed `sandbox.filesystem` or `allowWrite` stops the run with nothing written, as does a
27
+ state dir that resolves to `/`, your home or an ancestor of your home or the project (the message names the env
28
+ variable, or the declared default, to fix).
29
+ - **One derivation everywhere.** The new `tools/bridge-state-dirs.mjs` resolves and spells the dirs; the tier, the
30
+ Recommendations `bridge-tier` item, the danger check and the `--autonomy` preview read the same list. The item now
31
+ counts missing `*` exclusions and state dirs, so a project wired before 14.27.0 is offered the one re-run in
32
+ `/agent-workflow-kit upgrade`. The danger check admits only this list, additions only. The `--autonomy` preview
33
+ names each entry the tier derives for a used bridge's role whose consent rule is in the project file (a review role's
34
+ `code`-mode allow rule, or `codex-exec *` for a delegated codex) as part of your bridge consent and what it widens, instead of asking
35
+ you to remove it.
36
+ - The `velocity` and `recommendations` mode docs, the README advisor row and the manifest schema page say so, and
37
+ none of them says the state dirs stay hand-apply or that the kit withholds the grant for safety; two tool comments
38
+ (`tools/manifest/validate.mjs`, `tools/ack-write.mjs`) still say the kit never seeds filesystem allowances and are
39
+ queued as debt.
40
+
7
41
  ## 14.26.0 — the agent applies the harness's own configuration on your chat "yes" again, after a danger check of the concrete change (AD-176)
8
42
 
9
43
  **`/agent-workflow-kit upgrade` applies the harness's own configuration again.** In 14.25.0 an item that writes
package/README.md CHANGED
@@ -256,7 +256,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
256
256
  | `/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. |
257
257
  | `/agent-workflow-kit coverage-check` | any time | **the final-run checker** (D3(c)+(d)) — **certifies coverage ONLY inside the `--final` run that owns the lcov** (ownership is exclusive by CONVENTION over the fixed path, not enforced — a concurrent writer to it is a stated residual, queued as LCOV-EXCLUSIVE-OWNERSHIP): an artifact on disk proves nothing about the tree it came from, so a standalone run prints its findings and states `attested=no` / `NO VERDICT` rather than a PASS (an lcov that predates an edit would otherwise certify a line the suite never executed). `attested=` claims a verdict was ISSUED, pass or fail — a run over uncovered lines still reads `attested=yes` and still exits 1, and a run whose coverage arm never executed (no lcov) reads `attested=no` even inside `--final`, because nothing was read and nothing is certified. The runner passes a nonce whose one-way commitment over `{nonce, fingerprint, base}` is the `final-start.attempt` it recorded; a context describing another tree, or matching no recorded attempt, is a REFUSAL, never a verdict. Findings are unchanged — 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. |
258
258
  | `/agent-workflow-kit commit-guard` | any time | **the read-only pre-commit guard** (D10) — makes the commit capture the whole current working tree, so «verified» and «about to be committed» are the same bytes (the receipt itself has a stated residual — see the mode doc). FIRST it refuses an **INDEX that lags the verified working tree** (the gates and the fingerprint describe the WORKING tree while `git commit` takes the INDEX alone, and the fingerprint cannot tell them apart — so a lagging index used to ship a strict subset of what was verified): unstaged tracked paths or reviewable untracked-not-ignored paths, named up to a bounded cap with the remainder stated, a dirty tracked **submodule** named separately with its own recovery, and fail-closed on an undecidable probe. This deliberately blocks a partial commit. Then it 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. |
259
- | `/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, a feature-spec layer never adopted) 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, the worktrees-dir hand-apply-first grant advice, the agents hidden-mode reconcile follow-up, the spec-adoption decline preview, or another offer's decline preview) · 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); `velocity --bridge-tier` seeds the placed bridges' declared hosts into `sandbox.network.allowedDomains`, `filesystem.allowWrite` stays **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. |
259
+ | `/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, a feature-spec layer never adopted) 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, the worktrees-dir hand-apply-first grant advice, the agents hidden-mode reconcile follow-up, the spec-adoption decline preview, or another offer's decline preview) · 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); `velocity --bridge-tier` seeds the used bridges' review allow rules, `<wrapper> *` exclusions (`codex-exec *` where an execute slot delegates, keeping its permission prompt), declared hosts into `sandbox.network.allowedDomains`, and home-relative state dirs into `sandbox.filesystem.allowWrite`; 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); writes nothing, never commits, never runs a subscription CLI. |
260
260
  | `/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 the gate exit code a kit maintainer runs: the lint checks the kit's own package. Never writes, never commits, never runs a subscription CLI. |
261
261
  | `/agent-workflow-kit dispatch` | any time | **the delegation engine** — makes "how much does delegating a sub-task actually buy?" a measured number instead of a feeling. `check <file>` validates a sub-task brief's contract block and exits 0/1 naming the first violated field — **form only**: whether the task is genuinely bounded, its design decided and its acceptance adequate stays your judgment, and a well-formed absurdity passes here by construction. `advise --step-class <c>` answers the question that comes BEFORE that one — which vehicle should carry this kind of sub-task, whether it is even present on this machine, and what the ledger has already recorded for threads of that class (finished, failed, closed without a fold, and still open — counted separately) — and it decides nothing: it refuses no dispatch, gates no verb, and a choice that diverges from the advice is noted rather than blocked. It reads the filesystem only: nothing is launched to find out what is installed, an absent ledger prints "no recorded history", an unreadable one prints the store's own words, and either way the advice still prints. The same block appears as a footer under a form-valid `check` — never under a refusal, so it can never hide one. `register` pre-registers an acceptance wave (step classes, pairing key, minimum observations per class, the mean and first-pass thresholds) so the bar can never be chosen after the results it judges — immutable per wave. `observe` records ONE hand-written observation (`solo-construction`, the baseline whose ratio is 1 by construction — except over a scope measuring zero bytes, which has no ratio at all and is recorded ineligible by name — or `self-reported`) — delegated numbers are never hand-written, they are derived. The writer verbs are where that derivation happens: `open` puts a delegated thread on the record with every mint-time field copied from the brief's own header, refusing a deadline the wrapper's cap plus its kill grace would not fit inside, and recording whether the tree it started from was clean against the thread's base — HEAD by default, or the tree `--checkpoint <oid>` names, so the second task of a story is measured over the first one's uncommitted work instead of opening dirty (a dirty one makes the result honestly unmeasurable rather than quietly counted), while `--task <brief>` also records which task brief the thread carries out and the files that brief lists, refusing, since the last commit, a task thread whose files overlap by path prefix those of an open task thread or of a folded thread of another brief (`files-overlap`), and a mix of the two kinds — an untasked open beside an open task thread or, for a `code` open of backend `codex`, after a task fold, and a task open beside an open untasked thread (`mixed-open`), refusing at open a Files path the snapshot scope can never hold (`task-files-scope`), and measuring a task thread over its files alone, so `return` and `fold` refuse a changed path that no open or folded task thread on the same base claims (`out-of-files`); `await` waits for that one dispatch to answer and writes nothing — only the finished receipt satisfies it, a run still holding the nonce means keep waiting, and a wait that runs out says so with its own exit code, names whether the deadline or your own timeout ended it, and authorizes nothing: a wait that ended without an answer is a question for you, never permission to dispatch again; `return` absorbs the receipt the wrapper minted — only a finished one, checked against the contract it actually ran, its deadline and its own report, and refused outright when the tree hides changes from git or moves while the return is being computed — and derives the bytes from git rather than from anyone's claim; `fold` is the acceptance, and it refuses if the tree moved since the return — precisely, it re-confirms **equality of the visible canonical payload**, which is a change detector rather than a cryptographic identity of the tree: under the default head base that payload's untracked section is unframed and carries no file mode, so a content or symlink target that imitates the marker opening the next entry can alias two trees, and making an untracked file executable moves nothing (both named as residuals in the mode doc, with the fix queued); under a checkpoint base it is one indexed diff that carries modes. A change set carrying a binary, a non-regular path, a submodule, or a symlink whose target is not valid UTF-8 is **refused** at `return` and `fold` — CONSERVATIVELY, rather than promise what the default head payload may not be able to follow — and the same refusals hold for a checkpoint thread. `degrade` closes a thread that never earned a fold, on the record and with its reason. `aggregate` prints one wave: the registered thresholds, every observation as context, and per step class the finished threads — a folded success with git-provable bytes contributes its ratio, one whose bytes are unprovable is excluded from the mean but still counts in the first-pass rate, and a failed thread, a degrade-closed one, or one folded after failing acceptance each count as a real zero. Below the registered minimum nothing is computed (insufficient); at or above it is computed and labeled PILOT evidence. It REFUSES rather than guessing: no pre-registration, an unfinished thread in scope, a recorded refusal-to-delegate that never opened a thread, or an ambiguous wave each stop the computation by name. `handoff-return --slug <s>` closes the worktree loop after `land --prepare`: it re-attests that main still holds exactly the prepared tree under the same HEAD (both recorded in the handoff at prepare time), prints the satellite handoff's user-owned content byte for byte with its boundaries, byte lengths and the main-owned destinations each part folds into, prints the handoff digest and both OIDs as its proof, states the after-the-fold order (a fold landed after the gates leaves them stale), and records ONE self-reported worktree-stream observation only when the whole prepared change set is measurable — a deletion, a rename, a symlink, a submodule or a mode-only change ends with a named NOT RECORDED instead, never a partial number. Appends only to its own ledger inside the git dir (never committable); it also writes `handoff-return`'s `write-tree` tree object and, for a checkpoint measurement, the leaf's OWN temporary index file and the blobs its forced adds store — never a ref, never the real index; never commits, never runs a subscription CLI. |
262
262
  | `/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 — printed only for a link whose RAW TARGET is MAIN's `node_modules`, since a foreign or unreadable link is reported as such with no removal advised); 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); `prompt <slug>` is read-only too and re-prints the satellite's cold-start prompt — the same text `provision` ends its report with: where the worktree is, its ONE seeded plan, the handoff as the one channel back, MAIN's orientation (shared series index, landing) and, under its own heading because it is probed on the satellite, that checkout's install posture — every value derived LIVE rather than replayed from the frozen provision record, a recorded value that no longer matches NAMED as a divergence (with the cause its source makes likely) instead of printed as the runnable one, every offered command marked with WHO runs it (`MAIN $ …` for the landing, `HERE $ …` for this checkout's own install, a posture with nothing to run staying prose), and a control character in any rendered value a typed STOP rather than a forged prompt line; `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. |
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: agent-workflow-kit
3
3
  description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow". New to the epic, story and task tier? Run `/agent-workflow-kit tier`. A typed decision with a confidence from Jev? Run `/agent-workflow-kit jev`.
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '14.26.0'
6
+ version: '14.27.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
@@ -2,7 +2,7 @@
2
2
  name: codex-cli-bridge
3
3
  description: Delegate work to the OpenAI Codex CLI (`codex`) under a ChatGPT subscription — run plan/instruction EXECUTION in a sandboxed workspace, or get a read-only ADVISORY review of a plan or working-tree diff — as a second delegated-execution backend beside Antigravity. Use when the user wants to hand a bounded coding task or plan to `codex exec`, get a second-opinion review from codex, install or authenticate Codex CLI, understand its sandbox/network/approval policy, drive codex efficiently from the main agent (exec vs review, resume, the commit boundary), bridge project context (`AGENTS.md`) into codex, or troubleshoot codex flags, models, auth, or its no-TTY headless behaviour.
4
4
  metadata:
5
- version: '5.0.0'
5
+ version: '5.0.1'
6
6
  ---
7
7
 
8
8
  # codex-cli-bridge
@@ -430,7 +430,7 @@ CHATGPT_LOGIN_GUARD="Logged in using ChatGPT"
430
430
  # receipt this wrapper mints; scripts/release/version-sync.mjs bumps it under the one-anchor-per-file
431
431
  # rule, so a release can never leave it behind (the AD-053 drift class).
432
432
  AW_RECEIPT_BACKEND="codex"
433
- AW_BRIDGE_VERSION="5.0.0" # aw-version-anchor
433
+ AW_BRIDGE_VERSION="5.0.1" # aw-version-anchor
434
434
  # The kill grace handed to timeout(1) as --kill-after, and recorded in the receipt as killGraceS:
435
435
  # ONE constant, so the number the ledger checks against the dispatch deadline is the number the run
436
436
  # actually applied.
@@ -1495,9 +1495,9 @@ aw_scan_nested_sandbox() { # $1 = rc, $2 = trace path
1495
1495
  if [[ "$1" -ne 0 ]]; then
1496
1496
  if grep -qiE "$AW_NS_MECHANISM" "$2" 2>/dev/null && grep -qiE "$AW_NS_FAILURE" "$2" 2>/dev/null; then
1497
1497
  echo "hint: this looks like a NESTED-SANDBOX failure — codex-exec ships its own OS sandbox (bwrap)," >&2
1498
- echo " which cannot run nested inside a harness sandbox (the FS is read-only). Route codex-exec" >&2
1499
- echo " OUTSIDE the harness sandbox: add it to the harness sandbox excludedCommands, or dispatch" >&2
1500
- echo " this one run via a per-run consented bypass. Do NOT blanket-disable the sandbox." >&2
1498
+ echo " which cannot run nested inside a harness sandbox (the FS is read-only). Route codex-exec OUTSIDE it:" >&2
1499
+ echo " the sandbox excludedCommands entry 'codex-exec *' (velocity-profile --bridge-tier adds it where an execute" >&2
1500
+ echo " slot delegates to codex), or a per-run consented bypass for this one run. Do NOT blanket-disable the sandbox." >&2
1501
1501
  fi
1502
1502
  return 0
1503
1503
  fi
@@ -1508,9 +1508,9 @@ aw_scan_nested_sandbox() { # $1 = rc, $2 = trace path
1508
1508
  echo " signature. codex-exec ships its own OS sandbox (bwrap), which cannot run nested inside a" >&2
1509
1509
  echo " harness sandbox (the FS turns read-only), so the backend most likely could not read what" >&2
1510
1510
  echo " it was asked to check: the answer above may be UNGROUNDED — treat it as such rather than" >&2
1511
- echo " banking it. Re-dispatch OUTSIDE the harness sandbox: add codex-exec to the harness" >&2
1512
- echo " sandbox excludedCommands, or use a per-run consented bypass. Do NOT blanket-disable the" >&2
1513
- echo " sandbox. The exit status stays 0 on purpose — this is a warning, never a gate." >&2
1511
+ echo " banking it. Re-dispatch OUTSIDE the harness sandbox: the sandbox excludedCommands entry 'codex-exec *'" >&2
1512
+ echo " (velocity-profile --bridge-tier adds it where an execute slot delegates to codex), or a per-run consented" >&2
1513
+ echo " bypass. Do NOT blanket-disable the sandbox. The exit status stays 0 on purpose — this is a warning, never a gate." >&2
1514
1514
  return 0
1515
1515
  fi
1516
1516
  done <"$2"
@@ -342,7 +342,7 @@ aw_scrub_billing_keys() {
342
342
  # Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
343
343
  # version (drift-guarded by codex-review.test.mjs against capability.json).
344
344
  AW_RECEIPT_BACKEND="codex"
345
- AW_BRIDGE_VERSION="5.0.0" # aw-version-anchor
345
+ AW_BRIDGE_VERSION="5.0.1" # aw-version-anchor
346
346
  # Generous hard cap for a slow review at the host posture's effort (subscription latency varies).
347
347
  CODEX_HARD_TIMEOUT="${CODEX_HARD_TIMEOUT:-1800}"
348
348
  # Above this assembled-payload size (bytes), the diff goes via a git-dir-local temp
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "codex-cli-bridge",
5
5
  "kind": "execution-backend",
6
- "version": "5.0.0",
6
+ "version": "5.0.1",
7
7
  "posture": { "model": "gpt-6.1-sol", "effort": "high", "tier": null },
8
8
  "provides": ["execute", "review"],
9
9
  "roles": {
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "14.26.0",
6
+ "version": "14.27.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "14.26.0",
3
+ "version": "14.27.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",
@@ -28,11 +28,11 @@ The **consent route** for a harness item: run its `check:` line, show its result
28
28
 
29
29
  A variant's posture note, where its key has one, is printed by `init` from this installed mode doc (the one source); a variant without a note shows its preview, and a harness variant also the check's lines.
30
30
 
31
- - `bridge-tier` — the lane is harness and env-dependent: the agent applies the item on the chat yes after the check, writing the project `.claude/settings.json`; `init` previews `velocity-profile --bridge-tier` and applies it on the terminal y as the route the user may take instead. It seeds, for each placed bridge only, the review wrapper's `code`-mode allow rule (a wrapper then runs UNATTENDED: it reads any repo file it is pointed at and sends the assembled payload to its subscription backend), the quoted grounding pre-step rule when `agy-review` is placed, the wrapper names in `sandbox.excludedCommands`, and the declared hosts of the placed bridges (each bundled manifest's `networkHosts`) in `sandbox.network.allowedDomains`. Where the host honours the settings sandbox keys, every sandboxed command of the project can then reach those hosts, not only the wrappers, and the `--autonomy` preview reports them as a network weakening. The writable state dirs (`writableDirs`) stay hand-apply (the sandbox lanes below). The item fires while an allow rule, an exclusion or a declared host of a placed bridge is missing, so a project wired before the tier seeded hosts is offered the one re-run.
32
- - `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; the tier seeds `sandbox.network.allowedDomains` with the placed bridges' declared hosts only through the consented `--bridge-tier` (AD-172, which reverses the bridge council of 2026-07-11 for those hosts alone; the egress they widen for every sandboxed command is stated at that consent), and `sandbox.filesystem.allowWrite` stays unseeded (that council: 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.
31
+ - `bridge-tier` — the lane is harness and env-dependent: the agent applies the item on the chat yes after the check, writing the project `.claude/settings.json`; `init` previews `velocity-profile --bridge-tier` and applies it on the terminal y as the route the user may take instead. It wires the USED bridges from the project's effective recipes and host readiness: each used review wrapper's `code`-mode allow rule (a wrapper then runs UNATTENDED: it reads any repo file it is pointed at and sends the assembled payload to its subscription backend), the quoted grounding pre-step rule when `agy-review` is used, `codex-review *` / `agy-review *` in `sandbox.excludedCommands`, and `codex-exec *` where an execute slot delegates (no execution allow rule: it keeps its prompt). It also seeds the used bridges' declared hosts (`networkHosts`) in `sandbox.network.allowedDomains` and resolved state dirs (`writableDirs`), home-relative where under home, in `sandbox.filesystem.allowWrite`. Where the host honours the settings sandbox keys, an excluded wrapper runs outside the sandbox, every sandboxed command of the project can reach the seeded hosts, and every sandboxed command can write the seeded state dirs. The item fires while an allow rule, a `*` exclusion, a host or a state dir of a used bridge is missing, so a project wired before this revision is offered the one re-run.
32
+ - `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; the consented `--bridge-tier` seeds the used bridges' declared hosts in `sandbox.network.allowedDomains` and resolved state dirs in `sandbox.filesystem.allowWrite`, with each widening stated at that consent. 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.
33
33
 
34
34
  - `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.
35
- - `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.
35
+ - `worktrees-dir` — on a settings-native host that honors the key, the HAND-APPLY line widens the OS-sandbox WRITE surface to the whole worktrees parent dir: every sibling path under it (other repositories included) becomes agent-writable, and the widening persists for every later session. A harness-managed host may ignore that project setting; grant the narrow parent through host/session controls or use the provision terminal fallback instead. When that scope is wider than you want, narrow it FIRST: create a dedicated dir yourself (outside the agent's write surface), point `docs/ai/worktrees.json` `parentDir` at it, then re-run recommendations — the item re-renders with the narrowed dir. The kit does not write this worktrees grant itself; the line is always yours to paste. **Convergence** is two-path: a declared `sandbox.filesystem.allowWrite` entry covering the probed dir (either settings scope — `~` and `~/…` resolve against home, and coverage is path-segment-aware, so a grant on a sibling or on a child never counts), or — for a host that ignores that key — the neutral dir-bound acknowledgement recorded by this item's consent-gated apply one-liner (`ack-write --lane worktrees-dir` — a dry-run preview that prints the exact `--apply`; recorded as `worktreesDirAck` in the family-owned `docs/ai/acks.json`, never a security key), while the grant advice itself rides the `recipe:` line as the labeled FIRST step; against a trusted host NO the apply stays the HAND-APPLY grant advice and no ack is offered. Neither is proof of write CAPABILITY: the provision preflight's real create+delete probe stays the runtime truth, and the fingerprint is bound to the **resolved probe dir**, so the item re-fires only when that resolved dir changes (two absent `parentDir` values sharing an existing ancestor resolve to the same dir and keep the same ack). Risk profile: a real write-surface widening where honored — scope it deliberately.
36
36
 
37
37
  - `gates-inert` — the gate matrix is DECLARED but verifies nothing, and the two causes have different remedies, so the item renders a different apply for each. **Cause A** — a canonical `coverage-check` gate with no producer gate declared BEFORE it (none at all, or one declared after it, which writes the lcov too late): the run certifies no coverage of its own — `coverage=not-run` when nothing wrote an lcov, or a verdict over STALE bytes an earlier run left in the git dir, which is worse because it reads as `coverage=certified`. This item is what surfaces that state at upgrade instead of leaving it to be noticed. The remedy needs the producer to run BEFORE the checker — declared or MOVED there — and what the fill can do about that is exactly what the apply says. When the checker is the LAST declared gate and the project's own scripts yield an offerable producer, the fill PLACES that producer before it, so the apply is the ordinary `gates-init` dry-run preview. Otherwise — no offerable producer, or a checker that is not last — this arm stays **HAND-APPLY**: the maintainer's own step is to edit `docs/ai/gates.json` (the exact producer form lives in `${CLAUDE_SKILL_DIR}/references/modes/gates.md`), because the fill never reorders entries it did not write. **Cause B** — every declared gate is one of the kit's own canonical checkers, so the matrix runs no project-verification command at all: the apply is the `gates-init` dry-run preview, the same consent-gated seeder the `gates-declaration` item renders, and after the SAME confirmation you run the `--apply` line it prints. Neither cause changes any gate result, exit code or receipt — the item adds no enforcement, only the offer. Risk profile: `docs/ai/gates.json` is a PRIVILEGED file (a declared gate is auto-approvable by the wired hook), so both causes land in the same place — where the fill can help, it declares a project-controlled command on your explicit yes; where it cannot, the edit stays maintainer-only.
38
38
 
@@ -53,10 +53,10 @@ The sandbox items state: on native Windows Claude Code runs no sandbox: this ite
53
53
  Only Claude Code on Linux/WSL2 is measured; Codex, Devin and agy as hosts and macOS are unmeasured.
54
54
  A first session may meet ordinary prompts until the autonomy render lands; a chat apply is promptless only where the host auto-allows sandboxed commands.
55
55
 
56
- - **Settings-native sandbox** (the harness reads the `.claude/settings.json` sandbox keys): where the harness honours the `--bridge-tier` wiring's `excludedCommands`, the wrappers run OUTSIDE the sandbox and never consult `allowedDomains`/`allowWrite` — nothing beyond the tier. A harness may not honour that entry (observed 2026-10-06: Claude Code 2.1.291 ran an agent's plain `codex-review` inside its sandbox with the project entry present, and that run still reached `chatgpt.com`, which only `allowedDomains` admitted); there a recipe host outside `allowedDomains` is refused by the harness proxy or held for a network prompt (a refusal reads `Tunnel connection failed: 403` or `CONNECT tunnel failed, response 403` in the run). The lane for the hosts is the `--bridge-tier` re-run (the `bridge-tier` item offers it while a declared host of a placed bridge is missing): it seeds the placed bridges' declared hosts into `sandbox.network.allowedDomains`, and the agent never hand-adds them. For a wrapper the harness keeps sandboxed the hand-apply `allowWrite` step for the writable state dirs stays: the `filesystem.allowWrite` half of the shape in (2) below, filled from this recipe — the agent does not add it and the kit never writes it. Record the ack (the ack-write one-liner) once the lane is confirmed.
57
- - **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 writes `allowedDomains` only via `--bridge-tier`, and never `allowWrite`) 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.
56
+ - **Settings-native sandbox** (the harness reads the `.claude/settings.json` sandbox keys): where the harness honours the `--bridge-tier` wiring's `excludedCommands` entries (`codex-review *` / `agy-review *`, and `codex-exec *` where an execute slot delegates, keeping its prompt), the used bridges' wrappers run OUTSIDE the sandbox and never consult `allowedDomains`/`allowWrite` — nothing beyond the tier. A bare name matches only the argument-free command: observed 2026-10-06, Claude Code 2.1.291 ran an agent's plain `codex-review` inside its sandbox beside the bare entry; a harness may keep a `*`-excluded wrapper sandboxed too (unmeasured). There a recipe host outside `allowedDomains` is refused by the harness proxy or held for a network prompt (a refusal reads `Tunnel connection failed: 403` or `CONNECT tunnel failed, response 403` in the run). The lane for the hosts AND the state dirs is the `--bridge-tier` re-run: it seeds the used bridges' declared hosts into `sandbox.network.allowedDomains` and resolved state dirs into `sandbox.filesystem.allowWrite`, and the `bridge-tier` item offers it while any used-bridge surface is missing. Where the host honours the settings sandbox keys, every sandboxed command of the project can reach those hosts and every sandboxed command can write those state dirs. Record the ack (the ack-write one-liner) once the lane is confirmed.
57
+ - **Harness-managed sandbox** (an IDE/session-imposed sandbox that ignores the settings keys) — a NARROWEST-SCOPE ladder: (1) prefer wrapper/command-SCOPED sandbox rules where the harness offers them (only the review wrappers gain the egress + state-dir writes); (2) a SESSION-scoped allowance (the manifest hosts + the resolved state dirs for the whole session) is an INFORMED WIDENING — it carries the same blast-radius class the `--bridge-tier` keys carry where a host honours them (every sandboxed command in the session gains that egress and write access to those state dirs), acceptable only as the maintainer's deliberate choice for a review-heavy session. Its **paste-ready hand-apply shape** (for this harness-managed lane; the kit seeds the used bridges' `allowedDomains` and `allowWrite` through the consented `--bridge-tier`) 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.
58
58
  - 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).
59
59
 
60
- **Invariants:** read-only (never writes, never commits, never runs a subscription CLI) · `--cwd` required · present-even-when-empty · verdict-first from frozen templates · probe failures degrade to stated skip lines · chat apply one-liners are cwd-independent (absolute tool paths + a pinned `--cwd`; the ONE exception is the `set-autonomy` item, a conversational skill invocation explicitly labeled *run IN the target project*) · the bare `sandbox-provision` item pins via a `cd <root> &&` prefix — the doctor reads its cwd · harness items carry a check and an apply bound to its digest; user items, including `family-freshness`, carry the user's own step, and no-writer variants keep their text · `sandbox.filesystem.allowWrite` stays hand-apply territory, and `sandbox.network.allowedDomains` is seeded only by the consented `velocity-profile --bridge-tier` · 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).
60
+ **Invariants:** read-only (never writes, never commits, never runs a subscription CLI) · `--cwd` required · present-even-when-empty · verdict-first from frozen templates · probe failures degrade to stated skip lines · chat apply one-liners are cwd-independent (absolute tool paths + a pinned `--cwd`; the ONE exception is the `set-autonomy` item, a conversational skill invocation explicitly labeled *run IN the target project*) · the bare `sandbox-provision` item pins via a `cd <root> &&` prefix — the doctor reads its cwd · harness items carry a check and an apply bound to its digest; user items, including `family-freshness`, carry the user's own step, and no-writer variants keep their text · the used bridges' `sandbox.filesystem.allowWrite` and `sandbox.network.allowedDomains` are seeded only by the consented `velocity-profile --bridge-tier`; the harness-managed sandbox shape and worktrees grant keep their hand-apply steps · 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).
61
61
 
62
62
  **Exit codes:** `0` report rendered (items or the empty state); `1` error (e.g. `--cwd` is not a directory); `2` usage.
@@ -50,11 +50,11 @@ Honesty notes: tier entries get **NO PreToolUse-hook residual coverage — with
50
50
 
51
51
  **The boundary of that qualifier, stated so it is not guessed.** It covers claims about `sandbox.*` keys — the block whose honouring was observed to vary by host. It deliberately does NOT cover `permissions.*` semantics (an allow rule auto-approving a command, `local > project` precedence for a non-sandbox key): that is the harness's own documented permission model, the layer this whole mode rests on, and hedging it would say nothing true. **Stated residual:** which sentences count as settings-derived runtime claims is decided by review, not by a checker — deciding whether a sentence asserts a runtime effect is a question about prose meaning, and this family deliberately has no mechanism that judges that (the same limit that retired the byte-scanning deny rung). A new claim-bearing line therefore has to carry the qualifier by authorship; the acceptance criterion is that every line stating what a `sandbox.*` key DOES carries it, and the tests pin the surfaces that exist today. What does NOT change is the CLASSIFICATION: which red-line a key would weaken, and that a proven bridge-tier exclusion is a note rather than a weakening, are properties of the declaration and stay flat statements.
52
52
 
53
- **The `--bridge-tier` (own opt-in, AD-044).** Seeds what a promptless council review run needs — three surfaces: `permissions.allow` prefix rules, the wrapper names in `sandbox.excludedCommands` (where the host honors the settings sandbox keys the harness runs an excluded command OUTSIDE the sandbox — the wrappers need network — so a plain allowlisted invocation triggers no sandbox-bypass approval; where it does not, the wrapper starts sandboxed and simply prompts or fails loudly — fail-safe, never a silent widening), and the placed bridges' declared hosts in `sandbox.network.allowedDomains` (below). All three land in the **project** `.claude/settings.json` — the file this writer owns; an exclusion placed only in `settings.local.json` was live-observed NOT to route the command outside the sandbox (2026-07-11: the wrapper then starts sandboxed and dies on a read-only HOME + a network prompt), so hand-wiring the local file is not a working substitute for this tier. **Honesty note:** a session whose sandbox is imposed by the harness runtime itself (e.g. an IDE-managed session sandbox) may ignore settings-level exclusions entirely — there the wrappers need the session-level sandbox config (or a per-run consented bypass); the tier's seeded posture is correct for a settings-native sandbox that honours the exclusion and simply prompts again elsewhere (fail-safe, never a silent widening). A harness that reads the settings sandbox keys may still drop this exclusion (observed 2026-10-06: Claude Code 2.1.291 ran an agent's plain `codex-review` inside its sandbox with the project entry present); its lane is the settings-native one in `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`. Each bridge's observed egress hosts are declared in its `capability.json` `networkHosts` — the single documentation source (observed-minimal — a blocked host names itself at run time; read the manifests, this doc deliberately retypes no host list). The tier ALSO seeds the placed bridges' declared hosts — the `networkHosts` of every bundled manifest whose review wrapper is placed — into the project's `sandbox.network.allowedDomains` (AD-172, which reverses the bridge council of 2026-07-11 for those hosts only): where the host honors the settings sandbox keys, a wrapper a harness keeps sandboxed despite the exclusion is no longer refused them, and every sandboxed command of the project can then reach those hosts, not only the wrappers — that widening is part of this consent, and the `--autonomy` preview reports it as a network weakening. `sandbox.filesystem.allowWrite` stays unseeded: a write allowance on CLI state dirs (`~/.codex`, `~/.gemini/…`) would make credential dirs writable to every sandboxed command, so the writable state dirs stay hand-apply. Per-bridge picture under a harness-managed sandbox (live-observed 2026-07-11/12): an IDE-managed session sandbox ignores hand-applied `sandbox.network.allowedDomains` / `sandbox.filesystem.allowWrite` in BOTH settings scopes — its own per-host network consents govern egress; the durable zero-prompt lanes there are the session/host sandbox config (hosts from `networkHosts` + the CLI state-dir writes) or the per-run consented bypass. codex additionally needs a writable HOME (EROFS `~/.codex`); note the apex-vs-wildcard nuance — an apex domain is NOT covered by its `*.`-wildcard form, so the manifests carry both forms where observed (the blocked host names itself at run time). The upgrade Recommendations advisor surfaces exactly this recipe (hosts ∪ resolved `writableDirs`) as the `sandbox-lane` discoverability item — a consent-gated **ack writer** converging on a neutral fingerprint acknowledgement recorded into the family-owned `docs/ai/acks.json` (the writable state dirs stay hand-apply; the hosts are the tier's), with the posture notes at the consent moment (`${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`). Membership is the FROZEN review-wrapper constant, **never** the execution/probe wrappers (`codex-exec`, `agy-run` keep their human prompt — delegated execution is not covered by this consent; codex-exec's nested-sandbox recovery is the canon's observed-failure lane, not a preemptive tier seed), and only the **`code` review mode** — a `plan`/`diff` invocation takes a file argument that can point OUTSIDE the repo, so those modes keep their prompt; each wrapper entry derives ONLY when its bridge is **placed on PATH** (an absent bridge is a stated skip). The seeded byte-forms (this list IS the documented-invocation source for the bridge tier):
53
+ **The `--bridge-tier` (own opt-in, AD-044).** Seeds the USED bridges from every recipe's review and execute slots in `docs/ai/orchestration.json`, resolved over host readiness to the effective recipe: a degraded recipe uses its resolved providers; an absent config takes the computed default. A bridge no slot uses gets nothing. Per used review role it seeds `Bash(<wrapper> code:*)` into `permissions.allow` and, with agy-review used, the quoted grounding pre-step rule. Per used role it seeds `<wrapper> *` into `sandbox.excludedCommands`, the manifest's `networkHosts` into `sandbox.network.allowedDomains`, and its resolved `writableDirs` into `sandbox.filesystem.allowWrite`, home-relative under the home and absolute outside it, with env overrides honoured. All entries land in the **project** `.claude/settings.json`, preserving existing entries and sibling keys. **Where the host honors the settings sandbox keys**, an exclusion means the wrapper runs outside the sandbox; a host means every sandboxed command of the project can reach it; a dir means every sandboxed command can write it — each widening is part of this consent. A session-imposed sandbox may ignore those keys; its lane is in `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`. An exclusion placed only in `settings.local.json` was live-observed NOT to route the command outside the sandbox (2026-07-11: the wrapper then starts sandboxed and dies on a read-only HOME), so hand-wiring the local file is no substitute for this tier. Under a harness-managed session sandbox (live-observed 2026-07-11/12) an IDE-managed sandbox ignores `sandbox.network.allowedDomains` and `sandbox.filesystem.allowWrite` in BOTH settings scopes — its own per-host consents govern egress, and the durable lanes there are the session sandbox config (the hosts plus the CLI state-dir writes) or the per-run consented bypass. Each bridge's observed egress hosts are declared in its `capability.json` `networkHosts` — the single documentation source (this doc retypes no host list); an apex domain is NOT covered by its `*.`-wildcard form, so the manifests carry both forms where observed. The upgrade Recommendations advisor surfaces the same recipe (hosts ∪ resolved `writableDirs`) as the `sandbox-lane` discoverability item — a consent-gated **ack writer** converging on a neutral fingerprint acknowledgement recorded into the family-owned `docs/ai/acks.json`, with the posture notes at the consent moment (`${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`). **Bare-entry reading (measured 2026-10-06):** an exclusion uses Bash rule syntax; a bare name matches only the argument-free command, so the tier writes the form with ` *`; an earlier bare entry stays and does not satisfy that surface. Where an execute slot delegates, codex-exec gets its exclusion, hosts and state dirs, but no allow rule, so a delegated run keeps its prompt. Only the code review mode is auto-allowed; plan/diff keep their prompt. The grounding rule uses the double-quoted absolute kit path the procedures advisor renders; an unseedable path is a stated skip. Its writer guard accepts only system-temp scratch or a fresh gitignored in-repo destination, refusing tracked, non-ignored, existing in-repo, outside-repo non-temp and symlink/non-regular destinations. The seeded byte-forms (this list IS the documented-invocation source for the bridge tier):
54
54
 
55
- - `Bash(codex-review code:*)` — the code-mode prefix, args wildcard; plus `codex-review` in `sandbox.excludedCommands`
56
- - `Bash(agy-review code:*)` — the code-mode prefix, args wildcard; plus `agy-review` in `sandbox.excludedCommands`
57
- - `Bash(node "${CLAUDE_SKILL_DIR}/tools/grounding.mjs":*)` — the grounding pre-step, **double-quoted absolute path**: exactly the byte-form the `procedures` advisor renders, so the pasted pre-step matches the rule (seeded↔rendered byte-parity). It derives ONLY when `agy-review` is placed (the pre-step exists for agy's grounded reviews; codex grounds natively) — a codex-only install never auto-allows this writer. The quoted form is accepted for THIS one tool only — never a general quote allowance; an unseedable kit path (spaces/metacharacters) is a stated skip. Its writer surface is closed by the tool's own guard: `--out` accepts only system-temp scratch (rewritable) or a FRESH gitignored in-repo path (create-only, exclusive write) — tracked, in-repo-not-ignored, EXISTING in-repo (even gitignored), outside-repo non-temp, and symlink/non-regular destinations are all refused.
55
+ - `Bash(codex-review code:*)` — the review code-mode allow rule; `codex-review *` — its sandbox.excludedCommands entry.
56
+ - `Bash(agy-review code:*)` — the review code-mode allow rule; `agy-review *` — its sandbox.excludedCommands entry; `Bash(node "${CLAUDE_SKILL_DIR}/tools/grounding.mjs":*)` — its quoted grounding pre-step rule.
57
+ - `codex-exec *` — the sandbox.excludedCommands entry for a used execute role; no allow rule.
58
58
 
59
59
  **Consented posture (printed on every `--bridge-tier` run, stated not pretended away):** an auto-allowed review wrapper runs **UNATTENDED** — it reads any repo file it is pointed at and sends the assembled payload to its **subscription backend**; prefix rules cannot inspect arguments, so a code-mode argument that names a readable file (agy's `--facts`/`--decided`) rides the same consent. That is the tier's purpose (unattended council review runs) and its residual. Tier entries get no PreToolUse-hook coverage — the same documented residual class as the autonomy red-line rules. The grounding entry's writer surface is bounded by `grounding.mjs`'s own scratch-destination guard (a tracked or in-repo-not-ignored `--out` is refused by the tool itself).
60
60
 
@@ -6,12 +6,14 @@ import { homedir } from 'node:os';
6
6
  import { basename, dirname, isAbsolute, join, relative, resolve, win32 } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import {
9
- BRIDGE_REVIEW_WRAPPERS, CLAUDE_DIR, KIT_GROUNDING_TOOL, RENDER_OWNED_REDLINE_RULES,
9
+ CLAUDE_DIR, RENDER_OWNED_REDLINE_RULES,
10
10
  SAFE_DEFAULT_MODES, SETTINGS_FILE, SETTINGS_LOCAL_FILE, UNIVERSAL_READONLY_ALLOWLIST,
11
- deriveBridgeTierAllowlist, deriveKitToolsAllowlist, mergeAutonomySettings,
11
+ deriveKitToolsAllowlist, detectAtRoot, mergeAutonomySettings,
12
12
  writeAutonomyProfile, writeVelocityProfile,
13
13
  } from './velocity-profile.mjs';
14
- import { bundledSandboxRecipe } from './bridge-sandbox-recipe.mjs';
14
+ import { KIT_GROUNDING_TOOL, bundledSandboxRecipe, usedBridges } from './bridge-sandbox-recipe.mjs';
15
+ import { mergeAllowWrite, stateDirsOf } from './bridge-state-dirs.mjs';
16
+ import { loadConfig } from './orchestration-config.mjs';
15
17
  import { HOOK_FILE_REL, HOOKS_DIR, buildHookSettingsEntry, mergeHookEntry, writeGateHook } from './gate-hook.mjs';
16
18
  import { writeMcp } from './mcp.mjs';
17
19
  import { ENABLED_KEY, MCP_JSON_REL, SERVERS_KEY, SERVER_NAME, allowRulesFor, buildServerEntry } from './mcp-registration.mjs';
@@ -140,11 +142,12 @@ export const judgeChange = ({ variant, files, scope }) => {
140
142
  };
141
143
  const writerFacts = (facts, deps) => ({
142
144
  ...deps, ...facts,
145
+ detect: deps.detect ?? facts.detect,
143
146
  findWrapper: (name) => findOnPath(name, { getenv: facts.env }).state === 'present',
144
147
  findOnPath: (name) => findOnPath(name, { getenv: facts.env }),
145
148
  });
146
149
 
147
- export const scopeOf = (variant, facts) => {
150
+ export const scopeOf = (variant, facts, deps = {}) => {
148
151
  const allowed = {};
149
152
  const set = (key, values) => {
150
153
  allowed[key] = (value) => values.some((entry) => equal(entry, value));
@@ -153,12 +156,13 @@ export const scopeOf = (variant, facts) => {
153
156
  const allow = [...UNIVERSAL_READONLY_ALLOWLIST];
154
157
  if (variant === 'kit-tools-tier') allow.push(...deriveKitToolsAllowlist({ projectDir: facts.root }));
155
158
  if (variant === 'bridge-tier') {
156
- const deps = writerFacts(facts, {});
157
- const placed = BRIDGE_REVIEW_WRAPPERS.filter(deps.findWrapper);
158
- allow.push(...deriveBridgeTierAllowlist({ findWrapper: deps.findWrapper,
159
- groundingAbsPath: join(facts.toolsDir, '..', KIT_GROUNDING_TOOL) }).allow);
160
- set('sandbox.excludedCommands', placed);
161
- set('sandbox.network.allowedDomains', bundledSandboxRecipe(placed, deps).hosts);
159
+ const detect = deps.detect ?? facts.detect ?? (() => detectAtRoot(facts.root, { env: facts.env, home: facts.home }));
160
+ const used = usedBridges(loadConfig(facts.root).config, detect());
161
+ const recipe = bundledSandboxRecipe(used, { groundingAbsPath: join(facts.toolsDir, '..', KIT_GROUNDING_TOOL) });
162
+ allow.push(...recipe.allow);
163
+ set('sandbox.excludedCommands', recipe.excludedCommands);
164
+ set('sandbox.network.allowedDomains', recipe.hosts);
165
+ set('sandbox.filesystem.allowWrite', stateDirsOf(recipe.dirEntries, facts));
162
166
  }
163
167
  set('permissions.allow', allow);
164
168
  } else if (variant === 'autonomy-render') {
@@ -189,7 +193,7 @@ export const scopeOf = (variant, facts) => {
189
193
  return {
190
194
  admits: ({ path, key, change, value }) => {
191
195
  const correctFile = key.startsWith(`${SERVERS_KEY}.`) ? path === MCP_JSON_REL : path === SETTINGS_FILE;
192
- const gains = key === 'hooks.PreToolUse' || key === ENABLED_KEY || key === CREDENTIALS;
196
+ const gains = variant === 'bridge-tier' || key === 'hooks.PreToolUse' || key === ENABLED_KEY || key === CREDENTIALS;
193
197
  return correctFile && (!gains || change === 'added') && Boolean(allowed[key]?.(value));
194
198
  },
195
199
  admitsPath: (path, directory = false) => directory ? directories.includes(path)
@@ -220,7 +224,7 @@ export const plannedChange = (variant, facts, deps = {}) => {
220
224
  const plan = writeVelocityProfile({ cwd: facts.root, dryRun: true,
221
225
  kitTools: variant === 'kit-tools-tier', bridgeTier: variant === 'bridge-tier' }, writerDeps);
222
226
  const before = plan.projectSettings.data ?? {};
223
- const after = structuredClone(before);
227
+ let after = structuredClone(before);
224
228
  after.permissions = { ...before.permissions,
225
229
  allow: [...(before.permissions?.allow ?? []), ...plan.toAdd, ...plan.tierToAdd, ...plan.bridgeToAdd] };
226
230
  if (plan.excludedToAdd.length || plan.hostsToAdd.length) {
@@ -229,6 +233,7 @@ export const plannedChange = (variant, facts, deps = {}) => {
229
233
  if (plan.hostsToAdd.length) after.sandbox.network = { ...before.sandbox?.network,
230
234
  allowedDomains: [...(before.sandbox?.network?.allowedDomains ?? []), ...plan.hostsToAdd] };
231
235
  }
236
+ after = mergeAllowWrite(after, plan.dirsToAdd);
232
237
  addFile(SETTINGS_FILE, plan.projectSettings.data, after);
233
238
  } else if (variant === 'autonomy-render') {
234
239
  const render = writeAutonomyProfile({ cwd: facts.root, apply: false }, writerDeps);
@@ -272,7 +277,7 @@ export const plannedChange = (variant, facts, deps = {}) => {
272
277
 
273
278
  export const checkApply = (variant, facts, deps = {}) => {
274
279
  const plan = plannedChange(variant, facts, deps);
275
- const scope = scopeOf(variant, facts);
280
+ const scope = scopeOf(variant, facts, deps);
276
281
  const { entries, refused } = judgeChange({ variant, files: plan.files, scope });
277
282
  const lines = [];
278
283
  const display = (path) => facts.root && covers(facts.root, path) ? relative(facts.root, path) : path;
@@ -344,7 +349,7 @@ export const main = (argv, io = {}) => {
344
349
  platform: resolved.platform, claudeDir: args['claude-dir'] };
345
350
  let result;
346
351
  try {
347
- result = checkApply(args.variant, facts);
352
+ result = checkApply(args.variant, facts, { detect: resolved.detect });
348
353
  } catch (error) {
349
354
  for (const line of error.lines ?? [error.message]) resolved.log(line);
350
355
  return 3;
@@ -1,23 +1,91 @@
1
- // bridge-sandbox-recipe.mjs — the one reader of the bundled bridges' declared sandbox surfaces
2
- // (contract: bridges/velocity-profile). The bridge tier seeds the hosts it returns and the
3
- // Recommendations sandbox-lane recipe renders hosts and dirs from it, so the two cannot drift.
1
+ // bridge-sandbox-recipe.mjs — the one reader of the bundled bridges' declared sandbox surfaces and
2
+ // the one derivation of the bridge tier (contract: bridges/velocity-profile, revision 4). The tier
3
+ // writer, the advisor's delta, the danger check and the autonomy preview read the same
4
+ // `bundledSandboxRecipe(usedBridges(...))`, so the four cannot drift.
4
5
  //
5
6
  // Dependency-free, Node >= 22. No side effects on import.
6
7
 
7
8
  import { readFileSync, readdirSync } from 'node:fs';
8
- import { join } from 'node:path';
9
+ import { dirname, join, resolve } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
9
11
  import { DEFAULT_BUNDLE_ROOT } from './bridge-settings-read.mjs';
12
+ import { ACTIVITIES, BACKEND_PRIORITY, REVIEW_CMD_ALIASES } from './carriers.mjs';
13
+ import { planRecipe, resolveActivityRecipe } from './recipes.mjs';
14
+ import { isReadyMember } from './review-roster-resolve.mjs';
15
+ import { isSeedablePathToken } from './repo-lex.mjs';
10
16
 
11
- // The manifest-declared session-sandbox recipe surfaces of every BUNDLED bridge whose review
12
- // wrapper is in the wired set — networkHosts ∪ writableDirs, derived from the manifests (the
13
- // single documentation source), never hardcoded here.
14
- export const bundledSandboxRecipe = (placedWrappers, deps) => {
17
+ // The review wrappers, spelled bare as invoked. Only their `code` mode is auto-allowed: a bare
18
+ // `Bash(<wrapper>:*)` prefix would also cover the plan/diff file-argument modes, whose targets can
19
+ // point outside the repo, so those keep their prompt.
20
+ export const BRIDGE_REVIEW_WRAPPERS = Object.freeze(['codex-review', 'agy-review']);
21
+ export const BRIDGE_REVIEW_MODE = 'code';
22
+ // The grounding pre-step (agy's facts assembler), seeded in the EXACT quoted byte-form the
23
+ // procedures advisor renders, so seeded and rendered stay byte-equal.
24
+ export const KIT_GROUNDING_TOOL = 'tools/grounding.mjs';
25
+ const KIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
26
+ const GROUNDING_WRAPPER = 'agy-review';
27
+ // An exclusion is Bash rule syntax: a bare name matches only the argument-free command, so the
28
+ // tier writes `<wrapper> *`, which matches it with arguments.
29
+ export const exclusionOf = (wrapper) => `${wrapper} *`;
30
+
31
+ const ROLE_ORDER = Object.freeze(['review', 'execute']);
32
+ const SLOT_ROLE = Object.freeze({ review: 'review', execute: 'execute' });
33
+
34
+ // The bridges the project's recipes USE: every review slot and every execute-type slot of every
35
+ // activity, resolved to its EFFECTIVE recipe over the config and the readiness (a degraded recipe
36
+ // names what it degrades to; an absent config is the computed default). A ready provider has its
37
+ // wrapper on PATH, so used implies placed; a bridge no slot uses is absent however placed.
38
+ export const usedBridges = (config, readiness) => {
39
+ const roles = new Map();
40
+ const take = (bridge, role) => {
41
+ if (!BACKEND_PRIORITY.includes(bridge)) return;
42
+ if (!roles.has(bridge)) roles.set(bridge, new Set());
43
+ roles.get(bridge).add(role);
44
+ };
45
+ for (const [activity, def] of Object.entries(ACTIVITIES)) {
46
+ for (const [slot, type] of Object.entries(def.slots)) {
47
+ const role = SLOT_ROLE[type];
48
+ if (role === undefined) continue;
49
+ const resolved = resolveActivityRecipe({ config: config ?? {}, readiness, activity, slot });
50
+ if (resolved.roster) {
51
+ for (const row of resolved.roster) {
52
+ if (row.kind === 'bridge' && isReadyMember(row)) take(REVIEW_CMD_ALIASES[row.member].backend, role);
53
+ }
54
+ continue;
55
+ }
56
+ for (const step of planRecipe(resolved.recipe, readiness).dispatch) take(step.backend, role);
57
+ }
58
+ }
59
+ return [...roles.keys()].sort().map((bridge) => ({
60
+ bridge,
61
+ roles: ROLE_ORDER.filter((role) => roles.get(bridge).has(role)),
62
+ }));
63
+ };
64
+
65
+ // An array of review-wrapper NAMES (the sandbox-lane call) reads as review-role entries.
66
+ const rolesFor = (used, manifest) => {
67
+ if (used.every((entry) => typeof entry === 'string')) {
68
+ return used.includes(manifest?.roles?.review?.cmd) ? ['review'] : [];
69
+ }
70
+ return used.find((entry) => entry?.bridge === manifest?.name)?.roles ?? [];
71
+ };
72
+
73
+ const pushOnce = (list, value) => {
74
+ if (!list.includes(value)) list.push(value);
75
+ };
76
+
77
+ // The surfaces of the used bridges over the bundled manifests, in manifest-name order, each list in
78
+ // its own order, a value already taken skipped.
79
+ export const bundledSandboxRecipe = (used, deps = {}) => {
15
80
  const readFile = deps.readFile ?? readFileSync;
16
81
  const readDir = deps.readdir ?? readdirSync;
17
82
  const bundleRoot = deps.bundleRoot ?? DEFAULT_BUNDLE_ROOT;
83
+ const allow = [];
84
+ const excludedCommands = [];
18
85
  const hosts = [];
19
86
  const dirEntries = [];
20
- for (const dir of readDir(bundleRoot)) {
87
+ const skips = [];
88
+ for (const dir of [...readDir(bundleRoot)].sort()) {
21
89
  // An unreadable/unparsable bundled manifest must NOT thin the recipe silently — a partial
22
90
  // recipe rendered as complete is worse than none. The throw reaches the caller: a stated skip
23
91
  // in the advisor, a STOP with zero writes in the bridge tier.
@@ -29,12 +97,31 @@ export const bundledSandboxRecipe = (placedWrappers, deps) => {
29
97
  if (err?.code === 'ENOTDIR') continue;
30
98
  throw new Error(`bundled manifest unreadable: ${join(dir, 'capability.json')} — ${err?.message ?? err}`);
31
99
  }
32
- const reviewCmd = manifest?.roles?.review?.cmd;
33
- if (!reviewCmd || !placedWrappers.includes(reviewCmd)) continue;
34
- if (Array.isArray(manifest.networkHosts)) {
35
- for (const h of manifest.networkHosts) if (!hosts.includes(h)) hosts.push(h);
100
+ const roles = rolesFor(used, manifest);
101
+ if (roles.length === 0) continue;
102
+ for (const role of roles) {
103
+ const cmd = manifest?.roles?.[role]?.cmd;
104
+ if (!cmd) continue;
105
+ pushOnce(excludedCommands, exclusionOf(cmd));
106
+ if (role !== 'review') continue;
107
+ pushOnce(allow, `Bash(${cmd} ${BRIDGE_REVIEW_MODE}:*)`);
108
+ if (cmd === GROUNDING_WRAPPER) {
109
+ // groundingAbsPath binds the danger check's scope to the kit path it checks
110
+ // (apply-danger-check.mjs) and lets a test build an unseedable path; the writer never passes it.
111
+ const groundingAbs = deps.groundingAbsPath ?? join(KIT_ROOT, KIT_GROUNDING_TOOL);
112
+ if (isSeedablePathToken(groundingAbs)) pushOnce(allow, `Bash(node "${groundingAbs}":*)`);
113
+ else skips.push({
114
+ entry: groundingAbs,
115
+ reason: 'the kit path is not a POSIX absolute path free of spaces/metacharacters/quoting — the grounding pre-step rule is not seeded (its prompt stays); add a hand-picked entry if you accept the spelling',
116
+ });
117
+ }
118
+ }
119
+ if (Array.isArray(manifest.networkHosts)) for (const host of manifest.networkHosts) pushOnce(hosts, host);
120
+ if (Array.isArray(manifest.writableDirs)) {
121
+ for (const entry of manifest.writableDirs) {
122
+ if (!dirEntries.some((taken) => JSON.stringify(taken) === JSON.stringify(entry))) dirEntries.push(entry);
123
+ }
36
124
  }
37
- if (Array.isArray(manifest.writableDirs)) dirEntries.push(...manifest.writableDirs);
38
125
  }
39
- return { hosts, dirEntries };
126
+ return { allow, excludedCommands, hosts, dirEntries, skips };
40
127
  };