@sabaiway/agent-workflow-kit 5.1.0 → 5.3.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 (53) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/SKILL.md +13 -1
  3. package/bridges/antigravity-cli-bridge/SKILL.md +14 -3
  4. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +220 -30
  5. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +264 -8
  6. package/bridges/antigravity-cli-bridge/bin/agy.sh +12 -2
  7. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +18 -0
  8. package/bridges/antigravity-cli-bridge/capability.json +19 -13
  9. package/bridges/antigravity-cli-bridge/references/driving-agy.md +3 -2
  10. package/bridges/codex-cli-bridge/SKILL.md +17 -7
  11. package/bridges/codex-cli-bridge/bin/codex-exec.sh +156 -36
  12. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +228 -4
  13. package/bridges/codex-cli-bridge/bin/codex-review.sh +205 -34
  14. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +276 -5
  15. package/bridges/codex-cli-bridge/capability.json +10 -7
  16. package/bridges/codex-cli-bridge/references/driving-codex.md +7 -5
  17. package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +26 -12
  18. package/capability.json +1 -1
  19. package/package.json +1 -1
  20. package/references/modes/flow-writer.md +37 -0
  21. package/references/modes/gates.md +4 -4
  22. package/references/modes/procedures.md +4 -2
  23. package/references/modes/receipt-deadline.md +16 -0
  24. package/references/modes/review-state.md +1 -1
  25. package/references/modes/set-flow.md +22 -0
  26. package/references/scripts/archive-decisions.mjs +340 -15
  27. package/references/scripts/archive-decisions.test.mjs +522 -2
  28. package/tools/cheap-agents.mjs +8 -2
  29. package/tools/commands.mjs +24 -2
  30. package/tools/commit-guard.mjs +44 -9
  31. package/tools/core-evidence.mjs +25 -22
  32. package/tools/detect-backends.mjs +33 -11
  33. package/tools/dispatch-record.mjs +926 -0
  34. package/tools/doc-parity.mjs +21 -6
  35. package/tools/flow-check.mjs +842 -0
  36. package/tools/flow-record.mjs +795 -0
  37. package/tools/flow-store-read.mjs +114 -0
  38. package/tools/flow-store.mjs +1178 -0
  39. package/tools/flow-writer.mjs +1265 -0
  40. package/tools/fs-read-nofollow.mjs +128 -0
  41. package/tools/gates-declaration.mjs +184 -0
  42. package/tools/gates-init.mjs +59 -17
  43. package/tools/orchestration-config.mjs +87 -10
  44. package/tools/orchestration-write.mjs +3 -3
  45. package/tools/plan-files.mjs +35 -0
  46. package/tools/procedures.mjs +75 -11
  47. package/tools/receipt-deadline.mjs +242 -0
  48. package/tools/recipes.mjs +21 -0
  49. package/tools/repo-lex.mjs +22 -0
  50. package/tools/review-state.mjs +240 -80
  51. package/tools/run-gates.mjs +361 -139
  52. package/tools/set-flow.mjs +465 -0
  53. package/tools/velocity-profile.mjs +8 -2
@@ -42,8 +42,8 @@ case "${1:-}" in
42
42
  codex-review — read-only ADVISORY review by the OpenAI Codex CLI (subscription-only; frontier model at max effort).
43
43
 
44
44
  Usage:
45
- codex-review plan <plan-file>
46
- codex-review code [extra focus...]
45
+ codex-review plan <plan-file> [--nonce <n>]
46
+ codex-review code [--nonce <n>] [extra focus...]
47
47
 
48
48
  Grounding:
49
49
  automatic — the wrapper precomputes the full working-tree change set (repo map,
@@ -70,7 +70,17 @@ Receipt:
70
70
  wrapper minted it; re-run the review), one stderr banner line states the same posture, and a
71
71
  posture value carrying control bytes refuses pre-spend in every mode; a run whose final message
72
72
  carries NO recognized 'Verdict: <ship|revise|rethink>' line — empty or missing output included —
