@sabaiway/agent-workflow-kit 14.20.0 → 14.22.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 (56) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +2 -2
  3. package/SKILL.md +1 -1
  4. package/bridges/antigravity-cli-bridge/SKILL.md +38 -50
  5. package/bridges/antigravity-cli-bridge/bin/agy-model-posture.test.mjs +257 -0
  6. package/bridges/antigravity-cli-bridge/bin/agy-review-harness.test.mjs +10 -3
  7. package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +20 -21
  8. package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +4 -4
  9. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +120 -48
  10. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +22 -62
  11. package/bridges/antigravity-cli-bridge/bin/agy.sh +109 -16
  12. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +5 -5
  13. package/bridges/antigravity-cli-bridge/capability.json +14 -7
  14. package/bridges/antigravity-cli-bridge/references/driving-agy.md +16 -20
  15. package/bridges/antigravity-cli-bridge/references/models-and-flags.md +10 -27
  16. package/bridges/codex-cli-bridge/SKILL.md +51 -49
  17. package/bridges/codex-cli-bridge/bin/codex-exec.sh +145 -46
  18. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +28 -55
  19. package/bridges/codex-cli-bridge/bin/codex-model-posture.test.mjs +371 -0
  20. package/bridges/codex-cli-bridge/bin/codex-review.sh +134 -36
  21. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +33 -45
  22. package/bridges/codex-cli-bridge/capability.json +22 -8
  23. package/bridges/codex-cli-bridge/references/driving-codex.md +18 -15
  24. package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +16 -12
  25. package/bridges/codex-cli-bridge/setup/README.md +3 -3
  26. package/capability.json +1 -1
  27. package/package.json +1 -1
  28. package/references/authoring/TASK_TEMPLATE.md +11 -16
  29. package/references/modes/agents.md +2 -2
  30. package/references/modes/bridge-settings.md +10 -7
  31. package/references/modes/procedures.md +7 -7
  32. package/references/modes/recipes.md +2 -2
  33. package/references/modes/review-state.md +1 -1
  34. package/references/modes/set-recipe.md +1 -1
  35. package/references/modes/tier.md +1 -1
  36. package/references/modes/velocity.md +1 -1
  37. package/references/reference-profile.json +1 -1
  38. package/references/templates/SPEC_TEMPLATE.md +2 -1
  39. package/references/templates/agent_rules.md +7 -7
  40. package/tools/bridge-catalog.mjs +114 -0
  41. package/tools/bridge-posture.mjs +11 -2
  42. package/tools/bridge-settings-read.mjs +41 -5
  43. package/tools/bridge-settings.mjs +111 -23
  44. package/tools/carriers.mjs +6 -4
  45. package/tools/cheap-agents.mjs +1 -1
  46. package/tools/detect-backends.mjs +6 -6
  47. package/tools/family-registry.mjs +3 -3
  48. package/tools/held-session.mjs +1 -33
  49. package/tools/manifest/schema.md +1 -1
  50. package/tools/manifest/validate.mjs +3 -2
  51. package/tools/procedures.mjs +86 -116
  52. package/tools/recipes.mjs +2 -2
  53. package/tools/review-state.mjs +1 -1
  54. package/tools/rules-regions.mjs +2 -1
  55. package/tools/tier-guide-facts.mjs +145 -159
  56. package/tools/tier-guide.mjs +8 -14
@@ -1,8 +1,8 @@
1
1
  // agy-review-honesty.test.mjs — the round-1 hardening pins for the D4/D5 arms (strip Phase 4):
2
2
  // the verdict parse is EXACT (an inexact heading or a token buried mid-line never attests —
3
- // `NOT SHIP` must not read as SHIP), and the empty-AGY_MODEL pre-spend refusal is scoped to the
4
- // ATTESTING branch only (plan / diff / --ungrounded code never attest, so they run and record
5
- // posture.model null). Colocated separately from agy-review.test.mjs — that file is
3
+ // `NOT SHIP` must not read as SHIP), and an explicitly empty AGY_MODEL is a one-off on every
4
+ // branch: refused pre-spend unless AGY_PROBE=1, whose receipt records posture.model null.
5
+ // Colocated separately from agy-review.test.mjs — that file is
6
6
  // red-proof-frozen; this one carries its own minimal standalone harness (the family idiom).
7
7
 
8
8
  import { describe, it, after } from 'node:test';
@@ -156,30 +156,29 @@ describe('agy-review — the verdict parse is EXACT (M1): a buried token never a
156
156
  });
157
157
  });
158
158
 
