@sabaiway/agent-workflow-kit 3.14.0 → 4.0.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 (64) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +3 -3
  3. package/SKILL.md +1 -1
  4. package/bridges/antigravity-cli-bridge/SKILL.md +12 -8
  5. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +735 -55
  6. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +899 -51
  7. package/bridges/antigravity-cli-bridge/bin/agy.sh +4 -3
  8. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +23 -0
  9. package/bridges/antigravity-cli-bridge/capability.json +14 -4
  10. package/bridges/antigravity-cli-bridge/references/driving-agy.md +12 -4
  11. package/bridges/antigravity-cli-bridge/references/models-and-flags.md +4 -3
  12. package/bridges/antigravity-cli-bridge/references/review-prompt.md +65 -2
  13. package/bridges/codex-cli-bridge/SKILL.md +1 -1
  14. package/bridges/codex-cli-bridge/bin/codex-exec.sh +2 -1
  15. package/bridges/codex-cli-bridge/bin/codex-review.sh +63 -13
  16. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +38 -0
  17. package/bridges/codex-cli-bridge/capability.json +1 -1
  18. package/capability.json +1 -1
  19. package/package.json +1 -1
  20. package/references/agents/review-lens.md +39 -0
  21. package/references/hooks/gate-approve.mjs +15 -0
  22. package/references/modes/agents.md +11 -2
  23. package/references/modes/autonomy-doctor.md +2 -0
  24. package/references/modes/backends.md +2 -0
  25. package/references/modes/bootstrap.md +2 -0
  26. package/references/modes/bridge-settings.md +4 -1
  27. package/references/modes/commit-guard.md +2 -0
  28. package/references/modes/core-evidence.md +2 -0
  29. package/references/modes/coverage-check.md +2 -0
  30. package/references/modes/doc-parity.md +2 -0
  31. package/references/modes/gates.md +2 -0
  32. package/references/modes/grounding.md +2 -0
  33. package/references/modes/help.md +2 -0
  34. package/references/modes/hook.md +4 -1
  35. package/references/modes/migrate-adr-store.md +2 -0
  36. package/references/modes/procedures.md +2 -0
  37. package/references/modes/recipes.md +2 -0
  38. package/references/modes/recommendations.md +4 -3
  39. package/references/modes/review-state.md +2 -0
  40. package/references/modes/sandbox-masks.md +2 -0
  41. package/references/modes/set-autonomy.md +2 -0
  42. package/references/modes/set-recipe.md +3 -0
  43. package/references/modes/setup.md +2 -0
  44. package/references/modes/state-block-guard.md +9 -5
  45. package/references/modes/status.md +4 -1
  46. package/references/modes/uninstall.md +2 -0
  47. package/references/modes/upgrade.md +2 -0
  48. package/references/modes/velocity.md +7 -0
  49. package/references/modes/worktrees.md +2 -0
  50. package/tools/bridge-settings-read.mjs +40 -10
  51. package/tools/bridge-settings.mjs +22 -7
  52. package/tools/cheap-agents.mjs +15 -5
  53. package/tools/commands.mjs +2 -2
  54. package/tools/core-evidence.mjs +29 -2
  55. package/tools/detect-backends.mjs +1 -1
  56. package/tools/manifest/schema.md +7 -0
  57. package/tools/manifest/validate.mjs +8 -0
  58. package/tools/presentation.mjs +1 -1
  59. package/tools/procedures.mjs +9 -2
  60. package/tools/recipes.mjs +4 -1
  61. package/tools/recommendations.mjs +157 -59
  62. package/tools/renderers.mjs +10 -1
  63. package/tools/review-state.mjs +4 -0
  64. package/tools/view-model.mjs +3 -1
@@ -41,8 +41,9 @@
41
41
  # AGY_TIMEOUT default = AGY_HARD_TIMEOUT (agy's soft --print-timeout)
42
42
  # AGY_MAX_PROMPT_BYTES default 120000 (single-argv byte ceiling; see agy.sh)
43
43
  # AGY_PROBE=1 throwaway probe — silences the off-frontier model advisory
44
- # AGY_REVIEW_ALLOW_ADDDIR=1 oversized CODE review: offload the change set to a private
45
- # staging dir and pass it via --add-dir (re-enables Issue-001 stall risk)
44
+ # AGY_REVIEW_MAX_TOTAL_BYTES default 240000 (the chunked feed's total outgoing prompt-byte ceiling)
45
+ # AGY_REVIEW_ALLOW_ADDDIR RETIRED recognized so an existing settings line never warns, but it
46
+ # arms nothing; an oversized CODE review is delivered as a chunked feed
46
47
  # AW_REVIEW_RECEIPTS override the review-receipt file (default: <git dir>/
47
48
  # agent-workflow-review-receipts.jsonl — see the --help Receipt block)
48
49
  set -euo pipefail
@@ -108,14 +109,20 @@ Receipt:
108
109
  gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the
109
110
  review), one stderr banner line states the same posture, an ATTESTING review with AGY_MODEL
110
111
  explicitly emptied refuses pre-spend, and a model string carrying control bytes refuses
111
- pre-spend in every mode; a run whose output carries NO recognized '### Verdict' section empty
112
+ pre-spend in every mode; delivery = how the change set REACHED the model, currently emitted as
113
+ 'inline' (the whole set rode one prompt — proven by construction) or 'fed' (a chunked feed whose
114
+ per-part echo proof verified); REQUIRED on every agy code receipt and its ABSENCE is what stops a
115
+ pre-fed-lane receipt attesting, while the gate accepts any well-formed declaration rather than a
116
+ particular value; absent by construction on plan/diff/continuation receipts, which carry no change
117
+ set; a run whose output carries NO recognized '### Verdict' section — empty
112
118
  output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session
113
119
  error); a write failure warns, never fails the review
114
120
 
115
121
  Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins over built-in default):
116
122
  ${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf
117
123
  AGY_HARD_TIMEOUT — hard wall-clock cap, duration string like 5m/30m/90s (built-in default 30m)
118
- AGY_REVIEW_ALLOW_ADDDIR — boolean 0/1: 1 arms the oversized --add-dir escape (re-enables the Issue-001 stall risk; default 0)
124
+ AGY_REVIEW_ALLOW_ADDDIR — RETIRED: still recognized (an existing line never warns as unknown) but it arms NOTHING; a set value prints the retirement notice. The oversized code review is a chunked feed now
125
+ AGY_REVIEW_MAX_TOTAL_BYTES — integer bytes 1..100000000 (default 240000): the ceiling on the SUM of all outgoing prompt bytes an over-cap CODE review's chunked feed may send; past it the fed review refuses before spending turn 1
119
126
 
120
127
  Honesty + posture (D4/D5):
121
128
  a run whose output carries NO recognized '### Verdict' section — empty output included — exits 4
@@ -127,7 +134,7 @@ Honesty + posture (D4/D5):
127
134
  AGY_PROBE=1 is exempt), and a model string carrying control bytes refuses pre-spend in every
128
135
  mode.
129
136
 
130
- Closed grammar: unknown flags are rejected; no '--' passthrough (the flag escape is --ungrounded; the env escapes are AGY_PROBE=1 and AGY_REVIEW_ALLOW_ADDDIR=1).
137
+ Closed grammar: unknown flags are rejected; no '--' passthrough (the flag escape is --ungrounded; the env escape is AGY_PROBE=1).
131
138
  Requires at run time: the agy CLI on PATH + a Google AI subscription login (--help needs neither).
132
139
  HELP
133
140
  exit 0
@@ -135,7 +142,7 @@ HELP
135
142
  esac
136
143
 
137
144
  # This wrapper's applied settings-file subset (see the shared reader block below).
138
- AW_SETTINGS_APPLIED="AGY_HARD_TIMEOUT AGY_REVIEW_ALLOW_ADDDIR"
145
+ AW_SETTINGS_APPLIED="AGY_HARD_TIMEOUT AGY_REVIEW_ALLOW_ADDDIR AGY_REVIEW_MAX_TOTAL_BYTES"
139
146
 
140
147
  # --- Bridge settings file (host-level, kit-independent) — byte-identical across the four wrappers ---
141
148
  # ${XDG_CONFIG_HOME:-$HOME/.config}/agent-workflow/bridge-settings.conf holds KEY=VALUE lines,
@@ -156,7 +163,7 @@ aw_settings_file() {
156
163
  printf '%s/agent-workflow/bridge-settings.conf' "${XDG_CONFIG_HOME:-$HOME/.config}"
157
164
  }
158
165
  aw_settings_known() {
159
- case " CODEX_SERVICE_TIER CODEX_HARD_TIMEOUT CODEX_REVIEW_MAX_TOTAL_BYTES AGY_HARD_TIMEOUT AGY_REVIEW_ALLOW_ADDDIR " in
166
+ case " CODEX_SERVICE_TIER CODEX_HARD_TIMEOUT CODEX_REVIEW_MAX_TOTAL_BYTES AGY_HARD_TIMEOUT AGY_REVIEW_ALLOW_ADDDIR AGY_REVIEW_MAX_TOTAL_BYTES " in
160
167
  *" $1 "*) return 0 ;;
161
168
  *) return 1 ;;
162
169
  esac
@@ -179,6 +186,7 @@ aw_settings_valid() {
179
186
  CODEX_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] && aw_int_in_range "$v" 1 100000000 ;;
180
187
  AGY_HARD_TIMEOUT) [[ "$v" =~ $dur_re && ! "$v" =~ $zero_re ]] ;;