73
- exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); a write
73
+ exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); when the
74
+ dispatch nonce seam is supplied — the AW_REVIEW_NONCE environment value or its plain-argument
75
+ equivalent --nonce <n> (one seam: the flag assigns the same value; supplying both with
76
+ different values refuses pre-spend) — under the safe grammar [A-Za-z0-9._-]{1,64} (anything
77
+ else refuses pre-spend), the wrapper first mints the finding MANIFEST {schema, backend, nonce,
78
+ fingerprint, findings} beside the receipts file (agent-workflow-finding-manifest-<backend>-<nonce>.json;
79
+ atomic, no-clobber — a byte-identical rewrite is an idempotent no-op, different bytes refuse
80
+ loudly) ORDERED before the receipt append — a failed manifest write EXCLUDES the receipt append,
81
+ so a nonce-supplied dispatch can never land a receipt without its readable manifest; a nonce-less
82
+ invocation adds NO nonce field and mints NO finding manifest (the existing wrapperVersion field
83
+ still changes with each bridge release); a write
74
84
  failure warns, never fails the review
75
85
 
76
86
  Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins over built-in default):
@@ -80,9 +90,10 @@ Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins ov
80
90
  CODEX_REVIEW_MAX_TOTAL_BYTES — inline-payload cap, integer bytes 1..100000000 (default 1500000); above it the diff rides via a git-dir temp file
81
91
 
82
92
  Notes:
83
- the review posture banner appends a banner-only timeout=<duration|uncapped> field — exactly the
84
- duration handed to timeout(1), uncapped when no timeout/gtimeout binary caps the run;
85
- INFORMATIONAL only: it never enters the receipt posture or the D5 banner↔receipt parity
93
+ the review posture banner appends a banner-only timeout=<duration> field — exactly the duration
94
+ handed to timeout(1); the hard-timeout preflight fails CLOSED when no timeout/gtimeout binary
95
+ exists (the wrapper refuses by name before any CLI run, so an uncapped review run can no longer
96
+ happen), and the field never enters the receipt posture or the D5 banner↔receipt parity
86
97
  quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated
87
98
  posture; a prose re-type drifts
88
99
 
@@ -95,7 +106,7 @@ Honesty + posture (D4/D5):
95
106
  posture banner verbatim when labeling this dispatch. A posture value carrying control bytes
96
107
  refuses pre-spend in every mode.
97
108
 
98
- Environment: CODEX_REVIEW_SCHEMA=1 (structured JSON findings), CODEX_HARD_TIMEOUT (seconds, default 1800), CODEX_PROBE=1 (throwaway probe only), AW_REVIEW_RECEIPTS (receipt file override).
109
+ Environment: CODEX_REVIEW_SCHEMA=1 (structured JSON findings), CODEX_HARD_TIMEOUT (seconds, default 1800), CODEX_PROBE=1 (throwaway probe only), AW_REVIEW_RECEIPTS (receipt file override), AW_REVIEW_NONCE (dispatch nonce — mints the finding manifest beside the receipt; the --nonce <n> flag is its plain-argument equivalent).
99
110
  Requires at run time: the codex CLI on PATH, a ChatGPT-subscription login, a git work tree with a root AGENTS.md (--help needs none of these).
100
111
  HELP
101
112
  exit 0
@@ -213,8 +224,9 @@ aw_apply_settings
213
224
 
214
225
  # --- Effective-timeout resolver (D5 banner honesty; AD-061) --------------------
215
226
  # ONE rule, both bridges: the posture banner prints EXACTLY the duration handed to timeout(1) —
216
- # an integer-seconds value rendered with the `s` suffix, a duration string verbatim and
217
- # `timeout=uncapped` when no timeout/gtimeout binary can cap the run; never a fabricated number.
227
+ # an integer-seconds value rendered with the `s` suffix, a duration string verbatim; without a
228
+ # capping binary the EXEC wrappers print `timeout=uncapped` and run, while the REVIEW wrappers
229
+ # refuse pre-spend (fail-closed preflight) — never a fabricated number.
218
230
  # The EFFECTIVE value (env included — closing the aw_settings_valid env bypass) is validated by
219
231
  # the same per-key rule as the settings file, plus a 7-digit integer-part bound (overflow); an
220
232
  # invalid value warns + falls back to the built-in default — a typo never silently masquerades
