@sabaiway/agent-workflow-kit 1.46.0 → 1.47.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
+ ## 1.47.0 — REPORT-FACTS train: live-fact report contract · batched ledger writer · version-sync wrapper lane · sandbox-lanes canon & bridge contract twins (AD-054)
8
+
9
+ A **feature** release (ships with engine 1.17.0, memory 2.3.0, bridges 2.7.0/2.6.0 bundled) that
10
+ bundles three top-of-queue items plus this session's own live prompt-defects into one train, all
11
+ fixed kit-level:
12
+
13
+ - **Report-facts contract (D1/D2).** A binding clause at the point of use: any claim a report makes
14
+ about the CURRENT host or session state — prompts fired, sandbox scope, whether a bypass was
15
+ needed, network reachability, approval counts — must trace to **live tool output** run **this
16
+ session**; with no live signal the claim is **omitted or explicitly marked unverified**; a
17
+ memory/handover snapshot is **context, never report facts**. The full clause lives in
18
+ `references/shared/report-footer.md` (single home); upgrade steps 4/8 and the recommendations
19
+ advisor carry one binding line each, pinned by the new `report-facts-contract.test.mjs`.
20
+ - **Batched ledger writer (D3).** `review-ledger-write.mjs` gains a `batch` verb — one invocation
21
+ applies an ordered record/classify/override list through the SAME single-verb code paths (no forked
22
+ validator). Two passes: the whole envelope is validated structurally first with ZERO writes, then
23
+ ops apply sequentially and fail-fast on the first typed STOP with an honest partial-success report
24
+ (prior ops stay recorded, append-only). One writer call for a records stage instead of ~13.
25
+ - **version-sync wrapper lane (D4).** `version-sync --bump` for a bridge now also rewrites the
26
+ line-anchored `AW_BRIDGE_VERSION` in `bin/*.sh` (closed one-anchor rule — every assignment counted,
27
+ so a shadowing malformed line is caught; a non-canonical anchor or a downgrade is refused with zero
28
+ writes), and the no-flag verify checks it across all four constants (each wrapper + its kit mirror).
29
+ The bridge version constant is now first-class — dogfooded on this very release.
30
+ - **Bridge contract twins + codex-exec detection (D7).** Both bridge manifests gain a typed `notes[]`
31
+ (codex execute = the nested-sandbox limit; agy review = the pre-dispatch host-diff), rendered in the
32
+ `procedures` advisor and each wrapper `--help`, bidirectionally drift-guarded. `codex-exec.sh` now
33
+ detects the nested-sandbox failure class (a sandbox-mechanism token AND a permission/read-only
34
+ failure token together) and emits a stated recovery hint — route codex-exec OUTSIDE the harness
35
+ sandbox ON the observed failure, never a preemptive blanket. The velocity bridge-tier stays
36
+ REVIEW-wrappers-only; delegated execution keeps its human prompt.
37
+ - **Canon twins (D5/D6).** The prompt-economy canon gains a writer-batch clause and two sandbox-lane
38
+ sentences (pre-dispatch host-diff + nested-sandbox honesty), rendered across `orchestration.md` §5,
39
+ the agent-rules lens (re-rendered into both templates), and the kit cost-lanes advisor.
40
+
7
41
  ## 1.46.0 — Recommendations UX rework: verdict-first, shape-capped, user-language; sandbox-lane discoverability (REC-UX-REWORK, AD-053)
8
42
 
9
43
  A **feature** release (ships with engine 1.16.0, memory 2.2.0, bridges 2.6.0/2.5.0 bundled)
package/README.md CHANGED
@@ -238,7 +238,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
238
238
  | `/agent-workflow-kit review-state` | any time | **read-only review-receipt checker** — makes "reviewed ≠ shipped" detectable: the bridge review wrappers append a receipt per successful review (into a file inside the git dir — never committable); this checks that every backend your configured `plan-execution.review` recipe names holds a **fresh, grounded receipt for the current uncommitted tree** (any later edit moves the fingerprint and stales the receipt; a review continuation never re-attests a folded tree). `--check` gives a gate exit code to declare in `docs/ai/gates.json` **by hand or via the consent-gated seeder** (preview → your explicit yes; never without consent). `--await [--timeout <s>]` (AD-049) BLOCKS until every recipe-named backend has receipted the current tree — the durable completion signal is the receipt, never a process event — so you wait for the bridges instead of hand-polling. Never writes, never commits, never runs a subscription CLI; it spawns read-only `git` queries to fingerprint the tree — and `git commit --no-verify` stays possible (discipline, not a sandbox). |
239
239
  | `/agent-workflow-kit sandbox-masks` | any time | **cosmetic exclude lane for sandbox device masks** — an OS sandbox (Claude Code) injects character-device masks into the work tree as untracked `git status` noise; the review domain already ignores them **by construction** (never-committable untracked classes — char/block devices, FIFOs, sockets — are excluded from the fingerprint, the assembled review payload, and the clean checks). This mode hides them from `git status` too: flagless = read-only probe (derives the CURRENT mask set from the unfiltered walk + lstat — never a frozen list — and revalidates fenced entries, loudly flagging one that became a real path); `--apply` = consent-gated FULL-BLOCK replace of its own fenced block in `git rev-parse --git-path info/exclude` (stale masks drop by construction; `--clear` always means REMOVE the block — it takes precedence over the derivation). Writes ONLY its fence — never `.gitignore`, never global config; symlinked/non-regular exclude paths and malformed fences fail closed. Watch note: a real file at an excluded path is silently skipped by bulk staging (`git add -A`/`git add .`) — delete the stale line first; the probe flags exactly this case. |
240
240
  | `/agent-workflow-kit grounding` | any time | **grounded-review facts assembler** — mechanizes populating `agy-review --facts @f`: slices your entry-point's **Hard Constraints** section verbatim (exactly one match, else a loud stop) and/or a plan's decision-bearing sections (`## Approach` + `## Verification` required, `## Decisions (locked)` when present; duplicates stop), under the same byte budget the agy wrapper enforces (minus `--reserve-bytes` for the artifact share), with a loud tail-trim on overflow. `--autonomy` (AD-044) appends the COMPUTED effective autonomy policy from the git-top `docs/ai/autonomy.json` (every red-line + per-activity level, stated source line; absent file → the computed defaults ARE the policy, exit 0; a malformed policy fails CLOSED, exit 1). `--ledger-summary` (AD-049) appends a COMPUTED review-ledger digest for the single in-flight segment (rounds · origins · classifications · verdicts · overrides — unrelated loops excluded), so the reviewer sees the loop's own history without hand-copying. Prints to stdout; `--out` writes **one scratch file only** — system-temp outside the repo ($TMPDIR / /tmp, rewritable) or a **fresh** gitignored in-repo path (create-only, exclusive write; an existing in-repo file, even gitignored, is refused — the `.env` clobber class); tracked, not-ignored-in-repo, other outside-repo, and symlink/non-regular destinations are all refused. Never commits, never runs a subscription CLI. |
