@sabaiway/agent-workflow-kit 5.1.0 → 5.2.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 (49) hide show
  1. package/CHANGELOG.md +55 -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 +8 -5
  11. package/bridges/codex-cli-bridge/bin/codex-exec.sh +3 -2
  12. package/bridges/codex-cli-bridge/bin/codex-review.sh +205 -34
  13. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +276 -5
  14. package/bridges/codex-cli-bridge/capability.json +8 -6
  15. package/bridges/codex-cli-bridge/references/driving-codex.md +2 -2
  16. package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +2 -2
  17. package/capability.json +1 -1
  18. package/package.json +1 -1
  19. package/references/modes/flow-writer.md +37 -0
  20. package/references/modes/gates.md +4 -4
  21. package/references/modes/procedures.md +4 -2
  22. package/references/modes/receipt-deadline.md +16 -0
  23. package/references/modes/review-state.md +1 -1
  24. package/references/modes/set-flow.md +22 -0
  25. package/tools/cheap-agents.mjs +8 -2
  26. package/tools/commands.mjs +24 -2
  27. package/tools/commit-guard.mjs +44 -9
  28. package/tools/core-evidence.mjs +25 -22
  29. package/tools/detect-backends.mjs +32 -11
  30. package/tools/doc-parity.mjs +21 -6
  31. package/tools/flow-check.mjs +806 -0
  32. package/tools/flow-record.mjs +795 -0
  33. package/tools/flow-store-read.mjs +114 -0
  34. package/tools/flow-store.mjs +1178 -0
  35. package/tools/flow-writer.mjs +1265 -0
  36. package/tools/fs-read-nofollow.mjs +128 -0
  37. package/tools/gates-declaration.mjs +184 -0
  38. package/tools/gates-init.mjs +59 -17
  39. package/tools/orchestration-config.mjs +87 -10
  40. package/tools/orchestration-write.mjs +3 -3
  41. package/tools/plan-files.mjs +35 -0
  42. package/tools/procedures.mjs +75 -11
  43. package/tools/receipt-deadline.mjs +242 -0
  44. package/tools/recipes.mjs +21 -0
  45. package/tools/repo-lex.mjs +22 -0
  46. package/tools/review-state.mjs +240 -80
  47. package/tools/run-gates.mjs +361 -139
  48. package/tools/set-flow.mjs +465 -0
  49. package/tools/velocity-profile.mjs +8 -2
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
+ ## 5.2.0 — the flow machinery ships whole: recorded subset budgets, flow-bound finals, the round arms, and the dogfooded pipeline (AD-086)
8
+
9
+ **Everything the flow series built after 5.1.0 lands as one wave** — the record vocabulary, the
10
+ fd-custody store, chain identity + `flow-check`, `set-flow` arming, receipt-deadline + wrapper
11
+ finding manifests, and this release's closing moves. The flow schema (`schema: 1`) was never
12
+ published before, so every schema move below arrives migration-free.
13
+
14
+ - **`--pre-review` under an ARMED flow is a RECORDED lane** (new store-global `subset-attempt`
15
+ kind): every subset run appends an attempt with a lock-computed monotone `attemptIndex`; the
16
+ run producing the SECOND red at a key completes, records it, and exits red printing the
17
+ diagnosis rule; a third attempt requires a recorded, byte-distinct `--diagnosis`; past three
18
+ reds only a recorded fresh-eyes consult verdict reopens the key — every stop is self-servable,
19
+ never a wait-for-a-human. A `pregateExclude` change opens a fresh counting context (new
20
+ `subsetDigest`), and an unarmed repo's `--pre-review` stays byte-unchanged (characterized).
21
+ - **`run-gates --final` binds the flow store to the release decision** (AD-086 D10): the minted
22
+ `final` carries an OPTIONAL `evidenceHashes.flow` — the owner-scoped projection hash — with
23
+ projection movement under the run an `integrityFailure`; `commit-guard` (its lane only, after
24
+ the dead-green selection) refuses a green final whose hash no longer matches the live
25
+ projection, and an armed flow whose latest green final LACKS the field fails closed naming the
26
+ fresh-`--final` remedy. No `EVIDENCE_SCHEMA_VERSION` bump: the released 5.1.0 validator accepts
27
+ a flow-bearing final (proven against `4b08ace` worktree bytes, both directions).
28
+ - **The writer's round machinery arms**: `round-open` (pre-dispatch mint — per-backend watermark
29
+ + fresh nonce, printed as pasteable dispatch + `receipt-deadline` lines) · `round-land`
30
+ (arrival binding — receipt/manifest digests computed FROM the files; foreign, ambiguous,
31
+ malformed, or symlinked artifacts refuse) · per-finding `--dispose` ledgers ·
32
+ `freeze`/`unfreeze`/`converged` with completeness walks · `internal-attestation` refusing
33
+ while any in-flight plan lacks an adopted chain. Caps (3 rounds/cycle, 1 unfreeze, the
34
+ redesign valve) enforce at the arms; an over-cap mint requires a recorded `--justification`.
35
+ - **SECURITY — the receipts reader no longer follows symlinks** (new `fs-read-nofollow` leaf): a
36
+ symlinked/FIFO/directory receipts path surfaces as a loud `readError` through all four
37
+ consumers (review-state, core-evidence summary, flow-writer, flow-check) instead of content or
38
+ an empty success. The 5.1.0 reader read straight through a symlink — this is the one behavior
39
+ change for a conceivable existing loop, and the documented `AW_REVIEW_RECEIPTS` regular-file
40
+ lane is untouched (suite-pinned), so it ships as a flagged security fix, not an API break.
41
+ - **Bridge bumps ride the kit mirrors** — codex-cli-bridge **3.3.0** · antigravity-cli-bridge
42
+ **5.1.0**: an additive `--nonce <n>` flag on both review wrappers — the plain-argument lane
43
+ onto the existing `AW_REVIEW_NONCE` seam for hosts whose dispatch policy has no env-prefix
44
+ form (one seam: a disagreeing flag+env pair refuses pre-spend; a nonce-less run adds no nonce
45
+ field and mints no manifest — the `wrapperVersion` field every receipt carries moves with the
46
+ release), and a manifest-mint FAILURE whose temp unlink also fails now names the orphan
47
+ path on both failure codes.
48
+ - **Smaller rungs**: the veteran-store arming fence (#65 — arming a store with pre-flow red
49
+ finals no longer bricks `flow-check`; reds scope to the earliest own adoption instant,
50
+ fail-closed edges) · mixed declared+undeclared delta forks name the undeclared path ·
51
+ `velocity-profile` credential floor null-guards an unparseable harness version (was
52
+ fail-open via `null >= 0`) · `set-flow` empty/duplicate `pregateExclude` ids exit 2 at parse
53
+ time · the read surface's import closure is pinned write-free and acyclic
54
+ (`read-graph-purity.test.mjs`) · suite file-URL paths go through `fileURLToPath` (Windows).
55
+ - **The pipeline is dogfooded end to end**: a permanent hermetic tracked-`docs/ai` fixture
56
+ (`flow-dogfood.integration.test.mjs`) drives the REAL CLIs through set-flow → adoption →
57
+ subset-attempts → two nonce-bound rounds → bookkeeping-delta + refresh → freeze/converged →
58
+ `--final` with `evidenceHashes.flow` → guard PASS → the D10 post-final-append refusal — plus
59
+ the second-red stop pair and the foreign-worktree advisory lane. This repo itself released
60
+ under the armed flow.
61
+
7
62
  ## 5.1.0 — the config learns to carry the flow block before anything writes one (AD-085)