181
188
  AGY_REVIEW_ALLOW_ADDDIR) [[ "$v" == "0" || "$v" == "1" ]] ;;
189
+ AGY_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] && aw_int_in_range "$v" 1 100000000 ;;
182
190
  *) return 1 ;;
183
191
  esac
184
192
  }
@@ -240,6 +248,11 @@ aw_apply_settings() {
240
248
  done
241
249
  return 0
242
250
  }
251
+ # Snapshot BEFORE the shared reader runs: it EXPORTS a settings-file value under the same name, so a
252
+ # check made afterwards cannot tell a file value from an env override — and the retirement notice's
253
+ # recovery differs by source (a file `--unset` cannot clear an env override).
254
+ AGY_ALLOW_ADDDIR_FROM_ENV=0
255
+ if [[ -n "${AGY_REVIEW_ALLOW_ADDDIR+x}" ]]; then AGY_ALLOW_ADDDIR_FROM_ENV=1; fi
243
256
  aw_apply_settings
244
257
 
245
258
  # --- Effective-timeout resolver (D5 banner honesty; AD-061) --------------------
@@ -296,7 +309,7 @@ DEFAULT_AGY_REVIEW_MODEL="Gemini 3.1 Pro (High)"
296
309
  # Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
297
310
  # version (drift-guarded by agy-review.test.mjs against capability.json).
298
311
  AW_RECEIPT_BACKEND="agy"
299
- AW_BRIDGE_VERSION="4.1.0"
312
+ AW_BRIDGE_VERSION="5.0.0"
300
313
  # `-` not `:-` so an EXPLICIT empty AGY_MODEL= survives (drop --model, use settings.json — agy.sh:52).
301
314
  AGY_MODEL="${AGY_MODEL-$DEFAULT_AGY_REVIEW_MODEL}"
302
315
  # D5 control-byte screen — IMMEDIATELY after resolution, BEFORE the off-frontier advisory (or any
@@ -330,7 +343,33 @@ AGY_PROBE="${AGY_PROBE:-0}"
330
343
  # review-state gate rejects it — a guards-relaxed review must never attest a tree.
331
344
  REVIEW_PROBE=false
332
345
  if [[ "$AGY_PROBE" == "1" ]]; then REVIEW_PROBE=true; fi
346
+ # RETIRED (D3), not removed: the key stays RECOGNIZED by the shared settings registry so an existing
347
+ # settings line never starts warning as unknown — but it arms NOTHING. The `--add-dir` offload told
348
+ # agy to read a staging file; headless agy AUTO-DENIES its own read_file tool, so that lane could
349
+ # return a confident fabrication or an empty SHIP with no way to tell. Setting it says so, loudly.
350
+ # The notice keys on the key being CONFIGURED AT ALL, not on its value: a stale `=0` line is just as
351
+ # dead as a `=1` one, and staying silent about it would leave the operator believing a knob they can
352
+ # see still means something. The recovery is source-specific — a file `--unset` cannot clear an env
353
+ # override, so telling an env user to run it would hand them a command that changes nothing.
354
+ AGY_REVIEW_ALLOW_ADDDIR_SOURCE=""
355
+ if (( AGY_ALLOW_ADDDIR_FROM_ENV == 1 )); then AGY_REVIEW_ALLOW_ADDDIR_SOURCE="env"; fi
333
356
  AGY_REVIEW_ALLOW_ADDDIR="${AGY_REVIEW_ALLOW_ADDDIR:-0}"
357
+ if [[ -z "$AGY_REVIEW_ALLOW_ADDDIR_SOURCE" ]] && [[ -f "$(aw_settings_file)" ]] && grep -q "^AGY_REVIEW_ALLOW_ADDDIR=" "$(aw_settings_file)"; then
358
+ AGY_REVIEW_ALLOW_ADDDIR_SOURCE="file"
359
+ fi
360
+ if [[ -n "$AGY_REVIEW_ALLOW_ADDDIR_SOURCE" ]]; then
361
+ echo "notice: AGY_REVIEW_ALLOW_ADDDIR is set (${AGY_REVIEW_ALLOW_ADDDIR_SOURCE}) but it is RETIRED and arms nothing. An" >&2
362
+ echo " oversized 'agy-review code' is now DELIVERED as a chunked feed with a per-part delivery" >&2
363
+ echo " proof, so the change set never has to be read from disk — which headless agy cannot do" >&2
364
+ echo " (read_file is auto-denied)." >&2
365
+ if [[ "$AGY_REVIEW_ALLOW_ADDDIR_SOURCE" == "env" ]]; then
366
+ echo " Clear it where you set it: unset AGY_REVIEW_ALLOW_ADDDIR in the environment (a settings-file" >&2
367
+ echo " --unset cannot clear an env override)." >&2
368
+ else
369
+ echo " Clear the line: node <agent-workflow-kit>/tools/bridge-settings.mjs --unset" >&2
370
+ echo " AGY_REVIEW_ALLOW_ADDDIR --apply" >&2
371
+ fi
372
+ fi
334
373
  AGY_MAX_PROMPT_BYTES="${AGY_MAX_PROMPT_BYTES:-120000}"
335
374
  if [[ ! "$AGY_MAX_PROMPT_BYTES" =~ ^[0-9]+$ ]]; then
336
375
  echo "error: AGY_MAX_PROMPT_BYTES='$AGY_MAX_PROMPT_BYTES' is not a non-negative integer." >&2
@@ -344,6 +383,51 @@ if (( AGY_MAX_PROMPT_BYTES > AGY_ARGV_HARD_MAX )); then
344
383
  echo " The override may LOWER the ceiling (stricter), never raise it past the OS limit." >&2
345
384
  exit 2
346
385
  fi
386
+ # The repo file map's share of the prompt (see emit_repo_file_map). A PINNED constant, not a knob:
387
+ # a plain assignment, so an inherited environment value can never widen or narrow it. 8192 = 6.8%
388
+ # of agy's 120000-byte default ceiling — the map is context, the change set is the review.
389
+ AW_REVIEW_MAP_BUDGET_BYTES=8192
390
+
391
+ # --- The chunked-feed ceiling (D1b) -------------------------------------------
392
+ # "The whole conversation" is not knowable before turn 1 (model response sizes are unknown), so the
393
+ # bound is the SUM OF ALL OUTGOING PROMPT BYTES — every envelope, every body, and the final turn's
394
+ # prompt — which IS computable up front. Set deliberately BELOW the 262,967 bytes a live probe
395
+ # proved retained, because that probe measured part BODIES only and the shipped lane adds envelopes
396
+ # and a final prompt on top. This is an ECONOMY guard, not the correctness guard: correctness is the
397
+ # per-part body echo, which fails closed under this ceiling too.
398
+ AGY_REVIEW_MAX_TOTAL_BYTES="${AGY_REVIEW_MAX_TOTAL_BYTES:-240000}"
399
+ # CANONICALIZE BEFORE ANY ARITHMETIC. A hand-rolled `(( value < 1 ))` on `008000` makes bash read an
400
+ # invalid OCTAL constant: the arithmetic errors, both range tests yield false, and the ceiling simply
401
+ # stops existing — the guard silently absent, which is worse than no guard. Leading zeros are
402
+ # stripped exactly as the shared settings reader does, then the value is judged by the wrapper's OWN
403
+ # registry rule (aw_int_in_range is the overflow-safe comparator the manifest bounds are pinned to),
404
+ # never by a second hand-written bound that could drift from the manifest.
405
+ if [[ "$AGY_REVIEW_MAX_TOTAL_BYTES" =~ ^[0-9]+$ ]]; then
406
+ AGY_REVIEW_MAX_TOTAL_BYTES="${AGY_REVIEW_MAX_TOTAL_BYTES#"${AGY_REVIEW_MAX_TOTAL_BYTES%%[!0]*}"}"
407
+ AGY_REVIEW_MAX_TOTAL_BYTES="${AGY_REVIEW_MAX_TOTAL_BYTES:-0}"
408
+ fi
409
+ if ! aw_settings_valid AGY_REVIEW_MAX_TOTAL_BYTES "$AGY_REVIEW_MAX_TOTAL_BYTES"; then
410
+ # Only an EXPLICIT env override can reach here: the shared settings reader already drops an invalid
411
+ # FILE value before the wrapper body runs, leaving the built-in default, which is valid. So this is
412
+ # unconditionally a refusal — a warn-and-default arm would be dead code pretending to be a policy.
413
+ echo "error: AGY_REVIEW_MAX_TOTAL_BYTES='$AGY_REVIEW_MAX_TOTAL_BYTES' is not a valid byte ceiling (integer 1..100000000)." >&2
414
+ echo " Refusing rather than running with a spending ceiling you set and this wrapper ignored." >&2
415
+ exit 2
416
+ fi
417
+ # The echo-candidate window (D1): long enough that a line is distinctive and cannot be guessed,
418
+ # short enough that a model can reproduce it verbatim without truncating.
419
+ # Mirrors agy.sh's `timeout --kill-after=10s`. A turn that ignores TERM survives this long past its
420
+ # own cap, so the fed loop reserves it out of every turn's budget rather than letting the last turn
421
+ # spend it past the shared deadline. Keep in lockstep with agy.sh.
422
+ AGY_TURN_KILL_GRACE_S=10
423
+ FED_ECHO_MIN_BYTES=24
424
+ FED_ECHO_MAX_BYTES=200
425
+ # The largest proof ADDRESS number the parser will convert. It exists because the numbers come from
426
+ # the model: past this, awk's `%d` conversion is no longer faithful, and a silently-wrapped value
427
+ # could land back inside the requested range and impersonate a real address. An entry naming a
428
+ # bigger number is reported as address 0 — a value no request can ever carry — so it reads as an
429
+ # address nobody requested and FAILS the proof, rather than vanishing from the check entirely.
430
+ FED_PROOF_ADDRESS_MAX=1000000000
347
431
 