241
- | `/agent-workflow-kit review-ledger` | any time | **review-round ledger** — turns the review-loop stop rule into a **computed** signal: record each plan-execution review round, its triage, and any recorded **overrides** (`oracle-change` lifts named tampered test files; `red-proof` waives the observed-red proof for exactly one `testId`; v4 adds **`size-cap`** — the exact sanctioned magnitude for a surface over the diff cap, segment-scoped — all loud, durable, auditable waivers, never silent) into a JSONL ledger inside the git dir (never committable), and read the stop decision (`converged > resolved-residual > triage-required > continue`) from the records — never from a remembered rule. **Since v4 every record carries `base` = the HEAD commit and the whole loop is SEGMENT-scoped (AD-048):** round numbering, the caps, and every tooth reset ONLY at a gated commit — a multiphase plan records fully while round 4 within one segment stays refused; `gate-run` records (minted by `gates --record`) make gates-before-review a computed precondition; the triage class **`refuted`** is the honest phantom-finding lane (grounds mandatory); `--telemetry` renders counts-only gate-efficacy data across all loops and both ledgers. `--status` replaces the hand-composed per-round tally; `record --from-receipts` (AD-049) DRAFTS the `backends[]` from the current-fingerprint receipts (verdict per backend, counts from the supplied findings) instead of hand-composing them — a recipe-named backend with no receipt is a loud stop; `--check` is a fail-closed gate exit code for `docs/ai/gates.json` (by hand or via the consent-gated seeder). A `fixable-bug` triage **requires its red→green `testId`** (v1..v3 records stay valid on read — old records never enter a segment). The writer refuses a round (past the hard-max, while a triage is pending, or lacking a grounded review receipt) and refuses an override outside its single in-flight loop. Honest residual: records are forgeable — self-discipline, not a security boundary. Never commits, never runs a subscription CLI. |
241
+ | `/agent-workflow-kit review-ledger` | any time | **review-round ledger** — turns the review-loop stop rule into a **computed** signal: record each plan-execution review round, its triage, and any recorded **overrides** (`oracle-change` lifts named tampered test files; `red-proof` waives the observed-red proof for exactly one `testId`; v4 adds **`size-cap`** — the exact sanctioned magnitude for a surface over the diff cap, segment-scoped — all loud, durable, auditable waivers, never silent) into a JSONL ledger inside the git dir (never committable), and read the stop decision (`converged > resolved-residual > triage-required > continue`) from the records — never from a remembered rule. **Since v4 every record carries `base` = the HEAD commit and the whole loop is SEGMENT-scoped (AD-048):** round numbering, the caps, and every tooth reset ONLY at a gated commit — a multiphase plan records fully while round 4 within one segment stays refused; `gate-run` records (minted by `gates --record`) make gates-before-review a computed precondition; the triage class **`refuted`** is the honest phantom-finding lane (grounds mandatory); `--telemetry` renders counts-only gate-efficacy data across all loops and both ledgers. `--status` replaces the hand-composed per-round tally; `record --from-receipts` (AD-049) DRAFTS the `backends[]` from the current-fingerprint receipts (verdict per backend, counts from the supplied findings) instead of hand-composing them — a recipe-named backend with no receipt is a loud stop; a **`batch`** verb applies an ordered list of record/classify/override ops in ONE writer call (the prompt-economy lane — the whole envelope is validated structurally first with zero writes, then ops apply sequentially and fail-fast; prior ops stay recorded, append-only); `--check` is a fail-closed gate exit code for `docs/ai/gates.json` (by hand or via the consent-gated seeder). A `fixable-bug` triage **requires its red→green `testId`** (v1..v3 records stay valid on read — old records never enter a segment). The writer refuses a round (past the hard-max, while a triage is pending, or lacking a grounded review receipt) and refuses an override outside its single in-flight loop. Honest residual: records are forgeable — self-discipline, not a security boundary. Never commits, never runs a subscription CLI. |
242
242
  | `/agent-workflow-kit fold-completeness` | any time | **fold-completeness gate** — attests the review loop's folded fixes were proven the HONEST way (no fix theater): the runner executes your suite ONCE under `NODE_V8_COVERAGE`, checks every changed executable line is **executed**, probes every bound `testId` N times (`AW_FOLD_RERUNS`), records content hashes, and scans the test surface for tamper; the `--red` verb observes a test FAILING on the pre-fold tree — **BEFORE the fix** — and mints an **observed-red receipt**. The read-only checker (`--status` / `--check`) then requires, per bound test: the receipt, receipt-precedes-run order, N/N-green probes, and content **custody** (the green test is byte-identical to the one seen failing) — the receipt+custody half waivable for exactly one `testId` by a recorded `red-proof` override; mixed/timed-out probes are QUARANTINE (never converted, no override); **v3 records carry `base` and the proof is segment-scoped (AD-048)** — a committed phase's custody obligations close with its commit, and a receipt never crosses a commit boundary; tampered pre-existing test files fail closed unless covered by a recorded `oracle-change` override. Honest residuals: coverage proves **execution, not assertion**, and records are forgeable — self-discipline, not a security boundary. v1 scope is JS/V8 (pluggable via `AW_FOLD_BOUND_CMD`): changed TS/JSX fails closed, docs/config changes are listed but never block, and **no mutation testing ships**. Wire the gate by hand — the consent-gated seeder deliberately does not offer it yet (JS-only hold). Never commits, never runs a subscription CLI. |
243
243
  | `/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 · 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 sandbox-lane item is **HAND-APPLY** — the kit never seeds `sandbox.network.allowedDomains` / `filesystem.allowWrite` (the recipe is documented per bridge in `capability.json` `networkHosts` + `writableDirs`; convergence is a neutral fingerprint acknowledgement, never a security key). `--cwd` is required (the target project is explicit); never writes, never commits, never runs a subscription CLI. |
244
244
  | `/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. |
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: '1.46.0'
6
+ version: '1.47.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
@@ -2,7 +2,7 @@
2
2
  name: antigravity-cli-bridge
3
3
  description: Delegate work to Google's Antigravity CLI (`agy`) — the successor to Gemini CLI — to reach Gemini, Claude, and GPT-OSS models under a Google AI Pro/Ultra subscription from the terminal. Use when the user wants to run a headless `agy` prompt, hand a focused task or second-opinion review to `agy`, install or authenticate Antigravity CLI, check or economise its quota/models, bridge project context into `agy`, set up a second delegated-execution backend beside Codex, or troubleshoot `agy` flags, models, auth, conversations, or its no-JSON headless behaviour.
4
4
  metadata:
5
- version: '2.5.0'
5
+ version: '2.6.0'
6
6
  ---
7
7
 
8
8
  # antigravity-cli-bridge
@@ -69,6 +69,11 @@ Grounding:
69
69
  --facts @f = the verified facts to review AGAINST; --decided @f = decisions
70
70
  already made, do NOT re-raise (anti-circling)
71
71
 
72
+ Notes:
73
+ pre-dispatch host-diff: before the FIRST dispatch of this bridge, diff its declared networkHosts
74
+ against the live sandbox allow-list — a missing host is surfaced to the maintainer BEFORE
75
+ dispatching, never fired into a known prompt
76
+
72
77
  Round-2 / resume:
73
78
  agy-review --continue [--decided @f] [--focus "…"]
74
79
  agy-review --conversation <id> [--decided @f] [--focus "…"]
@@ -194,7 +199,7 @@ DEFAULT_AGY_REVIEW_MODEL="Gemini 3.1 Pro (High)"
194
199
  # Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
195
200
  # version (drift-guarded by agy-review.test.mjs against capability.json).
196
201
  AW_RECEIPT_BACKEND="agy"
197
- AW_BRIDGE_VERSION="2.5.0"
202
+ AW_BRIDGE_VERSION="2.6.0"
198
203
  # `-` not `:-` so an EXPLICIT empty AGY_MODEL= survives (drop --model, use settings.json — agy.sh:52).
199
204
  AGY_MODEL="${AGY_MODEL-$DEFAULT_AGY_REVIEW_MODEL}"
200
205
  # Frontier review models. ANY model is allowed; a sub-frontier one only earns a soft, silenceable warning.
@@ -550,6 +550,12 @@ describe('agy-review.sh — --help contract (manifest-pinned)', () => {
550
550
  assert.equal(norm(helpSection(help, 'Grounding:').join(' ')), norm(REVIEW_CONTRACT.grounding));
551
551
  });
552
552
 
553
+ it('Notes renders the manifest contract.notes verbatim (a typed contract key that MUST surface)', () => {
554
+ const help = runHelp('--help').stdout;
555
+ assert.ok(REVIEW_CONTRACT.notes.length > 0, 'manifest notes must be non-empty');
556
+ assert.equal(norm(helpSection(help, 'Notes:').join(' ')), norm(REVIEW_CONTRACT.notes.join(' ')));
557
+ });
558
+
553
559
  it('Round-2 / resume set-EQUALS the manifest continue descriptors', () => {
554
560
  const help = runHelp('--help').stdout;
555
561
  const got = helpSection(help, 'Round-2 / resume:').filter((l) => l.startsWith('agy-review')).map(norm);
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "antigravity-cli-bridge",
5
5
  "kind": "execution-backend",
6
- "version": "2.5.0",
6
+ "version": "2.6.0",
7
7
  "provides": ["review", "probe"],
8
8
  "roles": {
9
9
  "review": {
@@ -28,7 +28,10 @@
28
28
  "agy-review --continue [--decided @f] [--focus \"…\"]",
29
29
  "agy-review --conversation <id> [--decided @f] [--focus \"…\"]"
30
30
  ],
31
- "receipt": "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides; plan/diff outside a git tree: warn + skip unless overridden): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan/diff mode; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (an empty payload records grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts payload; a continuation receipt is fresh:false (informational-only — it cannot attest the folded tree); a write failure warns, never fails the review"
31
+ "receipt": "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides; plan/diff outside a git tree: warn + skip unless overridden): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan/diff mode; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (an empty payload records grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts payload; a continuation receipt is fresh:false (informational-only — it cannot attest the folded tree); a write failure warns, never fails the review",
32
+ "notes": [
33
+ "pre-dispatch host-diff: before the FIRST dispatch of this bridge, diff its declared networkHosts against the live sandbox allow-list — a missing host is surfaced to the maintainer BEFORE dispatching, never fired into a known prompt"
34
+ ]
32
35
  }
33
36
  },
34
37
  "probe": { "cmd": "agy-run", "source": "bin/agy.sh", "output": "advisory" }
@@ -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: '2.6.0'
5
+ version: '2.7.0'
6
6
  ---
7
7
 
8
8
  # codex-cli-bridge
@@ -67,6 +67,12 @@ Guarded passthrough after '--':
67
67
  blocked always: -c* --config* -s* --sandbox* --dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust --full-auto --oss --local-provider* -p* --profile* -m* --model* -o* --output-last-message* --json* --color* --output-schema* --ephemeral*
68
68
  relaxed only under CODEX_PROBE=1: --add-dir* -C* --cd* --skip-git-repo-check --ignore-rules --enable* --disable*
69
69
 
70
+ Notes:
71
+ nested-sandbox limit: codex-exec ships its OWN OS sandbox (bwrap workspace-write) and cannot run
72
+ nested inside a harness sandbox (the FS turns read-only) — route it OUTSIDE the harness sandbox
73
+ (excludedCommands / a per-run consented bypass) on the OBSERVED bwrap/EPERM failure, never a
74
+ preemptive blanket
75
+
70
76
  Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins over built-in default):