@@ -266,7 +278,7 @@ DEFAULT_CODEX_EFFORT="xhigh"
266
278
  # Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
267
279
  # version (drift-guarded by codex-review.test.mjs against capability.json).
268
280
  AW_RECEIPT_BACKEND="codex"
269
- AW_BRIDGE_VERSION="3.2.0"
281
+ AW_BRIDGE_VERSION="3.4.0"
270
282
  CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
271
283
  CODEX_EFFORT="${CODEX_EFFORT:-$DEFAULT_CODEX_EFFORT}"
272
284
  # Generous hard cap for a slow xhigh review (subscription latency varies).
@@ -297,6 +309,14 @@ for _posture_pair in "CODEX_MODEL=$CODEX_MODEL" "CODEX_EFFORT=$CODEX_EFFORT" "CO
297
309
  exit 2
298
310
  fi
299
311
  done
312
+ # The dispatch nonce seam (flow-orchestration Decision 2/P5): validated pre-spend under the SAFE
313
+ # grammar — a nonce that would escape the derived manifest name refuses before any CLI run. The
314
+ # bracket expression ENUMERATES the ASCII set (no ranges): a range like A-Z is locale-collation-
315
+ # dependent and could admit a non-ASCII nonce the kit's JS reader then refuses.
316
+ if [[ -n "${AW_REVIEW_NONCE:-}" && ! "${AW_REVIEW_NONCE}" =~ ^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-]{1,64}$ ]]; then
317
+ echo "error: AW_REVIEW_NONCE fails the safe nonce grammar ([A-Za-z0-9._-]{1,64}) — the derived manifest name would be unsafe; fix the nonce and re-run." >&2
318
+ exit 2
319
+ fi
300
320
  CODEX_HARD_TIMEOUT="$(aw_effective_timeout CODEX_HARD_TIMEOUT 1800)"
301
321
  if [[ -n "$CODEX_SERVICE_TIER" ]] && ! aw_settings_valid CODEX_SERVICE_TIER "$CODEX_SERVICE_TIER"; then
302
322
  echo "warning: CODEX_SERVICE_TIER='$CODEX_SERVICE_TIER' is not a supported service tier ('priority') — running on the standard tier." >&2
@@ -312,11 +332,19 @@ CHATGPT_LOGIN_GUARD="Logged in using ChatGPT"
312
332
  # The control-byte screen already ran above (BEFORE tier validation); the banner states the
313
333
  # EFFECTIVE posture (post tier validation) and the receipt records the same fields — except
314
334
  # `timeout`, a BANNER-ONLY informational field (AD-061): it prints exactly the duration handed
315
- # to timeout(1), or `uncapped` without a capping binary, and never enters the receipt.
335
+ # to timeout(1) (the fail-closed preflight below guarantees a capping binary) and never enters
336
+ # the receipt.
316
337
  # aw_resolve_timeout_bin: builtin type -P (an exported function can shadow neither `timeout` nor
317
338
  # `type` itself), normalized to an ABSOLUTE path fail-closed; the dispatch below reuses this SAME
318
- # resolved path (banner and run never make independent conclusions).
339
+ # resolved path (banner and run never make independent conclusions). The preflight fails CLOSED
340
+ # without a capping binary (flow-orchestration #26): an uncapped review run is refused pre-spend.
319
341
  timeout_bin="$(aw_resolve_timeout_bin)"
342
+ if [[ -z "$timeout_bin" ]]; then
343
+ echo "error: no 'timeout'/'gtimeout' binary on PATH — the hard-timeout preflight fails CLOSED:" >&2
344
+ echo " an uncapped review run is refused before any CLI spend. Install coreutils (timeout;" >&2
345
+ echo " on macOS: brew install coreutils for gtimeout), then re-run." >&2
346
+ exit 127
347
+ fi
320
348
  aw_timeout_banner="$(aw_timeout_label "$timeout_bin" "$CODEX_HARD_TIMEOUT")"
321
349
  echo "review posture: model=$CODEX_MODEL effort=$CODEX_EFFORT tier=${CODEX_SERVICE_TIER:-standard} timeout=$aw_timeout_banner" >&2
322
350
 