8
63
 
9
64
  **Upgrade this before any flow feature arrives — that ordering IS the release.** The shared
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: agent-workflow-kit
3
3
  description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow".
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '5.1.0'
6
+ version: '5.2.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
@@ -131,6 +131,14 @@ writer — read `${CLAUDE_SKILL_DIR}/references/modes/set-recipe.md` before acti
131
131
 
132
132
  writer — read `${CLAUDE_SKILL_DIR}/references/modes/set-autonomy.md` before acting.
133
133
 
134
+ ### Mode: set-flow
135
+
136
+ writer — read `${CLAUDE_SKILL_DIR}/references/modes/set-flow.md` before acting.
137
+
138
+ ### Mode: flow-writer
139
+
140
+ writer — read `${CLAUDE_SKILL_DIR}/references/modes/flow-writer.md` before acting.
141
+
134
142
  ### Mode: autonomy-doctor
135
143
 
136
144
  guarded — read `${CLAUDE_SKILL_DIR}/references/modes/autonomy-doctor.md` before acting.
@@ -171,6 +179,10 @@ guarded — read `${CLAUDE_SKILL_DIR}/references/modes/bridge-settings.md` befor
171
179
 
172
180
  read-only — read `${CLAUDE_SKILL_DIR}/references/modes/review-state.md` before acting.
173
181
 
182
+ ### Mode: receipt-deadline
183
+
184
+ read-only — read `${CLAUDE_SKILL_DIR}/references/modes/receipt-deadline.md` before acting.
185
+
174
186
  ### Mode: grounding
175
187
 
176
188
  writer — read `${CLAUDE_SKILL_DIR}/references/modes/grounding.md` before acting.
@@ -2,7 +2,7 @@
2
2
  name: antigravity-cli-bridge
3
3
  description: Delegate work to Google's Antigravity CLI (`agy`) — the successor to Gemini CLI — to reach Gemini, Claude, and GPT-OSS models under a Google AI Pro/Ultra subscription from the terminal. Use when the user wants to run a headless `agy` prompt, hand a focused task or second-opinion review to `agy`, install or authenticate Antigravity CLI, check or economise its quota/models, bridge project context into `agy`, set up a second delegated-execution backend beside Codex, or troubleshoot `agy` flags, models, auth, conversations, or its no-JSON headless behaviour.
4
4
  metadata:
5
- version: '5.0.0'
5
+ version: '5.1.0'
6
6
  ---
7
7
 
8
8
  # antigravity-cli-bridge
