@sabaiway/agent-workflow-kit 5.2.0 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +1 -1
  3. package/SKILL.md +1 -1
  4. package/bridges/codex-cli-bridge/SKILL.md +11 -3
  5. package/bridges/codex-cli-bridge/bin/codex-exec.sh +154 -35
  6. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +257 -4
  7. package/bridges/codex-cli-bridge/bin/codex-review.sh +1 -1
  8. package/bridges/codex-cli-bridge/capability.json +3 -2
  9. package/bridges/codex-cli-bridge/references/driving-codex.md +5 -3
  10. package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +33 -15
  11. package/capability.json +1 -1
  12. package/package.json +1 -1
  13. package/references/modes/core-evidence.md +1 -1
  14. package/references/modes/coverage-check.md +1 -1
  15. package/references/modes/gates.md +7 -2
  16. package/references/modes/recommendations.md +3 -1
  17. package/references/modes/upgrade.md +1 -1
  18. package/references/modes/velocity.md +5 -1
  19. package/references/scripts/archive-decisions.mjs +340 -15
  20. package/references/scripts/archive-decisions.test.mjs +522 -2
  21. package/references/scripts/migrate-gates.mjs +102 -10
  22. package/references/scripts/migrate-gates.test.mjs +37 -0
  23. package/tools/core-evidence.mjs +42 -2
  24. package/tools/coverage-check.mjs +23 -7
  25. package/tools/coverage-producer.mjs +68 -0
  26. package/tools/coverage-state.mjs +24 -0
  27. package/tools/declared-paths.mjs +32 -0
  28. package/tools/detect-backends.mjs +1 -0
  29. package/tools/dispatch-record.mjs +926 -0
  30. package/tools/doc-parity.mjs +19 -4
  31. package/tools/flow-check.mjs +48 -12
  32. package/tools/gates-declaration.mjs +49 -0
  33. package/tools/gates-init.mjs +83 -6
  34. package/tools/recommendations.mjs +63 -19
  35. package/tools/run-gates.mjs +111 -32
  36. package/tools/velocity-profile.mjs +102 -23