159
- describe('agy-review — the empty-model refusal is scoped to the ATTESTING branch (M3)', () => {
160
- it('plan and diff modes with AGY_MODEL= RUN (never attest) and record posture.model null', () => {
159
+ describe('agy-review — an explicitly empty AGY_MODEL is a one-off on every branch (M3)', () => {
160
+ it('plan, diff, --ungrounded and grounded code with AGY_MODEL= refuse pre-spend naming unset AGY_MODEL', () => {
161
+ for (const args of [['plan', 'plan.md'], ['diff', 'plan.md'], ['code', '--ungrounded'], ['code', '--facts', 'a tiny fact']]) {
162
+ const sb = makeSandbox();
163
+ const r = run(sb, { args, env: { AGY_MODEL: '' } });
164
+ const receipts = readReceipts(sb.repo);
165
+ rmSync(sb.home, { recursive: true, force: true });
166
+ assert.equal(r.status, 2, `${args.join(' ')}: ${r.stderr}`);
167
+ assert.equal(r.invoked, false, 'refused BEFORE any spend');
168
+ assert.equal(receipts.length, 0);
169
+ assert.match(r.stderr, /unset AGY_MODEL/, 'the remedy names unsetting it');
170
+ }
171
+ });
172
+
173
+ it('under AGY_PROBE=1 plan and diff modes with AGY_MODEL= run and record posture.model null', () => {
161
174
  for (const args of [['plan', 'plan.md'], ['diff', 'plan.md']]) {
162
175
  const sb = makeSandbox();
163
- const r = run(sb, { args, env: { AGY_MODEL: '', AGY_FAKE_OUTPUT: '### Verdict\nSHIP — fine.' } });
176
+ const r = run(sb, { args, env: { AGY_MODEL: '', AGY_PROBE: '1', AGY_FAKE_OUTPUT: '### Verdict\nSHIP — fine.' } });
164
177
  const receipts = readReceipts(sb.repo);
165
178
  rmSync(sb.home, { recursive: true, force: true });
166
- assert.equal(r.status, 0, `${args[0]} must run with an emptied model: ${r.stderr}`);
179
+ assert.equal(r.status, 0, `${args[0]} must run as a probe: ${r.stderr}`);
167
180
  assert.equal(r.invoked, true);
168
181
  assert.deepEqual(receipts[0].posture, { model: null }, 'an unknowable model is recorded null');
169
182
  }
170
183
  });
171
-
172
- it('code --ungrounded with AGY_MODEL= RUNS (its receipt never attests); grounded code still refuses', () => {
173
- const sb = makeSandbox();
174
- const un = run(sb, { args: ['code', '--ungrounded'], env: { AGY_MODEL: '', AGY_FAKE_OUTPUT: '### Verdict\nSHIP — throwaway.' } });
175
- const unReceipts = readReceipts(sb.repo);
176
- rmSync(sb.home, { recursive: true, force: true });
177
- assert.equal(un.status, 0, un.stderr);
178
- assert.deepEqual(unReceipts[0].posture, { model: null });
179
- const sb2 = makeSandbox();
180
- const grounded = run(sb2, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_MODEL: '' } });
181
- rmSync(sb2.home, { recursive: true, force: true });
182
- assert.notEqual(grounded.status, 0, 'the ATTESTING branch still refuses pre-spend');
183
- assert.equal(grounded.invoked, false);
184
- });
185
184
  });
@@ -1,6 +1,6 @@
1
1
  // agy-review-model-screen.test.mjs — the round-2 ordering pin (strip Phase 4, M6): the
2
- // control-byte screen fires IMMEDIATELY after AGY_MODEL resolution — BEFORE the off-frontier
3
- // advisory (or any other interpolation) can echo raw control bytes into stderr/the terminal.
2
+ // control-byte screen fires IMMEDIATELY after AGY_MODEL resolution — BEFORE the catalog check, the
3
+ // one-off refusal (or any other interpolation) can echo raw control bytes into stderr/the terminal.
4
4
  // Colocated separately: both earlier agy spec files are red-proof-frozen. Standalone harness.
5
5
 
6
6
  import { describe, it } from 'node:test';
@@ -33,7 +33,7 @@ const makePathWithout = (root, exclude = []) => {
33
33
  };
34
34
 
35
35
  describe('agy-review — the control-byte screen precedes EVERY interpolation of AGY_MODEL (M6)', () => {
36
- it('a control-byte NON-frontier model refuses pre-spend with NO advisory echo and NO raw byte on stderr', () => {
36
+ it('a control-byte model refuses pre-spend with NO later diagnostic and NO raw byte on stderr', () => {
37
37
  const home = mkdtempSync(join(tmpdir(), 'agy-model-screen-'));
38
38
  const bin = join(home, '.local', 'bin');
39
39
  mkdirSync(bin, { recursive: true });
@@ -70,7 +70,7 @@ describe('agy-review — the control-byte screen precedes EVERY interpolation of
70
70
  assert.equal(invoked, false, 'agy is never invoked');
71
71
  assert.equal(receipts, '', 'no receipt is minted');
72
72
  assert.match(r.stderr, /control/i, 'named as the control-byte class');
73
- assert.doesNotMatch(r.stderr, /non-frontier model/, 'the advisory never fires on a value the screen must refuse');
73
+ assert.doesNotMatch(r.stderr, /frontier|catalog|one-off/i, 'no retired advisory and no later check echoes a value the screen must refuse');
74
74
  assert.ok(!r.stderr.includes(String.fromCharCode(27)), 'no raw control byte ever reaches stderr');
75
75
  });
76
76
  });
@@ -35,12 +35,14 @@
35
35
  # agy-review --conversation <id> [--decided @f] [--focus "…"] # resume a specific conversation
36
36
  #
37
37
  # Environment (every optional var has an explicit default so a no-env run is safe under set -u):
38
- # AGY_MODEL default "Gemini 3.8 Flash (High)"; ANY model allowed (advisory warn off-frontier).
39
- # Set empty (AGY_MODEL=) to drop --model and use agy's settings.json.
38
+ # AGY_MODEL an environment value off the host posture (AGY_MODEL in the settings file, else
39
+ # "Gemini 3.8 Flash (High)") is a one-off — an explicitly empty one included —
40
+ # refused pre-spend unless AGY_PROBE=1. `/agent-workflow-kit bridge-settings`
41
+ # shows the models the installed agy offers and sets the host posture.
40
42
  # AGY_HARD_TIMEOUT default 30m (duration string; the timeout(1) hard cap via agy-run)
41
43
  # AGY_TIMEOUT default = AGY_HARD_TIMEOUT (agy's soft --print-timeout)
42
44
  # AGY_MAX_PROMPT_BYTES default 120000 (single-argv byte ceiling; see agy.sh)
43
- # AGY_PROBE=1 throwaway probe — silences the off-frontier model advisory
45
+ # AGY_PROBE=1 throwaway probe — the one route to a one-off model; its receipt never attests
44
46
  # AGY_REVIEW_MAX_TOTAL_BYTES default 240000 (the chunked feed's total outgoing prompt-byte ceiling)
45
47
  # AGY_REVIEW_ALLOW_ADDDIR RETIRED — recognized so an existing settings line never warns, but it
46
48
  # arms nothing; an oversized CODE review is delivered as a chunked feed
@@ -95,10 +97,12 @@ Notes:
95
97
  pre-dispatch host-diff: before the FIRST dispatch of this bridge, diff its declared networkHosts
96
98
  against the live sandbox allow-list — a missing host is surfaced to the maintainer BEFORE
97
99
  dispatching, never fired into a known prompt
98
- the review posture banner appends a banner-only timeout=<duration> field — exactly the duration
99
- agy-run hands to timeout(1); the hard-timeout preflight fails CLOSED when no timeout/gtimeout
100
- binary exists (the wrapper refuses by name before any CLI run, so an uncapped review run can no
101
- longer happen), and the field never enters the receipt posture or the D5 banner↔receipt parity
100
+ the review posture banner appends a banner-only source=model:<s> field — where the model came
101
+ from: default, setting or environment — then a banner-only timeout=<duration> field — exactly the
102
+ duration agy-run hands to timeout(1); the hard-timeout preflight fails CLOSED when no
103
+ timeout/gtimeout binary exists (the wrapper refuses by name before any CLI run, so an uncapped
104
+ review run can no longer happen), and neither field enters the receipt posture or the D5
105
+ banner↔receipt parity
102
106
  quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated
103
107
  posture; a prose re-type drifts
104
108
 
@@ -130,9 +134,9 @@ Receipt:
130
134
  review never attests) and equally rejects an unmarked one (silence is not a declaration);
131
135
  posture = the ACTUAL run posture {model} (agy has no tier), written on EVERY receipt (D5) — the
132
136
  gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the
133
- review), one stderr banner line states the same posture, an ATTESTING review with AGY_MODEL
134
- explicitly emptied refuses pre-spend, and a model string carrying control bytes refuses
135
- pre-spend in every mode; delivery = how the change set REACHED the model, currently emitted as
137
+ review), one stderr banner line states the same posture, a one-off model (an explicitly empty
138
+ AGY_MODEL included) refuses pre-spend unless AGY_PROBE=1, and a model string carrying control
139
+ bytes refuses pre-spend in every mode; delivery = how the change set REACHED the model, currently emitted as
136
140
  'inline' (the whole set rode one prompt — proven by construction) or 'fed' (a chunked feed whose
137
141
  per-part echo proof verified); REQUIRED on every agy code receipt and its ABSENCE is what stops a
138
142
  pre-fed-lane receipt attesting, while the gate accepts any well-formed declaration rather than a
@@ -160,11 +164,11 @@ Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins ov
160
164
  Honesty + posture (D4/D5):
161
165
  a run whose output carries NO recognized '### Verdict' section — empty output included — exits 4
162
166
  with NO receipt: a FAILED review to RE-RUN, never a fatal session error. One stderr banner line
163
- states the ACTUAL run posture (review posture: model=… timeout=…) and the receipt records the
167
+ states the ACTUAL run posture (review posture: model=… source=… timeout=…) and the receipt records the
164
168
  same posture {model} (agy has no tier; the timeout field is banner-only — never a receipt
165
- field). Quote the posture banner verbatim when labeling this dispatch. An ATTESTING review with
166
- AGY_MODEL explicitly emptied refuses pre-spend (the actual model would be unknowable;
167
- AGY_PROBE=1 is exempt), and a model string carrying control bytes refuses pre-spend in every
169
+ field). Quote the posture banner verbatim when labeling this dispatch. A one-off model — an
170
+ AGY_MODEL differing from the host posture, an explicitly empty one included — refuses pre-spend
171
+ unless AGY_PROBE=1, and a model string carrying control bytes refuses pre-spend in every
168
172
  mode.
169
173
 
170
174
  Closed grammar: unknown flags are rejected; no '--' passthrough (the flag escape is --ungrounded; the env escape is AGY_PROBE=1).
@@ -176,6 +180,7 @@ esac
176
180
 
177
181
  # This wrapper's applied settings-file subset (see the shared reader block below).
178
182
  AW_SETTINGS_APPLIED="AGY_HARD_TIMEOUT AGY_REVIEW_ALLOW_ADDDIR AGY_REVIEW_MAX_TOTAL_BYTES"
183
+ DEFAULT_AGY_REVIEW_MODEL="Gemini 3.8 Flash (High)"
179
184
 
180
185
  # --- Bridge settings file (host-level, kit-independent) — byte-identical across the four wrappers ---
181
186
  # ${XDG_CONFIG_HOME:-$HOME/.config}/agent-workflow/bridge-settings.conf holds KEY=VALUE lines,
@@ -196,7 +201,7 @@ aw_settings_file() {
196
201
  printf '%s/agent-workflow/bridge-settings.conf' "${XDG_CONFIG_HOME:-$HOME/.config}"
197
202
  }
198
203
  aw_settings_known() {
199
- 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
204
+ case " CODEX_SERVICE_TIER CODEX_HARD_TIMEOUT CODEX_REVIEW_MAX_TOTAL_BYTES AGY_HARD_TIMEOUT AGY_REVIEW_ALLOW_ADDDIR AGY_REVIEW_MAX_TOTAL_BYTES CODEX_MODEL CODEX_EFFORT AGY_MODEL " in
200
205
  *" $1 "*) return 0 ;;
201
206
  *) return 1 ;;
