@sabaiway/agent-workflow-kit 14.19.1 → 14.21.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.
- package/CHANGELOG.md +71 -0
- package/README.md +1 -1
- package/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +45 -50
- package/bridges/antigravity-cli-bridge/bin/agy-model-posture.test.mjs +257 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review-harness.test.mjs +10 -3
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +20 -21
- package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +4 -4
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +120 -48
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +22 -62
- package/bridges/antigravity-cli-bridge/bin/agy.sh +109 -16
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +5 -5
- package/bridges/antigravity-cli-bridge/capability.json +14 -7
- package/bridges/antigravity-cli-bridge/references/driving-agy.md +16 -20
- package/bridges/antigravity-cli-bridge/references/models-and-flags.md +10 -27
- package/bridges/codex-cli-bridge/SKILL.md +57 -49
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +145 -46
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +28 -55
- package/bridges/codex-cli-bridge/bin/codex-model-posture.test.mjs +371 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +134 -36
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +33 -45
- package/bridges/codex-cli-bridge/capability.json +22 -8
- package/bridges/codex-cli-bridge/references/driving-codex.md +18 -15
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +16 -12
- package/bridges/codex-cli-bridge/setup/README.md +3 -3
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/bridge-settings.md +10 -7
- package/references/modes/jev.md +3 -2
- package/references/modes/recipes.md +1 -1
- package/references/modes/recommendations.md +2 -1
- package/references/modes/review-state.md +1 -1
- package/references/vendor/typesafe-ai/LICENSE +21 -0
- package/references/vendor/typesafe-ai/SKILL.md.pinned +149 -0
- package/tools/ack-store.mjs +2 -0
- package/tools/bridge-catalog.mjs +114 -0
- package/tools/bridge-posture.mjs +11 -2
- package/tools/bridge-settings-read.mjs +41 -5
- package/tools/bridge-settings.mjs +111 -23
- package/tools/detect-backends.mjs +6 -6
- package/tools/family-registry.mjs +3 -3
- package/tools/jev-facts.mjs +101 -4
- package/tools/jev-guide.mjs +31 -12
- package/tools/jev-skill.mjs +231 -0
- package/tools/manifest/schema.md +1 -1
- package/tools/manifest/validate.mjs +3 -2
- package/tools/procedures.mjs +3 -3
- package/tools/recipes.mjs +2 -2
- package/tools/recommendations.mjs +49 -4
- package/tools/review-state.mjs +1 -1
|
@@ -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
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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 —
|
|
160
|
-
it('plan and
|
|
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
|
|
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
|
|
3
|
-
//
|
|
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
|
|
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, /
|
|
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
|
|
39
|
-
#
|
|
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 —
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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,
|
|
134
|
-
|
|
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.
|
|
166
|
-
AGY_MODEL
|
|
167
|
-
AGY_PROBE=1
|
|
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="
|
|
347
|
-
#
|
|
348
|
-
|
|
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
|
-
# ---
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
if [[
|
|
687
|
-
|
|
688
|
-
|
|
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
|
|
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
|
|
2080
|
-
#
|
|
2081
|
-
#
|
|
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('
|
|
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:
|
|
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.
|
|
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
|
-
//
|
|
1992
|
-
|
|
1993
|
-
const
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
assert.equal(off.
|
|
1997
|
-
assert.
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
const
|
|
2001
|
-
|
|
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
|
|
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
|
|
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
|
|