71
77
  ${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf
72
78
  CODEX_SERVICE_TIER — service tier: 'priority' (Fast — ~1.5x speed at a 2.5x credit rate on gpt-5.6-sol); a consented SPEND knob, default off (standard tier)
@@ -557,6 +563,20 @@ fi
557
563
  if [[ $rc -ne 0 ]]; then
558
564
  echo "error: codex exec failed (exit $rc). Last lines of the run trace:" >&2
559
565
  tail -n 40 "$trace" >&2
566
+ # Nested-sandbox detection: codex ships its OWN OS sandbox (bwrap); run nested inside a harness
567
+ # sandbox the FS is read-only and codex's own sandbox setup fails. Fire the STATED recovery hint
568
+ # only on a COMBINATION — a sandbox MECHANISM token AND a permission/read-only FAILURE token in the
569
+ # trace — so a lone 'bwrap' banner, or a lone 'permission denied' from unrelated code, is NOT enough
570
+ # (never a preemptive blanket). grep is line-oriented, so a plain alternation stays within a line —
571
+ # the old `[^\n]*` between the two halves was wrong twice over (it excluded the letter 'n', and grep
572
+ # never spans lines anyway), so the split into two `-q` passes both fixes it and states the intent.
573
+ if grep -qiE 'bwrap|landlock|user namespace|pivot_root|unshare|seccomp' "$trace" 2>/dev/null \
574
+ && grep -qiE 'read-only file system|erofs|operation not permitted|permission denied|eperm' "$trace" 2>/dev/null; then
575
+ echo "hint: this looks like a NESTED-SANDBOX failure — codex-exec ships its own OS sandbox (bwrap)," >&2
576
+ echo " which cannot run nested inside a harness sandbox (the FS is read-only). Route codex-exec" >&2
577
+ echo " OUTSIDE the harness sandbox: add it to the harness sandbox excludedCommands, or dispatch" >&2
578
+ echo " this one run via a per-run consented bypass. Do NOT blanket-disable the sandbox." >&2
579
+ fi
560
580
  exit $rc
561
581
  fi
562
582
 
@@ -21,6 +21,7 @@ const FAKE_CODEX = [
21
21
  '#!/usr/bin/env bash',
22
22
  'set -u',
23
23
  'if [[ "${1:-}" == "login" ]]; then echo "${CODEX_FAKE_LOGIN:-Logged in using ChatGPT}"; exit 0; fi',
24
+ 'if [[ -n "${CODEX_FAKE_STDERR:-}" ]]; then echo "$CODEX_FAKE_STDERR" >&2; fi',
24
25
  ': "${CODEX_FAKE_ARGV:=/dev/null}"',
25
26
  ': "${CODEX_FAKE_ENV:=/dev/null}"',
26
27
  ': "${CODEX_FAKE_STDIN:=/dev/null}"',
@@ -274,6 +275,49 @@ describe('codex-exec.sh — clean output + session capture (1.2)', () => {
274
275
  assert.equal(r.status, 7);
275
276
  assert.match(r.stderr, /codex exec failed \(exit 7\)/);
276
277
  assert.match(r.stderr, /thread\.started/, 'failure should surface the trace tail');
278
+ assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'a plain failure (no bwrap signature) never triggers the hint');
279
+ });
280
+
281
+ it('on a NESTED-SANDBOX failure (bwrap/read-only trace) surfaces the stated recovery hint', () => {
282
+ const sb = makeSandbox();
283
+ const r = run(sb, { env: { CODEX_FAKE_EXIT: '1', CODEX_FAKE_STDERR: 'bwrap: setting up sandbox: mkdir /newroot: Read-only file system' } });
284
+ rmSync(sb.root, { recursive: true, force: true });
285
+ assert.equal(r.status, 1);
286
+ assert.match(r.stderr, /NESTED-SANDBOX/, 'names the failure class');
287
+ assert.match(r.stderr, /excludedCommands|per-run consented bypass/, 'states the recovery route');
288
+ assert.match(r.stderr, /Do NOT blanket-disable/, 'warns against a preemptive blanket');
289
+ });
290
+
291
+ it('a NON-nested codex failure does NOT emit the nested-sandbox hint (no false positive)', () => {
292
+ const sb = makeSandbox();
293
+ const r = run(sb, { env: { CODEX_FAKE_EXIT: '3', CODEX_FAKE_STDERR: 'model error: rate limited, try again later' } });
294
+ rmSync(sb.root, { recursive: true, force: true });
295
+ assert.equal(r.status, 3);
296
+ assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'a generic failure never triggers the hint');
297
+ });
298
+
299
+ it('#6 fold: a mechanism+failure trace whose message contains the letter n (mkdir /newroot) still fires — the old [^\\n]* wrongly excluded n', () => {
300
+ const sb = makeSandbox();
301
+ const r = run(sb, { env: { CODEX_FAKE_EXIT: '1', CODEX_FAKE_STDERR: 'bwrap: mkdir /newroot: operation not permitted' } });
302
+ rmSync(sb.root, { recursive: true, force: true });
303
+ assert.equal(r.status, 1);
304
+ assert.match(r.stderr, /NESTED-SANDBOX/, 'bwrap (mechanism) + operation not permitted (failure) fire even through n-bearing words');
305
+ });
306
+
307
+ it('#6 fold: a LONE mechanism token (a bwrap banner, no failure) does NOT fire — a combination is required', () => {
308
+ const sb = makeSandbox();
309
+ const r = run(sb, { env: { CODEX_FAKE_EXIT: '2', CODEX_FAKE_STDERR: 'bwrap version 0.11.0' } });
310
+ rmSync(sb.root, { recursive: true, force: true });
311
+ assert.equal(r.status, 2);
312
+ assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'a mechanism token without a permission/read-only failure is not nested-sandbox proof');
313
+ });
314
+
315
+ it('#6 fold: a LONE failure token (permission denied, no sandbox mechanism) does NOT fire', () => {
316
+ const sb = makeSandbox();
317
+ const r = run(sb, { env: { CODEX_FAKE_EXIT: '2', CODEX_FAKE_STDERR: 'curl: (7) permission denied' } });
318
+ rmSync(sb.root, { recursive: true, force: true });
319
+ assert.equal(r.status, 2);
320
+ assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'a permission failure from unrelated code is not nested-sandbox proof');
277
321
  });
278
322
 