202
207
  esac
@@ -281,11 +286,40 @@ aw_apply_settings() {
281
286
  done
282
287
  return 0
283
288
  }
289
+ aw_read_posture() {
290
+ local key="$1" default="$2" empty="$3" file line value notify=1
291
+ AW_HOST_VALUE="$default"
292
+ AW_HOST_SOURCE="default"
293
+ file="$(aw_settings_file)"
294
+ [[ -n "${AW_SETTINGS_NOTIFIED:-}" ]] && notify=0
295
+ if [[ -f "$file" && -r "$file" ]]; then
296
+ line="$(grep "^${key}=" "$file" 2>/dev/null || true)"
297
+ if [[ -n "$line" ]]; then
298
+ value="${line##*$'\n'}"
299
+ value="${value#*=}"
300
+ if [[ -n "$value" && "$value" != *[$'\x01'-$'\x1f'$'\x7f']* ]]; then
301
+ AW_HOST_VALUE="$value"
302
+ AW_HOST_SOURCE="setting"
303
+ elif (( notify )); then
304
+ echo "warning: $key in bridge settings file '$file' is empty or carries a control byte — using the built-in default '$default'." >&2
305
+ fi
306
+ fi
307
+ fi
308
+ AW_RUN_VALUE="$AW_HOST_VALUE"
309
+ AW_RUN_SOURCE="$AW_HOST_SOURCE"
310
+ if [[ -n "${!key+x}" ]]; then
311
+ AW_RUN_VALUE="${!key:-$empty}"
312
+ AW_RUN_SOURCE="environment"
313
+ fi
314
+ return 0
315
+ }
284
316
  # Snapshot BEFORE the shared reader runs: it EXPORTS a settings-file value under the same name, so a