348
432
  HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
349
433
 
@@ -391,9 +475,8 @@ here and a known source of false positives. Review ONLY the engineering of the m
391
475
  AGAINST the grounded facts. If something contradicts your training, trust the facts, not your memory.
392
476
  GUARD
393
477
  }
394
- emit_shape() {
478
+ emit_shape_sections() {
395
479
  cat <<'SHAPE'
396
- ## Output — Markdown, this exact shape, nothing else
397
480
  ### Verdict
398
481
  One line: SHIP / SHIP WITH NITS / REWORK, plus a one-sentence reason.
399
482
  ### Blocking
@@ -404,6 +487,58 @@ Numbered. Simplifications, reuse, naming, missing tests. Cite file:line. Empty?
404
487
  Anything ambiguous that would change your verdict if answered.
405
488
  SHAPE
406
489
  }
490
+ emit_shape() {
491
+ echo "## Output — Markdown, this exact shape, nothing else"
492
+ emit_shape_sections
493
+ }
494
+ # The FED shape (D1): '### Delivery proof' is the FIRST section, so output truncation cannot
495
+ # silently drop the proof. The requested lines are named by ADDRESS only — revealing the text would
496
+ # let a model that never held the body copy it back, which is exactly what the proof exists to catch.
497
+ emit_shape_fed() { # $1 = the requested addresses, "part K line L; part K line L"
498
+ echo "## Output — Markdown, this exact shape, nothing else"
499
+ cat <<'PROOF'
500
+ ### Delivery proof
501
+ This section comes FIRST, before ### Verdict. For each requested address below print exactly one
502
+ line, in this form and nothing else:
503
+ part <K> line <L>: <the text of line L of part K, VERBATIM>
504
+ Count lines 1-based from the FIRST line after that part's BEGIN marker. Print the raw line text
505
+ after the colon — no code fences, no backticks, no quotes, no commentary, no ellipsis, no
506
+ re-indenting. If you cannot reproduce a line exactly, say so on that line rather than guessing.
507
+ PROOF
508
+ # ONE ADDRESS PER LINE, and every address line is shorter than FED_ECHO_MIN_BYTES. That is not
509
+ # cosmetic: it makes a collision CONSTRUCTIVELY impossible. A proof candidate is a single line of
510
+ # at least FED_ECHO_MIN_BYTES, and a single line can never match across a newline, so no candidate
511
+ # can occur inside this list — which is what lets the selector pick in ONE pass instead of
512
+ # re-picking against a request that does not exist yet.
513
+ printf 'Requested addresses, one per line:\n%s\n' "$1"
514
+ emit_shape_sections
515
+ }
516
+ # One feed turn's ENVELOPE (D1a). Nothing here ever enters the reviewed artifact: only the BODY
517
+ # between the BEGIN/END markers concatenates, and it concatenates byte-for-byte.
518
+ emit_feed_frame() { # $1 = part index, $2 = part count
519
+ printf '## Chunked delivery — part %s of %s\n' "$1" "$2"
520
+ cat <<'FEED'
521
+ This is one piece of ONE change set being delivered to you in order.
522
+ Reply with exactly OK and NOTHING else: do NOT review yet, do NOT summarise, do NOT comment, do NOT
523
+ ask questions. The review request arrives after the last part. The text between the BEGIN and END
524
+ markers below is the change set VERBATIM — the markers themselves are delivery framing and are not
525
+ part of it.
526
+ Work from THIS CONVERSATION only: do NOT use any tool, do NOT run any command, do NOT read any file.
527
+ Everything you need is already in this conversation, and on this host EVERY tool is auto-denied — a
528
+ denied tool produces no output at all and loses the whole answer.
529
+ FEED
530
+ }
531
+ emit_fed_final_head() {
532
+ cat <<'FINAL'
533
+ ## The change set is fully delivered
534
+ Every part above belongs to ONE change set, and it is now complete in this conversation. Review the
535
+ WHOLE change set against the grounded facts from the first message, under the same read-only posture.
536
+ Work from THIS CONVERSATION only: do NOT use any tool, do NOT run any command, do NOT read any file,
537
+ do NOT try to count lines with a tool — count them by reading. Everything you need is already in this
538
+ conversation, and on this host EVERY tool is auto-denied: a denied tool produces no output at all, so
539
+ reaching for one loses your entire review. Observed live on this lane's first real run.
540
+ FINAL
541
+ }
407
542
  emit_resume_reminder() {
408
543
  cat <<'REMINDER'
409
544
  CONTINUE the review you already started in THIS conversation, under the SAME read-only posture: do
@@ -565,7 +700,7 @@ posture_json() {
565
700
  # itself instead of inferring it from this wrapper's version (which bumps in a different release
566
701
  # phase). Silence is not a declaration — an unmarked receipt is untrustworthy and the gate rejects it.
567
702
  write_review_receipt() {
568
- local artifact="$1" fresh="$2" fingerprint="$3" verdict="$4" grounded="$5" facts_hash="$6" probe="${7:-false}"
703
+ local artifact="$1" fresh="$2" fingerprint="$3" verdict="$4" grounded="$5" facts_hash="$6" probe="${7:-false}" delivery="${8:-}"
569
704
  local receipts="${AW_REVIEW_RECEIPTS:-}"
570
705
  if [[ -z "$receipts" ]]; then
571
706
  local receipt_git_dir
@@ -575,12 +710,13 @@ write_review_receipt() {
575
710
  fi
576
711
  receipts="$receipt_git_dir/agent-workflow-review-receipts.jsonl"
577
712
  fi
578
- local line probe_field=',"probe":false'
713
+ local line probe_field=',"probe":false' delivery_field=""
579
714
  if [[ "$probe" == "true" ]]; then probe_field=',"probe":true'; fi
580
- 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}' \
715
+ if [[ -n "$delivery" ]]; then delivery_field=",\"delivery\":\"$delivery\""; fi
716
+ 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}' \
581
717
  "$(receipt_json_scalar "$artifact")" "$fresh" "$(receipt_json_scalar "$fingerprint")" \
582
718
  "$AW_RECEIPT_BACKEND" "$verdict" "$grounded" "$(receipt_json_scalar "$facts_hash")" \
583
- "$AW_BRIDGE_VERSION" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$probe_field" "$(posture_json)")"
719
+ "$AW_BRIDGE_VERSION" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$probe_field" "$(posture_json)" "$delivery_field")"
584
720
  if ! printf '%s\n' "$line" >>"$receipts" 2>/dev/null; then
585
721
  echo "warning: could not append the review receipt to $receipts — the review itself succeeded;" >&2
586
722
  echo " the review-state gate will read the current tree as un-receipted." >&2
@@ -602,15 +738,380 @@ parse_agy_verdict() { # $1 = captured-output file
602
738
  fi
603
739
  }
604
740
 
605
- # Emit the full review surface to stdout: repo map, status (never-committable untracked records
606
- # filtered), staged + unstaged diffs, and the CONTENTS of every untracked REGULAR file (NUL-safe
607
- # iteration over the SAME filtered walk as the fingerprint the payload is byte-identical with
608
- # and without a device mask). Symlinks are shown as their target (never followed — no out-of-repo
609
- # leak); directories/vanished paths are noted, never read (a `cat` on a FIFO would hang BEFORE the
610
- # hard timeout applies that class never reaches this loop).
741
+ # The repo file map is a FIXED cost that scales with REPO SIZE, not change size (measured 28,735
742
+ # bytes in the home repo 24% of agy's 120000-byte single-argv ceiling), so an unbounded map taxes
743
+ # the change budget of every review. AW_REVIEW_MAP_BUDGET_BYTES bounds it; the value is set by each
744
+ # wrapper so THIS BODY stays byte-identical across both (review-fingerprint-parity.test.mjs
745
+ # lockstep). Unset/0 = unbounded. Past the budget the map degrades to the CHANGED-path subset cut
746
+ # at the SAME budget, so a change touching very many long paths cannot re-breach the bound — plus a
747
+ # stated omitted count; a truncation-with-count, never a silent cut. The map was never part of the
748
+ # fingerprint domain (emit_fingerprint_payload does not contain it), so bounding it moves no receipt.
749
+ emit_repo_file_map() {
750
+ local budget="${AW_REVIEW_MAP_BUDGET_BYTES:-0}" total shown omitted subset tracked
751
+ # ONE deduplicated tracked-path SNAPSHOT, captured once and reused for all four uses — the
752
+ # byte-budget predicate, the printed map, `total`, and the index intersection. Re-running the query
753
+ # per use let a concurrent index change make the budget decision, the map and the counts describe
754
+ # DIFFERENT snapshots. Dedupe: an UNMERGED index lists a path once per stage, so a predicate
755
+ # counting duplicates against a map printing unique paths would push a map that fits into the
756
+ # truncated arm. `ls-files` output is sorted by path, so one path's stages are adjacent and `uniq`
757
+ # is exact — no git version floor needed.
758
+ tracked="$(git ls-files | LC_ALL=C uniq)"
759
+ if (( budget <= 0 )) || (( $(printf '%s\n' "$tracked" | wc -c) <= budget )); then
760
+ if [[ -n "$tracked" ]]; then printf '%s\n' "$tracked"; fi
761
+ return 0
762
+ fi
763
+ total=$(( $(printf '%s\n' "$tracked" | wc -l) ))
764
+ # The subset is INTERSECTED with the index before budgeting, so `shown` and `total` live in ONE
765
+ # domain and `shown + omitted == total` holds exactly: a STAGED DELETION is a changed path that is
766
+ # no longer in `git ls-files`, and counting it as shown would make the stated arithmetic a lie.
767
+ # (The NR==FNR reader is safe here: an empty index returns through the in-budget arm above.)
768
+ # awk never `exit`s early: a closed pipe would SIGPIPE `sort` and pipefail would abort the run.
769
+ subset="$(LC_ALL=C awk 'NR == FNR { known[$0] = 1; next } ($0 in known)' <(printf '%s\n' "$tracked") <(git diff --name-only --no-ext-diff; git diff --cached --name-only --no-ext-diff) |
770
+ LC_ALL=C sort -u |
771
+ LC_ALL=C awk -v cap="$budget" '{ n = length($0) + 1; if (!over && used + n <= cap) { used += n; print } else over = 1 }')"
772
+ shown=0
773
+ if [[ -n "$subset" ]]; then
774
+ printf '%s\n' "$subset"
775
+ shown=$(( $(printf '%s\n' "$subset" | wc -l) ))
776
+ fi
777
+ omitted=$(( total - shown ))
778
+ if (( omitted < 0 )); then omitted=0; fi
779
+ printf '=== repo file map TRUNCATED to the changed-path subset: %s of %s tracked paths shown, %s omitted (map budget %s bytes) ===\n' \
780
+ "$shown" "$total" "$omitted" "$budget"
781
+ }
782
+
783
+ # ── the chunked feed: partition, proof selection, turn targeting (Phase 3) ─────────────────────
784
+ # Delivery framing. `printf --` because the format itself begins with a dash.
785
+ FED_BEGIN_FMT='--- BEGIN CHANGE-SET PART %s OF %s ---\n'
786
+ FED_END_FMT='\n--- END CHANGE-SET PART %s OF %s ---\n'
787
+ FED_MODE=0
788
+ FED_PART_COUNT=0
789
+ FED_REQUESTED=""
790
+ declare -a FED_ECHO_LINE=()
791
+ declare -a FED_ECHO_TEXT=()
792
+
793
+ # Byte-exact slice of a file. pipefail is disabled INSIDE the subshell only: `head -c` closes the
794
+ # pipe early, which SIGPIPEs `tail` — under pipefail that would abort the whole run.
795
+ slice_bytes() { # $1 = file, $2 = 0-based byte offset, $3 = length
796
+ ( set +o pipefail; tail -c "+$(( $2 + 1 ))" "$1" | head -c "$3" )
797
+ }
798
+
799
+ # Part LENGTHS (bytes, one per line) cutting a file at LINE boundaries: part 1 up to $2, later parts
800
+ # up to $3. Every cut lands on a line boundary, so a split multi-byte code point is impossible — there
801
+ # is NO hard-cut fallback: a line that cannot fit an EMPTY active part makes the whole run refuse
802
+ # (status 1) BEFORE any turn is spent, because a part built from fragments of one giant line carries
803
+ # no line to prove delivery with anyway.
804
+ #
805
+ # Precisely what "does not fit an empty active part" means, because the loose version of this
806
+ # sentence was a review finding: when the current part already holds content, the flush below moves
807
+ # the long line into a later, LARGER part, and only a line longer than that later budget refuses.
808
+ # When the active part is still empty the refusal is immediate against the CURRENT cap — for part 1
809
+ # that is the smaller budget, since turn 1 also carries the grounding and a body-less turn would be
810
+ # a turn spent on nothing. In practice part 1 always accumulates the assembled header first, so the
811
+ # reachable refusal is a line longer than the LATER-part budget.
812
+ #
813
+ # The final record contributes no separator byte when the artifact does not end with a newline —
814
+ # inventing one would split a part that actually fits and cost an entire extra turn.
815
+ plan_fed_parts() { # $1 = file, $2 = first-part budget, $3 = later-part budget, $4 = line count, $5 = 1 if the file ends with a newline
816
+ LC_ALL=C awk -v b1="$2" -v bk="$3" -v last="$4" -v final_nl="$5" '
817
+ function flush() { if (used > 0) { print used; used = 0; cap = bk } }
818
+ BEGIN { cap = b1; used = 0 }
819
+ {
820
+ n = length($0) + ((NR == last && final_nl == 0) ? 0 : 1)
821
+ if (used + n > cap) {
822
+ flush()
823
+ if (n > cap) exit 1
824
+ }
825
+ used += n
826
+ }
827
+ END { flush() }
828
+ ' "$1"
829
+ }
830
+
831
+ # ORDERED delivery-proof candidates for ONE part (D1), nearest that part's middle first: an INTERIOR
832
+ # line whose trimmed text sits in the echo window and is unique as a WHOLE line across the change
833
+ # set. This is only the cheap prefilter — the exact fixed-string checks (exactly one occurrence
834
+ # across the bodies, ZERO anywhere the wrapper's own envelope text can reveal it) run in bash over
835
+ # this list, because a candidate that is merely a unique LINE can still occur as a SUBSTRING of
836
+ # another body's line, and a model without that part could then copy it.
837
+ # The order is produced by WALKING OUTWARD from the part's middle, never by sorting: a comparison
838
+ # sort here was quadratic, and the caller may walk the whole list, so the cost of merely ORDERING
839
+ # candidates must not depend on how many there are. There is no cap — capping made the wrapper claim
840
+ # "no usable candidate" while a usable one sat at position 26.
841
+ list_echo_candidates() { # $1 = part file, $2 = unique-trimmed-lines file → "<line number><TAB><text>"
842
+ LC_ALL=C awk -v minlen="$FED_ECHO_MIN_BYTES" -v maxlen="$FED_ECHO_MAX_BYTES" '
843
+ NR == FNR { uniq[$0] = 1; next }
844
+ { lines[FNR] = $0 }
845
+ function usable(i, t) {
846
+ if (i < 2 || i >= FNR) return 0
847
+ t = lines[i]
848
+ gsub(/^[ \t]+|[ \t]+$/, "", t)
849
+ if (length(t) < minlen || length(lines[i]) > maxlen) return 0
850
+ return (t in uniq)
851
+ }
852
+ END {
853
+ mid = int((FNR + 1) / 2)
854
+ if (usable(mid)) printf "%s\t%s\n", mid, lines[mid]
855
+ for (d = 1; d <= FNR; d++) {
856
+ if (usable(mid - d)) printf "%s\t%s\n", mid - d, lines[mid - d]
857
+ if (usable(mid + d)) printf "%s\t%s\n", mid + d, lines[mid + d]
858
+ }
859
+ }' "$2" "$1"
860
+ }
861
+
862
+ # Exact FIXED-STRING occurrence count of $1 in file $2. `|| true`: grep exits 1 on no match, which
863
+ # pipefail would otherwise turn into an aborted run.
864
+ fixed_occurrences() {
865
+ local n
866
+ n="$( { LC_ALL=C grep -o -F -- "$1" "$2" || true; } | wc -l )"
867
+ printf '%s' "$(( n ))"
868
+ }
869
+
870
+ # The conversation id agy writes into its own run log (D9, Arm A). The format is agy's own to change,
871
+ # which is exactly why an unparseable log DEGRADES LOUDLY to --continue instead of failing: the
872
+ # correctness guarantee is the D1 echo proof, which fails closed if the wrong conversation answers.
873
+ capture_conversation_id() { # $1 = run log → the id, or empty
874
+ local id
875
+ [[ -f "$1" ]] || { printf ''; return 0; }
876
+ id="$(LC_ALL=C awk '/onversation/ && match($0, /[0-9a-fA-F]+-[0-9a-fA-F]+-[0-9a-fA-F]+-[0-9a-fA-F]+-[0-9a-fA-F]+/) { print substr($0, RSTART, RLENGTH); exit }' "$1")"
877
+ if [[ "$id" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ ]]; then
878
+ printf '%s' "$id"
879
+ else
880
+ printf ''
881
+ fi
882
+ }
883
+
884
+ # A validated duration string → integer seconds (floor 1). The fed lane needs arithmetic on the cap
885
+ # because ONE wall-clock budget has to cover N+1 turns; the banner keeps printing the duration
886
+ # verbatim, so this conversion never changes what the user is told about the cap itself.
887
+ aw_duration_seconds() { # $1 = duration string matching the settings grammar
888
+ LC_ALL=C awk -v d="$1" 'BEGIN {
889
+ u = substr(d, length(d), 1)
890
+ n = substr(d, 1, length(d) - 1) + 0
891
+ m = (u == "s") ? 1 : (u == "m") ? 60 : (u == "h") ? 3600 : 86400
892
+ s = int(n * m)
893
+ if (s < 1) s = 1
894
+ print s
895
+ }'
896
+ }
897
+
898
+ trim_ws() {
899
+ local s="$1"
900
+ s="${s#"${s%%[![:space:]]*}"}"
901
+ s="${s%"${s##*[![:space:]]}"}"
902
+ printf '%s' "$s"
903
+ }
904
+
905
+ # A final turn that produced NO mandated section at all did not FAIL DELIVERY — it produced no
906
+ # review. Blaming delivery sends the reader hunting the wrong bug. What the wrapper can HONESTLY say
907
+ # is that it SENT every part and every feed turn exited zero; whether the model retained them is
908
+ # exactly what the unanswered proof leaves unknown, so it is never claimed.
909
+ fed_output_is_answerless() { # $1 = captured final-turn output
910
+ ! LC_ALL=C grep -qi -e '^###[[:space:]]*Delivery proof' -e '^###[[:space:]]*Verdict' "$1"
911
+ }
912
+
913
+ # agy's OWN refusal line when headless mode auto-denies a tool it chose to call. Recognizing it lets
914
+ # the wrapper name the real cause; anything else is reported as an UNKNOWN cause rather than guessed.
915
+ fed_output_names_tool_denial() { # $1 = captured final-turn output
916
+ LC_ALL=C grep -q 'no output produced.*permission that headless mode cannot prompt for' "$1"
917
+ }
918
+
919
+ # D1/D7: every part's selected line must come back VERBATIM, or the review FAILED — exit 4, no
920
+ # receipt, and a message naming the cause. Never a downgraded verdict, never a warning beside a kept
921
+ # receipt. The comparison tolerates surrounding whitespace only, never content.
922
+ # The proof section is the FIRST '### Delivery proof' block only, ending at the next '###' heading.
923
+ # Anything outside it is prose the model happens to have written and can never satisfy the proof.
924
+ # The FIRST '###' heading of the answer must BE the proof heading — that is the whole point of
925
+ # placing the proof first: output truncation can then never silently drop it. A proof block sitting
926
+ # after '### Verdict' is not the mandated shape and is not searched for.
927
+ fed_proof_section() { # $1 = captured output
928
+ LC_ALL=C awk '
929
+ !seen && /^###/ {
930
+ if (tolower($0) ~ /^###[ \t]*delivery proof[ \t]*$/) { seen = 1; inside = 1; next }
931
+ exit
932
+ }
933
+ inside && /^###/ { exit }
934
+ inside { print }
935
+ ' "$1"
936
+ }
937
+
938
+ # One recognized echo per output line: "K<TAB>L<TAB>payload". The anchor is STRICT — optional leading
939
+ # whitespace and at most ONE list marker that must itself be followed by whitespace, then the address
940
+ # at the START of the line. A marker buried mid-sentence is prose, not an echo.
941
+ #
942
+ # Two properties are load-bearing. (1) The addresses are NORMALIZED here, at the parse boundary:
943
+ # `%d` over `k + 0` hands bash a canonical decimal, because a model-written `part 08` would otherwise
944
+ # reach bash arithmetic and an array index as OCTAL and abort the wrapper with `value too great for
945
+ # base` instead of the contracted clean refusal — and a safely padded `01` would mismatch `1`. An
946
+ # out-of-range number is REPORTED as address 0 — never dropped, because a dropped address is an
947
+ # INVISIBLE one, and an answer carrying every correct echo plus one invented giant address would
948
+ # otherwise satisfy a grammar whose whole point is that it is closed. (2) The anchor is
949
+ # matched case-INSENSITIVELY on a lowered copy while the PAYLOAD is sliced from the ORIGINAL line:
950
+ # tolerating `Part 1 Line 5:` removes a false refusal, but the echoed text must keep its own case or
951
+ # the verbatim comparison would be checking something the model never wrote.
952
+ fed_proof_entries() { # reads the section on stdin
953
+ LC_ALL=C awk -v addrmax="$FED_PROOF_ADDRESS_MAX" '
954
+ {
955
+ line = $0
956
+ sub(/^[ \t]+/, "", line)
957
+ sub(/^[-*][ \t]+/, "", line)
958
+ probe = tolower(line)
959
+ if (match(probe, /^part[ \t]+[0-9]+[ \t]+line[ \t]+[0-9]+:/)) {
960
+ head = substr(probe, 1, RLENGTH)
961
+ rest = substr(line, RLENGTH + 1)
962
+ k = head; sub(/^part[ \t]+/, "", k); sub(/[ \t]+line.*$/, "", k)
963
+ l = head; sub(/^.*line[ \t]+/, "", l); sub(/:$/, "", l)
964
+ # An out-of-range address is REPORTED as address 0, never dropped: dropping it made a huge
965
+ # bogus address INVISIBLE, so an answer carrying every correct echo PLUS one invented
966
+ # `part 99999999999999999999 line …` satisfied the closed grammar and minted a receipt.
967
+ if (k + 0 > addrmax || l + 0 > addrmax) { printf "0\t0\t%s\n", rest; next }
968
+ printf "%d\t%d\t%s\n", k + 0, l + 0, rest
969
+ }
970
+ }'
971
+ }
972
+
973
+ verify_delivery_proof() { # $1 = captured final-turn output
974
+ local k l payload want got section entries
975
+ local -a seen_count=() seen_payload=()
976
+ section="$(fed_proof_section "$1")"
977
+ entries="$(printf '%s\n' "$section" | fed_proof_entries)"
978
+ for (( k = 1; k <= FED_PART_COUNT; k++ )); do
979
+ seen_count[k]=0
980
+ seen_payload[k]=""
981
+ done
982
+ if [[ -n "$entries" ]]; then
983
+ while IFS=$'\t' read -r k l payload; do
984
+ if (( k == 0 )); then
985
+ echo "error: the review did NOT prove delivery — its proof section echoes an address whose numbers are" >&2
986
+ echo " out of range (over ${FED_PROOF_ADDRESS_MAX}). An answer that invents its own addresses is not a proof:" >&2
987
+ echo " NO receipt was written. Re-run the review." >&2
988
+ return 1
989
+ fi
990
+ if (( k < 1 || k > FED_PART_COUNT )) || [[ "$l" != "${FED_ECHO_LINE[k]}" ]]; then
991
+ echo "error: the review did NOT prove delivery — its proof section echoes 'part ${k} line ${l}', which" >&2
992
+ echo " was never requested. An answer that invents its own addresses is not a proof: NO receipt" >&2
993
+ echo " was written. Re-run the review." >&2
994
+ return 1
995
+ fi
996
+ seen_count[k]=$(( seen_count[k] + 1 ))
997
+ if (( seen_count[k] == 1 )); then seen_payload[k]="$payload"; fi
998
+ done <<< "$entries"
999
+ fi
1000
+ for (( k = 1; k <= FED_PART_COUNT; k++ )); do
1001
+ if (( seen_count[k] == 0 )); then
1002
+ echo "error: the review did NOT prove delivery — its '### Delivery proof' section carries no echo for" >&2
1003
+ echo " part ${k} of ${FED_PART_COUNT} (requested: part ${k} line ${FED_ECHO_LINE[k]}). A review that cannot" >&2
1004
+ echo " reproduce a part's body never received it: NO receipt was written. Re-run the review." >&2
1005
+ return 1
1006
+ fi
1007
+ if (( seen_count[k] > 1 )); then
1008
+ echo "error: the review did NOT prove delivery — part ${k}'s address is echoed more than once" >&2
1009
+ echo " (${seen_count[k]} times). One address, one line: an ambiguous proof proves nothing, so NO" >&2
1010
+ echo " receipt was written. Re-run the review." >&2
1011
+ return 1
1012
+ fi
1013
+ got="${seen_payload[k]}"
1014
+ want="${FED_ECHO_TEXT[k]}"
1015
+ if [[ "$(trim_ws "$got")" != "$(trim_ws "$want")" ]]; then
1016
+ echo "error: the review did NOT prove delivery — part ${k}'s echoed line does not match the change set" >&2
1017
+ echo " (requested: part ${k} line ${FED_ECHO_LINE[k]}). Delivery is unproven, so NO receipt was written." >&2
1018
+ echo " Re-run the review; a model that DID receive every part but mis-transcribed one line is the" >&2
1019
+ echo " known false-refusal case." >&2
1020
+ return 1
1021
+ fi
1022
+ done
1023
+ return 0
1024
+ }
1025
+
1026
+ # Dispatch the prebuilt turns. Invariant E: a feed turn's stdout is captured PRIVATELY — nothing it
1027
+ # produced (an OK, or a premature verdict) reaches this wrapper's stdout or the parsed capture, and
1028
+ # the FIRST non-zero feed turn stops the run so no later turn is spent.
1029
+ run_fed_review() {
1030
+ local k conv_id="" log_file="$staging/turn1.log" feed_out="$staging/feed-turn-out" turn_rc turn_pass=()
1031
+ # ONE wall-clock budget for the WHOLE fed review, not one per turn. Handing every call the full
1032
+ # AGY_HARD_TIMEOUT multiplied the stated cap by the turn count — a 30m cap silently became up to
1033
+ # 30m × (N+1). Each turn now gets only what is LEFT of the shared deadline, and a review that runs
1034
+ # out of budget stops there instead of quietly outliving its own guarantee.
1035
+ # The grace agy.sh hands timeout(1) as --kill-after: a turn that ignores TERM keeps running that
1036
+ # much longer, so handing a turn the FULL remaining time lets the last one outlive the deadline by
1037
+ # the grace. Each turn is therefore given remaining MINUS the grace, and the whole N+1 sequence
1038
+ # stays inside the stated cap even when every turn has to be SIGKILLed.
1039
+ local deadline remaining turn_hard turn_soft soft_budget budget
1040
+ deadline=$(( $(date +%s) + $(aw_duration_seconds "$AGY_HARD_TIMEOUT") ))
1041
+ soft_budget=$(aw_duration_seconds "$AGY_TIMEOUT")
1042
+ for (( k = 1; k <= FED_PART_COUNT; k++ )); do
1043
+ remaining=$(( deadline - $(date +%s) ))
1044
+ # A turn needs strictly more than the SIGKILL grace to be dispatchable: hand it 1s and a
1045
+ # TERM-ignoring process still runs grace+1 seconds, breaking the very cap this loop enforces.
1046
+ # Too little left is therefore a refusal, never a shrunken turn.
1047
+ if (( remaining <= AGY_TURN_KILL_GRACE_S )); then
1048
+ echo "error: the fed review exhausted its hard wall-clock cap (AGY_HARD_TIMEOUT=${AGY_HARD_TIMEOUT}) before" >&2
1049
+ echo " feed turn ${k} of ${FED_PART_COUNT} — ${remaining}s left, and a turn needs more than the" >&2
1050
+ echo " ${AGY_TURN_KILL_GRACE_S}s SIGKILL grace to run without outliving the cap. The cap covers the WHOLE" >&2
1051
+ echo " review, not each turn — no later turn is spent and NO receipt is written." >&2
1052
+ echo " Re-run with a larger cap, or a smaller change set." >&2
1053
+ return 124
1054
+ fi
1055
+ budget=$(( remaining - AGY_TURN_KILL_GRACE_S ))
1056
+ turn_hard="${budget}s"
1057
+ turn_soft="$(( soft_budget < budget ? soft_budget : budget ))s"
1058
+ if (( k == 1 )); then turn_pass=(--log-file "$log_file")
1059
+ elif [[ -n "$conv_id" ]]; then turn_pass=(--conversation "$conv_id")
1060
+ else turn_pass=(--continue)
1061
+ fi
1062
+ set +e
1063
+ # Dispatched from the STAGING dir, never the work tree: agy surfaces the cwd's context file
1064
+ # (AGENTS.md / GEMINI.md / .antigravity.md) automatically, and that is text the model can see
1065
+ # which is NOT a body. The delivery proof is only sound when the wrapper knows every non-body
1066
+ # byte the model can read, so the fed lane removes the uncontrolled input instead of guessing
1067
+ # at it. Nothing is lost: the grounded facts already carry what the review must know.
1068
+ ( cd "$staging" && AGY_MODEL="$AGY_MODEL" AGY_TIMEOUT="$turn_soft" AGY_HARD_TIMEOUT="$turn_hard" \
1069
+ "$AGY_RUN" "@$staging/turn-$k" -- "${turn_pass[@]}" ) > "$feed_out"
1070
+ turn_rc=$?
1071
+ set -e
1072
+ if (( turn_rc != 0 )); then
1073
+ echo "error: feed turn ${k} of ${FED_PART_COUNT} failed (exit ${turn_rc}) — the change set was never fully" >&2
1074
+ echo " delivered, so no later turn is spent and NO receipt is written. Re-run the review." >&2
1075
+ return "$turn_rc"
1076
+ fi
1077
+ if (( k == 1 )); then
1078
+ conv_id="$(capture_conversation_id "$log_file")"
1079
+ if [[ -z "$conv_id" ]]; then
1080
+ echo "notice: could not capture the conversation id from agy's run log — the remaining turns ride" >&2
1081
+ echo " --continue instead. Stated, never silent; the delivery proof still fails closed if" >&2
1082
+ echo " another conversation answers." >&2
1083
+ fi
1084
+ fi
1085
+ done
1086
+ if [[ -n "$conv_id" ]]; then turn_pass=(--conversation "$conv_id"); else turn_pass=(--continue); fi
1087
+ remaining=$(( deadline - $(date +%s) ))
1088
+ if (( remaining <= AGY_TURN_KILL_GRACE_S )); then
1089
+ echo "error: the fed review exhausted its hard wall-clock cap (AGY_HARD_TIMEOUT=${AGY_HARD_TIMEOUT}) before the" >&2
1090
+ echo " final review turn — ${remaining}s left, less than the ${AGY_TURN_KILL_GRACE_S}s SIGKILL grace a turn" >&2
1091
+ echo " needs. Every part was delivered but the review itself was never asked for." >&2
1092
+ echo " NO receipt was written. Re-run with a larger cap, or a smaller change set." >&2
1093
+ return 124
1094
+ fi
1095
+ budget=$(( remaining - AGY_TURN_KILL_GRACE_S ))
1096
+ turn_hard="${budget}s"
1097
+ turn_soft="$(( soft_budget < budget ? soft_budget : budget ))s"
1098
+ set +e
1099
+ ( cd "$staging" && AGY_MODEL="$AGY_MODEL" AGY_TIMEOUT="$turn_soft" AGY_HARD_TIMEOUT="$turn_hard" \
1100
+ "$AGY_RUN" "@$staging/turn-final" -- "${turn_pass[@]}" ) | tee "$review_out_file"
1101
+ turn_rc=${PIPESTATUS[0]}
1102
+ set -e
1103
+ return "$turn_rc"
1104
+ }
1105
+
1106
+ # Emit the full review surface to stdout: repo map (bounded, see emit_repo_file_map), status
1107
+ # (never-committable untracked records filtered), staged + unstaged diffs, and the CONTENTS of every
1108
+ # untracked REGULAR file (NUL-safe iteration over the SAME filtered walk as the fingerprint — the
1109
+ # payload is byte-identical with and without a device mask). Symlinks are shown as their target
1110
+ # (never followed — no out-of-repo leak); directories/vanished paths are noted, never read (a `cat`
1111
+ # on a FIFO would hang BEFORE the hard timeout applies — that class never reaches this loop).
611
1112
  assemble_code_diff() {
612
1113
  echo "=== repo file map (git ls-files) ==="
613
- git ls-files
1114
+ emit_repo_file_map
614
1115
  echo
615
1116
  echo "=== git status (porcelain) ==="
616
1117
  emit_status_porcelain_filtered
@@ -671,6 +1172,11 @@ PLAN_CONTENT=""
671
1172
  DIFF_CONTENT=""
672
1173
  REVIEW_ARTIFACT=""
673
1174
  REVIEW_FINGERPRINT=""
1175
+ # D8/D8b: an agy `code` receipt SELF-DECLARES how the change set reached the model — `inline` when
1176
+ # the whole set rode ONE prompt (delivery proven BY CONSTRUCTION), `fed` when a chunked feed proved
1177
+ # it by echo. The kit's gate requires the field present and valid, never a particular value, and a
1178
+ # receipt that does not declare one no longer attests (the old --add-dir lane could not).
1179
+ REVIEW_DELIVERY=""
674
1180
  if [[ -z "$resume_mode" ]]; then
675
1181
  mode="${1:-}"; shift || true
676
1182
  case "$mode" in
@@ -727,7 +1233,7 @@ while [[ $# -gt 0 ]]; do
727
1233
  need_value "$1" "${2:-}"; FOCUS_PARTS+=("$2"); shift 2 ;;
728
1234
  --)
729
1235
  echo "error: this wrapper OWNS the review posture — no '--' passthrough. The only escapes are" >&2
730
- echo " AGY_PROBE=1 (off-frontier model) and AGY_REVIEW_ALLOW_ADDDIR=1 (oversized code review)." >&2
1236
+ echo " AGY_PROBE=1 (off-frontier model). An oversized code review is a chunked feed, not a flag." >&2
731
1237
  exit 2 ;;
732
1238
  --*)
733
1239
  echo "error: unknown flag '$1'." >&2; usage; exit 2 ;;
@@ -864,6 +1370,7 @@ else
864
1370
  # so the receipt attests exactly the reviewed state.
865
1371
  REVIEW_ARTIFACT="code"
866
1372
  REVIEW_FINGERPRINT="$(compute_tree_fingerprint || true)"
1373
+ REVIEW_DELIVERY="inline"
867
1374
  fi
868
1375
 
869
1376
  emit_artifact() {
@@ -894,46 +1401,193 @@ else
894
1401
  fi
895
1402
  }
896
1403
 
1404
+ # Partition the assembled change set into under-cap parts, pick each part's delivery-proof line,
1405
+ # and BUILD every turn's prompt UP FRONT — so the per-turn ceiling and the total outgoing byte
1406
+ # count (D1b) are both checked BEFORE a single subscription turn is spent. A refusal here costs
1407
+ # nothing; a refusal after turn 1 would have burned quota for no review.
1408
+ plan_fed_review() {
1409
+ local artifact_file="$staging/change-set" uniques="$staging/uniques" nonbody="$staging/nonbody"
1410
+ local grounding_bytes frame_bytes begin_bytes end_bytes overhead1 overheadk budget1 budgetk
1411
+ local artifact_bytes concat_bytes offset len k n picked built total_bytes
1412
+ local art_lines final_nl part_lengths
1413
+
1414
+ ( umask 077; emit_artifact > "$artifact_file" )
1415
+ ( umask 077; emit_grounding > "$staging/grounding" )
1416
+ artifact_bytes=$(( $(wc -c < "$artifact_file") ))
1417
+ grounding_bytes=$(( $(wc -c < "$staging/grounding") ))
1418
+ # Measured at the WIDEST index rendering the loop can produce, so a part count that only becomes
1419
+ # known during planning can never push an already-budgeted turn past the ceiling.
1420
+ frame_bytes=$(( $(emit_feed_frame 9999 9999 | wc -c) ))
1421
+ begin_bytes=$(( $(printf -- "$FED_BEGIN_FMT" 9999 9999 | wc -c) ))
1422
+ end_bytes=$(( $(printf -- "$FED_END_FMT" 9999 9999 | wc -c) ))
1423
+ overhead1=$(( grounding_bytes + frame_bytes + begin_bytes + end_bytes ))
1424
+ overheadk=$(( frame_bytes + begin_bytes + end_bytes ))
1425
+ budget1=$(( AGY_MAX_PROMPT_BYTES - overhead1 ))
1426
+ budgetk=$(( AGY_MAX_PROMPT_BYTES - overheadk ))
1427
+ if (( budget1 < 1 || budgetk < 1 )); then
1428
+ echo "error: the fed lane's fixed overhead (grounding ${grounding_bytes} B + delivery framing ${overheadk} B)" >&2
1429
+ echo " leaves no room for a change-set body under AGY_MAX_PROMPT_BYTES=${AGY_MAX_PROMPT_BYTES}." >&2
1430
+ echo " Trim --facts / --decided / --focus, or raise AGY_MAX_PROMPT_BYTES (never past ~${AGY_ARGV_HARD_MAX})." >&2
1431
+ exit 2
1432
+ fi
1433
+
1434
+ art_lines=$(( $(LC_ALL=C awk 'END { print NR + 0 }' "$artifact_file") ))
1435
+ final_nl=1
1436
+ if [[ "$(tail -c 1 "$artifact_file" | wc -l)" -eq 0 ]]; then final_nl=0; fi
1437
+ # Run the partitioner ONCE into a variable: a refusal must be seen, and a `< <(…)` process
1438
+ # substitution would swallow its status.
1439
+ if ! part_lengths="$(plan_fed_parts "$artifact_file" "$budget1" "$budgetk" "$art_lines" "$final_nl")"; then
1440
+ echo "error: a single change-set line does not fit even an EMPTY fed part (first part budget" >&2
1441
+ echo " ${budget1} B, later parts ${budgetk} B). The lane cuts only at line boundaries, and a part" >&2
1442
+ echo " built from fragments of one giant line carries no line to prove delivery with —" >&2
1443
+ echo " refusing before any turn is spent. Exclude the oversized file, or review it alone." >&2
1444
+ exit 2
1445
+ fi
1446
+ offset=0
1447
+ k=0
1448
+ if [[ -n "$part_lengths" ]]; then
1449
+ while IFS= read -r len; do
1450
+ k=$(( k + 1 ))
1451
+ if (( len < 1 )); then
1452
+ echo "error: the partitioner produced an empty part — refusing rather than spending a turn on nothing." >&2
1453
+ exit 2
1454
+ fi
1455
+ ( umask 077; slice_bytes "$artifact_file" "$offset" "$len" > "$staging/part-$k" )
1456
+ offset=$(( offset + len ))
1457
+ done <<< "$part_lengths"
1458
+ fi
1459
+ n=$k
1460
+ concat_bytes=0
1461
+ for (( k = 1; k <= n; k++ )); do concat_bytes=$(( concat_bytes + $(wc -c < "$staging/part-$k") )); done
1462
+ if (( n < 1 || concat_bytes != artifact_bytes )); then
1463
+ echo "error: the change-set partition does not reassemble to the assembled change set" >&2
1464
+ echo " (${concat_bytes} of ${artifact_bytes} bytes across ${n} part(s)) — refusing rather than" >&2
1465
+ echo " reviewing a change set the model would receive incompletely." >&2
1466
+ exit 2
1467
+ fi
1468
+
1469
+ ( umask 077; LC_ALL=C awk '{ t = $0; gsub(/^[ \t]+|[ \t]+$/, "", t); c[t]++ } END { for (l in c) if (c[l] == 1) print l }' "$artifact_file" > "$uniques" )
1470
+ if [[ ! -s "$uniques" ]]; then
1471
+ echo "error: the change set carries no line unique enough to serve as a delivery proof — refusing," >&2
1472
+ echo " because an unprovable delivery is exactly what this lane exists to prevent." >&2
1473
+ exit 2
1474
+ fi
1475
+ # Everything the wrapper itself will SEND that is not a BODY. A candidate occurring anywhere in
1476
+ # here would be revealed to a model that never received its part, so the proof would prove
1477
+ # nothing. Rendered with the REAL part indices, because the frames carry them.
1478
+ ( umask 077; {
1479
+ cat "$staging/grounding"
1480
+ for (( k = 1; k <= n; k++ )); do
1481
+ emit_feed_frame "$k" "$n"
1482
+ printf -- "$FED_BEGIN_FMT" "$k" "$n"
1483
+ printf -- "$FED_END_FMT" "$k" "$n"
1484
+ done
1485
+ emit_fed_final_head
1486
+ emit_shape_fed "(addresses are appended below)"
1487
+ } > "$nonbody" )
1488
+
1489
+ # ONE pass, no re-picking. The address list cannot reveal a candidate BY CONSTRUCTION (see
1490
+ # emit_shape_fed: one address per line, every such line shorter than a candidate's minimum), so
1491
+ # there is nothing to re-check once the addresses exist — the earlier two-pass blacklist loop was
1492
+ # solving a problem the FORMAT removes, and it could not be made a complete search anyway.
1493
+ local cand_no cand_text cand_trim address
1494
+ FED_REQUESTED=""
1495
+ for (( k = 1; k <= n; k++ )); do
1496
+ picked=""
1497
+ while IFS=$'\t' read -r cand_no cand_text; do
1498
+ cand_trim="$(trim_ws "$cand_text")"
1499
+ [[ -n "$cand_trim" ]] || continue
1500
+ (( $(fixed_occurrences "$cand_trim" "$artifact_file") == 1 )) || continue
1501
+ (( $(fixed_occurrences "$cand_trim" "$nonbody") == 0 )) || continue
1502
+ picked="${cand_no}"$'\t'"${cand_text}"
1503
+ break
1504
+ done < <(list_echo_candidates "$staging/part-$k" "$uniques")
1505
+ if [[ -z "$picked" ]]; then
1506
+ echo "error: part ${k} of ${n} carries no line the wrapper can use as a delivery proof — it needs an" >&2
1507
+ echo " interior line of ${FED_ECHO_MIN_BYTES}..${FED_ECHO_MAX_BYTES} bytes that occurs exactly ONCE in the whole change set and" >&2
1508
+ echo " nowhere in the wrapper's own framing. Refusing: delivery could not be proven." >&2
1509
+ exit 2
1510
+ fi
1511
+ FED_ECHO_LINE[k]="${picked%%$'\t'*}"
1512
+ FED_ECHO_TEXT[k]="${picked#*$'\t'}"
1513
+ address="part ${k} line ${FED_ECHO_LINE[k]}"
1514
+ # The invariant the one-pass selection rests on, ASSERTED rather than assumed: an address line
1515
+ # long enough to contain a candidate would break the constructive guarantee.
1516
+ if (( ${#address} >= FED_ECHO_MIN_BYTES )); then
1517
+ echo "error: the address '${address}' is ${#address} bytes, at or past the ${FED_ECHO_MIN_BYTES}-byte proof-candidate minimum —" >&2
1518
+ echo " the request list could then reveal a candidate. Refusing rather than sending an unsound proof." >&2
1519
+ exit 2
1520
+ fi
1521
+ if [[ -n "$FED_REQUESTED" ]]; then FED_REQUESTED+=$'\n'; fi
1522
+ FED_REQUESTED+="$address"
1523
+ done
1524
+
1525
+ for (( k = 1; k <= n; k++ )); do
1526
+ ( umask 077; {
1527
+ if (( k == 1 )); then cat "$staging/grounding"; fi
1528
+ emit_feed_frame "$k" "$n"
1529
+ printf -- "$FED_BEGIN_FMT" "$k" "$n"
1530
+ cat "$staging/part-$k"
1531
+ printf -- "$FED_END_FMT" "$k" "$n"
1532
+ } > "$staging/turn-$k" )
1533
+ done
1534
+ ( umask 077; { emit_fed_final_head; echo; emit_shape_fed "$FED_REQUESTED"; } > "$staging/turn-final" )
1535
+
1536
+ total_bytes=0
1537
+ for (( k = 1; k <= n; k++ )); do
1538
+ built=$(( $(wc -c < "$staging/turn-$k") ))
1539
+ if (( built > AGY_MAX_PROMPT_BYTES )); then
1540
+ echo "error: fed turn ${k} would be ${built} bytes, over AGY_MAX_PROMPT_BYTES=${AGY_MAX_PROMPT_BYTES} —" >&2
1541
+ echo " refusing before any turn is spent." >&2
1542
+ exit 2
1543
+ fi
1544
+ total_bytes=$(( total_bytes + built ))
1545
+ done
1546
+ built=$(( $(wc -c < "$staging/turn-final") ))
1547
+ if (( built > AGY_MAX_PROMPT_BYTES )); then
1548
+ echo "error: the final review turn would be ${built} bytes, over AGY_MAX_PROMPT_BYTES=${AGY_MAX_PROMPT_BYTES}." >&2
1549
+ echo " Trim --facts / --decided / --focus." >&2
1550
+ exit 2
1551
+ fi
1552
+ total_bytes=$(( total_bytes + built ))
1553
+ if (( total_bytes > AGY_REVIEW_MAX_TOTAL_BYTES )); then
1554
+ echo "error: the fed review would send ${total_bytes} outgoing prompt bytes across $(( n + 1 )) turns," >&2
1555
+ echo " over AGY_REVIEW_MAX_TOTAL_BYTES=${AGY_REVIEW_MAX_TOTAL_BYTES} — refusing BEFORE the first turn is spent." >&2
1556
+ echo " Split the change set into focused per-area reviews, or raise AGY_REVIEW_MAX_TOTAL_BYTES" >&2
1557
+ echo " knowing that retention past ~263000 bytes of body has never been probed." >&2
1558
+ exit 2
1559
+ fi
1560
+
1561
+ FED_PART_COUNT=$n
1562
+ echo "notice: the assembled prompt is ${prompt_bytes} bytes (> AGY_MAX_PROMPT_BYTES=${AGY_MAX_PROMPT_BYTES}) —" >&2
1563
+ echo " feeding the change set in ${n} part(s) over $(( n + 1 )) subscription turns, then reviewing." >&2
1564
+ echo " Total outgoing prompt bytes: ${total_bytes} (AGY_REVIEW_MAX_TOTAL_BYTES=${AGY_REVIEW_MAX_TOTAL_BYTES})." >&2
1565
+ # 4.3: state the boundary fact at the point of use. agy's own denial names the permission rule it
1566
+ # wants; granting it would widen a boundary for ALL agy use on this machine, so the kit surfaces
1567
+ # it and never writes it. The feed exists precisely so no permission is needed.
1568
+ echo " (The change set is DELIVERED, not read: headless agy auto-denies its own read_file" >&2
1569
+ echo " tool, and this kit never grants that permission — it would widen a boundary for every" >&2
1570
+ echo " agy run on this machine. Delivery is proven per part instead.)" >&2
1571
+ }
1572
+
897
1573
  # Full prompt = grounding + artifact (inline) + shape.
898
1574
  ( umask 077; { emit_grounding; emit_artifact; echo; emit_shape; } > "$prompt_file" )
899
1575
  prompt_bytes=$(( $(wc -c < "$prompt_file") ))
900
1576
 
901
1577
  if (( prompt_bytes > AGY_MAX_PROMPT_BYTES )); then
902
- if [[ "$AGY_REVIEW_ALLOW_ADDDIR" == "1" ]]; then
903
- # Offload ONLY the artifact to a private 0600 file; the -p prompt still carries the full grounding
904
- # inline and points agy at the file. --add-dir targets the private staging dir, never .git/work-tree.
905
- artifact_file="$staging/precomputed-change-set"
906
- ( umask 077; emit_artifact > "$artifact_file" )
907
- ( umask 077; {
908
- emit_grounding
909
- echo "## The change set under review"
910
- echo "The full change set is too large to inline. It is in the precomputed file at:"
911
- echo " $artifact_file"
912
- echo "Read it IN FULL, then review it against the grounded facts above."
913
- echo
914
- emit_shape
915
- } > "$prompt_file" )
916
- small_bytes=$(( $(wc -c < "$prompt_file") ))
917
- if (( small_bytes > AGY_MAX_PROMPT_BYTES )); then
918
- echo "error: even the grounding-only prompt is ${small_bytes} bytes (> AGY_MAX_PROMPT_BYTES=${AGY_MAX_PROMPT_BYTES})." >&2
919
- echo " Trim --facts / --decided / --focus." >&2
920
- exit 2
921
- fi
922
- echo "warning: the assembled prompt was ${prompt_bytes} bytes (> AGY_MAX_PROMPT_BYTES=${AGY_MAX_PROMPT_BYTES})." >&2
923
- echo " AGY_REVIEW_ALLOW_ADDDIR=1 — offloading the change set to a private staging dir and" >&2
924
- echo " passing it via --add-dir. This RE-ENABLES the Issue-001 stall risk (heavy agentic" >&2
925
- echo " roaming); the inherited hard timeout (AGY_HARD_TIMEOUT=$AGY_HARD_TIMEOUT) bounds it." >&2
926
- run_passthrough=(--add-dir "$staging")
1578
+ if [[ "$mode" == "code" ]]; then
1579
+ # The change set cannot be FETCHED on this host (agy's native read_file is auto-denied
1580
+ # headlessly), so it is DELIVERED: under-cap parts over continuation turns, proven by echo.
1581
+ plan_fed_review
1582
+ FED_MODE=1
1583
+ REVIEW_DELIVERY="fed"
927
1584
  else
1585
+ # D2: chunking is code-mode only — a plan/diff artifact is an operator-supplied file the
1586
+ # operator can split, so those modes keep the refuse-over-cap contract verbatim.
928
1587
  echo "error: the assembled prompt is ${prompt_bytes} bytes, over AGY_MAX_PROMPT_BYTES=${AGY_MAX_PROMPT_BYTES}." >&2
929
1588
  echo " agy takes the prompt as a single argv; past ~131072 bytes it fails with a cryptic" >&2
930
1589
  echo " 'Argument list too long'. Trim to the relevant hunks, or split into focused per-area" >&2
931
- if [[ "$mode" == "code" ]]; then
932
- echo " reviews. For a large CODE review, AGY_REVIEW_ALLOW_ADDDIR=1 offloads the change set via" >&2
933
- echo " a private --add-dir staging dir (re-enables the Issue-001 stall risk)." >&2
934
- else
935
- echo " reviews (split the $mode into focused parts)." >&2
936
- fi
1590
+ echo " reviews (split the $mode into focused parts)." >&2
937
1591
  exit 2
938
1592
  fi
939
1593
  fi
@@ -966,7 +1620,10 @@ echo "review posture: model=${AGY_MODEL:-<agy settings default>} timeout=$aw_tim
966
1620
  # parsed into the review receipt — the user-facing stream is unchanged.
967
1621
  review_out_file="$staging/review-output"
968
1622
  set +e
969
- if (( ${#run_passthrough[@]} > 0 )); then
1623
+ if (( FED_MODE == 1 )); then
1624
+ run_fed_review
1625
+ rc=$?
1626
+ elif (( ${#run_passthrough[@]} > 0 )); then
970
1627
  AGY_MODEL="$AGY_MODEL" AGY_TIMEOUT="$AGY_TIMEOUT" AGY_HARD_TIMEOUT="$AGY_HARD_TIMEOUT" \
971
1628
  "$AGY_RUN" "@$prompt_file" -- "${run_passthrough[@]}" | tee "$review_out_file"
972
1629
  rc=${PIPESTATUS[0]}
@@ -982,6 +1639,29 @@ set -e
982
1639
  # included — is a FAILED review: non-zero exit, NO receipt. This is a failed review to RE-RUN,
983
1640
  # never a fatal session error (documented in --help).
984
1641
  if [[ $rc -eq 0 ]]; then
1642
+ # D1/D7: on the fed lane, delivery is checked BEFORE the verdict is even parsed — an unproven
1643
+ # delivery is never a downgraded verdict and never a warning beside a kept receipt.
1644
+ if (( FED_MODE == 1 )); then
1645
+ if fed_output_is_answerless "$review_out_file"; then
1646
+ echo "error: the fed review produced no review at all — the final turn returned neither a" >&2
1647
+ echo " '### Delivery proof' nor a '### Verdict' section. The wrapper SENT every one of" >&2
1648
+ echo " the ${FED_PART_COUNT} part(s) and every feed turn exited zero; whether the model retained them is" >&2
1649
+ echo " precisely what the unanswered proof leaves unknown, so nothing is claimed here." >&2
1650
+ if fed_output_names_tool_denial "$review_out_file"; then
1651
+ echo " CAUSE (named by agy itself): it called a tool and headless mode auto-denied it, which" >&2
1652
+ echo " produces NO output at all. Observed on very large fed reviews; the fix is a SMALLER" >&2
1653
+ echo " review — split the change set, or leave AGY_REVIEW_MAX_TOTAL_BYTES at its default so" >&2
1654
+ echo " an oversized feed refuses before spending any turn." >&2
1655
+ else
1656
+ echo " CAUSE: unknown — agy returned no recognizable diagnostic. Inspect the run output." >&2
1657
+ fi
1658
+ echo " NO receipt was written. Re-run the review." >&2
1659
+ exit 4
1660
+ fi
1661
+ if ! verify_delivery_proof "$review_out_file"; then
1662
+ exit 4
1663
+ fi
1664
+ fi
985
1665
  verdict="$(parse_agy_verdict "$review_out_file")"
986
1666
  if [[ "$verdict" == "unknown" ]]; then
987
1667
  echo "error: the review output carries no recognized '### Verdict' section (closed vocabulary:" >&2
@@ -1003,7 +1683,7 @@ if [[ $rc -eq 0 ]]; then
1003
1683
  grounded=true
1004
1684
  facts_hash="$(printf '%s' "$FACTS_CONTENT" | sha256_stdin || true)"
1005
1685
  fi
1006
- write_review_receipt "$REVIEW_ARTIFACT" true "$REVIEW_FINGERPRINT" "$verdict" "$grounded" "$facts_hash" "$REVIEW_PROBE"
1686
+ write_review_receipt "$REVIEW_ARTIFACT" true "$REVIEW_FINGERPRINT" "$verdict" "$grounded" "$facts_hash" "$REVIEW_PROBE" "$REVIEW_DELIVERY"
1007
1687
  fi
1008
1688
  fi
1009
1689
  exit $rc