@sabaiway/agent-workflow-kit 10.4.0 → 10.5.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 (47) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/SKILL.md +1 -1
  4. package/bridges/antigravity-cli-bridge/SKILL.md +7 -1
  5. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +69 -17
  6. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +73 -2
  7. package/bridges/antigravity-cli-bridge/capability.json +2 -2
  8. package/bridges/antigravity-cli-bridge/references/review-prompt.md +3 -0
  9. package/bridges/codex-cli-bridge/SKILL.md +8 -1
  10. package/bridges/codex-cli-bridge/bin/codex-exec.sh +1 -1
  11. package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +1 -1
  12. package/bridges/codex-cli-bridge/bin/codex-review.sh +89 -18
  13. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +55 -2
  14. package/bridges/codex-cli-bridge/capability.json +2 -2
  15. package/capability.json +1 -1
  16. package/package.json +1 -1
  17. package/references/agents/review-lens.md +5 -3
  18. package/references/modes/agents.md +1 -1
  19. package/references/modes/procedures.md +9 -5
  20. package/references/modes/recipes.md +2 -2
  21. package/references/modes/set-recipe.md +4 -4
  22. package/references/modes/status.md +1 -1
  23. package/references/modes/velocity.md +1 -0
  24. package/references/templates/orchestration.json +1 -1
  25. package/tools/bridge-posture.mjs +48 -0
  26. package/tools/carriers.mjs +21 -9
  27. package/tools/cheap-agents-read.mjs +86 -24
  28. package/tools/cheap-agents.mjs +47 -7
  29. package/tools/detect-backends.mjs +2 -2
  30. package/tools/direct-run.mjs +3 -0
  31. package/tools/fold-scope.mjs +5 -60
  32. package/tools/grounding.mjs +2 -2
  33. package/tools/orchestration-config.mjs +19 -78
  34. package/tools/orchestration-readme.mjs +70 -0
  35. package/tools/plan-shape-cli.mjs +112 -0
  36. package/tools/plan-shape-facts.mjs +204 -0
  37. package/tools/plan-shape.mjs +348 -0
  38. package/tools/procedures.mjs +132 -31
  39. package/tools/recipes.mjs +60 -79
  40. package/tools/repo-lex.mjs +40 -0
  41. package/tools/review-roster-resolve.mjs +104 -0
  42. package/tools/review-roster.mjs +128 -0
  43. package/tools/review-rounds-cli.mjs +92 -0
  44. package/tools/review-rounds.mjs +115 -0
  45. package/tools/set-recipe-roster.mjs +167 -0
  46. package/tools/set-recipe.mjs +80 -23
  47. package/tools/velocity-profile.mjs +8 -22