285
317
  # check made afterwards cannot tell a file value from an env override — and the retirement notice's
286
318
  # recovery differs by source (a file `--unset` cannot clear an env override).
287
319
  AGY_ALLOW_ADDDIR_FROM_ENV=0
288
320
  if [[ -n "${AGY_REVIEW_ALLOW_ADDDIR+x}" ]]; then AGY_ALLOW_ADDDIR_FROM_ENV=1; fi
321
+ aw_read_posture AGY_MODEL "$DEFAULT_AGY_REVIEW_MODEL" ""
322
+ HOST_AGY_MODEL="$AW_HOST_VALUE"; AGY_MODEL="$AW_RUN_VALUE"; AGY_MODEL_SOURCE="$AW_RUN_SOURCE"
289
323
  aw_apply_settings
290
324
 
291
325
  # --- Effective-timeout resolver (D5 banner honesty; AD-061) --------------------
@@ -339,23 +373,16 @@ aw_resolve_timeout_bin() {
339
373
  printf '%s' "$bin"
340
374
  }
341
375
 
342
- DEFAULT_AGY_REVIEW_MODEL="Gemini 3.8 Flash (High)"
343
376
  # Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
344
377
  # version (drift-guarded by agy-review.test.mjs against capability.json).
345
378
  AW_RECEIPT_BACKEND="agy"
346
- AW_BRIDGE_VERSION="5.7.1" # aw-version-anchor
347
- # `-` not `:-` so an EXPLICIT empty AGY_MODEL= survives (drop --model, use settings.json — agy.sh:52).
348
- AGY_MODEL="${AGY_MODEL-$DEFAULT_AGY_REVIEW_MODEL}"
349
- # D5 control-byte screen — IMMEDIATELY after resolution, BEFORE the off-frontier advisory (or any
350
- # other interpolation) can echo raw newline/ESC bytes into stderr/the terminal (round-2 fold).
379
+ AW_BRIDGE_VERSION="6.0.0" # aw-version-anchor
380
+ # D5 control-byte screen — IMMEDIATELY after resolution, BEFORE any interpolation can echo raw
381
+ # newline/ESC bytes into stderr/the terminal (round-2 fold).
351
382
  if [[ "$AGY_MODEL" == *[$'\x01'-$'\x1f'$'\x7f']* ]]; then
352
383
  echo "error: AGY_MODEL contains control bytes — fix the setting (env or bridge-settings.conf) and re-run." >&2
353
384
  exit 2
354
385
  fi
355
- # Frontier review models. ANY model is allowed; a sub-frontier one only earns a soft, silenceable
356
- # warning. Gemini 3.8 Flash (High) is asserted frontier-grade (AD-136, maintainer 2026-09-09); 3.7 Flash
357
- # (High) stays in the set (fork (a), maintainer 2026-08-14).
358
- FRONTIER_SET=("Gemini 3.8 Flash (High)" "Gemini 3.7 Flash (High)" "Gemini 3.1 Pro (High)" "Claude Opus 4.6 (Thinking)" "Claude Sonnet 4.6 (Thinking)")
359
386
 