279
323
  it('warns (never silently) when the session sidecar cannot be written', () => {
@@ -734,6 +778,12 @@ describe('codex-exec.sh — --help contract (manifest-pinned)', () => {
734
778
  setEq(tier('relaxed only under CODEX_PROBE=1:'), EXEC_CONTRACT.passthrough.probeRelaxed, 'help tier-2 ⟷ manifest probeRelaxed');
735
779
  });
736
780
 
781
+ it('Notes renders the manifest contract.notes verbatim (a typed contract key that MUST surface)', () => {
782
+ const help = runHelp('--help').stdout;
783
+ assert.ok(EXEC_CONTRACT.notes.length > 0, 'manifest notes must be non-empty');
784
+ assert.equal(norm(helpSection(help, 'Notes:').join(' ')), norm(EXEC_CONTRACT.notes.join(' ')));
785
+ });
786
+
737
787
  it('--help after the -- separator is passthrough payload, never intercepted', () => {
738
788
  const sb = makeSandbox();
739
789
  const r = run(sb, { args: ['-', '--', '--help'], input: 'go' });
@@ -172,7 +172,7 @@ DEFAULT_CODEX_EFFORT="xhigh"
172
172
  # Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
173
173
  # version (drift-guarded by codex-review.test.mjs against capability.json).
174
174
  AW_RECEIPT_BACKEND="codex"
175
- AW_BRIDGE_VERSION="2.6.0"
175
+ AW_BRIDGE_VERSION="2.7.0"
176
176
  CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
177
177
  CODEX_EFFORT="${CODEX_EFFORT:-$DEFAULT_CODEX_EFFORT}"
178
178
  # Generous hard cap for a slow xhigh review (subscription latency varies).
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "codex-cli-bridge",
5
5
  "kind": "execution-backend",
6
- "version": "2.6.0",
6
+ "version": "2.7.0",
7
7
  "provides": ["execute", "review"],
8
8
  "roles": {
9
9
  "execute": {
@@ -24,7 +24,10 @@
24
24
  "policy": "guarded",
25
25
  "blocked": ["-c*", "--config*", "-s*", "--sandbox*", "--dangerously-bypass-approvals-and-sandbox", "--dangerously-bypass-hook-trust", "--full-auto", "--oss", "--local-provider*", "-p*", "--profile*", "-m*", "--model*", "-o*", "--output-last-message*", "--json*", "--color*", "--output-schema*", "--ephemeral*"],
26
26
  "probeRelaxed": ["--add-dir*", "-C*", "--cd*", "--skip-git-repo-check", "--ignore-rules", "--enable*", "--disable*"]
27
- }
27
+ },
28
+ "notes": [
29
+ "nested-sandbox limit: codex-exec ships its OWN OS sandbox (bwrap workspace-write) and cannot run nested inside a harness sandbox (the FS turns read-only) — route it OUTSIDE the harness sandbox (excludedCommands / a per-run consented bypass) on the OBSERVED bwrap/EPERM failure, never a preemptive blanket"
30
+ ]
28
31
  }
29
32
  },
30
33
  "review": {
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": "1.46.0",
6
+ "version": "1.47.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": "1.46.0",
3
+ "version": "1.47.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",
@@ -1,7 +1,16 @@
1
1
  ### Mode: recommendations
2
2
 
3
+ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md
4
+
3
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 · 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).
4
6
 
7
+ **Live host/session facts are tool-composed only.** Every fact this section states about the current
8
+ host or session — prompts fired, sandbox scope, whether a bypass was needed, network reachability,
9
+ approval counts — is **live tool output** from **this session** (the tool computed it on this run); a
10
+ memory/handover snapshot is **context, never report facts**, and a claim with no live signal is
11
+ **omitted or explicitly marked unverified**, never asserted from recollection. Full clause: *Live
12
+ host/session facts* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`.
13
+
5
14
  Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--json]`:
6
15
 
7
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).
@@ -12,11 +12,12 @@ The review-round **LEDGER** (DEBT-REVIEW-CAP / AD-045) — it turns the prose re
12
12
  - `--json` → the structured state + decision.
13
13
  - **`--telemetry`** → read-only COUNTS across ALL loops and BOTH ledgers (never combined with the other flags — a mixed-mode gate cmd would silently pass): rounds/segments per loop, finding-origin totals, classification distribution (incl. `refuted`), per-backend verdict + divergence-round counts, override usage by scope, gate-run counts (quality-green / red results by gate id), fold runs, observed-red receipts, quarantined probes. Counts only — which gates earn their keep stays the maintainer's judgment.
14
14
 
15
- 2. **Writer** — `node ${CLAUDE_SKILL_DIR}/tools/review-ledger-write.mjs record|classify|override --json '<payload>'` (the SOLE writer, over the shared `atomic-write` core):
15
+ 2. **Writer** — `node ${CLAUDE_SKILL_DIR}/tools/review-ledger-write.mjs record|classify|override|batch --json '<payload>'` (the SOLE writer, over the shared `atomic-write` core):
16
16
  - `record` appends one round — `{ loop, round, origins, backends, findings }` (activity defaults to `plan-execution`). **The teeth (Decision 5 + AD-048, all per SEGMENT):** it REFUSES (typed STOP) to append a round WHILE `decideStop` on the segment's records is `triage-required` (an UNCLASSIFIED surviving blocking finding at/after the cap); refuses ANY round beyond the **hard-max ceiling of 3 within one segment** unconditionally (the counter reopens only at the next gated commit); refuses while a blocking finding of the segment's previous round **VANISHED unclassified** (present means present-as-blocking — a downgrade to minor does not survive; a pending `escalate` never clears; `refuted` is the honest phantom lane); refuses while the changed source surface exceeds the **diff-size cap** (`AW_REVIEW_DIFF_CAP`, default 400 new-side lines of assessable + unsupported SOURCE — tests and out-of-domain never count, pure deletions are free) without a recorded segment `size-cap` override; and refuses without a **quality-green gate-run** at the current fingerprint (`run-gates --record` — gates-before-review is computed, not remembered; a `--only` subset or a tree-changed run never satisfies; red PROCESS gates — the kit's own `--check` loop gates — never block). **Integrity binding (Decision 7):** each NON-degraded backend needs a grounded **code** receipt (from `codex-review code` / `agy-review code --facts @f`) for the current tree — a round cannot be recorded for a tree no bridge reviewed. **`--from-receipts`** (BUGFREE-3 / AD-049) DRAFTS the `backends[]` from those receipts instead of hand-composing them: each recipe-named backend's verdict comes from its fresh grounded code receipt and its counts are computed from the supplied `findings` (`origins` / `findings` stay explicit input); a recipe-named backend with no receipt is a LOUD stop — the draft never invents one (supply it explicitly as a degraded backend if the bridge really is down).
17
17
  - `classify` appends one triage — each surviving blocking finding of a SEGMENT round classified `fixable-bug` / `inherent-layer-residual` / `escalate` / **`refuted`** (v4 — the honest lane for a phantom finding, refuted against code: a MANDATORY non-empty `note` cites the grounds; never silently dropped, never folded). **A `fixable-bug` REQUIRES a `testId`** (M2/AD-046, schema v2) — the red→green test that pins the fold, formatted `<test-file>#<test-name-pattern>` (a `#` separator, both halves non-empty; no file-suffix rule); `inherent-layer-residual` / `escalate` may omit it. The writer validates the FORMAT only. **This is what BREAKS the deadlock:** once every surviving blocking finding is classified, `record` permits the next round (a `fixable-bug` classification lets the fix round run — no deadlock).
18
18
  - `override` appends one override record (schema v3, BUGFREE-1/AD-047) — the LOUD, durable waiver the **fold-completeness** gate consumes, `{ loop, round, scope, reason }` plus the per-scope payload (EXACT — a stray key is refused): scope **`oracle-change`** → `files[]`, the repo-relative pre-existing test files whose tamper flag it lifts (a fix diff that deliberately rewrote test expectations); scope **`red-proof`** → `testId`, the ONE bound test whose observed-red receipt + custody it waives (a red genuinely unestablishable pre-fold, or a custody file legitimately edited post-fix); scope **`size-cap`** (v4) → `sanctionedLines`, the EXACT changed-surface magnitude the waiver sanctions — **segment-scoped** (it dies at the next commit; a grown surface needs a fresh recorded sanction). Teeth: schema validation, fail-closed ledger read, round-sequence integrity, and the loop must be the SINGLE in-flight plan. QUARANTINE (a flaky/timed-out probe) has NO override lane. Overrides never enter `decideStop` — the two v3 scopes are fold-completeness inputs; `size-cap` is a writer-tooth input.
19
19
  - **`gate-run` records (v4, kind `gate-run`)** are minted by `run-gates --record` through the writer's `recordGateRun` API (the runner never opens the ledger itself) — the D5 green-baseline receipt: the FULL declaration + exactly what ran + the tree fingerprint BEFORE and AFTER the run, segment-framed with NO round number, minted only inside the single in-flight loop. A red run records honestly (telemetry fuel); **quality-green** (every declared NON-process gate green, tree unchanged under the run) is judged at read time. Revert-first beyond this is not mechanically observable at this layer — it ships as protocol + telemetry visibility (consecutive red gate-runs), stated plainly.
20
+ - **`batch`** applies an ordered list of `record` / `classify` / `override` operations in ONE invocation — the prompt-economy lane for a records stage (WRITER-BURST-BATCH: one writer call, not one per op). The payload is `{ operations: [ { verb, …that verb's payload } ] }` (a `record` op may carry `fromReceipts: true`); every op rides the SAME per-verb teeth (no forked validator), so N batched ops are record-equivalent to the same N single invocations. **Two passes:** the whole envelope is validated STRUCTURALLY first with ZERO writes (a malformed envelope stops before any op runs), then ops apply sequentially and **fail-fast** on the first typed STOP — the ops already applied stay recorded (append-only, no rollback) and the STOP names the failing op index + the applied count; **resume by re-running the REMAINING ops**.
20
21
 
21
22
  **The computed stop (`decideStop`, precedence `converged > resolved-residual > triage-required > continue`).** Machine fields only (counts, `class`, `accepted`, `fingerprint`), never free-text:
22
23
  - **converged** — every recipe-named backend present, **non-degraded, at 0 blockers + 0 majors** in the latest round, at the current tree fingerprint. **"Surviving blocking finding" = a blocker OR a major** (a minor never forces triage).
@@ -59,9 +59,10 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
59
59
  - **Report step 3's outcome in plain language** — for **each** pointer (workflow-methodology, orchestration-recipes and autonomy-policy) whether it was *added*, was *already present* (nothing changed), or was *skipped* (the soft-skip from step 3, with its reason — over the line limit / engine too old / the autonomy pointer's anchor absent); whether the `docs/ai/orchestration.json` config was *seeded* (created from the template), had its onboarding note *refreshed*, was *already current*, or carried a *customized note that was preserved* (a user edit is never clobbered); whether the `docs/ai/gates.json` gate declaration was *seeded* or was *already present* (preserved byte-for-byte); whether the `docs/ai/verification-profile.json` profile was *seeded* or was *already present* (preserved byte-for-byte); whether the `docs/ai/autonomy.json` declaration was *seeded* (the sparse defaults-equivalent note) or was *already present* (preserved byte-for-byte); whether the enforcement-script ensure *added* the `archive-decisions` pair to `scripts/`, found it *already present*, or found an *old ADR layout — migration instructed*; the **placed-bridge refresh** outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / *could not refresh* + recovery); the **agent-rules lens** outcome (*refreshed* / *already current* / *custom edit preserved + note* / *file absent* / *engine too old* / *over the line cap*); the **bridge-settings reconcile** outcome (paste the tool's line verbatim); and, for a hidden deployment, whether the hidden-mode footprint was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility / a leftover machine-wide block). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
60
60
  - **Never surface the structure number on this exit.** Whatever step 3 did, do **not** recite the `docs/ai` structure version, the internal versioning vocabulary, or the two-axes note here — the number is inert on an equal-head exit; it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md` (shown at the never-downgrade STOP, the explicit status view, or on an explicit ask). Frame the success itself per the final bullet: if step 3 changed anything, say **what changed** in plain human terms; only a pure zero-diff no-op is *settings already current — no update needed*.
61
61
  - **Render the mandatory Recommendations section — on this exit too, BEFORE the footer.** Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language: every fact, count and item from the tool, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; show the raw tool block on request. The section is present-even-when-empty (with everything optimal the body is exactly `no recommendations — flow optimal.`) and VERDICT-FIRST — the composed verdict line renders from the frozen templates `{K} item(s) need attention` / `nothing is broken` / `{N} optional recommendation(s), apply any you want` / `optimality NOT attested — {M} probe check(s) skipped`. Then OFFER the consent-gated applies: the user picks items in plain language; surface each picked item's posture note, get the explicit confirm, then run EXACTLY the rendered one-liners (a HAND-APPLY item is never run by you) — the full lane in `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`. Pinned order on this exit: Recommendations block → optional applies → report footer → the commit ask (the advisor/apply lane never lands after the commit ask).
62
+ - **Live host/session facts are tool-composed only.** Any claim this report makes about the current host or session state — prompts fired, sandbox scope, whether a bypass was needed, network reachability, approval counts — must trace to **live tool output** from **this session** (the lines you just composed, or a probe you ran this run); a memory/handover snapshot is **context, never report facts**, and a claim with no live signal is **omitted or explicitly marked unverified** — never asserted from recollection. Full clause: *Live host/session facts* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`.
62
63
  - **Print the report footer** in the canonical order (version block → one-line backend-status line → welcome mat — the shared contracts in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`; rendered from the helpers, same host-can't-run skip-with-reason). The welcome mat closes on **one** caveat-aware next step (a behind member first, else `setup` / `recipes` / `velocity` / `agents` / `hook`).
63
64
  - **Then ask before committing — never auto-commit.** If step 3 added the slot (or anything else changed), report it and ask. If step 3 was a pure zero-diff no-op and nothing else changed, give the plain **settings already current — no update needed** message (the *Success state* contract in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`) and still print the read-only version block (installed package versions) + backend line — but **no `docs/ai` structure version and no two-axes note** (nothing changed, so the number is inert here).
64
65
  5. Show the relevant `${CLAUDE_SKILL_DIR}/CHANGELOG.md` diff (entries newer than the project's stamp).
65
66
  6. **Collect the migration answers FIRST, then apply.** If `AGENTS.md` is missing BOTH the *Communication language* and *Attribution* blocks — i.e. both blocks are missing (a pre-1.1.0 deployment) — ask the two questions as ONE structured multi-question prompt; record each answer individually, write nothing until ALL are answered, and carry the answers into the migrations below: a migration whose answer was already collected never re-asks (its own "Ask the user" step is the standalone fallback); a single missing block keeps its single ask (step 7). Then apply `${CLAUDE_SKILL_DIR}/migrations/<version>-<slug>.md` in **semver order**, only those newer than the project's stamp. Migrations are **idempotent** — safe to re-run.
66
67
  7. Reconcile drift: add any kernel files/scripts the project is missing; never clobber project-authored content (their `decisions.md`, `known_issues.md`, page specs stay). Any user question a migration raises follows the same rule as bootstrap — **structured multiple-choice where supported** (`AskUserQuestion` in Claude Code), otherwise prose. If `AGENTS.md` has no *Communication language* block (pre-1.1.0 deployment), **ask the user their conversational language** and insert the block — see `migrations/1.1.0-communication-language.md`. If it has no *Attribution* block (pre-1.2.0 deployment), **ask whether the agent may attribute work to itself / AI** and insert the block (defaulting to `off`) — see `migrations/1.2.0-agent-attribution.md`. (An answer already collected by the step-6 batched prompt is carried in — never re-asked here.)
67
- 8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`2.0.0`, not the package version — mechanics unchanged: the atomic write to the stamp file). In the report, **describe what the upgrade changed in plain human terms** — which parts of their `docs/ai` are now different (the migrations that ran), plus the step-3 **placed-bridge refresh** lines (pasted verbatim), the step-3 **agent-rules lens** outcome (same outcome set as step 4), the step-3 **bridge-settings reconcile** outcome, and the step-3 **autonomy-declaration ensure** outcome (*seeded* / *already present, preserved*) — rather than reciting a version number; **omit the raw structure number**, and do **not** print the two-axes note here (it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`, on demand only). Then **render the mandatory Recommendations section**: run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language (every fact, count and item, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; raw tool block on request; present-even-when-empty: `no recommendations — flow optimal.`), then OFFER the consent-gated applies (per picked item: posture note → explicit confirm → run EXACTLY the rendered one-liner; a HAND-APPLY item is never run by you — `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`). Then **print the report footer** in the canonical order (version block → one-line backend-status line → welcome mat — the shared contracts in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`; rendered from the helpers, same host-can't-run skip-with-reason; the welcome mat closes on one caveat-aware next step). Then **ask before committing** — the pinned order on this exit is: Recommendations block → optional applies → report footer → the commit ask.
68
+ 8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`2.0.0`, not the package version — mechanics unchanged: the atomic write to the stamp file). In the report, **describe what the upgrade changed in plain human terms** — which parts of their `docs/ai` are now different (the migrations that ran), plus the step-3 **placed-bridge refresh** lines (pasted verbatim), the step-3 **agent-rules lens** outcome (same outcome set as step 4), the step-3 **bridge-settings reconcile** outcome, and the step-3 **autonomy-declaration ensure** outcome (*seeded* / *already present, preserved*) — rather than reciting a version number; **omit the raw structure number**, and do **not** print the two-axes note here (it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`, on demand only). Then **render the mandatory Recommendations section**: run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language (every fact, count and item, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; raw tool block on request; present-even-when-empty: `no recommendations — flow optimal.`), then OFFER the consent-gated applies (per picked item: posture note → explicit confirm → run EXACTLY the rendered one-liner; a HAND-APPLY item is never run by you — `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`). **Every current host/session claim in this report is tool-composed only** — prompts fired, sandbox scope, whether a bypass was needed, network reachability and approval counts must trace to **live tool output** from **this session**, a memory/handover snapshot is **context, never report facts**, and an unbacked claim is **omitted or explicitly marked unverified** (full clause: *Live host/session facts* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`). Then **print the report footer** in the canonical order (version block → one-line backend-status line → welcome mat — the shared contracts in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`; rendered from the helpers, same host-can't-run skip-with-reason; the welcome mat closes on one caveat-aware next step). Then **ask before committing** — the pinned order on this exit is: Recommendations block → optional applies → report footer → the commit ask.
@@ -31,7 +31,7 @@ Honesty notes: tier entries get **NO PreToolUse-hook residual coverage** — the
31
31
 
32
32
  **Invariants:** creates `.claude/` if absent and writes **only** `.claude/settings.json` (no other file); **never** allowlists commit/push/publish; **never** writes `settings.local.json`; never commits; opt-in `acceptEdits`, never silent.
33
33
 
34
- **The `--bridge-tier` (own opt-in, AD-044).** Seeds what a promptless council review run needs — BOTH surfaces: `permissions.allow` prefix rules AND the wrapper names in `sandbox.excludedCommands` (the harness runs an excluded command OUTSIDE the sandbox — the wrappers need network — so a plain allowlisted invocation triggers no sandbox-bypass approval). Both 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 the settings-native sandbox and simply prompts again elsewhere (fail-safe, never a silent widening). 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 kit **never seeds** `sandbox.network.allowedDomains` or `sandbox.filesystem.allowWrite` (bridge council 2026-07-11, both backends concur): a network pre-allow widens egress for EVERY sandboxed command, and a write allowance on CLI state dirs (`~/.codex`, `~/.gemini/…`) would make credential dirs writable to every sandboxed command. 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 HAND-APPLY `sandbox-lane` discoverability item — converging on a neutral fingerprint acknowledgement, 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), 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):
34
+ **The `--bridge-tier` (own opt-in, AD-044).** Seeds what a promptless council review run needs — BOTH surfaces: `permissions.allow` prefix rules AND the wrapper names in `sandbox.excludedCommands` (the harness runs an excluded command OUTSIDE the sandbox — the wrappers need network — so a plain allowlisted invocation triggers no sandbox-bypass approval). Both 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 the settings-native sandbox and simply prompts again elsewhere (fail-safe, never a silent widening). 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 kit **never seeds** `sandbox.network.allowedDomains` or `sandbox.filesystem.allowWrite` (bridge council 2026-07-11, both backends concur): a network pre-allow widens egress for EVERY sandboxed command, and a write allowance on CLI state dirs (`~/.codex`, `~/.gemini/…`) would make credential dirs writable to every sandboxed command. 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 HAND-APPLY `sandbox-lane` discoverability item — converging on a neutral fingerprint acknowledgement, 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):
35
35
 
36
36
  - `Bash(codex-review code:*)` — the code-mode prefix, args wildcard; plus `codex-review` in `sandbox.excludedCommands`
37
37
  - `Bash(agy-review code:*)` — the code-mode prefix, args wildcard; plus `agy-review` in `sandbox.excludedCommands`
@@ -125,3 +125,17 @@ token. Pair it with **one plain-language line** telling the two axes apart, on d
125
125
 
126
126
  **Never** print this two-axes line on a successful equal-head exit — only at the STOP, the status view,
127
127
  or on an explicit ask.
128
+
129
+ ### Live host/session facts — tool-composed only
130
+
131
+ Any claim a report makes about the **current host or session state** — whether a permission prompt
132
+ fired, what the session sandbox allows or blocks, whether a bypass was needed, which network hosts
133
+ are reachable, how many approvals happened — must trace to **live tool output** produced **this
134
+ session**: a status / recipe / recommendations line the composer emitted on this run, or a probe you
135
+ ran and observed this session. It is never asserted from recollection. A memory or handover note is
136
+ **context, never report facts** — it records what was true when written, not the live state, and is
137
+ never the source of a current-state claim. When no live signal backs such a claim, the claim is
138
+ **omitted or explicitly marked unverified** — never stated as fact and never back-filled from a
139
+ snapshot. This binds **every** report surface (the bootstrap / upgrade exits, the recommendations
140
+ advisor, any ad-hoc status summary); the mode files carry a one-line pointer to this clause, never
141
+ their own copy of it.
@@ -76,7 +76,7 @@ Apply these when authoring a plan, reviewing, folding a finding, or editing code
76
76
  - **Per-round emission.** Every review round emits **{round N · finding-origin tally · per-backend verdict}** so the crossover is a computed, visible signal, not a remembered rule.
77
77
  - **Recipe fidelity.** Council runs every backend the recipe names, **every round**; silently dropping a ready backend for quota/convenience is a forbidden downgrade — an unavailable backend is a LOUD, stated degrade, never a quiet drop.
78
78
  - **ExitPlanMode ≠ execute.** A harness "approved — start coding" prompt authorizes the PLAN only; this methodology overrides it. Continue into execution only as a DELIBERATE transition after the plan + cold-start prompt exist, never an implicit slide.
79
- - **Cost lanes.** Route every step to the **cheapest adequate executor** — L0 deterministic script (the batched gate matrix over `gates.json`, the rotation `--check`s) · L1 cheap subagent (extraction/drafting only; the orchestrator verifies) · L2 subscription bridge · L3 frontier judgment. A step with **no named guardrail does not move down** a lane, and the **red lines never move down** (council review models · real code · ADR/handover/changelog-entry wording · persuasive copy · go/no-go · the approval asks). Own-error repair: salvage recorded state first (L0/L1, batched), never frontier re-derivation. **Prompt economy:** read-only fan-out (research/sweeps/extraction) runs ONLY on restricted-tool vehicles — a full-tool subagent for read-only work is a forbidden lane downgrade (invisible prompt-flood + blast radius), and a subagent is never told to shell out for facts obtainable read-only; the orchestrator's own shell form is ONE plain pipeline per call (a `;`/`&&` chain or env-prefixed invocation never matches a prefix allow rule); a fan-out launcher that gates per call yields to the agent-spawn lane — capability-gated: without restricted-tool vehicles (generic full-tool spawning does not count), read-only research stays in the orchestrator's own context, never a vehicle mandate a host cannot satisfy. Judgment, code, synthesis stay at the frontier lane (a task that genuinely runs/writes keeps a full-tool subagent); honest limit: no deterministic gate classifies a dispatch — canon at the point of use + placed vehicles + the retro loop.
79
+ - **Cost lanes.** Route every step to the **cheapest adequate executor** — L0 deterministic script (the batched gate matrix over `gates.json`, the rotation `--check`s) · L1 cheap subagent (extraction/drafting only; the orchestrator verifies) · L2 subscription bridge · L3 frontier judgment. A step with **no named guardrail does not move down** a lane, and the **red lines never move down** (council review models · real code · ADR/handover/changelog-entry wording · persuasive copy · go/no-go · the approval asks). Own-error repair: salvage recorded state first (L0/L1, batched), never frontier re-derivation. **Prompt economy:** read-only fan-out (research/sweeps/extraction) runs ONLY on restricted-tool vehicles — a full-tool subagent for read-only work is a forbidden lane downgrade (invisible prompt-flood + blast radius), and a subagent is never told to shell out for facts obtainable read-only; the orchestrator's own shell form is ONE plain pipeline per call (a `;`/`&&` chain or env-prefixed invocation never matches a prefix allow rule); a fan-out launcher that gates per call yields to the agent-spawn lane — capability-gated: without restricted-tool vehicles (generic full-tool spawning does not count), read-only research stays in the orchestrator's own context, never a vehicle mandate a host cannot satisfy. Judgment, code, synthesis stay at the frontier lane (a task that genuinely runs/writes keeps a full-tool subagent); honest limit: no deterministic gate classifies a dispatch — canon at the point of use + placed vehicles + the retro loop. **Writer economy:** a stage's repeated WRITER commands batch into ONE invocation — the review-ledger triad rides one batched write, other stage writers combine via one launcher per stage; never one writer call at a time (each write is its own prompt).
80
80
 
81
81
  ---
82
82
 
@@ -80,6 +80,9 @@ const RAW_BACKENDS = [
80
80
  blocked: ['-c*', '--config*', '-s*', '--sandbox*', '--dangerously-bypass-approvals-and-sandbox', '--dangerously-bypass-hook-trust', '--full-auto', '--oss', '--local-provider*', '-p*', '--profile*', '-m*', '--model*', '-o*', '--output-last-message*', '--json*', '--color*', '--output-schema*', '--ephemeral*'],
81
81
  probeRelaxed: ['--add-dir*', '-C*', '--cd*', '--skip-git-repo-check', '--ignore-rules', '--enable*', '--disable*'],
82
82
  },
83
+ notes: [
84
+ 'nested-sandbox limit: codex-exec ships its OWN OS sandbox (bwrap workspace-write) and cannot run nested inside a harness sandbox (the FS turns read-only) — route it OUTSIDE the harness sandbox (excludedCommands / a per-run consented bypass) on the OBSERVED bwrap/EPERM failure, never a preemptive blanket',
85
+ ],
83
86
  },
84
87
  review: {
85
88
  invocations: [
@@ -122,6 +125,9 @@ const RAW_BACKENDS = [
122
125
  'agy-review --conversation <id> [--decided @f] [--focus "…"]',
123
126
  ],
124
127
  receipt: "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides; plan/diff outside a git tree: warn + skip unless overridden): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan/diff mode; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (an empty payload records grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts payload; a continuation receipt is fresh:false (informational-only — it cannot attest the folded tree); a write failure warns, never fails the review",
128
+ notes: [
129
+ 'pre-dispatch host-diff: before the FIRST dispatch of this bridge, diff its declared networkHosts against the live sandbox allow-list — a missing host is surfaced to the maintainer BEFORE dispatching, never fired into a known prompt',
130
+ ],
125
131
  },
126
132
  },
127
133
  bin: 'agy',
@@ -296,8 +296,9 @@ const costLanesAdvice = () => [
296
296
  ' • L2 subscription bridge (codex / agy) — reviews per the resolved recipe above, on frontier bridge models (quality-first).',
297
297
  ' • L3 frontier — judgment: plan/fold/synthesis, ADR/handover/changelog-entry wording, persuasive copy, go/no-go, real code.',
298
298
  ' • A step with no named guardrail does not move down a lane; red lines never move down (council review models · real code · memory/copy wording · the maintainer approval asks).',
299
- ' • Sandbox lanes (under an OS sandbox): the L0 surfaces are sandbox-safe — gates/ledger/state/fold checks, git reads, plain no-network tests; the bridge wrappers are genuinely unsandboxed (network); npm-cache-touching commands are COMMAND-SHAPE dependent — first try the sandbox-safe shape (cache under $TMPDIR, offline/notifier off). Move ONLY the failing command out of the sandbox, never its class; BATCH consecutive unsandboxed calls.',
299
+ ' • Sandbox lanes (under an OS sandbox): the L0 surfaces are sandbox-safe — gates/ledger/state/fold checks, git reads, plain no-network tests; the bridge wrappers are genuinely unsandboxed (network); npm-cache-touching commands are COMMAND-SHAPE dependent — first try the sandbox-safe shape (cache under $TMPDIR, offline/notifier off). Move ONLY the failing command out of the sandbox, never its class; BATCH consecutive unsandboxed calls. Pre-dispatch host-diff: before the FIRST dispatch of each bridge, diff its manifest networkHosts against the live sandbox allow-list — a missing host is surfaced to the maintainer BEFORE dispatching, never fired into a known prompt. Nested-sandbox honesty: a backend CLI shipping its OWN OS sandbox cannot run nested inside a harness sandbox — route it outside (excludedCommands / a per-run consented bypass) on the OBSERVED failure, never a preemptive blanket.',
300
300
  ' • Prompt economy (autonomy-preserving dispatch): read-only fan-out (research/sweeps/extraction) runs ONLY on restricted-tool vehicles — a full-tool subagent for read-only work is a forbidden lane downgrade (invisible prompt-flood + blast radius), and a subagent is never told to shell out for facts obtainable read-only; the orchestrator\'s own shell form is ONE plain pipeline per call (a ;/&& chain or env-prefixed invocation never matches a prefix allow rule); a fan-out LAUNCHER that gates per call yields to the agent-spawn lane. Capability-gated: on a host with restricted-tool subagent vehicles use them; without restricted-tool vehicles (a host offering only full-tool agents included), read-only research stays in the orchestrator\'s own context — never a vehicle mandate a host cannot satisfy.',
301
+ ' • Writer economy (autonomy-preserving dispatch): a stage\'s repeated WRITER commands batch into ONE invocation — combine a stage\'s writers via one batched write or one launcher per stage; never one writer call at a time (each write is its own prompt).',
301
302
  ' • The prompt-economy clause narrows TOOLS for read-only work only — judgment, code, synthesis stay at the frontier lane; a task that genuinely needs to run or write keeps a full-tool subagent. Honest limit: no deterministic gate classifies a dispatch — enforcement is the canon at the point of use + the placed vehicles + the retro loop.',
302
303
  ];
303
304
 
@@ -320,6 +321,10 @@ const contractLines = ({ cmd, contract, settings }) => {
320
321
  if (contract.passthrough) {
321
322
  lines.push(` passthrough after '--' is ${contract.passthrough.policy}: blocked always: ${contract.passthrough.blocked.join(' ')}; relaxed only under CODEX_PROBE=1: ${contract.passthrough.probeRelaxed.join(' ')}`);
322
323
  }
324
+ // Contract notes — a TYPED renderer (AD-054): a manifest note MUST render here, else adding a
325
+ // notes[] key would silently disappear (the manifest validator tolerates unknown keys). Drift-
326
+ // guarded both directions against the manifest AND each wrapper's --help.
327
+ for (const note of contract.notes ?? []) lines.push(` note: ${note}`);
323
328
  // Host-level settings knobs this wrapper honors (fact-only, from the bundled manifests; explicit
324
329
  // branch — contractLines drops any contract key it does not name, so this must be enumerated here).
325
330
  if ((settings ?? []).length) {
@@ -406,7 +406,91 @@ export const recordTriage = (params, deps = {}) => {
406
406
  return appendRecord(ledgerPath, record, deps);
407
407
  };
408
408
 
409
- // ── CLI (record / classify) ──────────────────────────────────────────────────────────────────────
409
+ // ── batch (D4/D5 — one invocation applies an ordered list of record/classify/override ops) ────────
410
+ // The prompt-economy lane (WRITER-BURST-BATCH): the ledger triad a records stage would otherwise
411
+ // fire one writer call at a time — records, classifications, overrides — rides ONE `batch`
412
+ // invocation. Every op runs the SAME single-verb code path (no forked validator, D4), so a batch of
413
+ // N ops is record-equivalent to the same N single-verb invocations. TWO passes (D5): pass 1
414
+ // validates the WHOLE envelope structurally with ZERO writes (a bad envelope stops before any op
415
+ // runs); pass 2 applies the ops sequentially — a DOMAIN failure (a tooth, a missing receipt, a
416
+ // malformed record) stops the batch with an honest report, and the ops already applied stay recorded
417
+ // (the ledger is append-only — no rollback pretense).
418
+
419
+ // The verbs a batch operation may carry — the SAME functions the single verbs dispatch.
420
+ const BATCH_VERBS = new Set(['record', 'classify', 'override']);
421
+
422
+ // Pass 1 — structural envelope validation, ZERO writes (a usage failure, exit 2). Deep per-op payload
423
+ // validity is NOT checked here (that is pass 2's single-verb code path — checking it here would fork
424
+ // the validator); pass 1 proves only the envelope shape + a known verb per op, so no raw TypeError
425
+ // and no silent success reaches pass 2.
426
+ export const validateBatchEnvelope = (payload) => {
427
+ if (payload == null || typeof payload !== 'object' || Array.isArray(payload)) {
428
+ throw usageFail('batch payload must be an object of the form { "operations": [ … ] }');
429
+ }
430
+ const operations = payload.operations;
431
+ if (!Array.isArray(operations)) throw usageFail('batch payload.operations must be an array of operations');
432
+ if (operations.length === 0) throw usageFail('batch payload.operations is empty — nothing to record');
433
+ operations.forEach((op, i) => {
434
+ if (op == null || typeof op !== 'object' || Array.isArray(op)) {
435
+ throw usageFail(`batch operation [${i}] must be an object of the form { "verb": …, … }`);
436
+ }
437
+ if (!BATCH_VERBS.has(op.verb)) {
438
+ throw usageFail(`batch operation [${i}] names an unknown verb "${op.verb}" (expected: record | classify | override)`);
439
+ }
440
+ // The batch resolves cwd/env from the INVOCATION, never per operation — a per-op override could
441
+ // read one project's state and write another's ledger. Reject it loudly.
442
+ for (const forbidden of ['cwd', 'env']) {
443
+ if (forbidden in op) {
444
+ throw usageFail(`batch operation [${i}] carries a "${forbidden}" field — the batch resolves cwd/env from the invocation, never per operation`);
445
+ }
446
+ }
447
+ // fromReceipts must be a real boolean: a string "false" is truthy and would silently enable the
448
+ // draft. Reject any non-boolean when the field is present.
449
+ if ('fromReceipts' in op && typeof op.fromReceipts !== 'boolean') {
450
+ throw usageFail(`batch operation [${i}] has a non-boolean fromReceipts (${JSON.stringify(op.fromReceipts)}) — it must be the literal true or false`);
451
+ }
452
+ if ('fromReceipts' in op && op.verb !== 'record') {
453
+ throw usageFail(`batch operation [${i}] sets fromReceipts on "${op.verb}" — it applies only to record (even the literal false does not belong here)`);
454
+ }
455
+ });
456
+ return operations;
457
+ };
458
+
459
+ // Apply ONE operation through the SAME single-verb code path main() dispatches (including the
460
+ // --from-receipts backends[] draft for a record op). deps is threaded so a batch is as hermetic as
461
+ // the single verbs.
462
+ const applyOperation = (op, { cwd, env }, deps = {}) => {
463
+ const { verb, fromReceipts, ...payload } = op;
464
+ // The invocation's cwd/env WIN (spread last) — validateBatchEnvelope already forbids them in the
465
+ // payload, so this is defense-in-depth against a direct runBatch call bypassing the preflight.
466
+ if (verb === 'classify') return recordTriage({ ...payload, cwd, env }, deps);
467
+ if (verb === 'override') return recordOverride({ ...payload, cwd, env }, deps);
468
+ const params = { ...payload };
469
+ if (fromReceipts) {
470
+ const state = deps.buildState ? deps.buildState({ cwd, env }) : buildState({ cwd, env });
471
+ params.backends = draftBackendsFromReceipts({ state, findings: params.findings, explicitBackends: params.backends ?? [] }, deps);
472
+ }
473
+ return recordRound({ ...params, cwd, env }, deps);
474
+ };
475
+
476
+ // runBatch({ cwd, env, operations }, deps) → { applied: [ per-op results ], count }. Pass 2: apply
477
+ // sequentially, fail-fast on the first typed STOP; the ops already appended stay recorded.
478
+ export const runBatch = ({ cwd = process.cwd(), env = process.env, operations }, deps = {}) => {
479
+ const applied = [];
480
+ for (const [i, operation] of operations.entries()) {
481
+ try {
482
+ applied.push(applyOperation(operation, { cwd, env }, deps));
483
+ } catch (err) {
484
+ // Honest partial-success (D5): the STOP itself names the failing op index + the applied count.
485
+ // The ledger is append-only, so those ops are durable — resume by re-running the REMAINING ops.
486
+ err.message = `${err.message} [batch stopped at operation [${i}]: ${applied.length} of ${operations.length} operation(s) recorded before the stop and durable (append-only ledger); re-run the remaining operations]`;
487
+ throw err;
488
+ }
489
+ }
490
+ return { applied, count: applied.length };
491
+ };
492
+
493
+ // ── CLI (record / classify / override / batch) ─────────────────────────────────────────────────────
410
494
 
411
495
  const HELP = `review-ledger-write — the review-round ledger WRITER (agent-workflow family, AD-045 + AD-047 + AD-048).
412
496
 
@@ -414,6 +498,7 @@ Usage:
414
498
  node review-ledger-write.mjs record --json '<round-payload>' [--from-receipts] [--cwd <dir>]
415
499
  node review-ledger-write.mjs classify --json '<triage-payload>' [--cwd <dir>]
416
500
  node review-ledger-write.mjs override --json '<override-payload>' [--cwd <dir>]
501
+ node review-ledger-write.mjs batch --json '<{ "operations": [ … ] }>' [--cwd <dir>]
417
502
  (every verb also accepts --json @<file> — the payload read from a file, keeping the command
418
503
  line PLAIN: an inline JSON argv falls outside plain-invocation allow heuristics and prompts)
419
504
 
@@ -448,10 +533,24 @@ override appends one override record — the LOUD, durable waiver the gates cons
448
533
  sanctions, SEGMENT-scoped — it dies at the next commit, D4).
449
534
  REFUSES for a loop that is not the in-flight plan. QUARANTINE (a flaky/timed-out probe)
450
535
  has NO override lane.
536
+ batch applies an ordered list of record / classify / override operations in ONE invocation — the
537
+ prompt-economy lane for a records stage (one writer call, not one per op). The payload is
538
+ { "operations": [ { "verb": "record"|"classify"|"override", …that verb's payload } ] }; a
539
+ record op may carry "fromReceipts": true (the SAME draft as --from-receipts). Each op runs
540
+ the SAME per-verb code path (no forked validator), so a batch of N ops is record-equivalent
541
+ to the same N single invocations. TWO passes: the WHOLE envelope is validated structurally
542
+ first with ZERO writes (a bad envelope stops before any op runs); then ops apply sequentially
543
+ and fail-fast on the first typed STOP — the ops already applied stay recorded (the ledger is
544
+ append-only, no rollback), and the STOP names the failing op index + the applied count.
545
+ Resume by re-running the REMAINING operations.
451
546
 
452
547
  The read-only checker is a SEPARATE tool: node review-ledger.mjs --check / --status / --json / --telemetry.
453
548
  Exit codes: 0 written; 1 a typed STOP (teeth / malformed / missing receipt / fs error); 2 usage.`;
454
549
 
550
+ // The CLI subcommand set — the SINGLE source the dispatch and the doc-contract pin both read, so a
551
+ // documented verb list can never lag the dispatch (Phase 2.3 — the contract-test class).
552
+ export const SUBCOMMANDS = ['record', 'classify', 'override', 'batch'];
553
+
455
554
  const parseArgs = (argv) => {
456
555
  const opts = { cwd: undefined, json: undefined, fromReceipts: false };
457
556
  for (let i = 0; i < argv.length; i += 1) {
@@ -500,11 +599,18 @@ export const main = (argv, ctx = {}) => {
500
599
  try {
501
600
  if (argv.includes('--help') || argv.includes('-h') || argv.length === 0) return { code: argv.length === 0 ? 2 : 0, stdout: HELP, stderr: '' };
502
601
  const sub = argv[0];
503
- if (sub !== 'record' && sub !== 'classify' && sub !== 'override') throw usageFail(`unknown subcommand "${sub}" (expected: record | classify | override)`);
602
+ if (!SUBCOMMANDS.includes(sub)) throw usageFail(`unknown subcommand "${sub}" (expected: ${SUBCOMMANDS.join(' | ')})`);
504
603
  const opts = parseArgs(argv.slice(1));
505
- if (opts.fromReceipts && sub !== 'record') throw usageFail('--from-receipts applies only to `record`');
604
+ if (opts.fromReceipts && sub !== 'record') throw usageFail('--from-receipts applies only to `record` (a batch carries fromReceipts per operation)');
506
605
  const payload = parsePayload(opts.json);
507
606
  const cwd = opts.cwd ?? cwd0;
607
+ if (sub === 'batch') {
608
+ // Pass 1 (structural, ZERO writes) → pass 2 (sequential apply, fail-fast — prior ops durable).
609
+ const operations = validateBatchEnvelope(payload);
610
+ const { applied, count } = runBatch({ cwd, env, operations });
611
+ const writtenPath = applied.length ? applied[applied.length - 1].writtenPath : '(none)';
612
+ return { code: 0, stdout: `review-ledger-write: recorded ${count} operation(s) in one batch → ${writtenPath}`, stderr: '' };
613
+ }
508
614
  if (opts.fromReceipts) {
509
615
  // Draft backends[] from the current-fingerprint receipts + the supplied findings; origins /
510
616
  // findings stay explicit. The drafted array then rides the normal recordRound teeth.
@@ -513,10 +619,10 @@ export const main = (argv, ctx = {}) => {
513
619
  }
514
620
  const result =
515
621
  sub === 'record'
516
- ? recordRound({ cwd, env, ...payload })
622
+ ? recordRound({ ...payload, cwd, env })
517
623
  : sub === 'classify'
518
- ? recordTriage({ cwd, env, ...payload })
519
- : recordOverride({ cwd, env, ...payload });
624
+ ? recordTriage({ ...payload, cwd, env })
625
+ : recordOverride({ ...payload, cwd, env });
520
626
  return { code: 0, stdout: `review-ledger-write: recorded a ${result.record.kind} for loop "${result.record.loop}" round ${result.record.round} → ${result.writtenPath}`, stderr: '' };
521
627
  } catch (err) {
522
628
  return { code: err.exitCode ?? 1, stdout: '', stderr: `review-ledger-write: ${err.message}` };
@@ -568,7 +568,7 @@ gated commit, so the round-counter reset is earned, never declared.
568
568
  rounds, override usage by scope, gate-run counts (quality-green / red-by-gate), fold runs,
569
569
  observed-red receipts, quarantined probes. Counts only — interpretation stays with you.
570
570
 
571
- The writer is a SEPARATE tool (review-ledger-write.mjs record/classify/override) — this read-only
571
+ The writer is a SEPARATE tool (review-ledger-write.mjs record/classify/override/batch) — this read-only
572
572
  checker never imports it. Sandbox-safe: runs fully inside an OS sandbox (fs + git reads, no
573
573
  network) — the D4 sandbox lane. Human residual: git commit --no-verify, ledger-file editing, and
574
574
  forged counts remain possible — a self-discipline mechanism, not a security boundary.
@@ -163,9 +163,12 @@ const RUN_GATES_CWD_FLAG = '--cwd';
163
163
  // ── the opt-in --bridge-tier (AD-044 Plan 4, Decision 2) ────────────────────────────────
164
164
  // The FROZEN membership source: review-role wrapper NAMES only, spelled bare — that is how the
165
165
  // wrappers are invoked (setup-backends places them as symlinks in ~/.local/bin). Roles come from
166
- // THIS constant, never from the detector's wrapperCmds (which carries no role labels):
167
- // codex-exec (execution role) and agy-run (probe role) are deliberately ABSENT delegated
168
- // execution keeps its human prompt; the velocity pain this tier closes is council REVIEW runs.
166
+ // THIS constant, never from the detector's wrapperCmds (which carries no role labels): codex-exec
167
+ // (execution role) and agy-run (probe role) are deliberately ABSENT. The velocity pain this tier
168
+ // closes is council REVIEW runs; codex-exec's nested-sandbox recovery is handled the canon way
169
+ // route it outside ON the OBSERVED failure (orchestration.md §5), NOT a preemptive tier seed (AD-054
170
+ // final council: a blanket exclusion at opt-in contradicts «never a preemptive blanket»), guided by
171
+ // codex-exec.sh's own nested-sandbox detection hint.
169
172
  export const BRIDGE_REVIEW_WRAPPERS = Object.freeze(['codex-review', 'agy-review']);
170
173
  // Only the `code` review mode is auto-allowed (codex R2, Segment A): a bare `Bash(<wrapper>:*)`
171
174
  // prefix would also cover the plan/diff file-argument modes, whose targets can point OUTSIDE the
@@ -190,13 +193,12 @@ const isQuotedGroundingToken = (token) => {
190
193
  };
191
194
 
192
195
  /**
193
- * Derive the opt-in bridge-wrappers tier: one bare-name wildcard allow rule per PLACED review
194
- * wrapper (the frozen constant is the membership source; placement is a probe, never a role
195
- * source), the SAME wrapper names for sandbox.excludedCommands (the harness runs an excluded
196
- * command OUTSIDE the sandbox, so a plain allowlisted invocation needs no sandbox-bypass
197
- * approval — the zero-prompt wiring), and the grounding pre-step rule in its rendered quoted
198
- * byte-form (derived only when ≥1 review bridge is placed; an unseedable kit path is a stated
199
- * skip). An absent bridge is a stated skip — its entry never derives.
196
+ * Derive the opt-in bridge-wrappers tier: one code-mode allow rule per PLACED review wrapper (the
197
+ * frozen constant is the membership source; placement is a probe, never a role source), the SAME
198
+ * wrapper names for sandbox.excludedCommands (the harness runs an excluded command OUTSIDE the
199
+ * sandbox, so a plain allowlisted invocation needs no sandbox-bypass approval — the zero-prompt
200
+ * wiring), and the grounding pre-step rule in its rendered quoted byte-form (derived only when
201
+ * agy-review is placed; an unseedable kit path is a stated skip). An absent bridge is a stated skip.
200
202
  */
201
203
  export const deriveBridgeTierAllowlist = ({ findWrapper, groundingAbsPath } = {}) => {
202
204
  const probe = findWrapper ?? ((cmd) => findOnPath(cmd).state === WRAPPER_PLACED);