package/CHANGELOG.md CHANGED
@@ -4,6 +4,61 @@ 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
+ ## 10.5.0 — the plan-review loop's measured costs are rungs, values and receipt fields (AD-125)
8
+
9
+ One feature cost five hours of review by the kit's own defaults: plan findings that were plan SHAPE
10
+ no tool checked, round-1 blockers that were READERS of a changed value nobody enumerated, a
11
+ consult-before-fold rule living in an agent's memory, folds riding the executor vehicle, no run
12
+ duration anywhere, and a crossover stop that had to be remembered. Each of those is now a checker,
13
+ a procedure step, a slot or a receipt field — advisory by contract (`docs/ai/specs/kit/plan-review-loop/`,
14
+ S1–S28): no new gate by default, the commit gate stays on the bridges, a lens never becomes a receipt.
15
+
16
+ **The plan-shape checker** (`tools/plan-shape.mjs` the rules, `plan-shape-facts.mjs` one repository
17
+ snapshot, `plan-shape-cli.mjs`): `--check <plan>` refuses a plan by shape BEFORE its first review — the
18
+ five headings in order, the row cap (100 lines AND ≤25 ledger rows, 200 UTF-8 bytes per row counted
19
+ without the path and anchor fields; the 8000-byte cap is gone), red-first row order, a
20
+ `package-content` pin row for every shipped `create` (from the package `files`), budgets under the cap
21
+ and the recorded size, anchors that resolve to exactly one candidate; `--verify <plan>` closes a
22
+ ledger after execution; `--in-flight` judges every titled plan by shape. Exit 0 / 1 / 2, every refusal
23
+ naming the line, the rule and the fix. `grounding` and `fold-scope` import the headings from it.
24
+
25
+ **The round table** (`tools/review-rounds.mjs`, `review-rounds-cli.mjs --artifact <plan>
26
+ [--activity …]`): receipts grouped by fingerprint into rounds, latest per backend, complete by the
27
+ configured slot's obligation; ONE of six signals in order — `converged` is a complete round (every listed backend under a
28
+ per-backend obligation, at least the obligation's minimum otherwise) whose every present backend
29
+ reports blocking 0 with a ship-class verdict, `crossover` needs a negative — with the receipted duration per round and
30
+ cumulative, the pathless and malformed receipts counted. `procedures plan-authoring` prints that
31
+ command populated with the plan in flight, beside the grounding pre-step, as the verdict half of the
32
+ per-round emission; the finding-origin tally stays the orchestrator's. Beneath the command one fact
33
+ line holds for every source: the table judges the obligation `review-rounds-cli` resolves from the config
34
+ (S27) — never a run's `--override` or a recipe the advisor degraded — and reads receipts only (a backend
35
+ that did not run shows as missing; its degrade record is `review-state`'s and `core-evidence summary`'s);
36
+ a requested review recipe renders the command even when every bridge is unavailable here (a degradation
37
+ removes no configured obligation); a roster with no bridge gets, in the command's place, the fact that no
38
+ receipt can exist; a mixed roster adds the lens reminder. The `--kit-tools` tier gains
39
+ `tools/review-rounds-cli.mjs` (read-only: the receipts store plus read-only `git rev-parse` queries —
40
+ `--show-toplevel` here and `--absolute-git-dir` through the receipts-path resolver; the advisor
41
+ names it every round, so it renders BARE in the tier's byte-form). The plan operand of every rendered
42
+ command — the round table, the grounding pre-step, `agy-review plan` — rides `shellQuoteArg` (bare when
43
+ safe, single-quoted otherwise); a plan name a one-line render cannot carry (a control or line-breaking
44
+ character) falls back to the placeholder on every command, and a name the receipt encoder refuses (a
45
+ double quote, a backslash, a control byte — the S21 set) falls back on the commands that mint or match
46
+ a receipt (`agy-review plan`, the round table) while the file-reading `grounding --plan` stays
47
+ populated; the caveat names the plan escaped and the operand(s) that fell back.
48
+
49
+ **The procedure and the config.** `plan-authoring` gains the `fold` carrier slot (`solo | subagent`,
50
+ default `solo` even beside `author = subagent`; resolved, validated, previewed, written and unset like
51
+ every carrier slot; the slice wording keyed by slot). The advisor renders the readers sweep for
52
+ plan-authoring — the typed `repo_search` line when the MCP registration is complete on disk and ALWAYS
53
+ the command line in the tier's seedable byte-form — and the consult-before-fold order in the review-loop
54
+ block (agy `--continue --decided @f --focus …`, codex a fresh `codex-review plan <consult-brief>`, a
55
+ placed lens a re-dispatch; on an armed flow the bridge `consult-attestation` sequence). A `Slots:`
56
+ line an installed engine renders differently from the kit's registry is said in `warnings`.
57
+ `requiredBackendsForConfiguredRecipe` takes an `activity`; the shell lexing predicate
58
+ `isSeedablePathToken` lives in the pure leaf `repo-lex.mjs`. Bundled bridges: codex-cli-bridge
59
+ 3.6.0 and antigravity-cli-bridge 5.5.0 (the receipt fields, the node preflight, the no-file-read
60
+ prepend, the single schema parse — see each bridge's SKILL.md). `package-content` pins 275 files.
61
+
7
62
  ## 10.4.0 — who does what is a project setting, and a subagent is a first-class carrier of every kind of work (AD-124)
8
63
 
9
64
  The rule "design, then hand execution to a frontier subagent" lived in an agent's memory and in a
package/README.md CHANGED
@@ -233,7 +233,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
233
233
  | `/agent-workflow-kit setup [backend]` | opt-in, any time | **link-only** auto-setup of a bridge: places the bundled bridge skill (only into an absent / empty / managed dir — never overwrites an unmanaged one) + links its wrappers onto `PATH` via managed symlinks (idempotent; refuses to clobber a non-symlink; try `--dry-run` to preview). The binary install + the one-time subscription login stay **manual**: it prints the exact **login** command and points the binary install at each bridge's `setup/README.md`. POSIX wrappers — on Windows use WSL. Never commits, never runs a subscription CLI. |
234
234
  | `/agent-workflow-kit status` | any time | **read-only** single view of **versions + deployment + settings + bridges**: which members (kit / memory / engine / the two bridges) are installed and at what version (with an honest "installed on this machine" note when one is behind) and — in a project — what's deployed (`docs/ai`, the version stamps, and the **visibility**: visible / hidden / unclear), and the **feature-spec adoption state** (`not adopted` / `adopting (N draft)` / `adopted (N live, M draft)` / `could not be read`, plus `declined` when recorded), plus your settings (orchestration recipes, attribution, velocity) and the bridges' readiness. The two version axes (package number vs deployment-structure head) stay decoupled. Never writes, never commits, never runs a subscription CLI. |
235
235
  | `/agent-workflow-kit recipes` | any time | **read-only** orchestration advisor: presents five named recipes for composing the carriers of a step — the bridges and the full-tool executor subagent — into plan → execute → review — **Solo / Reviewed / Council / Delegated / Subagent** — plans + recommends one for your environment (degrading with a stated reason when a backend isn't ready, or when the executor vehicle is missing/unusable), and offers the choice. The activity/slot registry (three activities) is rendered by `procedures` and by `set-recipe --help`. The orchestrator runs it via the bridge skills or the executor vehicle and **always commits**; the kit never executes a recipe, never runs a subscription CLI, never commits. |
236
- | `/agent-workflow-kit procedures <activity>` | any time | **read-only** activity-procedures advisor: prints a named activity's ordered steps (`plan-authoring` / `plan-execution` / `routine`) read **live** from the engine, plus the **resolved recipe per slot** from your `docs/ai/orchestration.json` (agent-writable via `set-recipe`, or hand-edited) + carrier readiness (default Reviewed when a backend is ready, Council on request, slot-aware incl. Delegated and Subagent) — and, for every dispatched backend, the **full driving contract at the point of use** (exact copy-pasteable invocation, grounding levers like agy's `--facts`/`--decided`, the round-2 `--continue` delta, guarded passthrough), verbatim from the bridge manifests (drift-guarded; each wrapper's `--help` prints the same). `--override <slot>=<value>` adjusts one slot per run. Composes with `recipes`; never writes, never commits, never runs a subscription CLI. |
236
+ | `/agent-workflow-kit procedures <activity>` | any time | **read-only** activity-procedures advisor: prints a named activity's ordered steps (`plan-authoring` / `plan-execution` / `routine`) read **live** from the engine, plus the **resolved recipe per slot** from your `docs/ai/orchestration.json` (agent-writable via `set-recipe`, or hand-edited) + carrier readiness (`plan-authoring`: author, fold, review; default Reviewed when a backend is ready, Council on request; slot-aware incl. Delegated and Subagent) — and, for every dispatched backend, the **full driving contract at the point of use** (exact copy-pasteable invocation, grounding levers like agy's `--facts`/`--decided`, the round-2 `--continue` delta, guarded passthrough), verbatim from the bridge manifests (drift-guarded; each wrapper's `--help` prints the same). `--override <slot>=<value>` adjusts one slot per run. Composes with `recipes`; never writes, never commits, never runs a subscription CLI. |
237
237
  | `/agent-workflow-kit set-recipe` | any time | **config writer** for `docs/ai/orchestration.json`: tell the agent your preference in plain language and it maps it to explicit `--set <activity>.<slot>=<value>` / `--unset` ops; the kit validates, **previews by default**, and writes only on `--write` (deployment-gated, atomic, symlink/TOCTOU-safe), resolving the effective recipe vs live readiness. Writes **only** that file — **never runs a backend or a subagent, never commits**; hand-editing stays fully supported. |
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. |
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: '10.4.0'
6
+ version: '10.5.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 headless behaviour.
4
4
  metadata:
5
- version: '5.4.0'
5
+ version: '5.5.0'
6
6
  ---
7
7
 
8
8
  # antigravity-cli-bridge
@@ -173,6 +173,12 @@ hands to `timeout(1)`; without a capping binary `agy-review` fails CLOSED pre-sp
173
173
  informational, never a receipt field. **Quote the posture banner verbatim** when labeling a
174
174
  dispatch.
175
175
 
176
+ Every successful review receipt carries integer `durationS` and `blocking`; the wrapper prints
177
+ `review duration: <n>s`. A plan or diff receipt also carries `artifactPath`, normalized to a
178
+ repo-relative realpath inside the work tree and an absolute realpath otherwise. A double quote,
179
+ backslash or control byte in that path refuses pre-spend because the receipt encoder cannot carry
180
+ it. Agy counts numbered items in the first `### Blocking` section.
181
+
176
182
  **Flow dispatch nonce (`AW_REVIEW_NONCE`, unset by default):** under the safe grammar
177
183
  `[A-Za-z0-9._-]{1,64}` (anything else refuses pre-spend), a successful review first mints the
178
184
  finding MANIFEST `agent-workflow-finding-manifest-agy-<nonce>.json` beside the receipts file —
@@ -114,8 +114,13 @@ Receipt:
114
114
  uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the
115
115
  review-payload domain; never-committable untracked paths — character/block devices, FIFOs,
116
116
  sockets — are excluded from the domain entirely, untracked symlinks/directories ride as
117
- name-only notes) in code mode, the artifact-file sha256 in plan/diff mode; verdict
118
- recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK);
117
+ name-only notes) in code mode, the artifact-file sha256 in plan/diff mode; durationS = integer
118
+ wall-clock seconds from CLI start through verdict parsing and the wrapper prints review duration:
119
+ <n>s; blocking = the count of numbered items in the first ### Blocking section; artifactPath =
120
+ the normalized realpath on plan/diff receipts only (repo-relative inside the work tree, absolute
121
+ otherwise), while a double quote, backslash or control byte refuses pre-spend because the receipt
122
+ encoder cannot carry it; verdict recorded verbatim from the mandated '### Verdict' section
123
+ (SHIP / SHIP WITH NITS / REWORK);
119
124
  grounded = whether a NON-EMPTY --facts payload was supplied (code mode refuses pre-spend without
120
125
  one — no run, no receipt — unless --ungrounded/AGY_PROBE=1; in plan/diff an empty payload records
121
126
  grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts
@@ -163,7 +168,7 @@ Honesty + posture (D4/D5):
163
168
  mode.
164
169
 
165
170
  Closed grammar: unknown flags are rejected; no '--' passthrough (the flag escape is --ungrounded; the env escape is AGY_PROBE=1).
166
- Requires at run time: the agy CLI on PATH + a Google AI subscription login (--help needs neither).
171
+ Requires at run time: the agy CLI on PATH, a Google AI subscription login, Node >= 22 (--help needs none of these).
167
172
  HELP
168
173
  exit 0
169
174
  ;;
@@ -338,7 +343,7 @@ DEFAULT_AGY_REVIEW_MODEL="Gemini 3.7 Flash (High)"
338
343
  # Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
339
344
  # version (drift-guarded by agy-review.test.mjs against capability.json).
340
345
  AW_RECEIPT_BACKEND="agy"
341
- AW_BRIDGE_VERSION="5.4.0" # aw-version-anchor
346
+ AW_BRIDGE_VERSION="5.5.0" # aw-version-anchor
342
347
  # `-` not `:-` so an EXPLICIT empty AGY_MODEL= survives (drop --model, use settings.json — agy.sh:52).
343
348
  AGY_MODEL="${AGY_MODEL-$DEFAULT_AGY_REVIEW_MODEL}"
344
349
  # D5 control-byte screen — IMMEDIATELY after resolution, BEFORE the off-frontier advisory (or any
@@ -891,6 +896,42 @@ receipt_json_scalar() {
891
896
  if [[ -z "${1:-}" ]]; then printf 'null'; else printf '"%s"' "$1"; fi
892
897
  }
893
898
 
899
+ # refuse_uncarriable_artifact_byte <label> <value> — the receipt encoder escapes nothing beyond its
900
+ # grammar-bound fields, so an artifact path is BOUNDED, never escaped: a byte the line could not
901
+ # carry refuses pre-spend, by name.
902
+ refuse_uncarriable_artifact_byte() {
903
+ local label="$1" value="$2" byte=""
904
+ case "$value" in
905
+ *'"'*) byte="a double quote" ;;
906
+ *'\'*) byte="a backslash" ;;
907
+ *[$'\x01'-$'\x1f'$'\x7f']*) byte="a control" ;;
908
+ esac
909
+ if [[ -z "$byte" ]]; then return 0; fi
910
+ echo "error: $label contains $byte byte, which the receipt encoder cannot carry." >&2
911
+ return 2
912
+ }
913
+
914
+ normalize_artifact_path() {
915
+ local input="$1" normalized
916
+ refuse_uncarriable_artifact_byte "artifact path" "$input" || return 2
917
+ if ! normalized="$(node -e '
918
+ const { realpathSync } = require("node:fs");
919
+ const { isAbsolute, relative, sep } = require("node:path");
920
+ const { spawnSync } = require("node:child_process");
921
+ const absolute = realpathSync(process.argv[1]);
922
+ const git = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" });
923
+ const root = git.status === 0 ? realpathSync(git.stdout.replace(/\r?\n$/, "")) : null;
924
+ const rel = root === null ? null : relative(root, absolute);
925
+ const contained = rel !== null && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
926
+ process.stdout.write((contained ? rel : absolute).split(sep).join("/"));
927
+ ' -- "$input" 2>/dev/null)"; then
928
+ echo "error: artifact path '$input' could not be normalized to a real path." >&2
929
+ return 2
930
+ fi
931
+ refuse_uncarriable_artifact_byte "normalized artifact path" "$normalized" || return 2
932
+ printf '%s' "$normalized"
933
+ }
934
+
894
935
  # STRICT JSON string encoding for the ONE free-form receipt field (the posture model display