360
387
  # Duration-string timeouts (NOT codex's bare seconds): agy-run forwards a duration to --print-timeout,
361
388
  # and the timeout(1) hard cap is a duration too — never numerically compared, so 30m vs 2h is fine.
@@ -678,14 +705,69 @@ if [[ ! -f "$AGY_ENVELOPE_READER" ]]; then
678
705
  exit 127
679
706
  fi
680
707
 
681
- # --- Model policy (advisory, NOT a gate) -------------------------------------
682
- is_frontier=0
683
- for _m in "${FRONTIER_SET[@]}"; do
684
- [[ "$AGY_MODEL" == "$_m" ]] && { is_frontier=1; break; }
685
- done
686
- if [[ "$is_frontier" != "1" && "$AGY_PROBE" != "1" ]]; then
687
- echo "warning: reviewing with a non-frontier model '${AGY_MODEL:-<settings.json default>}' — results may be" >&2
688
- echo " weaker (quality-first). Set AGY_PROBE=1 to silence, or AGY_MODEL to a frontier model." >&2
708
+ # --- The effective model against the installed CLI's catalog, then the one-off rule (pre-spend) ---
709
+ # ONE catalog read per user-visible run: the exported marker makes every child agy-run (the single
710
+ # dispatch and every fed turn) skip its own read, whatever this read's outcome or its absence.
711
+ AGY_CATALOG_TIMEOUT=30s
712
+ aw_agy_remedy() {
713
+ if [[ -z "$1" ]]; then
714
+ echo " remedy: unset AGY_MODEL in the environment (the host posture then decides)." >&2
715
+ return 0
716
+ fi
717
+ echo " remedy: set this line in $(aw_settings_file):" >&2
718
+ echo " AGY_MODEL=$1" >&2
719
+ echo " and unset AGY_MODEL in the environment." >&2
720
+ }
721
+ aw_agy_shown_model() {
722
+ if [[ -z "$AGY_MODEL" ]]; then printf '%s' "AGY_MODEL= (explicitly empty)"; return 0; fi
723
+ if [[ "$AGY_MODEL" == *[$'\x01'-$'\x1f'$'\x7f']* ]]; then printf '%s' "AGY_MODEL (a value carrying a control byte)"; return 0; fi
724
+ printf '%s' "AGY_MODEL='$AGY_MODEL'"
725
+ }
726
+ aw_agy_catalog_check() {
727
+ local raw="" rc=0 line="" remedy="" reason="" offered=()
728
+ if [[ -n "$1" ]]; then
729
+ raw="$("$1" --kill-after=5s "$AGY_CATALOG_TIMEOUT" agy models </dev/null 2>/dev/null)" || rc=$?
730
+ else
731
+ raw="$(agy models </dev/null 2>/dev/null)" || rc=$?
732
+ fi
733
+ if (( rc == 0 )); then
734
+ while IFS= read -r line; do
735
+ line="${line%$'\r'}"
736
+ [[ "$line" == *$'\t'* ]] || continue
737
+ line="${line#*$'\t'}"
738
+ if [[ -n "$line" && "$line" != *[$'\x01'-$'\x1f'$'\x7f']* ]]; then offered+=("$line"); fi
739
+ done <<<"$raw"
740
+ fi
741
+ if (( rc == 124 || rc == 137 )); then
742
+ reason="it exceeded $AGY_CATALOG_TIMEOUT"
743
+ elif (( rc != 0 )); then
744
+ reason="agy models exited $rc"
745
+ elif (( ${#offered[@]} == 0 )); then
746
+ reason="it offers no model"
747
+ fi
748
+ if [[ -n "$reason" ]]; then
749
+ echo "warning: the agy model catalog is unreadable ($reason) — the run proceeds unchecked; agy itself refuses a model it does not serve." >&2
750
+ return 0
751
+ fi
752
+ for line in "${offered[@]}"; do
753
+ if [[ "$line" == "$AGY_MODEL" ]]; then return 0; fi
754
+ done
755
+ remedy="${offered[0]}"
756
+ for line in "${offered[@]}"; do
757
+ if [[ "$line" == "$2" ]]; then remedy="$line"; fi
758
+ done
759
+ echo "error: $(aw_agy_shown_model) ($AGY_MODEL_SOURCE) is not a model the installed agy offers — refusing before any run is spent." >&2
760
+ printf -v line '%s, ' "${offered[@]}"
761
+ echo " offered: ${line%, }" >&2
762
+ aw_agy_remedy "$remedy"
763
+ exit 2
764
+ }
765
+ export AW_AGY_CATALOG_READ=1
766
+ if [[ -n "$AGY_MODEL" ]]; then aw_agy_catalog_check "$aw_review_timeout_bin" "$DEFAULT_AGY_REVIEW_MODEL"; fi
767
+ if [[ "$AGY_MODEL" != "$HOST_AGY_MODEL" && "$AGY_PROBE" != "1" ]]; then
768
+ echo "error: $(aw_agy_shown_model) is a one-off — the host posture is '$HOST_AGY_MODEL', and only a probe (AGY_PROBE=1) runs a one-off; refusing before any run is spent." >&2
769
+ aw_agy_remedy "$AGY_MODEL"
770
+ exit 2
689
771
  fi
690
772
 
691
773
  # --- Output shape + grounding helpers (the wrapper is the source of truth) -----
@@ -1691,7 +1773,7 @@ while [[ $# -gt 0 ]]; do
1691
1773
  NONCE_FLAG="$2"; NONCE_FLAG_SET=1; shift 2 ;;
1692
1774
  --)
1693
1775
  echo "error: this wrapper OWNS the review posture — no '--' passthrough. The only escapes are" >&2
1694
- echo " AGY_PROBE=1 (off-frontier model). An oversized code review is a chunked feed, not a flag." >&2
1776
+ echo " AGY_PROBE=1 (a probe, the one route to a one-off model). An oversized code review is a chunked feed, not a flag." >&2
1695
1777
  exit 2 ;;
1696
1778
  --*)
1697
1779
  echo "error: unknown flag '$1'." >&2; usage; exit 2 ;;
@@ -2076,26 +2158,16 @@ else
2076
2158
  fi
2077
2159
  fi
2078
2160
 
2079
- # --- D5 pre-spend posture gate + banner (one line, the ACTUAL run posture) ------------------
2080
- # The control-byte screen already ran at AGY_MODEL resolution (before any interpolation). An
2081
- # ATTESTING review (fresh code mode, guards on) whose wrapper cannot know the actual model
2082
- # (AGY_MODEL explicitly emptied → the CLI's own settings default decides) refuses pre-spend;
2083
- # AGY_PROBE=1 runs are exempt (their receipts never attest and record model null).
2084
- # Scoped to the ATTESTING branch only (grounded code, guards on): plan / diff / --ungrounded
2085
- # code mint receipts that never attest, so an emptied model just records posture.model null.
2086
- if [[ -z "$AGY_MODEL" && -z "$resume_mode" && "$REVIEW_PROBE" != "true" && "$REVIEW_ARTIFACT" == "code" && -n "$FACTS_CONTENT" ]]; then
2087
- echo "error: AGY_MODEL is explicitly empty, so the ACTUAL review model is unknowable (the agy CLI's" >&2
2088
- echo " own settings default decides) — an attesting review refuses pre-spend. Fix: unset" >&2
2089
- echo " AGY_MODEL (wrapper default), or set the real model display string; AGY_PROBE=1 is exempt." >&2
2090
- exit 2
2091
- fi
2161
+ # --- D5 banner (one line, the ACTUAL run posture) ------------------------------------------
2162
+ # An explicitly empty AGY_MODEL reaches here only as a probe (the one-off rule above); its receipt
2163
+ # records model null.
2092
2164
  # The timeout field is BANNER-ONLY (AD-061): it prints exactly the duration agy-run hands to
2093
2165
  # timeout(1) and never enters the receipt posture. The banner uses the PARENT-preflight-resolved
2094
2166
  # path (builtin type -P, absolute); the child re-resolves at dispatch under the exported
2095
2167
  # AGY_REQUIRE_TIMEOUT_BIN seam, whose missing-binary lane refuses — so an uncapped review
2096
2168
  # dispatch cannot exist on either stage.
2097
2169
  aw_timeout_banner="$(aw_timeout_label "$aw_review_timeout_bin" "$AGY_HARD_TIMEOUT")"
2098
- echo "review posture: model=${AGY_MODEL:-<agy settings default>} timeout=$aw_timeout_banner" >&2
2170
+ echo "review posture: model=${AGY_MODEL:-<agy settings default>} source=model:$AGY_MODEL_SOURCE timeout=$aw_timeout_banner" >&2
2099
2171
 
2100
2172
  # --- Execute via agy-run (single home of timeout + subscription + byte ceiling) ---
2101
2173
  # The dispatch is captured PRIVATELY as an envelope and its `response` becomes both the
@@ -17,44 +17,6 @@ import {
17
17
  isAssemblerBanner, seedFedChangeSet, inlineArtifactOf, bodyOf, requestedBlockOf, requestedOf, fedRun,
18
18
  } from './agy-review-harness.test.mjs';
19
19
 
20
- describe('agy-review.sh — model policy advisory (1)', { concurrency: 2 }, () => {
21
- it('warns for a non-frontier model but still runs', async () => {
22
- const sb = makeSandbox();
23
- const r = await run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_MODEL: 'Gemini 3.5 Flash (Low)' } });
24
- rmSync(sb.home, { recursive: true, force: true });
25
- assert.equal(r.status, 0, r.stderr);
26
- assert.match(r.stderr, /non-frontier model 'Gemini 3.5 Flash \(Low\)'/);
27
- assert.equal(r.invoked, true, 'a non-frontier model still runs (advisory, not a gate)');
28
- assert.match(r.argv, /Gemini 3\.5 Flash \(Low\)/, 'the chosen model reaches agy via --model');
29
- });
30
-
31
- it('AGY_PROBE=1 silences the advisory', async () => {
32
- const sb = makeSandbox();
33
- const r = await run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_MODEL: 'Gemini 3.5 Flash (Low)', AGY_PROBE: '1' } });
34
- rmSync(sb.home, { recursive: true, force: true });
35
- assert.equal(r.status, 0, r.stderr);
36
- assert.doesNotMatch(r.stderr, /non-frontier model/);
37
- });
38
-
39
- it('the frontier default (no AGY_MODEL) earns no advisory', async () => {
40
- const sb = makeSandbox();
41
- const r = await run(sb, { args: ['code', '--facts', 'a tiny fact'] });
42
- rmSync(sb.home, { recursive: true, force: true });
43
- assert.equal(r.status, 0, r.stderr);
44
- assert.doesNotMatch(r.stderr, /non-frontier model/);
45
- assert.match(r.argv, /Gemini 3\.8 Flash \(High\)/, 'the frontier default reaches agy');
46
- });
47
-
48
- it('an explicit Gemini 3.7 Flash (High) stays FRONTIER after the 3.8 pin move — no advisory (AD-136)', async () => {
49
- const sb = makeSandbox();
50
- const r = await run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_MODEL: 'Gemini 3.7 Flash (High)' } });
51
- rmSync(sb.home, { recursive: true, force: true });
52
- assert.equal(r.status, 0, r.stderr);
53
- assert.doesNotMatch(r.stderr, /non-frontier model/, 'a FRONTIER_SET member never warns');
54
- assert.match(r.argv, /Gemini 3\.7 Flash \(High\)/);
55
- });
56
- });
57
-
58
20
  // ── the pre-spend capability door (AGY-1.1.13, Decision 2) ───────────────────────────────────────