package/CHANGELOG.md CHANGED
@@ -4,6 +4,116 @@ 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
+ ## 5.4.0 — a check that certifies nothing now says so, and the render stops promising what no host guarantees (AD-089)
8
+
9
+ **A gate matrix could run green while verifying nothing, and the kit itself was what built it.**
10
+ Neither declaration path ever wired a coverage PRODUCER, yet both declared the coverage CHECKER — so
11
+ a fresh `gates-init` and an `upgrade` alike could leave a project whose `--final` prints three
12
+ `PASS`, `lcov-sha256=none`, `attested=yes`, `status=ok`, exit 0. The honest signal existed on one
13
+ inner line; it reached no surface a consumer keys on. This release stops both kit-owned declaration
14
+ paths from ever ADDING a producer-less pair, makes the withheld verdict travel, and stops the
15
+ autonomy render asserting host behaviour it cannot know. A pair you declare by hand stays yours —
16
+ the kit reports it, it never removes it. Nothing gains a state: exit codes, the `status=` token, the
17
+ receipt `status` enum, `--final` acceptance and the commit-guard disposition are untouched, so an
18
+ optional-coverage project never goes red by surprise.
19
+
20
+ - **Neither kit-owned declaration path ADDS a coverage checker without a producer.**
21
+ `gates-init` wires the lcov reporter flags onto the one allowlist suite body that produces
22
+ coverage unaided, WITHHOLDS the `coverage-check` candidate with a named reason when neither the
23
+ offer nor the existing declaration carries a producer, and — this path alone — refuses at WRITE
24
+ time on the MERGED declaration, because an offer-level check alone still let
25
+ `--apply --only coverage-check` write a dead checker, still let a producer land after an
26
+ already-last checker, and still let a SECOND canonical checker in under a different id. The
27
+ ordering refusal names ORDERING as the cause, so you reorder instead of concluding the producer
28
+ was rejected. `migrate-gates` stops adding the checker into a producer-less declaration and
29
+ reports an already-declared one as INERT — loudly, with the remedy, and removing nothing for you.
30
+ - **One CLOSED producer predicate, shared by every consumer.** Recognition covers the full command
31
+ forms the kit itself emits — never a substring probe, because `echo "$AW_GIT_DIR/…"` or a
32
+ half-written flag set would pass one. The tail rule is a POSITIVE path-shaped grammar rather than
33
+ an operator blocklist: `node --test <flags> && rm -f <lcov>` runs the suite and then deletes the
34
+ file, so an open-ended tail would certify a producer that leaves nothing behind. The residual is
35
+ stated in the source rather than papered over — the screen judges source bytes, so the claim is
36
+ "configured with the reporters", never "the lcov survives the command"; a run that produces none
37
+ is still caught honestly at runtime as `skipped-no-lcov`.
38
+ - **`attested=` binds to the BYTES consumed.** A run that read nothing never certifies, however
39
+ valid its final-run context. The field states that a VERDICT WAS ISSUED — not that coverage
40
+ passed — so a valid handshake over uncovered lines still reads `yes` and still exits 1.
41
+ - **The summary line carries a new closed `coverage=` field** — `certified` / `not-run` / `none` /
42
+ `unknown`, one value defined for every run outcome — and the checker's table row names a withheld
43
+ verdict. Both are derived by CROSS-READING the checker's two anchored machine lines, so a missing,
44
+ duplicated or self-contradicting pair reads `unknown` rather than `certified`. The `--final`
45
+ receipt records the run's own token, and `core-evidence summary` renders the qualifier FROM it
46
+ instead of guessing from `lcovSha256` — which says what a receipt binds and never whether a
47
+ verdict was issued. A pre-5.4.0 receipt is named as a legacy receipt, never as a claim about what
48
+ it read.
49
+ - **New advisor item `gates-inert`** for a declaration that runs green and certifies nothing, with
50
+ two causes and two remedies. Cause A — a canonical `coverage-check` with no producer BEFORE it
51
+ (none at all, or one declared after it, which writes the lcov too late and lets stale bytes be
52
+ certified) — is HAND-APPLY, since the append-only fill cannot reorder an existing declaration.
53
+ Cause B — every declared gate is one of the kit's own checkers — renders the consent-gated
54
+ `gates-init` preview. Cause A decides through the SAME producer-before-checker rule the
55
+ `gates-init` fill enforces at write time — one home, so the advisor and the fill cannot drift
56
+ apart; cause B is advisor-only, with no refusal path behind it. `--final` acceptance is unchanged
57
+ either way: it still requires exactly one canonical coverage checker declared last, and it never
58
+ inspects producers. `flow optimal` no longer renders over either cause.
59
+ - **Every settings-derived RUNTIME claim in the autonomy render is now host-conditional.** Not only
60
+ the bridge-tier exclusion: `allowWrite`, `allowedDomains` and foreign exclusions are all claims
61
+ about what a HOST does with a settings key, and whether a host honours them is runtime-unknowable
62
+ — this kit's own mode doc has recorded that from live observation, while three separate surfaces
63
+ went on promising the bridge wrappers run outside the sandbox. The classification is unchanged;
64
+ only the unconditional promise is gone. The `allowWrite` degrade now resolves its entries
65
+ (tilde, relative and absolute, on segment-aware containment shared with the advisor's
66
+ worktrees-dir lane) and NAMES the external survivors instead of counting them — an entry pointing
67
+ inside the repo or `$TMPDIR` is no longer reported as an `fs_outside_repo` weakening at all, and
68
+ an unreadable value is its own UNVERIFIABLE class that asserts no weakening.
69
+ - **The bundled `codex-cli-bridge` mirror moves to 3.4.1** — a bundled-only bump carrying the fixed
70
+ `codex exec resume` argv, so the changed bridge bytes in this tarball ship under a version of
71
+ their own instead of silently.
72
+
73
+ Upgrading an existing project: nothing breaks and nothing is rewritten for you. If your declaration
74
+ already carries the dead pair, `status`/`upgrade` now say so and hand you the remedy — the reorder
75
+ case deliberately stays hand-applied.
76
+
77
+ ## 5.3.0 — a check must speak where it is built to speak: the two silent checks (AD-088)
78
+
79
+ **Two shipped checks stayed SILENT at exactly the point they exist to speak** — one a GATE that
80
+ refuses and blocks a commit, one a WARNING that never changes an exit status. They were one
81
+ sentence, so they ship together.
82
+
83
+ - **The `flow-check` gate stops demanding the receipt its own run will write.** A red final on the
84
+ current base was cleared only through a LATER completed retry — but `run-gates` appends the final
85
+ receipt only after every gate has run, so the in-matrix `flow-check` could never see the receipt
86
+ its OWN run would write, and each `--final` on an unchanged base minted red N+1. No number of
87
+ rerun-causes converged; the only exit was a hook bypass. The `#65` rung is now consumer-aware, the
88
+ same lane split the flow→final comparison already applies one arm away: on the `gate` lane a
89
+ current-base red is
90
+ ALSO answered by a provable IN-PROGRESS retry — an authoritative `rerun-cause` naming its attempt
91
+ and binding the current fingerprint, a `final-start` at that fingerprint ordered strictly after
92
+ that red whose attempt has no completed final, and that fingerprint correlating to exactly ONE
93
+ base, the current one. Inside a real final run the conjunction holds by construction; a standalone
94
+ check on a quiet tree still refuses.
95
+ - **`commit-guard` is unchanged.** It keeps the strict completed-retry demand, so the commit
96
+ boundary still sees a real receipt. The rung's own `consumer` defaults to that strict lane, the
97
+ relaxation is opt-in by EXACT match, and a tree whose fingerprint is unresolvable or ambiguously
98
+ base-correlated never relaxes.
99
+ - **The interrupted-run residual is stated, not papered over.** An interrupted final run leaves the
100
+ same record shape with no live run behind it, so a standalone `flow-check --check` reads PASS in
101
+ that window. It authorizes nothing — the commit boundary refuses that shape twice over,
102
+ independently — and the rung's own source comment says so, pinned end to end by a fixture that
103
+ asserts all three conditions at once.
104
+ - **The bundled `codex-cli-bridge` mirror moves to 3.4.0** — its nested-sandbox scan now runs on
105
+ every completed run, not only a failed one, so a delegated run that SURVIVES the failure and exits
106
+ 0 is no longer silent about it. On a successful run, it warns when the scanner recognises the
107
+ expected per-item shape — both tokens inside the `aggregated_output` of ONE `command_execution`
108
+ with a proven failure. The heuristic is biased toward under-firing on ambiguous or schema-drifted
109
+ input: field reordering stops the match, and only the first matching item on a line is judged.
110
+ The exit status deliberately stays 0 there — a heuristic scan must never gain the power to refuse
111
+ real work.
112
+
113
+ Evidence: an end-to-end fixture drives a caused red at the current tree through ONE real
114
+ `run-gates --final` to a GREEN newest-authoritative final and a passing `commit-guard --check` — the
115
+ test that would have failed before this release.
116
+
7
117
  ## 5.2.0 — the flow machinery ships whole: recorded subset budgets, flow-bound finals, the round arms, and the dogfooded pipeline (AD-086)