895
936
  # string): backslash then double-quote escaped. Control bytes never reach here — the posture
896
937
  # resolution refuses them pre-spend (D5), so these two escapes make the encoding total.
@@ -1008,7 +1049,7 @@ process.exit(code);
1008
1049
  return 0
1009
1050
  }
1010
1051
 
1011
- # write_review_receipt <artifact|""> <fresh: true|false> <fingerprint|""> <verdict> <grounded: true|false> <factsHash|""> [probe: true|false] [delivery|""] [findings-file]
1052
+ # write_review_receipt <artifact|""> <fresh: true|false> <fingerprint|""> <verdict> <grounded: true|false> <factsHash|""> [probe: true|false] [delivery|""] [findings-file] [artifact-path] [durationS] [blocking]
1012
1053
  # Appends ONE receipt line (the AD-038 fixture shape) as a side effect of a SUCCESSFUL review —
1013
1054
  # to $AW_REVIEW_RECEIPTS when set, else <git dir>/agent-workflow-review-receipts.jsonl (inside the
1014
1055
  # git dir by construction, so it is never committable). Fail-safe: every failure here warns loudly
@@ -1022,6 +1063,7 @@ process.exit(code);
1022
1063
  # minted FIRST (atomic, no-clobber, ORDERED) and a failed mint EXCLUDES the receipt append.