@@ -140,8 +140,19 @@ session error. One stderr banner states the actual posture (`review posture: mod
140
140
  and the receipt records the same `posture {model}`; an attesting review with `AGY_MODEL` explicitly
141
141
  emptied refuses pre-spend (`AGY_PROBE=1` exempt), and control bytes in a model string refuse
142
142
  pre-spend in every mode. The `timeout=` field is **banner-only** (exactly the duration `agy-run`
143
- hands to `timeout(1)`, or `uncapped`) informational, never a receipt field. **Quote the posture
144
- banner verbatim** when labeling a dispatch.
143
+ hands to `timeout(1)`; without a capping binary `agy-review` fails CLOSED pre-spend)
144
+ informational, never a receipt field. **Quote the posture banner verbatim** when labeling a
145
+ dispatch.
146
+
147
+ **Flow dispatch nonce (`AW_REVIEW_NONCE`, unset by default):** under the safe grammar
148
+ `[A-Za-z0-9._-]{1,64}` (anything else refuses pre-spend), a successful review first mints the
149
+ finding MANIFEST `agent-workflow-finding-manifest-agy-<nonce>.json` beside the receipts file —
150
+ atomic, no-clobber, ORDERED before the receipt append; a failed mint EXCLUDES the receipt, so a
151
+ nonce-supplied dispatch never lands a receipt without its readable manifest. Nonce-less runs add
152
+ no nonce field and mint nothing (the `wrapperVersion` field every receipt carries moves with each
153
+ release). `agy-review … --nonce <n>` is the plain-argument equivalent
154
+ (one seam; flag and a non-empty env must agree, a disagreeing pair refuses pre-spend) — the lane
155
+ for hosts whose dispatch policy has no env-prefix form.
145
156
 
146
157
  Frontier default `Gemini 3.1 Pro (High)`; **any** model is allowed (a sub-frontier one earns a
147
158
  silenceable `AGY_PROBE=1` advisory). An oversized `code` review is **DELIVERED, not refused**: the
@@ -46,6 +46,10 @@
46
46
  # arms nothing; an oversized CODE review is delivered as a chunked feed
47
47
  # AW_REVIEW_RECEIPTS override the review-receipt file (default: <git dir>/
48
48
  # agent-workflow-review-receipts.jsonl — see the --help Receipt block)
49
+ # AW_REVIEW_NONCE the dispatch nonce (safe grammar [A-Za-z0-9._-]{1,64}) — when supplied,
50
+ # a successful review first mints the finding manifest beside the receipt;
51
+ # the --nonce <n> flag is its plain-argument equivalent (one seam — a
52
+ # disagreeing flag+env pair refuses pre-spend)
49
53
  set -euo pipefail
50
54
 
51
55
  # --- --help / -h (pre-preflight: no agy, no login, no git tree needed) ---------
@@ -57,15 +61,16 @@ case "${1:-}" in
57
61
  agy-review — grounded read-only ADVISORY review by Google's Antigravity CLI (agy; subscription-only).
58
62
 
59
63
  Usage:
60
- agy-review code [--facts @f] [--ungrounded] [--decided @f] [--focus "…"] [extra focus…]
61
- agy-review plan <plan-file> [--facts @f] [--decided @f] [--focus "…"]
62
- agy-review diff <diff-file> [--facts @f] [--decided @f] [--focus "…"]
64
+ agy-review code [--facts @f] [--ungrounded] [--decided @f] [--focus "…"] [--nonce <n>] [extra focus…]
65
+ agy-review plan <plan-file> [--facts @f] [--decided @f] [--focus "…"] [--nonce <n>]
66
+ agy-review diff <diff-file> [--facts @f] [--decided @f] [--focus "…"] [--nonce <n>]
63
67
 
64
68
  Flags:
65
69
  --facts @f — verified facts the review runs AGAINST (code mode REQUIRES a non-empty payload; plan/diff warn loudly when omitted)
66
70
  --ungrounded — deliberately ungrounded CODE review, a throwaway opinion (code mode only, contradicts --facts; the receipt records grounded:false and never attests)
67
71
  --decided @f — already-decided / already-addressed list; do NOT re-raise (anti-circling; the round-2 payload)
68
72
  --focus "…" — extra focus (repeatable; code mode also takes trailing focus words)
73
+ --nonce <n> — the flow dispatch nonce, the plain-argument lane onto the AW_REVIEW_NONCE seam (one seam: flag and a non-empty env must agree; a disagreeing pair refuses pre-spend)
69
74
 
70
75
  Grounding:
71
76
  grounded review — agy reads NOTHING by default, an ungrounded review GUESSES:
@@ -78,15 +83,16 @@ Notes:
78
83
  pre-dispatch host-diff: before the FIRST dispatch of this bridge, diff its declared networkHosts
79
84
  against the live sandbox allow-list — a missing host is surfaced to the maintainer BEFORE
80
85
  dispatching, never fired into a known prompt
81
- the review posture banner appends a banner-only timeout=<duration|uncapped> field — exactly the
82
- duration agy-run hands to timeout(1), uncapped when no timeout/gtimeout binary caps the run;
83
- INFORMATIONAL only: it never enters the receipt posture or the D5 banner↔receipt parity
86
+ the review posture banner appends a banner-only timeout=<duration> field — exactly the duration
87
+ agy-run hands to timeout(1); the hard-timeout preflight fails CLOSED when no timeout/gtimeout
88
+ binary exists (the wrapper refuses by name before any CLI run, so an uncapped review run can no
89
+ longer happen), and the field never enters the receipt posture or the D5 banner↔receipt parity
84
90
  quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated
85
91
  posture; a prose re-type drifts
86
92
 
87
93
  Round-2 / resume:
88
- agy-review --continue [--decided @f] [--focus "…"]
89
- agy-review --conversation <id> [--decided @f] [--focus "…"]
94
+ agy-review --continue [--decided @f] [--focus "…"] [--nonce <n>]
95
+ agy-review --conversation <id> [--decided @f] [--focus "…"] [--nonce <n>]
90
96
  (a continuation sends a small delta — agy holds the artifact server-side; --facts is invalid on a continuation)
91
97
 
92
98
  Receipt:
@@ -116,7 +122,17 @@ Receipt:
116
122
  particular value; absent by construction on plan/diff/continuation receipts, which carry no change
117
123
  set; a run whose output carries NO recognized '### Verdict' section — empty
118
124
  output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session
119
- error); a write failure warns, never fails the review
125
+ error); when the dispatch nonce seam is supplied — the AW_REVIEW_NONCE environment value or
126
+ its plain-argument equivalent --nonce <n> (one seam: the flag assigns the same value;
127
+ supplying both with different values refuses pre-spend) — under the safe grammar
128
+ [A-Za-z0-9._-]{1,64} (anything else refuses pre-spend), the wrapper first mints the finding
129
+ MANIFEST {schema, backend, nonce, fingerprint, findings} beside the receipts file
130
+ (agent-workflow-finding-manifest-<backend>-<nonce>.json; atomic, no-clobber — a byte-identical
131
+ rewrite is an idempotent no-op, different bytes refuse loudly) ORDERED before the receipt
132
+ append — a failed manifest write EXCLUDES the receipt append, so a nonce-supplied dispatch can
133
+ never land a receipt without its readable manifest; a nonce-less invocation adds NO nonce field
134
+ and mints NO finding manifest (the existing wrapperVersion field still changes with each bridge
135
+ release); a write failure warns, never fails the review
120
136
 
121
137
  Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins over built-in default):
122
138
  ${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf
@@ -257,8 +273,9 @@ aw_apply_settings
257
273
 
258
274
  # --- Effective-timeout resolver (D5 banner honesty; AD-061) --------------------
259
275
  # ONE rule, both bridges: the posture banner prints EXACTLY the duration handed to timeout(1) —
260
- # an integer-seconds value rendered with the `s` suffix, a duration string verbatim and
261
- # `timeout=uncapped` when no timeout/gtimeout binary can cap the run; never a fabricated number.
276
+ # an integer-seconds value rendered with the `s` suffix, a duration string verbatim; without a
277
+ # capping binary the EXEC wrappers print `timeout=uncapped` and run, while the REVIEW wrappers
278
+ # refuse pre-spend (fail-closed preflight) — never a fabricated number.
262
279
  # The EFFECTIVE value (env included — closing the aw_settings_valid env bypass) is validated by
263
280
  # the same per-key rule as the settings file, plus a 7-digit integer-part bound (overflow); an
264
281
  # invalid value warns + falls back to the built-in default — a typo never silently masquerades
@@ -309,7 +326,7 @@ DEFAULT_AGY_REVIEW_MODEL="Gemini 3.1 Pro (High)"
309
326
  # Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
310
327
  # version (drift-guarded by agy-review.test.mjs against capability.json).
311
328
  AW_RECEIPT_BACKEND="agy"
312
- AW_BRIDGE_VERSION="5.0.0"
329
+ AW_BRIDGE_VERSION="5.1.0"
313
330
  # `-` not `:-` so an EXPLICIT empty AGY_MODEL= survives (drop --model, use settings.json — agy.sh:52).
314
331
  AGY_MODEL="${AGY_MODEL-$DEFAULT_AGY_REVIEW_MODEL}"
315
332
  # D5 control-byte screen — IMMEDIATELY after resolution, BEFORE the off-frontier advisory (or any
@@ -343,6 +360,14 @@ AGY_PROBE="${AGY_PROBE:-0}"
343
360
  # review-state gate rejects it — a guards-relaxed review must never attest a tree.
344
361
  REVIEW_PROBE=false
345
362
  if [[ "$AGY_PROBE" == "1" ]]; then REVIEW_PROBE=true; fi
363
+ # The dispatch nonce seam (flow-orchestration Decision 2/P5): validated pre-spend under the SAFE
364
+ # grammar — a nonce that would escape the derived manifest name refuses before any CLI run. The
365
+ # bracket expression ENUMERATES the ASCII set (no ranges): a range like A-Z is locale-collation-
366
+ # dependent and could admit a non-ASCII nonce the kit's JS reader then refuses.
367
+ if [[ -n "${AW_REVIEW_NONCE:-}" && ! "${AW_REVIEW_NONCE}" =~ ^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-]{1,64}$ ]]; then
368
+ 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
369
+ exit 2
370
+ fi
346
371
  # RETIRED (D3), not removed: the key stays RECOGNIZED by the shared settings registry so an existing
347
372
  # settings line never starts warning as unknown — but it arms NOTHING. The `--add-dir` offload told
348
373
  # agy to read a staging file; headless agy AUTO-DENIES its own read_file tool, so that lane could
@@ -442,6 +467,19 @@ if ! command -v agy >/dev/null 2>&1; then
442
467
  echo "error: 'agy' (Antigravity CLI) not found on PATH. See this skill's setup/README.md." >&2
443
468
  exit 127
444
469
  fi
470
+ # The hard-timeout cap is a TWO-STAGE guarantee (flow-orchestration #26): (1) THIS parent
471
+ # preflight fails CLOSED without a capping binary — an uncapped review run is refused before any
472
+ # CLI spend; (2) the agy-run child re-resolves the binary at dispatch time, so the exported seam
473
+ # below makes ITS missing-binary lane refuse too — a delete-between race can never silently void
474
+ # the cap. The banner uses the parent-resolved path; the child's mandatory recheck is the seam's.
475
+ aw_review_timeout_bin="$(aw_resolve_timeout_bin)"
476
+ if [[ -z "$aw_review_timeout_bin" ]]; then
477
+ echo "error: no 'timeout'/'gtimeout' binary on PATH — the hard-timeout preflight fails CLOSED:" >&2
478
+ echo " an uncapped review run is refused before any CLI spend. Install coreutils (timeout;" >&2
479
+ echo " on macOS: brew install coreutils for gtimeout), then re-run." >&2
480
+ exit 127
481
+ fi
482
+ export AGY_REQUIRE_TIMEOUT_BIN=1
445
483
  # Delegate execution to agy-run (the single home of the timeout + subscription + byte-ceiling guards);
446
484
  # fall back to the sibling agy.sh on a fresh checkout / hermetic test where agy-run is not yet linked.
447
485
  if command -v agy-run >/dev/null 2>&1; then
@@ -449,6 +487,16 @@ if command -v agy-run >/dev/null 2>&1; then
449
487
  else
450
488
  AGY_RUN="$HERE/agy.sh"
451
489
  fi
490
+ # The seam is a guarantee only when the RESOLVED child honors it: a stale installed agy-run that
491
+ # never reads AGY_REQUIRE_TIMEOUT_BIN would keep its uncapped lane past the parent preflight —
492
+ # refuse loudly and name the refresh recovery instead of dispatching on a hope.
493
+ aw_child_path="$(command -v "$AGY_RUN" 2>/dev/null || printf '%s' "$AGY_RUN")"
494
+ if ! grep -q "AGY_REQUIRE_TIMEOUT_BIN" "$aw_child_path" 2>/dev/null; then
495
+ echo "error: the resolved agy-run child ($aw_child_path) does not honor the AGY_REQUIRE_TIMEOUT_BIN seam —" >&2
496
+ echo " a stale bridge install could run uncapped past the parent preflight. Refresh the placed" >&2
497
+ echo " bridges (/agent-workflow-kit setup --refresh-placed), then re-run." >&2
498
+ exit 127
499
+ fi
452
500
 
453
501
  # --- Model policy (advisory, NOT a gate) -------------------------------------
454
502
  is_frontier=0
@@ -689,7 +737,104 @@ posture_json() {
689
737
  fi
690
738
  }
691
739
 
692
- # write_review_receipt <artifact|""> <fresh: true|false> <fingerprint|""> <verdict> <grounded: true|false> <factsHash|""> [probe: true|false]
740
+ # write_finding_manifest <receipts-path> <fingerprint|""> <findings-file> the wrapper-minted
741
+ # finding MANIFEST (flow-orchestration Decision 2 / P5 / P24-25), minted ONLY when the dispatch
742
+ # nonce seam AW_REVIEW_NONCE is supplied: {schema, backend, nonce, fingerprint, findings} lands
743
+ # beside the receipts file under the {backend, nonce}-derived name. The write is ATOMIC (temp +
744
+ # hard-link publish) and NO-CLOBBER: a byte-identical re-write is an idempotent no-op, different
745
+ # bytes refuse loudly. Returns non-zero on ANY failure — the caller then EXCLUDES the receipt
746
+ # append, so a nonce-supplied dispatch can never land a receipt without its readable manifest.
747
+ # A nonce-less invocation returns 0 untouched (no nonce field, no manifest — the receipt
748
+ # otherwise proceeds unchanged).
749
+ write_finding_manifest() {
750
+ local receipts="$1" fingerprint="$2" findings_file="$3" nonce="${AW_REVIEW_NONCE:-}"
751
+ [[ -n "$nonce" ]] || return 0
752
+ if [[ ! "$nonce" =~ ^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-]{1,64}$ ]]; then
753
+ 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
754
+ return 1
755
+ fi
756
+ if [[ -z "$findings_file" || ! -f "$findings_file" ]]; then
757
+ echo "error: no captured findings file to mint the manifest from — NO manifest was written." >&2
758
+ return 1
759
+ fi
760
+ local manifest mint_rc
761
+ manifest="$(dirname -- "$receipts")/agent-workflow-finding-manifest-${AW_RECEIPT_BACKEND}-${nonce}.json"
762
+ # The WHOLE mint core rides ONE node script (a family floor; the verdict parse already leans on
763
+ # it): FATAL UTF-8 compose (BOM kept — invalid bytes refuse rather than silently mutate the
764
+ # digest domain), an UNPREDICTABLE sibling temp opened with "wx" (O_CREAT|O_EXCL — a planted
765
+ # node at the name refuses, and ONLY a temp we provably created is ever unlinked), a hard-link
766
+ # publish (atomic no-clobber), and an EEXIST loser judged through ONE O_NOFOLLOW|O_NONBLOCK
767
+ # descriptor whose fstat must say REGULAR before the same-fd byte compare — no TOCTOU window,
768
+ # no symlink read-through, no FIFO hang. Exit: 0 minted or byte-identical no-op; 3 different
769
+ # bytes or a non-regular/symlink manifest; 5 minted-but-temp-left (SUCCESS with the orphan path
770
+ # on stdout — the caller warns loudly, never silently); 1 unreadable/non-UTF-8 findings or an
771
+ # fs failure.
772
+ local mint_out
773
+ mint_out="$( umask 077; node -e '
774
+ const fs = require("node:fs");
775
+ const { join, dirname, basename } = require("node:path");
776
+ const { randomBytes } = require("node:crypto");
777
+ const [file, backend, nonce, fingerprint, manifest] = process.argv.slice(1);
778
+ let code = 1;
779
+ let tmp = null;
780
+ try {
781
+ const findings = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(fs.readFileSync(file));
782
+ const bytes = Buffer.from(`${JSON.stringify({ schema: 1, backend, nonce, fingerprint: fingerprint || null, findings })}\n`);
783
+ const candidate = join(dirname(manifest), `.${basename(manifest)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
784
+ const fd = fs.openSync(candidate, "wx", 0o600);
785
+ tmp = candidate;
786
+ try {
787
+ fs.writeFileSync(fd, bytes);
788
+ } finally {
789
+ fs.closeSync(fd);
790
+ }
791
+ try {
792
+ fs.linkSync(tmp, manifest);
793
+ code = 0;
794
+ } catch (err) {
795
+ if (err && err.code === "EEXIST") {
796
+ try {
797
+ const mfd = fs.openSync(manifest, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
798
+ try {
799
+ code = !fs.fstatSync(mfd).isFile() ? 3 : fs.readFileSync(mfd).equals(bytes) ? 0 : 3;
800
+ } finally {
801
+ fs.closeSync(mfd);
802
+ }
803
+ } catch {
804
+ code = 3;
805
+ }
806
+ }
807
+ }
808
+ } catch {
809
+ code = 1;
810
+ }
811
+ if (tmp !== null) { try { fs.unlinkSync(tmp); } catch { if (code === 0) code = 5; process.stdout.write(tmp); } }
812
+ process.exit(code);
813
+ ' "$findings_file" "$AW_RECEIPT_BACKEND" "$nonce" "$fingerprint" "$manifest" 2>/dev/null )"
814
+ mint_rc=$?
815
+ if [[ $mint_rc -eq 5 ]]; then
816
+ echo "warning: the finding manifest was minted, but its temporary sibling could not be removed —" >&2
817
+ echo " orphan left at: ${mint_out} — remove it by hand (the manifest and the receipt are intact)." >&2
818
+ return 0
819
+ fi
820
+ if [[ $mint_rc -eq 3 ]]; then
821
+ 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
822
+ if [[ -n "$mint_out" ]]; then
823
+ echo " orphan left at: ${mint_out} — its temporary sibling could not be removed either; remove it by hand." >&2
824
+ fi
825
+ return 1
826
+ fi
827
+ if [[ $mint_rc -ne 0 ]]; then
828
+ 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
829
+ if [[ -n "$mint_out" ]]; then
830
+ echo " orphan left at: ${mint_out} — its temporary sibling could not be removed either; remove it by hand." >&2
831
+ fi
832
+ return 1
833
+ fi
834
+ return 0
835
+ }
836
+
837
+ # write_review_receipt <artifact|""> <fresh: true|false> <fingerprint|""> <verdict> <grounded: true|false> <factsHash|""> [probe: true|false] [delivery|""] [findings-file]
693
838
  # Appends ONE receipt line (the AD-038 fixture shape) as a side effect of a SUCCESSFUL review —
694
839
  # to $AW_REVIEW_RECEIPTS when set, else <git dir>/agent-workflow-review-receipts.jsonl (inside the
695
840
  # git dir by construction, so it is never committable). Fail-safe: every failure here warns loudly
@@ -699,8 +844,10 @@ posture_json() {
699
844
  # marker is written ALWAYS, true or false: the receipt SELF-DECLARES, so the gate reads the fact
700
845
  # itself instead of inferring it from this wrapper's version (which bumps in a different release
701
846
  # phase). Silence is not a declaration — an unmarked receipt is untrustworthy and the gate rejects it.
847
+ # The 9th argument feeds the finding-manifest mint: on a nonce-supplied dispatch the manifest is
848
+ # minted FIRST (atomic, no-clobber, ORDERED) and a failed mint EXCLUDES the receipt append.
702
849
  write_review_receipt() {
703
- local artifact="$1" fresh="$2" fingerprint="$3" verdict="$4" grounded="$5" facts_hash="$6" probe="${7:-false}" delivery="${8:-}"
850
+ local artifact="$1" fresh="$2" fingerprint="$3" verdict="$4" grounded="$5" facts_hash="$6" probe="${7:-false}" delivery="${8:-}" findings_file="${9:-}"
704
851
  local receipts="${AW_REVIEW_RECEIPTS:-}"
705
852
  if [[ -z "$receipts" ]]; then
706
853
  local receipt_git_dir
@@ -710,13 +857,24 @@ write_review_receipt() {
710
857
  fi
711
858
  receipts="$receipt_git_dir/agent-workflow-review-receipts.jsonl"
712
859
  fi
713
- local line probe_field=',"probe":false' delivery_field=""
860
+ if ! write_finding_manifest "$receipts" "$fingerprint" "$findings_file"; then
861
+ echo "warning: the finding manifest could not be minted — the receipt append is EXCLUDED (a" >&2
862
+ echo " nonce-supplied dispatch never lands a receipt without its readable manifest);" >&2
863
+ echo " the review itself succeeded — re-run it to mint the pair." >&2
864
+ return 0
865
+ fi
866
+ # A nonce-SUPPLIED dispatch stamps its nonce into the receipt too (the flow round-land matcher
867
+ # requires exact {backend, nonce} equality — dispatch identity end-to-end); the nonce is
868
+ # grammar-safe by the pre-spend check, and a nonce-less receipt stays BYTE-EXACT (the frozen
869
+ # compatibility floor).
870
+ local line probe_field=',"probe":false' delivery_field="" nonce_field=""
714
871
  if [[ "$probe" == "true" ]]; then probe_field=',"probe":true'; fi
715
872
  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}' \
873
+ if [[ -n "${AW_REVIEW_NONCE:-}" ]]; then nonce_field=",\"nonce\":\"${AW_REVIEW_NONCE}\""; fi
874
+ 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}' \
717
875
  "$(receipt_json_scalar "$artifact")" "$fresh" "$(receipt_json_scalar "$fingerprint")" \
718
876
  "$AW_RECEIPT_BACKEND" "$verdict" "$grounded" "$(receipt_json_scalar "$facts_hash")" \
719
- "$AW_BRIDGE_VERSION" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$probe_field" "$(posture_json)" "$delivery_field")"
877
+ "$AW_BRIDGE_VERSION" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$probe_field" "$(posture_json)" "$delivery_field" "$nonce_field")"
720
878
  if ! printf '%s\n' "$line" >>"$receipts" 2>/dev/null; then
721
879
  echo "warning: could not append the review receipt to $receipts — the review itself succeeded;" >&2
722
880
  echo " the review-state gate will read the current tree as un-receipted." >&2
@@ -1158,11 +1316,11 @@ case "${1:-}" in
1158
1316
  esac
1159
1317
 
1160
1318
  usage() {
1161
- echo "usage: $0 code [--facts @f] [--ungrounded] [--decided @f] [--focus \"…\"] [extra focus…]" >&2
1162
- echo " $0 plan <plan-file> [--facts @f] [--decided @f] [--focus \"…\"]" >&2
1163
- echo " $0 diff <diff-file> [--facts @f] [--decided @f] [--focus \"…\"]" >&2
1164
- echo " $0 --continue [--decided @f] [--focus \"…\"]" >&2
1165
- echo " $0 --conversation <id> [--decided @f] [--focus \"…\"]" >&2
1319
+ echo "usage: $0 code [--facts @f] [--ungrounded] [--decided @f] [--focus \"…\"] [--nonce <n>] [extra focus…]" >&2
1320
+ echo " $0 plan <plan-file> [--facts @f] [--decided @f] [--focus \"…\"] [--nonce <n>]" >&2
1321
+ echo " $0 diff <diff-file> [--facts @f] [--decided @f] [--focus \"…\"] [--nonce <n>]" >&2
1322
+ echo " $0 --continue [--decided @f] [--focus \"…\"] [--nonce <n>]" >&2
1323
+ echo " $0 --conversation <id> [--decided @f] [--focus \"…\"] [--nonce <n>]" >&2
1166
1324
  }
1167
1325
 
1168
1326
  # --- Mode dispatch (non-resume) ----------------------------------------------
@@ -1203,6 +1361,8 @@ fi
1203
1361
  FACTS_RAW=""
1204
1362
  DECIDED_RAW=""
1205
1363
  UNGROUNDED=0
1364
+ NONCE_FLAG=""
1365
+ NONCE_FLAG_SET=0
1206
1366
  FOCUS_PARTS=()
1207
1367
  # A value-taking flag must be followed by a real value — never end-of-args and never another flag.
1208
1368
  # Otherwise `agy-review code --facts --focus x` would silently take "--focus" as the facts and spend a
@@ -1231,6 +1391,20 @@ while [[ $# -gt 0 ]]; do
1231
1391
  need_value "$1" "${2:-}"; DECIDED_RAW="$2"; shift 2 ;;
1232
1392
  --focus)
1233
1393
  need_value "$1" "${2:-}"; FOCUS_PARTS+=("$2"); shift 2 ;;
1394
+ --nonce)
1395
+ # Not need_value: the nonce's CLOSED grammar admits leading-dash values and validates
1396
+ # deterministically below — the next argument is taken unconditionally (only end-of-args
1397
+ # refuses here), and presence rides its own flag so an EMPTY value still hits the grammar
1398
+ # screen and a duplicate after it still refuses.
1399
+ if [[ $# -lt 2 ]]; then
1400
+ echo "error: --nonce needs a value; got '<end of args>'." >&2
1401
+ exit 2
1402
+ fi
1403
+ if [[ "$NONCE_FLAG_SET" == "1" ]]; then
1404
+ echo "error: duplicate --nonce — one dispatch carries one nonce." >&2
1405
+ exit 2
1406
+ fi
1407
+ NONCE_FLAG="$2"; NONCE_FLAG_SET=1; shift 2 ;;
1234
1408
  --)
1235
1409
  echo "error: this wrapper OWNS the review posture — no '--' passthrough. The only escapes are" >&2
1236
1410
  echo " AGY_PROBE=1 (off-frontier model). An oversized code review is a chunked feed, not a flag." >&2
@@ -1252,6 +1426,23 @@ done
1252
1426
  # Merge --focus values and trailing focus words, in parse order, into ONE focus block.
1253
1427
  FOCUS="${FOCUS_PARTS[*]:-}"
1254
1428
 
1429
+ # --nonce rides the EXISTING AW_REVIEW_NONCE seam (flow FLOW-NONCE-DISPATCH-LANE): the
1430
+ # plain-argument lane for hosts whose dispatch policy has no env-prefix form. The flag and a
1431
+ # non-empty env value must AGREE — two disagreeing sources would mint an ambiguous dispatch
1432
+ # identity (fail closed). Grammar screen identical to the env screen above (enumerated ASCII —
1433
+ # locale-independent), pre-spend.
1434
+ if [[ "$NONCE_FLAG_SET" == "1" ]]; then
1435
+ if [[ ! "$NONCE_FLAG" =~ ^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-]{1,64}$ ]]; then
1436
+ 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
1437
+ exit 2
1438
+ fi
1439
+ if [[ -n "${AW_REVIEW_NONCE:-}" && "${AW_REVIEW_NONCE}" != "$NONCE_FLAG" ]]; then
1440
+ echo "error: --nonce disagrees with the AW_REVIEW_NONCE environment value — one dispatch carries one nonce; drop one source and re-run." >&2
1441
+ exit 2
1442
+ fi
1443
+ AW_REVIEW_NONCE="$NONCE_FLAG"
1444
+ fi
1445
+
1255
1446
  # --ungrounded closed grammar (D4): code-mode only, and an explicit contradiction with --facts refuses.
1256
1447
  if [[ "$UNGROUNDED" == "1" ]]; then
1257
1448
  if [[ -n "$FACTS_RAW" ]]; then
@@ -1607,12 +1798,11 @@ if [[ -z "$AGY_MODEL" && -z "$resume_mode" && "$REVIEW_PROBE" != "true" && "$REV
1607
1798
  exit 2
1608
1799
  fi
1609
1800
  # The timeout field is BANNER-ONLY (AD-061): it prints exactly the duration agy-run hands to
1610
- # timeout(1), or `uncapped` without a capping binary, and never enters the receipt posture.
1611
- # aw_resolve_timeout_bin: builtin type -P (an exported function can shadow neither `timeout` nor
1612
- # `type` itself), normalized to an absolute path fail-closed the banner can never claim a cap
1613
- # the agy-run child would not have.
1614
- aw_banner_timeout_bin="$(aw_resolve_timeout_bin)"
1615
- aw_timeout_banner="$(aw_timeout_label "$aw_banner_timeout_bin" "$AGY_HARD_TIMEOUT")"
1801
+ # timeout(1) and never enters the receipt posture. The banner uses the PARENT-preflight-resolved
1802
+ # path (builtin type -P, absolute); the child re-resolves at dispatch under the exported
1803
+ # AGY_REQUIRE_TIMEOUT_BIN seam, whose missing-binary lane refusesso an uncapped review
1804
+ # dispatch cannot exist on either stage.
1805
+ aw_timeout_banner="$(aw_timeout_label "$aw_review_timeout_bin" "$AGY_HARD_TIMEOUT")"
1616
1806
  echo "review posture: model=${AGY_MODEL:-<agy settings default>} timeout=$aw_timeout_banner" >&2
1617
1807
 
1618
1808
  # --- Execute via agy-run (single home of timeout + subscription + byte ceiling) ---
@@ -1673,7 +1863,7 @@ if [[ $rc -eq 0 ]]; then
1673
1863
  # A continuation never re-embeds the current artifact (agy holds the ORIGINAL round server-side;
1674
1864
  # --facts is rejected above), so it cannot attest the folded tree: fresh:false, artifact /
1675
1865
  # fingerprint / factsHash null, grounded false — informational-only, ignored by the state gate.
1676
- write_review_receipt "" false "" "$verdict" false "" "$REVIEW_PROBE"
1866
+ write_review_receipt "" false "" "$verdict" false "" "$REVIEW_PROBE" "" "$review_out_file"
1677
1867
  echo "notice: a continuation receipt is fresh:false (informational-only) — only a fresh grounded run" >&2
1678
1868
  echo " (agy-review code --facts @f) mints a receipt that satisfies the review-state gate." >&2
1679
1869
  else
@@ -1683,7 +1873,7 @@ if [[ $rc -eq 0 ]]; then
1683
1873
  grounded=true
1684
1874
  facts_hash="$(printf '%s' "$FACTS_CONTENT" | sha256_stdin || true)"
1685
1875
  fi
1686
- write_review_receipt "$REVIEW_ARTIFACT" true "$REVIEW_FINGERPRINT" "$verdict" "$grounded" "$facts_hash" "$REVIEW_PROBE" "$REVIEW_DELIVERY"
1876
+ write_review_receipt "$REVIEW_ARTIFACT" true "$REVIEW_FINGERPRINT" "$verdict" "$grounded" "$facts_hash" "$REVIEW_PROBE" "$REVIEW_DELIVERY" "$review_out_file"
1687
1877
  fi
1688
1878
  fi
1689
1879
  exit $rc