@@ -540,7 +568,104 @@ posture_json() {
540
568
  "$(json_string_escape "$CODEX_MODEL")" "$(json_string_escape "$CODEX_EFFORT")" "$tier_json"
541
569
  }
542
570
 
543
- # write_review_receipt <artifact|""> <fresh: true|false> <fingerprint|""> <verdict> <grounded: true|false> <factsHash|""> [probe: true|false]
571
+ # write_finding_manifest <receipts-path> <fingerprint|""> <findings-file> the wrapper-minted
572
+ # finding MANIFEST (flow-orchestration Decision 2 / P5 / P24-25), minted ONLY when the dispatch
573
+ # nonce seam AW_REVIEW_NONCE is supplied: {schema, backend, nonce, fingerprint, findings} lands
574
+ # beside the receipts file under the {backend, nonce}-derived name. The write is ATOMIC (temp +
575
+ # hard-link publish) and NO-CLOBBER: a byte-identical re-write is an idempotent no-op, different
576
+ # bytes refuse loudly. Returns non-zero on ANY failure — the caller then EXCLUDES the receipt
577
+ # append, so a nonce-supplied dispatch can never land a receipt without its readable manifest.
578
+ # A nonce-less invocation returns 0 untouched (no nonce field, no manifest — the receipt
579
+ # otherwise proceeds unchanged).
580
+ write_finding_manifest() {
581
+ local receipts="$1" fingerprint="$2" findings_file="$3" nonce="${AW_REVIEW_NONCE:-}"
582
+ [[ -n "$nonce" ]] || return 0
583
+ if [[ ! "$nonce" =~ ^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-]{1,64}$ ]]; then
584
+ echo "error: AW_REVIEW_NONCE fails the safe nonce grammar ([A-Za-z0-9._-]{1,64}) — the derived manifest name would be unsafe; NO manifest was written." >&2
585
+ return 1
586
+ fi
587
+ if [[ -z "$findings_file" || ! -f "$findings_file" ]]; then
588
+ echo "error: no captured findings file to mint the manifest from — NO manifest was written." >&2
589
+ return 1
590
+ fi
591
+ local manifest mint_rc
592
+ manifest="$(dirname -- "$receipts")/agent-workflow-finding-manifest-${AW_RECEIPT_BACKEND}-${nonce}.json"
593
+ # The WHOLE mint core rides ONE node script (a family floor; the verdict parse already leans on
594
+ # it): FATAL UTF-8 compose (BOM kept — invalid bytes refuse rather than silently mutate the
595
+ # digest domain), an UNPREDICTABLE sibling temp opened with "wx" (O_CREAT|O_EXCL — a planted
596
+ # node at the name refuses, and ONLY a temp we provably created is ever unlinked), a hard-link
597
+ # publish (atomic no-clobber), and an EEXIST loser judged through ONE O_NOFOLLOW|O_NONBLOCK
598
+ # descriptor whose fstat must say REGULAR before the same-fd byte compare — no TOCTOU window,
599
+ # no symlink read-through, no FIFO hang. Exit: 0 minted or byte-identical no-op; 3 different
600
+ # bytes or a non-regular/symlink manifest; 5 minted-but-temp-left (SUCCESS with the orphan path
601
+ # on stdout — the caller warns loudly, never silently); 1 unreadable/non-UTF-8 findings or an
602
+ # fs failure.
603
+ local mint_out
604
+ mint_out="$( umask 077; node -e '
605
+ const fs = require("node:fs");
606
+ const { join, dirname, basename } = require("node:path");
607
+ const { randomBytes } = require("node:crypto");
608
+ const [file, backend, nonce, fingerprint, manifest] = process.argv.slice(1);
609
+ let code = 1;
610
+ let tmp = null;
611
+ try {
612
+ const findings = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(fs.readFileSync(file));
613
+ const bytes = Buffer.from(`${JSON.stringify({ schema: 1, backend, nonce, fingerprint: fingerprint || null, findings })}\n`);
614
+ const candidate = join(dirname(manifest), `.${basename(manifest)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
615
+ const fd = fs.openSync(candidate, "wx", 0o600);
616
+ tmp = candidate;
617
+ try {
618
+ fs.writeFileSync(fd, bytes);
619
+ } finally {
620
+ fs.closeSync(fd);
621
+ }
622
+ try {
623
+ fs.linkSync(tmp, manifest);
624
+ code = 0;
625
+ } catch (err) {
626
+ if (err && err.code === "EEXIST") {
627
+ try {
628
+ const mfd = fs.openSync(manifest, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
629
+ try {
630
+ code = !fs.fstatSync(mfd).isFile() ? 3 : fs.readFileSync(mfd).equals(bytes) ? 0 : 3;
631
+ } finally {
632
+ fs.closeSync(mfd);
633
+ }
634
+ } catch {
635
+ code = 3;
636
+ }
637
+ }
638
+ }
639
+ } catch {
640
+ code = 1;
641
+ }
642
+ if (tmp !== null) { try { fs.unlinkSync(tmp); } catch { if (code === 0) code = 5; process.stdout.write(tmp); } }
643
+ process.exit(code);
644
+ ' "$findings_file" "$AW_RECEIPT_BACKEND" "$nonce" "$fingerprint" "$manifest" 2>/dev/null )"
645
+ mint_rc=$?
646
+ if [[ $mint_rc -eq 5 ]]; then
647
+ echo "warning: the finding manifest was minted, but its temporary sibling could not be removed —" >&2
648
+ echo " orphan left at: ${mint_out} — remove it by hand (the manifest and the receipt are intact)." >&2
649
+ return 0
650
+ fi
651
+ if [[ $mint_rc -eq 3 ]]; then
652
+ echo "error: the finding manifest $manifest already exists with DIFFERENT bytes or is not a regular file — no-clobber refuses loudly (one dispatch identity, one manifest)." >&2
653
+ if [[ -n "$mint_out" ]]; then
654
+ echo " orphan left at: ${mint_out} — its temporary sibling could not be removed either; remove it by hand." >&2
655
+ fi
656
+ return 1
657
+ fi
658
+ if [[ $mint_rc -ne 0 ]]; then
659
+ echo "error: could not compose or write the finding manifest (unreadable or non-UTF-8 findings, or an fs failure) — NO manifest was written." >&2
660
+ if [[ -n "$mint_out" ]]; then
661
+ echo " orphan left at: ${mint_out} — its temporary sibling could not be removed either; remove it by hand." >&2
662
+ fi
663
+ return 1
664
+ fi
665
+ return 0
666
+ }
667
+
668
+ # write_review_receipt <artifact|""> <fresh: true|false> <fingerprint|""> <verdict> <grounded: true|false> <factsHash|""> [probe: true|false] [delivery|""] [findings-file]
544
669
  # Appends ONE receipt line (the AD-038 fixture shape) as a side effect of a SUCCESSFUL review —
545
670
  # to $AW_REVIEW_RECEIPTS when set, else <git dir>/agent-workflow-review-receipts.jsonl (inside the
546
671
  # git dir by construction, so it is never committable). Fail-safe: every failure here warns loudly
@@ -550,8 +675,10 @@ posture_json() {
550
675
  # marker is written ALWAYS, true or false: the receipt SELF-DECLARES, so the gate reads the fact
551
676
  # itself instead of inferring it from this wrapper's version (which bumps in a different release
552
677
  # phase). Silence is not a declaration — an unmarked receipt is untrustworthy and the gate rejects it.
678
+ # The 9th argument feeds the finding-manifest mint: on a nonce-supplied dispatch the manifest is
679
+ # minted FIRST (atomic, no-clobber, ORDERED) and a failed mint EXCLUDES the receipt append.
553
680
  write_review_receipt() {
554
- local artifact="$1" fresh="$2" fingerprint="$3" verdict="$4" grounded="$5" facts_hash="$6" probe="${7:-false}" delivery="${8:-}"
681
+ local artifact="$1" fresh="$2" fingerprint="$3" verdict="$4" grounded="$5" facts_hash="$6" probe="${7:-false}" delivery="${8:-}" findings_file="${9:-}"
555
682
  local receipts="${AW_REVIEW_RECEIPTS:-}"
556
683
  if [[ -z "$receipts" ]]; then
557
684
  local receipt_git_dir
@@ -561,13 +688,24 @@ write_review_receipt() {
561
688
  fi
562
689
  receipts="$receipt_git_dir/agent-workflow-review-receipts.jsonl"
563
690
  fi
564
- local line probe_field=',"probe":false' delivery_field=""
691
+ if ! write_finding_manifest "$receipts" "$fingerprint" "$findings_file"; then
692
+ echo "warning: the finding manifest could not be minted — the receipt append is EXCLUDED (a" >&2
693
+ echo " nonce-supplied dispatch never lands a receipt without its readable manifest);" >&2
694
+ echo " the review itself succeeded — re-run it to mint the pair." >&2
695
+ return 0
696
+ fi
697
+ # A nonce-SUPPLIED dispatch stamps its nonce into the receipt too (the flow round-land matcher
698
+ # requires exact {backend, nonce} equality — dispatch identity end-to-end); the nonce is
699
+ # grammar-safe by the pre-spend check, and a nonce-less receipt stays BYTE-EXACT (the frozen
700
+ # compatibility floor).
701
+ local line probe_field=',"probe":false' delivery_field="" nonce_field=""
565
702
  if [[ "$probe" == "true" ]]; then probe_field=',"probe":true'; fi
566
703
  if [[ -n "$delivery" ]]; then delivery_field=",\"delivery\":\"$delivery\""; fi
567
- 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}' \
704
+ if [[ -n "${AW_REVIEW_NONCE:-}" ]]; then nonce_field=",\"nonce\":\"${AW_REVIEW_NONCE}\""; fi
705
+ 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}' \
568
706
  "$(receipt_json_scalar "$artifact")" "$fresh" "$(receipt_json_scalar "$fingerprint")" \
569
707
  "$AW_RECEIPT_BACKEND" "$verdict" "$grounded" "$(receipt_json_scalar "$facts_hash")" \
570
- "$AW_BRIDGE_VERSION" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$probe_field" "$(posture_json)" "$delivery_field")"
708
+ "$AW_BRIDGE_VERSION" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$probe_field" "$(posture_json)" "$delivery_field" "$nonce_field")"
571
709
  if ! printf '%s\n' "$line" >>"$receipts" 2>/dev/null; then
572
710
  echo "warning: could not append the review receipt to $receipts — the review itself succeeded;" >&2
573
711
  echo " the review-state gate will read the current tree as un-receipted." >&2
@@ -655,6 +793,48 @@ assemble_code_diff() {
655
793
  mode="${1:-}"
656
794
  shift || true
657
795
 
796
+ # --nonce <v> — the plain-argument lane onto the EXISTING AW_REVIEW_NONCE seam (flow
797
+ # FLOW-NONCE-DISPATCH-LANE): a host whose dispatch policy has no env-prefix lane passes the
798
+ # round-open nonce as an argument instead. Stripped for EVERY mode before mode handling
799
+ # (plan-mode consult dispatches carry nonces too); the flag and a non-empty env value must
800
+ # AGREE — two disagreeing sources would mint an ambiguous dispatch identity (fail closed).
801
+ # Grammar screen identical to the env screen above (enumerated ASCII — locale-independent),
802
+ # pre-spend. The NEXT argument is taken unconditionally (only end-of-args refuses here): the
803
+ # closed grammar is the single validity door, so a grammar-valid leading-dash value is legal —
804
+ # and presence rides its own flag, so an EMPTY value still hits the grammar screen and a
805
+ # duplicate after it still refuses.
806
+ nonce_flag=""
807
+ nonce_flag_set=0
808
+ scan_args=()
809
+ while [[ $# -gt 0 ]]; do
810
+ case "$1" in
811
+ --nonce)
812
+ if [[ $# -lt 2 ]]; then
813
+ echo "error: --nonce needs a value; got '<end of args>'." >&2
814
+ exit 2
815
+ fi
816
+ if [[ "$nonce_flag_set" == "1" ]]; then
817
+ echo "error: duplicate --nonce — one dispatch carries one nonce." >&2
818
+ exit 2
819
+ fi
820
+ nonce_flag="$2"; nonce_flag_set=1; shift 2 ;;
821
+ *)
822
+ scan_args+=("$1"); shift ;;
823
+ esac
824
+ done
825
+ if [[ "$nonce_flag_set" == "1" ]]; then
826
+ if [[ ! "$nonce_flag" =~ ^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-]{1,64}$ ]]; then
827
+ echo "error: --nonce fails the safe nonce grammar ([A-Za-z0-9._-]{1,64}) — the derived manifest name would be unsafe; fix the nonce and re-run." >&2
828
+ exit 2
829
+ fi
830
+ if [[ -n "${AW_REVIEW_NONCE:-}" && "${AW_REVIEW_NONCE}" != "$nonce_flag" ]]; then
831
+ echo "error: --nonce disagrees with the AW_REVIEW_NONCE environment value — one dispatch carries one nonce; drop one source and re-run." >&2
832
+ exit 2
833
+ fi
834
+ AW_REVIEW_NONCE="$nonce_flag"
835
+ fi
836
+ set -- "${scan_args[@]+"${scan_args[@]}"}"
837
+
658
838
  REVIEW_ARTIFACT=""
659
839
  REVIEW_FINGERPRINT=""
660
840
 
@@ -711,7 +891,7 @@ case "$mode" in
711
891
  fi
712
892
  ;;
713
893
  *)
714
- echo "usage: $0 plan <plan-file> | code [extra focus...]" >&2
894
+ echo "usage: $0 plan <plan-file> [--nonce <n>] | code [--nonce <n>] [extra focus...]" >&2
715
895
  exit 2
716
896
  ;;
717
897
  esac
@@ -780,25 +960,16 @@ fence_env=(env
780
960
  CODEX_HOME="$real_codex_home")
781
961
 
782
962
  # --- Hard wall-clock cap via timeout(1) (gtimeout on macOS) -------------------
783
- # $timeout_bin was resolved ONCE (absolute path, type -P) at the banner emit above the run
784
- # reuses the exact binary the banner reported.
785
- if [[ -z "$timeout_bin" ]]; then
786
- echo "warning: no 'timeout'/'gtimeout' on PATH — running codex WITHOUT a hard wall-clock cap" >&2
787
- echo " (install coreutils to enable CODEX_HARD_TIMEOUT=$CODEX_HARD_TIMEOUT)." >&2
788
- fi
963
+ # $timeout_bin was resolved ONCE (absolute path, type -P) at the banner emit above and is
964
+ # guaranteed non-empty there (the fail-closed preflight) — the run reuses the exact binary the
965
+ # banner reported.
789
966
 
790
967
  # Run codex with the current ${codex_cmd[@]}; capture rc, stream → $trace.
791
968
  invoke_codex() {
792
969
  set +e
793
- if [[ -n "$timeout_bin" ]]; then
794
- printf '%s' "$prompt" \
795
- | "${fence_env[@]}" "$timeout_bin" --kill-after=15s "$CODEX_HARD_TIMEOUT" "${codex_cmd[@]}" >"$trace" 2>&1
796
- rc=$?
797
- else
798
- printf '%s' "$prompt" \
799
- | "${fence_env[@]}" "${codex_cmd[@]}" >"$trace" 2>&1
800
- rc=$?
801
- fi
970
+ printf '%s' "$prompt" \
971
+ | "${fence_env[@]}" "$timeout_bin" --kill-after=15s "$CODEX_HARD_TIMEOUT" "${codex_cmd[@]}" >"$trace" 2>&1
972
+ rc=$?
802
973
  set -e
803
974
  }
804
975
 
@@ -875,4 +1046,4 @@ fi
875
1046
  # codex is grounded by construction (AGENTS.md auto-merge + the precomputed change set): grounded
876
1047
  # true, factsHash null (native grounding — no separate facts payload exists). Every codex run is a
877
1048
  # full fresh run (one-shot, no resume) → fresh:true. $REVIEW_PROBE marks a guards-relaxed run.
878
- write_review_receipt "$REVIEW_ARTIFACT" true "$REVIEW_FINGERPRINT" "$verdict" true "" "$REVIEW_PROBE"
1049
+ write_review_receipt "$REVIEW_ARTIFACT" true "$REVIEW_FINGERPRINT" "$verdict" true "" "$REVIEW_PROBE" "" "$out"