1023
1064
  write_review_receipt() {
1024
1065
  local artifact="$1" fresh="$2" fingerprint="$3" verdict="$4" grounded="$5" facts_hash="$6" probe="${7:-false}" delivery="${8:-}" findings_file="${9:-}"
1066
+ local artifact_path="${10:-}" duration_s="${11:-0}" blocking="${12:-0}"
1025
1067
  local receipts="${AW_REVIEW_RECEIPTS:-}"
1026
1068
  if [[ -z "$receipts" ]]; then
1027
1069
  local receipt_git_dir
@@ -1039,16 +1081,16 @@ write_review_receipt() {
1039
1081
  fi
1040
1082
  # A nonce-SUPPLIED dispatch stamps its nonce into the receipt too (the flow round-land matcher
1041
1083
  # requires exact {backend, nonce} equality — dispatch identity end-to-end); the nonce is
1042
- # grammar-safe by the pre-spend check, and a nonce-less receipt stays BYTE-EXACT (the frozen
1043
- # compatibility floor).
1044
- local line probe_field=',"probe":false' delivery_field="" nonce_field=""
1084
+ # grammar-safe by the pre-spend check; a nonce-less receipt adds NO nonce field.
1085
+ local line probe_field=',"probe":false' artifact_path_field="" delivery_field="" nonce_field=""
1045
1086
  if [[ "$probe" == "true" ]]; then probe_field=',"probe":true'; fi
1087
+ if [[ -n "$artifact_path" ]]; then artifact_path_field=",\"artifactPath\":\"$artifact_path\""; fi
1046
1088
  if [[ -n "$delivery" ]]; then delivery_field=",\"delivery\":\"$delivery\""; fi
1047
1089
  if [[ -n "${AW_REVIEW_NONCE:-}" ]]; then nonce_field=",\"nonce\":\"${AW_REVIEW_NONCE}\""; fi
1048
- line="$(printf '{"schema":1,"artifact":%s,"fresh":%s,"fingerprint":%s,"backend":"%s","verdict":"%s","grounded":%s,"factsHash":%s,"wrapperVersion":"%s","timestamp":"%s"%s,"posture":%s%s%s}' \
1090
+ line="$(printf '{"schema":1,"artifact":%s,"fresh":%s,"fingerprint":%s,"backend":"%s","verdict":"%s","grounded":%s,"factsHash":%s,"wrapperVersion":"%s","timestamp":"%s"%s,"durationS":%s,"blocking":%s%s,"posture":%s%s%s}' \
1049
1091
  "$(receipt_json_scalar "$artifact")" "$fresh" "$(receipt_json_scalar "$fingerprint")" \
1050
1092
  "$AW_RECEIPT_BACKEND" "$verdict" "$grounded" "$(receipt_json_scalar "$facts_hash")" \
1051
- "$AW_BRIDGE_VERSION" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$probe_field" "$(posture_json)" "$delivery_field" "$nonce_field")"
1093
+ "$AW_BRIDGE_VERSION" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$probe_field" "$duration_s" "$blocking" "$artifact_path_field" "$(posture_json)" "$delivery_field" "$nonce_field")"
1052
1094
  if ! printf '%s\n' "$line" >>"$receipts" 2>/dev/null; then
1053
1095
  echo "warning: could not append the review receipt to $receipts — the review itself succeeded;" >&2
1054
1096
  echo " the review-state gate will read the current tree as un-receipted." >&2
@@ -1074,8 +1116,8 @@ parse_agy_verdict_line() { # $1 = captured-output file
1074
1116
  awk '/^### Verdict[[:space:]]*$/{flag=1; next} flag && NF {print; exit}' "$1" 2>/dev/null
1075
1117
  }
1076
1118
 
1077
- parse_agy_blocking_first_numbered() { # $1 = captured-output file
1078
- awk 'flag && /^### /{exit} /^### Blocking[[:space:]]*$/{flag=1; next} flag && /^[0-9]+[.)]/{print; exit}' "$1" 2>/dev/null
1119
+ parse_agy_blocking_numbered() { # $1 = captured-output file
1120
+ awk 'flag && /^### /{exit} /^### Blocking[[:space:]]*$/{flag=1; next} flag && /^[0-9]+[.)]/{print}' "$1" 2>/dev/null
1079
1121
  }
1080
1122
 
1081
1123
  # The repo file map is a FIXED cost that scales with REPO SIZE, not change size (measured 28,735
@@ -1569,6 +1611,7 @@ PLAN_CONTENT=""
1569
1611
  DIFF_CONTENT=""
1570
1612
  REVIEW_ARTIFACT=""
1571
1613
  REVIEW_FINGERPRINT=""
1614
+ REVIEW_ARTIFACT_PATH=""
1572
1615
  # D8/D8b: an agy `code` receipt SELF-DECLARES how the change set reached the model — `inline` when
1573
1616
  # the whole set rode ONE prompt (delivery proven BY CONSTRUCTION), `fed` when a chunked feed proved
1574
1617
  # it by echo. The kit's gate requires the field present and valid, never a particular value, and a
@@ -1586,6 +1629,7 @@ if [[ -z "$resume_mode" ]]; then
1586
1629
  if [[ ! -f "$target" ]]; then
1587
1630
  echo "error: $mode file '$target' not found." >&2; exit 2
1588
1631
  fi
1632
+ if ! REVIEW_ARTIFACT_PATH="$(normalize_artifact_path "$target")"; then exit 2; fi
1589
1633
  # Read the target NOW (before any cd) — its path is relative to the invocation cwd.
1590
1634
  if [[ "$mode" == "plan" ]]; then PLAN_CONTENT="$(cat -- "$target")"; else DIFF_CONTENT="$(cat -- "$target")"; fi
1591
1635
  # Plan/diff receipt identity: the artifact-file sha256 (informational-only for the tree checker).
@@ -1803,11 +1847,13 @@ else
1803
1847
  REVIEW_DELIVERY="inline"
1804
1848
  fi
1805
1849
 
1850
+ # Plan/diff carry the prohibition themselves: headless agy auto-denies read_file, and a denied
1851
+ # read ends the review without an answer.
1806
1852
  emit_artifact() {
1807
1853
  case "$mode" in
1808
1854
  code) echo "## The change set under review (assembled working-tree diff — repo-complete)"; assemble_code_diff ;;
1809
- plan) echo "## The implementation plan under review"; printf '%s\n' "$PLAN_CONTENT" ;;
1810
- diff) echo "## The diff under review"; printf '%s\n' "$DIFF_CONTENT" ;;
1855
+ plan) echo "The artifact below is complete and inline. Do not read any file, call any tool, or request one."; echo; echo "## The implementation plan under review"; printf '%s\n' "$PLAN_CONTENT" ;;
1856
+ diff) echo "The artifact below is complete and inline. Do not read any file, call any tool, or request one."; echo; echo "## The diff under review"; printf '%s\n' "$DIFF_CONTENT" ;;
1811
1857
  esac