59
21
  // The wrapper drives agy in --output-format json and reads the returned envelope in node, so a host
60
22
  // that cannot do that must refuse BEFORE a subscription turn is spent. The door probes CAPABILITY,
@@ -1974,9 +1936,9 @@ describe('agy-review.sh — mode catalog ⟷ wrapper reality (manifest-pinned)',
1974
1936
  }
1975
1937
  });
1976
1938
 
1977
- it('AGY_PROBE really silences the advisory on EVERY review parent the catalog claims (behavioural)', async () => {
1939
+ it('a one-off is refused on EVERY review parent and AGY_PROBE=1 runs it (behavioural)', async () => {
1978
1940
  // The catalog CLAIMS these modes are modified by the hook; prove it per parent rather than
1979
- // trusting a source scan: the off-frontier advisory fires without it, is silent with it.
1941
+ // trusting a source scan: a one-off model is refused without it and runs with it.
1980
1942
  const hook = catalog.find((e) => e.key === 'AGY_PROBE');
1981
1943
  const drive = {
1982
1944
  'review.code': () => ['code', '--facts', 'f'],
@@ -1985,24 +1947,22 @@ describe('agy-review.sh — mode catalog ⟷ wrapper reality (manifest-pinned)',
1985
1947
  'review.continue': () => ['--continue'],
1986
1948
  'review.conversation': () => ['--conversation', 'conv-1'],
1987
1949
  };
1988
- assert.ok(hook.parents.length > 0, 'AGY_PROBE must claim at least one parent');
1950
+ assert.deepEqual(hook.parents, Object.keys(drive), 'AGY_PROBE claims every review parent');
1989
1951
  for (const parent of hook.parents) {
1990
1952
  assert.ok(drive[parent], `no behavioural drive for claimed parent "${parent}" — add one`);
1991
- // Both runs must really REACH agy: asserting the diagnostic text alone would let an early
1992
- // failure that never dispatched pass the probe-on branch (its stderr simply lacks the string).
1993
- const noisy = makeSandbox();
1994
- const off = await run(noisy, { args: drive[parent](noisy), env: { AGY_MODEL: 'Some Weak Model' } });
1995
- rmSync(noisy.home, { recursive: true, force: true });
1996
- assert.equal(off.status, 0, `${parent}: ${off.stderr}`);
1997
- assert.equal(off.invoked, true, `${parent}: the control run must reach agy`);
1998
- assert.match(off.stderr, /non-frontier model/, `${parent}: the advisory must fire without the hook`);
1999
-
2000
- const quiet = makeSandbox();
2001
- const on = await run(quiet, { args: drive[parent](quiet), env: { AGY_MODEL: 'Some Weak Model', AGY_PROBE: '1' } });
2002
- rmSync(quiet.home, { recursive: true, force: true });
1953
+ // The probe run must really REACH agy: an early failure that never dispatched must not pass.
1954
+ const refused = makeSandbox();
1955
+ const off = await run(refused, { args: drive[parent](refused), env: { AGY_MODEL: 'Gemini 3.7 Flash (Low)' } });
1956
+ rmSync(refused.home, { recursive: true, force: true });
1957
+ assert.equal(off.status, 2, `${parent}: ${off.stderr}`);
1958
+ assert.equal(off.invoked, false, `${parent}: a one-off is refused BEFORE any spend`);
1959
+ assert.match(off.stderr, /one-off/, `${parent}: the refusal names the one-off`);
1960
+
1961
+ const probed = makeSandbox();
1962
+ const on = await run(probed, { args: drive[parent](probed), env: { AGY_MODEL: 'Gemini 3.7 Flash (Low)', AGY_PROBE: '1' } });
1963
+ rmSync(probed.home, { recursive: true, force: true });
2003
1964
  assert.equal(on.status, 0, `${parent}: ${on.stderr}`);
2004
- assert.equal(on.invoked, true, `${parent}: AGY_PROBE=1 must still reach agy — silence must come from the hook, not from an early exit`);
2005
- assert.doesNotMatch(on.stderr, /non-frontier model/, `${parent}: AGY_PROBE=1 must really silence it — the catalog claims it does`);
1965
+ assert.equal(on.invoked, true, `${parent}: AGY_PROBE=1 must really run the one-off — the catalog claims it does`);
2006
1966
  }
2007
1967
  });
2008
1968
  });
@@ -2098,7 +2058,7 @@ describe('agy-review.sh — review receipts (AD-038)', { concurrency: 2 }, () =>
2098
2058
  });