8
118
 
9
119
  **Everything the flow series built after 5.1.0 lands as one wave** — the record vocabulary, the
package/README.md CHANGED
@@ -239,7 +239,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
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). 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
241
  | `/agent-workflow-kit core-evidence` | any time | **the ONE loop-evidence writer** (strip-the-kit) — every core evidence record lands in a single append-only JSONL store inside the git dir (never committable; versioned schema, latest-per-key supersession, byte-identical duplicates refused, malformed lines fail every reader closed). `red-proof "<file>#<pattern>"` declares an observed-red **BEFORE a bugfix** (N/N red runs + content custody + base + the pre-fix fingerprint; green/mixed/timeout are DISTINGUISHED refusals — nothing written); `degrade --backend --reason` is the ONLY escape for an unavailable review backend (per-tree, never all backends); `summary` renders the whole loop state statelessly (gate result · per-backend verdicts · red-proofs · degrades) — no ledger, no rounds, nothing remembered. Honest residual: records are forgeable — self-discipline, not a security boundary. Never commits, never runs a subscription CLI. |
242
- | `/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). 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. |
242
+ | `/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. |
243
243
  | `/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. |
244
244
  | `/agent-workflow-kit recommendations` | any time (every `upgrade` ends with it) | **read-only deployment advisor** (AD-044) — computes what in THIS deployment is configured sub-optimally (allowlist not seeded, autonomy render drifted, OS sandbox unavailable, gates undeclared, bridge friction, sandbox-mask clutter, an unacknowledged sandbox recipe) and renders **verdict-first**: one composed verdict line (does anything need attention?), then each item as **{severity · what · one-line benefit · an optional `recipe:` line (the sandbox-lane live recipe, the worktrees-dir hand-apply-first grant advice, or the agents hidden-mode reconcile follow-up) · the exact consent-gated apply one-liner}**. The agent PRESENTS the section in the user's conversational language — every fact and count, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; raw tool block on request — and runs EXACTLY the rendered one-liners only on your yes, surfacing each item's posture note first. Renders **present-even-when-empty** (`no recommendations — flow optimal.`); a failed probe degrades to a stated skip line. Registry strings are fact-true frozen one-line data (posture/risk notes live in the mode doc at the consent moment); the kit never seeds `sandbox.network.allowedDomains` / `filesystem.allowWrite` (**HAND-APPLY** territory), and the sandbox-lane item's convergence is a neutral fingerprint acknowledgement recorded by a consent-gated ack writer into `docs/ai/acks.json` — never a security key (the recipe is documented per bridge in `capability.json` `networkHosts` + `writableDirs`). `--cwd` is required (the target project is explicit); never writes, never commits, never runs a subscription CLI. |
245
245
  | `/agent-workflow-kit doc-parity` | any time | **read-only doc-parity lint** (AD-049) — kills the doc-drift class where a mode-contract doc silently lags a code constant (a `--check` doc still reading `300` after the diff cap moved to `400`): a **closed, exported registry** binds each live constant (review caps, schema versions, the ledger's own class/scope vocabulary, and the autonomy-doctor EXIT/status/trusted-dir contract) to the exact token its `references/modes/*.md` contract must carry, and asserts the CURRENT value renders into every bound file — a drifted doc, an unreadable file, or an absent token **fails closed**. The values are sourced from the live imports (never re-typed), so the lint can't itself go stale; adding a binding is adding a checked entry (closed-world, edit-safe). `--check` is a gate exit code for `docs/ai/gates.json`. Never writes, never commits, never runs a subscription CLI. |
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: '5.2.0'
6
+ version: '5.4.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: '3.3.0'
5
+ version: '3.4.1'
6
6
  ---
7
7
 
8
8
  # codex-cli-bridge
@@ -110,8 +110,16 @@ receipt field. **Quote the posture banner verbatim** when labeling a dispatch.
110
110
 
111
111
  `codex exec` is headless: there is **no TTY**, so `approval_policy=never` — anything needing
112
112
  escalation is refused and reported, never interactively approved. The wrappers capture only codex's
113
- **final message** (`-o`; the JSON event stream + reasoning go to a discarded trace), so output is
114
- clean; a successful **non-resume** `codex-exec` also records the session id to a sidecar
113
+ **final message** (`-o`), so output is clean; the JSON event stream + reasoning go to a run trace
114
+ that is **read before it is discarded**. Fresh and resumed runs share **ONE** capture posture
115
+ (`-o` + `--json`, everything redirected into the trace; `--color never` rides the fresh lane only —
116
+ `codex exec resume` does not accept it), so both modes have the
117
+ same evidence surface — and the wrapper scans that surface on **every completed run**: a run that
118
+ SURVIVES a nested-sandbox failure exits 0 with an ungrounded answer, so on `rc == 0` it prints the
119
+ answer first and then warns loudly on stderr when one `command_execution` item with a **proven**
120
+ failure carries both a sandbox-mechanism and a permission/read-only token in its `aggregated_output`.
121
+ The exit status stays 0 there (warning, not gate) — read the stderr line. A successful **non-resume**
122
+ `codex-exec` also records the session id to a sidecar
115
123
  (`${CODEX_SESSION_FILE:-./.codex-last-session}`) so `--resume-last` can find it. Extra `codex` flags
116
124
  go after a literal `--`; the wrapper rejects any that would defeat the policy or the pinned model (see
117
125
  [§ Environment knobs](#environment-knobs) and the flag tiers in
@@ -86,6 +86,18 @@ Notes:
86
86
  INFORMATIONAL only: it is never persisted in a receipt or session sidecar
87
87
  quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated
88
88
  posture; a prose re-type drifts
89
+ every-run nested-sandbox scan (DUAL policy, deliberately two different rules): the scan runs on
90
+ EVERY completed run, not only a failed one, because a run that SURVIVES the nested-sandbox failure
91
+ exits 0 with an ungrounded answer and nothing said so. Failed run (rc != 0): the existing loose
92
+ whole-trace combination rule prints the recovery hint. Successful run (rc == 0): a warning fires
93
+ ONLY on precise per-item evidence — both a sandbox-mechanism token AND a permission/read-only
94
+ failure token inside the aggregated_output of ONE command_execution item whose failure is PROVEN
95
+ (a nonzero exit_code, or the serialized status "failed"); a null exit_code is never failure by
96
+ itself, tokens split across two items never fire, and a successful command's output never fires.
97
+ The answer is printed FIRST on stdout, then the warning on stderr. HONEST RESIDUAL: the exit
98
+ status does NOT change on that lane (a distinct nonzero exit would give a heuristic scan DENY
99
+ polarity, refusing real work whenever the scan over-warns), so an orchestrator keying on exit
100
+ status alone can still bank an ungrounded answer — the stderr warning is the signal
89
101
 
90
102
  Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins over built-in default):
91
103
  ${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf
@@ -412,9 +424,9 @@ fi
412
424
 
413
425
  # --- Resume detection (must be the FIRST argument) ---------------------------
414
426
  # A dedicated entrypoint for iterating on a session without re-sending context.
415
- # `codex exec resume` RESETS posture and rejects the -s/--add-dir/-C posture flags
416
- # (it DOES accept -o/--json on 0.142.3, but we capture stdout directly), so we
417
- # restate the FULL policy via -c.
427
+ # `codex exec resume` RESETS posture and rejects the -s/--add-dir/-C posture flags,
428
+ # so we restate the FULL policy via -c. It DOES accept -o/--json/--color, and it now
429
+ # carries the same capture posture as a fresh run (one evidence surface, both modes).
418
430
  resume_mode=""
419
431
  resume_id=""
420
432
  case "${1:-}" in
@@ -607,9 +619,12 @@ chmod 755 "$shim_dir/git"
607
619
 
608
620
  # --- Build the codex invocation + the prompt ---------------------------------
609
621
  if [[ -n "$resume_mode" ]]; then
610
- # Resume RESETS posture and rejects the -s/--add-dir/-C posture flags, so restate
611
- # the entire policy via -c. We deliberately pass no -o/--json (resume DOES accept
612
- # them) codex prints the final message to stdout, which we capture into $out.
622
+ # Resume RESETS posture and rejects the -s/--add-dir/-C posture flags, so restate the entire
623
+ # policy via -c. Its accepted flag set is NARROWER than `codex exec`'s and is probed, never
624
+ # assumed: `codex exec resume --help` (codex-cli 0.147.0) accepts -o and --json but NOT --color,
625
+ # so the capture posture is shared with a fresh run MINUS that flag. Shipping --color here once
626
+ # broke every resume invocation with a pre-spend exit 2 — the fake CLI in the suite accepts any
627
+ # argv, so only the real one can answer this question. See RESUME_ACCEPTED_FLAGS in the test.
613
628
  codex_cmd=(codex exec resume "$resume_id"
614
629
  --ignore-user-config
615
630
  -m "$CODEX_MODEL"
@@ -620,6 +635,8 @@ if [[ -n "$resume_mode" ]]; then
620
635
  -c hide_agent_reasoning=true
621
636
  -c model_reasoning_summary=none
622
637
  "${tier_flags[@]+"${tier_flags[@]}"}"
638
+ -o "$out"
639
+ --json
623
640
  -)
624
641
  full_prompt="$RESUME_REMINDER"$'\n\n'"$task"
625
642
  else
@@ -672,25 +689,136 @@ aw_session_label="fresh"
672
689
  [[ -n "$resume_mode" ]] && aw_session_label="resume:$resume_id"
673
690
  echo "exec posture: model=$CODEX_MODEL effort=$CODEX_EFFORT tier=${CODEX_SERVICE_TIER:-standard} sandbox=workspace-write session=$aw_session_label timeout=$aw_timeout_banner" >&2
674
691
 
675
- # Normal mode: -o writes $out, the JSON stream + logs go to $trace. Resume mode: the
676
- # final message is codex's stdout $out, logs $trace. Either way the final lands
677
- # in $out and diagnostics in $trace, so the post-processing below is shared.
678
- set +e
679
- if [[ -n "$resume_mode" ]]; then
680
- if [[ -n "$timeout_bin" ]]; then
681
- printf '%s' "$full_prompt" | "${run_env[@]}" "$timeout_bin" --kill-after=15s "$CODEX_HARD_TIMEOUT" "${codex_cmd[@]}" >"$out" 2>"$trace"
692
+ # --- Nested-sandbox evidence scan: ONE entry point, TWO policies ---------------
693
+ # The class: codex ships its OWN OS sandbox (bwrap); run nested inside a harness sandbox the FS is
694
+ # read-only and codex's sandbox setup fails. The scan runs on EVERY completed run, not only a failed
695
+ # one — when the backend SURVIVES the failure (degrades to "I cannot check" and exits 0) a paid run
696
+ # is spent on an ungrounded answer, and nothing said so.
697
+ #
698
+ # The two arms have DIFFERENT rules, deliberately:
699
+ # FAILED run (rc != 0) — the loose whole-trace COMBINATION rule, unchanged: a sandbox MECHANISM
700
+ # token AND a permission/read-only FAILURE token anywhere in the trace. The run already failed and
701
+ # the operator is already reading the tail, so an extra hint costs nothing. grep is line-oriented,
702
+ # so a plain alternation stays within a line — hence two `-q` passes rather than one pattern.
703
+ # SUCCESSFUL run (rc == 0) — per-item evidence ONLY: both tokens inside the aggregated_output of
704
+ # ONE command_execution item whose FAILURE is proven. Here a false positive would libel a good
705
+ # answer, so nothing loose is allowed near it.
706
+ # The successful-run scan is LINE-ORIENTED and TOLERANT: after the unified 2>&1 the trace
707
+ # legitimately mixes plain log lines with JSONL, so every line is judged alone, a line that is not a
708
+ # well-formed command_execution item is simply not evidence (never a parse error, never a stop), and
709
+ # no line can mask a later one.
710
+ AW_NS_MECHANISM='bwrap|landlock|user namespace|pivot_root|unshare|seccomp'
711
+ AW_NS_FAILURE='read-only file system|erofs|operation not permitted|permission denied|eperm'
712
+
713
+ # BOTH token classes present in one piece of text. Fed by here-string rather than `printf | grep`:
714
+ # with `pipefail` on, a producer that takes EPIPE when `grep -q` exits early on a match would make
715
+ # the pipeline non-zero and silently DROP a real signature — an under-fire invisible by
716
+ # construction. Not reproduced on this host, folded as portability hardening: with no explicit
717
+ # pipeline there is no producer left to fail.
718
+ aw_ns_both_tokens() {
719
+ grep -qiE "$AW_NS_MECHANISM" <<<"$1" || return 1
720
+ grep -qiE "$AW_NS_FAILURE" <<<"$1" || return 1
721
+ return 0
722
+ }
723
+
724
+ # Valid JSON string CONTENT — no UNESCAPED quote. This is what proves a delimiter slice stayed
725
+ # inside ONE string instead of crossing an object boundary: without it, a decoy object carrying the
726
+ # anchor lets the walk leave its own string and land in another item's fields.
727
+ # The predicate is the parity rule — a quote is escaped iff an ODD number of backslashes precedes
728
+ # it — expressed as an ERE and evaluated by grep: `(^|[^\])` then an EVEN run `(\\)*` then the
729
+ # quote. It is measured, not assumed: the obvious bash spelling (delete the `\\` and `\"` pairs,
730
+ # then look for a survivor) is the SAME predicate but bash's `${var//…}` is quadratic, and it hung
731
+ # the wrapper outright on a 200KB aggregated_output — a real tool call's output reaches that size.
732
+ aw_ns_is_string_content() {
733
+ if grep -qE '(^|[^\\])(\\\\)*"' <<<"$1"; then return 1; fi
734
+ return 0
735
+ }
736
+
737
+ # One trace line → 0 only when THAT line is a command_execution item with a PROVEN failure whose
738
+ # aggregated_output carries both token classes. The wrapper stays dependency-free, so this is not a
739
+ # JSON parse — it is ONE anchored walk over the CLI's observed serialization in which every SKIPPED
740
+ # gap is PROVEN to be a single JSON string's content (aw_ns_is_string_content). Both halves are
741
+ # load-bearing: testing the fields as independent substrings lets a decoy object supply the anchor
742
+ # while the failure fields belong to another item, and skipping a gap without validating it lets the
743
+ # walk leave its own string and land in that other item anyway.
744
+ # The observed shape (codex-cli 0.147.0, live-probed):
745
+ # {"id":…,"type":"command_execution","command":"…","aggregated_output":"…","exit_code":2,"status":"failed"}
746
+ # Failure proofs: A = a nonzero exit_code, B = the status "failed" immediately after it. A null
747
+ # exit_code is never failure by itself (an in-flight item carries "exit_code":null,"status":"in_progress").
748
+ # Two stated consequences, both deliberate:
749
+ # - a future CLI that REORDERS these fields makes the scan stop firing rather than misfire.
750
+ # Under-firing is the right direction here: over-firing would libel a correct answer.
751
+ # - only the FIRST matching item on a line is judged; a second item's evidence is missed.
752
+ # A hand-crafted trace line is outside the threat model (the same boundary the wrapper declares for
753
+ # a hostile parent environment): the trace's only non-CLI content is plain stderr, which does not
754
+ # start with `{`, and a tool call's own output is JSON-escaped into a string and cannot inject
755
+ # structure. Anything not matching the walk is "not evidence" — never an error, never a stop.
756
+ # Every slice is taken by a SHORT-pattern `#*` cut plus length arithmetic. That is not a style
757
+ # choice: `${var%%<long>*}` and a prefix removal whose PATTERN is a huge variable are both
758
+ # quadratic in bash, and either one hangs the wrapper outright on a 200KB aggregated_output
759
+ # (measured, not assumed — a real tool call's output reaches that size).
760
+ aw_ns_item_evidence() {
761
+ local line="$1" d1='","aggregated_output":"' d2='","exit_code":' rest tail cmd agg code after
762
+ case "$line" in '{'*) ;; *) return 1 ;; esac
763
+ rest="${line#*'"type":"command_execution","command":"'}"
764
+ if [[ "$rest" == "$line" ]]; then return 1; fi
765
+ tail="${rest#*"$d1"}"
766
+ if [[ "$tail" == "$rest" ]]; then return 1; fi
767
+ cmd="${rest:0:$(( ${#rest} - ${#tail} - ${#d1} ))}"
768
+ aw_ns_is_string_content "$cmd" || return 1
769
+ after="${tail#*"$d2"}"
770
+ if [[ "$after" == "$tail" ]]; then return 1; fi
771
+ agg="${tail:0:$(( ${#tail} - ${#after} - ${#d2} ))}"
772
+ aw_ns_is_string_content "$agg" || return 1
773
+ code="${after%%,*}"
774
+ if [[ "$code" =~ ^-?[0-9]+$ && "$code" != "0" ]]; then
775
+ :
776
+ elif [[ "${after#"$code",}" == '"status":"failed"'* ]]; then
777
+ :
682
778
  else
683
- printf '%s' "$full_prompt" | "${run_env[@]}" "${codex_cmd[@]}" >"$out" 2>"$trace"
779
+ return 1
684
780
  fi
685
- rc=$?
686
- else
687
- if [[ -n "$timeout_bin" ]]; then
688
- printf '%s' "$full_prompt" | "${run_env[@]}" "$timeout_bin" --kill-after=15s "$CODEX_HARD_TIMEOUT" "${codex_cmd[@]}" >"$trace" 2>&1
689
- else
690
- printf '%s' "$full_prompt" | "${run_env[@]}" "${codex_cmd[@]}" >"$trace" 2>&1
781
+ aw_ns_both_tokens "$agg"
782
+ }
783
+
784
+ aw_scan_nested_sandbox() { # $1 = rc, $2 = trace path
785
+ [[ -r "$2" ]] || return 0
786
+ if [[ "$1" -ne 0 ]]; then
787
+ if grep -qiE "$AW_NS_MECHANISM" "$2" 2>/dev/null && grep -qiE "$AW_NS_FAILURE" "$2" 2>/dev/null; then
788
+ echo "hint: this looks like a NESTED-SANDBOX failure — codex-exec ships its own OS sandbox (bwrap)," >&2
789
+ echo " which cannot run nested inside a harness sandbox (the FS is read-only). Route codex-exec" >&2
790
+ echo " OUTSIDE the harness sandbox: add it to the harness sandbox excludedCommands, or dispatch" >&2
791
+ echo " this one run via a per-run consented bypass. Do NOT blanket-disable the sandbox." >&2
792
+ fi
793
+ return 0
691
794
  fi
692
- rc=$?
795
+ local line
796
+ while IFS= read -r line || [[ -n "$line" ]]; do
797
+ if aw_ns_item_evidence "$line"; then
798
+ echo "warning: NESTED-SANDBOX — this run COMPLETED, but a tool call inside it FAILED with a sandbox-setup" >&2
799
+ echo " signature. codex-exec ships its own OS sandbox (bwrap), which cannot run nested inside a" >&2
800
+ echo " harness sandbox (the FS turns read-only), so the backend most likely could not read what" >&2
801
+ echo " it was asked to check: the answer above may be UNGROUNDED — treat it as such rather than" >&2
802
+ echo " banking it. Re-dispatch OUTSIDE the harness sandbox: add codex-exec to the harness" >&2
803
+ echo " sandbox excludedCommands, or use a per-run consented bypass. Do NOT blanket-disable the" >&2
804
+ echo " sandbox. The exit status stays 0 on purpose — this is a warning, never a gate." >&2
805
+ return 0
806
+ fi
807
+ done <"$2"
808
+ return 0
809
+ }
810
+
811
+ # ONE capture posture for BOTH modes: -o writes the final message to $out, the JSON
812
+ # event stream and every log line go to $trace (stderr merged in). The final always
813
+ # lands in $out and the diagnostics always in $trace, so the post-processing below —
814
+ # including the evidence scan — is genuinely shared instead of mode-dependent.
815
+ set +e
816
+ if [[ -n "$timeout_bin" ]]; then
817
+ printf '%s' "$full_prompt" | "${run_env[@]}" "$timeout_bin" --kill-after=15s "$CODEX_HARD_TIMEOUT" "${codex_cmd[@]}" >"$trace" 2>&1
818
+ else
819
+ printf '%s' "$full_prompt" | "${run_env[@]}" "${codex_cmd[@]}" >"$trace" 2>&1
693
820
  fi
821
+ rc=$?
694
822
  set -e
695
823
 
696
824
  if [[ $rc -eq 124 || $rc -eq 137 ]]; then
@@ -701,20 +829,7 @@ fi
701
829
  if [[ $rc -ne 0 ]]; then
702
830
  echo "error: codex exec failed (exit $rc). Last lines of the run trace:" >&2
703
831
  tail -n 40 "$trace" >&2
704
- # Nested-sandbox detection: codex ships its OWN OS sandbox (bwrap); run nested inside a harness
705
- # sandbox the FS is read-only and codex's own sandbox setup fails. Fire the STATED recovery hint
706
- # only on a COMBINATION — a sandbox MECHANISM token AND a permission/read-only FAILURE token in the
707
- # trace — so a lone 'bwrap' banner, or a lone 'permission denied' from unrelated code, is NOT enough
708
- # (never a preemptive blanket). grep is line-oriented, so a plain alternation stays within a line —
709
- # the old `[^\n]*` between the two halves was wrong twice over (it excluded the letter 'n', and grep
710
- # never spans lines anyway), so the split into two `-q` passes both fixes it and states the intent.
711
- if grep -qiE 'bwrap|landlock|user namespace|pivot_root|unshare|seccomp' "$trace" 2>/dev/null \
712
- && grep -qiE 'read-only file system|erofs|operation not permitted|permission denied|eperm' "$trace" 2>/dev/null; then
713
- echo "hint: this looks like a NESTED-SANDBOX failure — codex-exec ships its own OS sandbox (bwrap)," >&2
714
- echo " which cannot run nested inside a harness sandbox (the FS is read-only). Route codex-exec" >&2
715
- echo " OUTSIDE the harness sandbox: add it to the harness sandbox excludedCommands, or dispatch" >&2
716
- echo " this one run via a per-run consented bypass. Do NOT blanket-disable the sandbox." >&2
717
- fi
832
+ aw_scan_nested_sandbox "$rc" "$trace"
718
833
  exit $rc
719
834
  fi
720
835
 
@@ -739,3 +854,7 @@ else
739
854
  echo "warning: codex produced no final-message file — printing the run-trace tail instead." >&2
740
855
  tail -n 40 "$trace"
741
856
  fi
857
+
858
+ # The answer is printed FIRST, then the evidence speaks: a run that COMPLETED can still have been
859
+ # ungrounded, and saying so after the answer keeps stdout byte-identical for every caller.
860
+ aw_scan_nested_sandbox "$rc" "$trace"