1812
1858
  }
1813
1859
  emit_grounding() { # POSTURE + GUARD + FACTS + DECIDED + FOCUS
@@ -2061,6 +2107,7 @@ review_out_file="$staging/review-output"
2061
2107
  envelope_file="$staging/review-envelope"
2062
2108
  dispatch_flags=("${AGY_TRANSPORT_FLAGS[@]}")
2063
2109
  if (( ${#run_passthrough[@]} > 0 )); then dispatch_flags+=("${run_passthrough[@]}"); fi
2110
+ review_started_at="$(date +%s)"
2064
2111
  set +e
2065
2112
  if (( FED_MODE == 1 )); then
2066
2113
  run_fed_review
@@ -2119,8 +2166,9 @@ if [[ $rc -eq 0 ]]; then
2119
2166
  echo " the review; if it recurs, inspect the captured output for what the model produced." >&2
2120
2167
  exit 4
2121
2168
  fi
2169
+ blocking_items="$(parse_agy_blocking_numbered "$review_out_file")"
2122
2170
  if [[ "$verdict" == "SHIP" || "$verdict" == "SHIP WITH NITS" ]]; then
2123
- first_blocking_item="$(parse_agy_blocking_first_numbered "$review_out_file")"
2171
+ first_blocking_item="${blocking_items%%$'\n'*}"
2124
2172
  if [[ -n "$first_blocking_item" ]]; then
2125
2173
  verdict_line="$(parse_agy_verdict_line "$review_out_file")"
2126
2174
  echo "error: verdict-body contradiction — '### Verdict' reads '$verdict_line' while '### Blocking'" >&2
@@ -2129,11 +2177,14 @@ if [[ $rc -eq 0 ]]; then
2129
2177
  exit 4
2130
2178
  fi
2131
2179
  fi
2180
+ blocking_count=0
2181
+ if [[ -n "$blocking_items" ]]; then blocking_count=$(( $(printf '%s\n' "$blocking_items" | wc -l) )); fi
2182
+ review_duration_s=$(( $(date +%s) - review_started_at ))
2132
2183
  if [[ -n "$resume_mode" ]]; then
2133
2184
  # A continuation never re-embeds the current artifact (agy holds the ORIGINAL round server-side;
2134
2185
  # --facts is rejected above), so it cannot attest the folded tree: fresh:false, artifact /
2135
2186
  # fingerprint / factsHash null, grounded false — informational-only, ignored by the state gate.
2136
- write_review_receipt "" false "" "$verdict" false "" "$REVIEW_PROBE" "" "$review_out_file"
2187
+ write_review_receipt "" false "" "$verdict" false "" "$REVIEW_PROBE" "" "$review_out_file" "" "$review_duration_s" "$blocking_count"
2137
2188
  echo "notice: a continuation receipt is fresh:false (informational-only) — only a fresh grounded run" >&2
2138
2189
  echo " (agy-review code --facts @f) mints a receipt that satisfies the review-state gate." >&2
2139
2190
  else
@@ -2143,7 +2194,8 @@ if [[ $rc -eq 0 ]]; then
2143
2194
  grounded=true
2144
2195
  facts_hash="$(printf '%s' "$FACTS_CONTENT" | sha256_stdin || true)"
2145
2196
  fi
2146
- write_review_receipt "$REVIEW_ARTIFACT" true "$REVIEW_FINGERPRINT" "$verdict" "$grounded" "$facts_hash" "$REVIEW_PROBE" "$REVIEW_DELIVERY" "$review_out_file"
2197
+ write_review_receipt "$REVIEW_ARTIFACT" true "$REVIEW_FINGERPRINT" "$verdict" "$grounded" "$facts_hash" "$REVIEW_PROBE" "$REVIEW_DELIVERY" "$review_out_file" "$REVIEW_ARTIFACT_PATH" "$review_duration_s" "$blocking_count"
2147
2198
  fi
2199
+ echo "review duration: ${review_duration_s}s" >&2
2148
2200
  fi
2149
2201
  exit $rc
@@ -2,7 +2,7 @@ import { describe, it, after } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import {
4
4
  mkdtempSync, mkdirSync, writeFileSync, chmodSync, rmSync, readFileSync,
5
- existsSync, readdirSync, symlinkSync, cpSync,
5
+ existsSync, readdirSync, symlinkSync, cpSync, realpathSync,
6
6
  } from 'node:fs';
7
7
  import { tmpdir } from 'node:os';
8
8
  import { join, dirname, resolve } from 'node:path';
@@ -10,6 +10,7 @@ import { fileURLToPath } from 'node:url';
10
10
  import { spawnSync, execFile } from 'node:child_process';
11
11
 
12
12
  const HERE = dirname(fileURLToPath(import.meta.url));
13
+ const NO_FILE_READ = 'The artifact below is complete and inline. Do not read any file, call any tool, or request one.';
13
14
  import {
14
15
  WRAPPER, farmFor, makeSandbox, run, runAsync, readReceipts, RECEIPTS_REL,
15
16
  ARTIFACT_HEADER, SHAPE_HEADER, FED_CAP, FED_WIDE_CAP, MAX_COUNTABLE_PROOF_ADDRESS,
@@ -411,6 +412,25 @@ describe('agy-review.sh — guard + grounding (2, 3)', { concurrency: 2 }, () =>
411
412
  assert.match(r.prompt, /Do NOT comment on AI model names\/versions or your own knowledge cutoff/);
412
413
  });
413
414
 
415
+ it('plan and diff prepend the no-file-read instruction before the artifact; code mode stays unchanged', async () => {
416
+ // spec:plan-review-loop/S23
417
+ for (const [mode, file, header] of [['plan', 'p.md', '## The implementation plan under review'], ['diff', 'd.patch', '## The diff under review']]) {
418
+ const sb = makeSandbox();
419
+ writeFileSync(join(sb.repo, file), `${mode} body\n`);
420
+ const r = await run(sb, { args: [mode, file, '--facts', 'f'] });
421
+ rmSync(sb.home, { recursive: true, force: true });
422
+ assert.equal(r.status, 0, r.stderr);
423
+ assert.ok(r.prompt.indexOf(NO_FILE_READ) >= 0, `${mode}: instruction present`);
424
+ assert.ok(r.prompt.indexOf(NO_FILE_READ) < r.prompt.indexOf(header), `${mode}: instruction precedes artifact`);
425
+ }
426
+
427
+ const code = makeSandbox();
428
+ const r = await run(code, { args: ['code', '--facts', 'f'] });
429
+ rmSync(code.home, { recursive: true, force: true });
430
+ assert.equal(r.status, 0, r.stderr);
431
+ assert.doesNotMatch(r.prompt, new RegExp(NO_FILE_READ.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), 'code prompt is unchanged');
432
+ });
433
+
414
434
  it('--facts / --decided / --focus all reach the prompt', async () => {
415
435
  const sb = makeSandbox();
416
436
  const r = await run(sb, { args: [
@@ -2041,7 +2061,7 @@ describe('agy-review.sh — declared contract is really accepted (forward guard)
2041
2061
  // The normative fixture: the AD-038 shape + the D3 self-declaring probe marker (backend/verdict here
2042
2062
  // carry this bridge's vocabulary; dynamic values are asserted by shape):
2043
2063
  const RECEIPT_FIXTURE = JSON.parse(
2044
- '{"schema":1,"artifact":"code","fresh":true,"fingerprint":"<sha256hex>","backend":"codex","verdict":"revise","grounded":true,"factsHash":null,"wrapperVersion":"2.3.0","timestamp":"2026-07-03T12:00:00Z","probe":false,"posture":{"model":"<display>"},"delivery":"inline"}',
2064
+ '{"schema":1,"artifact":"code","fresh":true,"fingerprint":"<sha256hex>","backend":"codex","verdict":"revise","grounded":true,"factsHash":null,"wrapperVersion":"2.3.0","timestamp":"2026-07-03T12:00:00Z","probe":false,"durationS":0,"blocking":0,"posture":{"model":"<display>"},"delivery":"inline"}',
2045
2065
  );
2046
2066
  const sha256HexOf = async (buf) => {
2047
2067
  const { createHash } = await import('node:crypto');
@@ -2051,6 +2071,7 @@ const VERDICT_OUTPUT = '### Verdict\nSHIP WITH NITS — solid, two nits.\n### Bl
2051
2071
 
2052
2072
  describe('agy-review.sh — review receipts (AD-038)', { concurrency: 2 }, () => {
2053
2073
  it('a fresh grounded code review appends ONE fixture-shaped receipt (verdict verbatim, factsHash real)', async () => {
2074
+ // spec:plan-review-loop/S22
2054
2075
  const sb = makeSandbox();
2055
2076
  const r = await run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
2056
2077
  const receipts = readReceipts(sb.repo);
@@ -2069,6 +2090,11 @@ describe('agy-review.sh — review receipts (AD-038)', { concurrency: 2 }, () =>
2069
2090
  assert.equal(receipt.factsHash, await sha256HexOf('a tiny fact'), 'sha256 of the facts payload — an empty/changed facts file is visible');
2070
2091
  assert.equal(receipt.wrapperVersion, MANIFEST.version, 'receipt version ⟷ capability.json version');
2071
2092
  assert.match(receipt.timestamp, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
2093
+ assert.ok(Number.isInteger(receipt.durationS) && receipt.durationS >= 0);
2094
+ assert.equal(receipt.blocking, 0);
2095
+ assert.equal(Object.hasOwn(receipt, 'artifactPath'), false, 'a code receipt carries no artifact path');
2096
+ assert.match(r.stderr, /review duration: \d+s/);
2097
+ for (const field of ['durationS', 'blocking', 'artifactPath']) assert.ok(REVIEW_CONTRACT.receipt.includes(`${field} = `), `the manifest receipt contract declares ${field}`);
2072
2098
  });
2073
2099
 
2074
2100
  // The probe marker (BRIDGE-MODES-CATALOG, D3) — the twin of the sibling bridge's arm: an
@@ -2141,6 +2167,34 @@ describe('agy-review.sh — review receipts (AD-038)', { concurrency: 2 }, () =>
2141
2167
  }
2142
2168
  });
2143
2169
 
2170
+ it('a REWORK receipt counts every numbered item in the first Blocking section', async () => {
2171
+ const sb = makeSandbox();
2172
+ const output = '### Verdict\nREWORK\n### Blocking\n1. first\n2) second\n### Non-blocking\nnone';
2173
+ const r = await run(sb, { args: ['code', '--facts', 'f'], env: { AGY_FAKE_OUTPUT: output } });
2174
+ const receipts = readReceipts(sb.repo);
2175
+ rmSync(sb.home, { recursive: true, force: true });
2176
+ assert.equal(r.status, 0, r.stderr);
2177
+ assert.equal(receipts[0].blocking, 2);
2178
+ });
2179
+
2180
+ it('a large Blocking section is counted whole and the contradiction arm still exits 4 (no SIGPIPE)', async () => {
2181
+ const items = Array.from({ length: 5000 }, (_, i) => `${i + 1}. finding ${i + 1}`).join('\n');
2182
+ const rework = makeSandbox();
2183
+ const r = await run(rework, { args: ['code', '--facts', 'f'], env: { AGY_FAKE_OUTPUT: `### Verdict\nREWORK\n### Blocking\n${items}\n### Non-blocking\nnone` } });
2184
+ const receipts = readReceipts(rework.repo);
2185
+ rmSync(rework.home, { recursive: true, force: true });
2186
+ assert.equal(r.status, 0, r.stderr);
2187
+ assert.equal(receipts[0].blocking, 5000);
2188
+
2189
+ const ship = makeSandbox();
2190
+ const s = await run(ship, { args: ['code', '--facts', 'f'], env: { AGY_FAKE_OUTPUT: `### Verdict\nSHIP\n### Blocking\n${items}\n### Non-blocking\nnone` } });
2191
+ const shipReceipts = readReceipts(ship.repo);
2192
+ rmSync(ship.home, { recursive: true, force: true });
2193
+ assert.equal(s.status, 4, s.stderr);
2194
+ assert.match(s.stderr, /verdict-body contradiction/);
2195
+ assert.equal(shipReceipts.length, 0);
2196
+ });
2197
+
2144
2198
  // The wrapper-minted finding manifest (flow-orchestration Phase 4.2, Decision 2/P5/P24-25):
2145
2199
  // nonce-supplied dispatches mint {schema, backend, nonce, fingerprint, findings} beside the
2146
2200
  // receipt, atomic + no-clobber + ORDERED — a failed mint EXCLUDES the receipt append.
@@ -2363,6 +2417,21 @@ describe('agy-review.sh — review receipts (AD-038)', { concurrency: 2 }, () =>
2363
2417
  assert.equal(r.status, 0, r.stderr);
2364
2418
  assert.equal(receipts[0].artifact, 'plan');
2365
2419
  assert.equal(receipts[0].fingerprint, await sha256HexOf('# plan body\n'), 'plan fingerprint = file sha256');
2420
+ assert.equal(receipts[0].artifactPath, 'p.md', 'realpath inside the work tree normalizes repo-relative');
2421
+ });
2422
+
2423
+ it('plan and diff refuse an uncarriable artifact-path byte before agy runs', async () => {
2424
+ for (const mode of ['plan', 'diff']) {
2425
+ for (const [name, diagnostic] of [['bad"name.md', /double quote/], ['bad\\name.md', /backslash/], ['bad\nname.md', /control byte/]]) {
2426
+ const sb = makeSandbox();
2427
+ writeFileSync(join(sb.repo, name), '# artifact\n');
2428
+ const r = await run(sb, { args: [mode, name, '--facts', 'f'] });
2429
+ rmSync(sb.home, { recursive: true, force: true });
2430
+ assert.equal(r.status, 2, `${mode}: ${name}`);
2431
+ assert.match(r.stderr, diagnostic, `${mode}: ${name}`);
2432
+ assert.equal(r.invoked, false, `${mode}: ${name}: refusal is pre-spend`);
2433
+ }
2434
+ }
2366
2435
  });
2367
2436
 
2368
2437
  it('plan/diff outside a git work tree: warn + skip the receipt (exit 0) unless AW_REVIEW_RECEIPTS is set', async () => {
@@ -2382,10 +2451,12 @@ describe('agy-review.sh — review receipts (AD-038)', { concurrency: 2 }, () =>
2382
2451
  env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT, AW_REVIEW_RECEIPTS: override },
2383
2452
  });
2384
2453
  const body = existsSync(override) ? readFileSync(override, 'utf8') : '';
2454
+ const expectedPath = realpathSync(join(outside, 'p.md'));
2385
2455
  rmSync(sb.home, { recursive: true, force: true });
2386
2456
  assert.equal(written.status, 0, written.stderr);
2387
2457
  assert.match(body, /"backend":"agy"/, 'the override path receives the receipt outside a git tree');
2388
2458
  assert.match(body, /"artifact":"plan"/);
2459
+ assert.equal(JSON.parse(body).artifactPath, expectedPath, 'outside a work tree the normalized path is absolute');
2389
2460
  });
2390
2461
 
2391
2462
  it('a receipt write failure warns loudly but never fails the review (fail-safe direction)', async () => {
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "antigravity-cli-bridge",
5
5
  "kind": "execution-backend",
6
- "version": "5.4.0",
6
+ "version": "5.5.0",
7
7
  "provides": ["review", "probe"],
8
8
  "posture": { "model": "Gemini 3.7 Flash (High)" },
9
9
  "roles": {
@@ -31,7 +31,7 @@
31
31
  "agy-review --continue [--decided @f] [--focus \"…\"] [--nonce <n>]",
32
32
  "agy-review --conversation <id> [--decided @f] [--focus \"…\"] [--nonce <n>]"
33
33
  ],
34
- "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 (code mode refuses pre-spend without one — no run, no receipt — unless --ungrounded/AGY_PROBE=1; in plan/diff 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); probe = whether the run relaxed the quality guards (AGY_PROBE=1), written on EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); posture = the ACTUAL run posture {model} (agy has no tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, an ATTESTING review with AGY_MODEL explicitly emptied refuses pre-spend, and a model string carrying control bytes refuses pre-spend in every mode; delivery = how the change set REACHED the model, currently emitted as 'inline' (the whole set rode one prompt — proven by construction) or 'fed' (a chunked feed whose per-part echo proof verified); REQUIRED on every agy code receipt and its ABSENCE is what stops a pre-fed-lane receipt attesting, while the gate accepts any well-formed declaration rather than a particular value; absent by construction on plan/diff/continuation receipts, which carry no change set; a run whose output carries NO recognized '### Verdict' section — empty output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); when the dispatch nonce seam is supplied — the AW_REVIEW_NONCE environment value or its plain-argument equivalent --nonce <n> (one seam: the flag assigns the same value; supplying both with different values refuses pre-spend) — under the safe grammar [A-Za-z0-9._-]{1,64} (anything else refuses pre-spend), the wrapper first mints the finding MANIFEST {schema, backend, nonce, fingerprint, findings} beside the receipts file (agent-workflow-finding-manifest-<backend>-<nonce>.json; atomic, no-clobber — a byte-identical rewrite is an idempotent no-op, different bytes refuse loudly) ORDERED before the receipt append — a failed manifest write EXCLUDES the receipt append, so a nonce-supplied dispatch can never land a receipt without its readable manifest; a nonce-less invocation adds NO nonce field and mints NO finding manifest (the existing wrapperVersion field still changes with each bridge release); a write failure warns, never fails the review",
34
+ "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; durationS = integer wall-clock seconds from CLI start through verdict parsing and the wrapper prints review duration: <n>s; blocking = the count of numbered items in the first ### Blocking section; artifactPath = the normalized realpath on plan/diff receipts only (repo-relative inside the work tree, absolute otherwise), while a double quote, backslash or control byte refuses pre-spend because the receipt encoder cannot carry it; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (code mode refuses pre-spend without one — no run, no receipt — unless --ungrounded/AGY_PROBE=1; in plan/diff 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); probe = whether the run relaxed the quality guards (AGY_PROBE=1), written on EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); posture = the ACTUAL run posture {model} (agy has no tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, an ATTESTING review with AGY_MODEL explicitly emptied refuses pre-spend, and a model string carrying control bytes refuses pre-spend in every mode; delivery = how the change set REACHED the model, currently emitted as 'inline' (the whole set rode one prompt — proven by construction) or 'fed' (a chunked feed whose per-part echo proof verified); REQUIRED on every agy code receipt and its ABSENCE is what stops a pre-fed-lane receipt attesting, while the gate accepts any well-formed declaration rather than a particular value; absent by construction on plan/diff/continuation receipts, which carry no change set; a run whose output carries NO recognized '### Verdict' section — empty output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); when the dispatch nonce seam is supplied — the AW_REVIEW_NONCE environment value or its plain-argument equivalent --nonce <n> (one seam: the flag assigns the same value; supplying both with different values refuses pre-spend) — under the safe grammar [A-Za-z0-9._-]{1,64} (anything else refuses pre-spend), the wrapper first mints the finding MANIFEST {schema, backend, nonce, fingerprint, findings} beside the receipts file (agent-workflow-finding-manifest-<backend>-<nonce>.json; atomic, no-clobber — a byte-identical rewrite is an idempotent no-op, different bytes refuse loudly) ORDERED before the receipt append — a failed manifest write EXCLUDES the receipt append, so a nonce-supplied dispatch can never land a receipt without its readable manifest; a nonce-less invocation adds NO nonce field and mints NO finding manifest (the existing wrapperVersion field still changes with each bridge release); a write failure warns, never fails the review",
35
35
  "notes": [
36
36
  "transport: every review dispatch drives the CLI in --output-format json (plus --disable-slash-commands) and the returned envelope is parsed in node (bin/agy-envelope.mjs) — the operator-facing invocations and flags above do NOT change, and on a ZERO exit the wrapper still PRINTS the review text, never JSON. A missing or unreadable envelope on a zero exit is a loud failure with NO receipt, never a downgraded verdict and never a fallback to raw-stdout parsing; a non-zero CLI exit keeps its own code and message, and publishes the captured stdout unchanged from the SINGLE dispatch or the FINAL fed turn (which may therefore be a JSON or partial payload — the envelope is parsed only on a zero exit); an INTERMEDIATE feed turn is the exception, its output stays private (Invariant E) and its failure prints only a named error. Enforced by a PRE-SPEND capability probe, not a version floor: agy --help must advertise --output-format and --disable-slash-commands, node must be >= 22, and bin/agy-envelope.mjs must be present — otherwise the review refuses before any run is spent and names the missing capability",
37
37
  "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",
@@ -31,6 +31,9 @@ GUARD Do NOT comment on AI model names/versions or your own knowledge cutoff
31
31
  ## Focus
32
32
  {{FOCUS}} # from --focus "…" + any trailing focus words, merged in parse order (optional)
33
33
 
34
+ PLAN/DIFF ONLY The artifact below is complete and inline. Do not read any file, call any tool,
35
+ or request one.
36
+
34
37
  ## The change set / plan / diff under review
35
38
  {{ARTIFACT}} # code: the assembled, repo-complete working-tree change set (when oversized it
36
39
  # is not inlined at all — see the chunked feed below)
@@ -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.5.0'
5
+ version: '3.6.0'
6
6
  ---
7
7
 
8
8
  # codex-cli-bridge
@@ -124,6 +124,13 @@ is **banner-only** (exactly the duration handed to `timeout(1)`; on exec `uncapp
124
124
  capping binary, while `codex-review` **fails CLOSED pre-spend** there) — informational, never a
125
125
  receipt field. **Quote the posture banner verbatim** when labeling a dispatch.
126
126
 
127
+ Every successful review receipt carries integer `durationS` and `blocking`; the wrapper prints
128
+ `review duration: <n>s`. A plan receipt also carries `artifactPath`, normalized to a repo-relative
129
+ realpath inside the work tree and an absolute realpath otherwise. A double quote, backslash or
130
+ control byte in that path refuses pre-spend because the receipt encoder cannot carry it. Codex
131
+ counts `[blocker]` and `[major]` lines, or those two finding severities in schema mode — a schema
132
+ payload whose findings cannot be counted fails the run (exit 4, no receipt).
133
+
127
134
  `codex exec` is headless: there is **no TTY**, so `approval_policy=never` — anything needing
128
135
  escalation is refused and reported, never interactively approved. The wrappers capture only codex's
129
136
  **final message** (`-o`), so output is clean; the JSON event stream + reasoning go to a run trace
@@ -372,7 +372,7 @@ CHATGPT_LOGIN_GUARD="Logged in using ChatGPT"
372
372
  # receipt this wrapper mints; scripts/release/version-sync.mjs bumps it under the one-anchor-per-file
373
373
  # rule, so a release can never leave it behind (the AD-053 drift class).
374
374
  AW_RECEIPT_BACKEND="codex"
375
- AW_BRIDGE_VERSION="3.5.0" # aw-version-anchor
375
+ AW_BRIDGE_VERSION="3.6.0" # aw-version-anchor
376
376
  # The kill grace handed to timeout(1) as --kill-after, and recorded in the receipt as killGraceS:
377
377
  # ONE constant, so the number the ledger checks against the dispatch deadline is the number the run
378
378
  # actually applied.
@@ -114,7 +114,7 @@ describe('codex-review — schema-mode verdict is parsed STRUCTURALLY (M2)', ()
114
114
 
115
115
  it('a DECOY "verdict" inside a findings STRING never substitutes the top-level field', () => {
116
116
  const sb = makeSandbox();
117
- const decoy = '{"verdict":"revise","findings":["the text \\"verdict\\": \\"ship\\" appeared in a doc line"]}';
117
+ const decoy = '{"verdict":"revise","findings":[{"severity":"nit","location":"doc","issue":"the text \\"verdict\\": \\"ship\\" appeared in a doc line","suggested_change":"none"}]}';
118
118
  const r = run(sb, { env: { CODEX_REVIEW_SCHEMA: '1', CODEX_FAKE_FINAL: decoy } });
119
119
  const receipts = readReceipts(sb.repo);
120
120
  rmSync(sb.root, { recursive: true, force: true });