2099
2059
 
2100
2060
  // The probe marker (BRIDGE-MODES-CATALOG, D3) — the twin of the sibling bridge's arm: an
2101
- // AGY_PROBE=1 review runs with the frontier-model advisory silenced, so its receipt is marked and
2061
+ // AGY_PROBE=1 review is the one route to a one-off model, so its receipt is marked and
2102
2062
  // the kit's review-state gate rejects it. EVERY receipt carries the marker (true or false): it
2103
2063
  // self-declares, so the gate reads the fact rather than inferring it from a version string that
2104
2064
  // bumps in a different release phase. Silence is not a declaration.
@@ -2689,7 +2649,7 @@ describe('agy-review.sh — settings surface ⟷ manifest (D6, manifest-pinned)'
2689
2649
  const help = runHelp('--help').stdout;
2690
2650
  const section = helpSection(help, SETTINGS_HEADER);
2691
2651
  const got = section.filter((l) => /^[A-Z][A-Z0-9_]+ —/.test(l)).map((l) => l.split(' ')[0]);
2692
- const want = (MANIFEST.settings ?? []).filter((s) => s.appliesTo.includes(SETTINGS_CMD)).map((s) => s.key);
2652
+ const want = (MANIFEST.settings ?? []).filter((s) => s.kind !== 'posture' && s.appliesTo.includes(SETTINGS_CMD)).map((s) => s.key);
2693
2653
  assert.ok(want.length > 0, 'the manifest must declare settings for this wrapper');
2694
2654
  setEq(got, want, 'help Settings keys ⟷ manifest settings.appliesTo');
2695
2655
  assert.ok(section.some((l) => l.includes('agent-workflow/bridge-settings.conf')), 'the section names the settings file');
@@ -2707,7 +2667,7 @@ describe('agy-review.sh — settings surface ⟷ manifest (D6, manifest-pinned)'
2707
2667
  it('AW_SETTINGS_APPLIED equals the manifest appliesTo subset for this wrapper', async () => {
2708
2668
  const m = source.match(/^AW_SETTINGS_APPLIED="([^"]*)"$/m);
2709
2669
  assert.ok(m, 'AW_SETTINGS_APPLIED not found');
2710
- const want = ALL_SETTINGS.filter((s) => s.appliesTo.includes(SETTINGS_CMD)).map((s) => s.key);
2670
+ const want = ALL_SETTINGS.filter((s) => s.kind !== 'posture' && s.appliesTo.includes(SETTINGS_CMD)).map((s) => s.key);
2711
2671
  assert.ok(want.length > 0);
2712
2672
  setEq(m[1].trim().split(/\s+/), want, 'applied subset ⟷ manifest appliesTo');
2713
2673
  });
@@ -2716,8 +2676,8 @@ describe('agy-review.sh — settings surface ⟷ manifest (D6, manifest-pinned)'
2716
2676
  const body = source.match(/aw_settings_valid\(\) \{[\s\S]*?\n\}/);
2717
2677
  assert.ok(body, 'aw_settings_valid not found');
2718
2678
  const armKeys = [...body[0].matchAll(/^ ([A-Z][A-Z0-9_]*)\)/gm)].map((x) => x[1]);
2719
- setEq(armKeys, ALL_SETTINGS.map((s) => s.key), 'validation arms ⟷ manifest keys');
2720
- for (const s of ALL_SETTINGS) {
2679
+ setEq(armKeys, ALL_SETTINGS.filter((s) => s.kind !== 'posture').map((s) => s.key), 'validation arms ⟷ manifest keys (a posture key is read by aw_read_posture, never validated here)');
2680
+ for (const s of ALL_SETTINGS.filter((x) => x.kind !== 'posture')) {
2721
2681
  const arm = body[0].match(new RegExp(`^ ${s.key}\\) (.*) ;;$`, 'm'));
2722
2682
  assert.ok(arm, `no validation arm for ${s.key}`);
2723
2683
  if (s.kind === 'enum') for (const v of s.values) assert.ok(arm[1].includes(`"${v}"`), `${s.key}: enum value '${v}' not pinned`);
@@ -2805,7 +2765,7 @@ describe('agy-review.sh — dispatch-posture labeling (D5)', { concurrency: 2 },
2805
2765
  it('a HOSTILE model string (quotes + backslash) rides the receipt strictly JSON-encoded', async () => {
2806
2766
  const hostile = 'we"ird \\ mo"del';
2807
2767
  const sb = makeSandbox();
2808
- const r = await run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_MODEL: hostile } });
2768
+ const r = await run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_MODEL: hostile, AGY_PROBE: '1', AGY_FAKE_MODELS: `id\t${hostile}\n` } });
2809
2769
  const receipts = readReceipts(sb.repo); // JSON.parse throwing here IS the encoding failure
2810
2770
  rmSync(sb.home, { recursive: true, force: true });
2811
2771
  assert.equal(r.status, 0, r.stderr);
@@ -2830,7 +2790,7 @@ describe('agy-review.sh — dispatch-posture labeling (D5)', { concurrency: 2 },
2830
2790
  const receipts = readReceipts(sb.repo);
2831
2791
  rmSync(sb.home, { recursive: true, force: true });
2832
2792
  assert.equal(r.status, 0, r.stderr);
2833
- assert.match(r.stderr, /^review posture: model=Gemini 3\.8 Flash \(High\) timeout=30m$/m);
2793
+ assert.match(r.stderr, /^review posture: model=Gemini 3\.8 Flash \(High\) source=model:default timeout=30m$/m);
2834
2794
  assert.deepEqual(Object.keys(receipts[0].posture), ['model'], 'timeout never enters the receipt posture');
2835
2795
  });
2836
2796