@sabaiway/agent-workflow-kit 5.8.0 → 5.10.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 +83 -0
- package/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +32 -11
- package/bridges/antigravity-cli-bridge/bin/agy-envelope.mjs +160 -0
- package/bridges/antigravity-cli-bridge/bin/agy-envelope.test.mjs +235 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +23 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +242 -38
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +482 -38
- package/bridges/antigravity-cli-bridge/capability.json +3 -2
- package/bridges/antigravity-cli-bridge/references/models-and-flags.md +45 -12
- package/bridges/antigravity-cli-bridge/references/review-prompt.md +6 -3
- package/bridges/antigravity-cli-bridge/setup/README.md +18 -5
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/hooks/state-block-guard.mjs +107 -45
- package/references/modes/bootstrap.md +6 -2
- package/references/modes/set-recipe.md +8 -5
- package/references/modes/state-block-guard.md +39 -31
- package/references/modes/upgrade.md +8 -5
- package/references/scripts/check-docs-size-cli.test.mjs +7 -6
- package/references/scripts/check-docs-size-ensure.test.mjs +332 -0
- package/references/scripts/check-docs-size.mjs +181 -30
- package/references/shared/composition-handoff.md +10 -0
- package/references/shared/report-footer.md +2 -2
- package/references/templates/agent_rules.md +1 -0
- package/tools/detect-backends.mjs +1 -0
- package/tools/doc-parity.mjs +5 -1
- package/tools/ensure-configs.mjs +37 -19
- package/tools/ensure-ops.mjs +79 -1
- package/tools/ensure-vocabulary.mjs +17 -3
- package/tools/known-footprint.mjs +10 -0
- package/tools/lens-region.mjs +13 -1
- package/tools/source-size-scope.mjs +3 -1
- package/tools/upgrade-runlist.mjs +1 -0
|
@@ -18,18 +18,70 @@ const WRAPPER = join(HERE, 'agy-review.sh');
|
|
|
18
18
|
// --add-dir escape — the staging dir's perms + the offloaded artifact's perms/contents WHILE they
|
|
19
19
|
// still exist (agy-review's trap removes the staging dir on exit). Kept inline so the file is
|
|
20
20
|
// standalone (the kit mirror is byte-equality; no shared helper grows that set).
|
|
21
|
+
// The envelope `agy --output-format json` returns: ONE object whose `response` carries the model's
|
|
22
|
+
// text VERBATIM. Encoded in node so an arbitrary body (quotes, backslashes, multibyte) round-trips
|
|
23
|
+
// exactly — hand-rolled JSON quoting in bash is the defect farm this fake exists to avoid.
|
|
24
|
+
const FAKE_ENVELOPE_ENCODER = [
|
|
25
|
+
'const text = require("node:fs").readFileSync(0, "utf8");',
|
|
26
|
+
'const [cid, status, shape] = process.argv.slice(1);',
|
|
27
|
+
'const envelope = { conversation_id: cid, status, response: text, duration_seconds: 1.5, num_turns: 1,',
|
|
28
|
+
' usage: { input_tokens: 10, output_tokens: 5, thinking_tokens: 0, cache_read_tokens: 0, total_tokens: 15 } };',
|
|
29
|
+
// The fed lane ROUTES later turns at this field, so the two ways it can be present-but-unusable —
|
|
30
|
+
// absent, and present with the wrong TYPE — are knobs, not just a bad-grammar string.
|
|
31
|
+
'if (shape === "missing") delete envelope.conversation_id;',
|
|
32
|
+
'if (shape === "number") envelope.conversation_id = 42;',
|
|
33
|
+
'process.stdout.write(`${JSON.stringify(envelope)}\\n`);',
|
|
34
|
+
].join('\n');
|
|
35
|
+
|
|
21
36
|
const FAKE_AGY = [
|
|
22
37
|
'#!/usr/bin/env bash',
|
|
23
38
|
'set -u',
|
|
39
|
+
// A capability probe is NOT a paid dispatch: --help / --version answer BEFORE any capture file is
|
|
40
|
+
// touched, so the wrapper's pre-spend door can never read as a spent run (the sentinel) nor shift
|
|
41
|
+
// the fed lane's per-turn counter. Keyed on the FIRST argument like the real CLI, so a prompt that
|
|
42
|
+
// merely CONTAINS "--help" is never intercepted.
|
|
43
|
+
'case "${1:-}" in',
|
|
44
|
+
' --help|-h)',
|
|
45
|
+
' if [[ -n "${AGY_FAKE_HELP_EXIT:-}" ]]; then echo "fake agy: help unavailable" >&2; exit "$AGY_FAKE_HELP_EXIT"; fi',
|
|
46
|
+
' for f in --output-format --json-schema --disable-slash-commands --effort --mode; do',
|
|
47
|
+
' if [[ "$f" == "${AGY_FAKE_HELP_OMIT:-}" ]]; then continue; fi',
|
|
48
|
+
' printf " %s fake capability line\\n" "$f"',
|
|
49
|
+
' done',
|
|
50
|
+
' if [[ -n "${AGY_FAKE_HELP_EXTRA:-}" ]]; then printf "%s\\n" "$AGY_FAKE_HELP_EXTRA"; fi',
|
|
51
|
+
' exit 0 ;;',
|
|
52
|
+
' --version) printf "%s\\n" "${AGY_FAKE_VERSION:-1.1.13}"; exit 0 ;;',
|
|
53
|
+
'esac',
|
|
24
54
|
': "${AGY_FAKE_ARGV:=/dev/null}"',
|
|
25
55
|
': "${AGY_FAKE_ENV:=/dev/null}"',
|
|
26
56
|
': "${AGY_FAKE_PROMPT:=/dev/null}"',
|
|
27
57
|
': "${AGY_FAKE_SENTINEL:=/dev/null}"',
|
|
28
58
|
'printf invoked > "$AGY_FAKE_SENTINEL"',
|
|
59
|
+
// The fed lane dispatches from the private staging dir (deliberately — see the wrapper), so the
|
|
60
|
+
// fake's own cwd is the only handle a test has on a directory the exit trap then removes.
|
|
61
|
+
'printf "%s" "$PWD" > "${AGY_FAKE_CWD:-/dev/null}"',
|
|
29
62
|
'{ for a in "$@"; do printf "%s\\n" "$a"; done; } > "$AGY_FAKE_ARGV"',
|
|
30
63
|
'{ echo "FOO_API_KEY=${FOO_API_KEY:-<unset>}"; echo "ANTIGRAVITY_API_KEY=${ANTIGRAVITY_API_KEY:-<unset>}"; } > "$AGY_FAKE_ENV"',
|
|
31
64
|
'prompt=""',
|
|
32
65
|
'prev=""; for a in "$@"; do if [[ "$prev" == "-p" ]]; then prompt="$a"; printf "%s" "$a" > "$AGY_FAKE_PROMPT"; fi; prev="$a"; done',
|
|
66
|
+
// Every answer leaves through ONE emitter, so the transport is a property of the INVOCATION
|
|
67
|
+
// (`--output-format json` ⇒ an envelope, otherwise text) and every existing output knob keeps
|
|
68
|
+
// meaning exactly what it meant. AGY_FAKE_RAW_STDOUT bypasses the encoder entirely — the
|
|
69
|
+
// unparseable-payload arm; AGY_FAKE_STDERR is the CLI's own diagnostic.
|
|
70
|
+
'aw_fmt=""',
|
|
71
|
+
'prev=""; for a in "$@"; do if [[ "$prev" == "--output-format" ]]; then aw_fmt="$a"; fi; prev="$a"; done',
|
|
72
|
+
// AGY_FAKE_BAD_TURN scopes the two transport-breaking knobs to ONE turn: a fed run whose turn 1
|
|
73
|
+
// answers normally and whose turn 2 does not is the only way to reach the "no LATER turn is spent"
|
|
74
|
+
// arms — an unscoped knob breaks turn 1 and the run never gets there.
|
|
75
|
+
'aw_fake_emit() {',
|
|
76
|
+
' local body="$1" aw_status="${AGY_FAKE_STATUS:-SUCCESS}" aw_raw=""',
|
|
77
|
+
' if [[ -n "${AGY_FAKE_RAW_STDOUT+x}" ]]; then aw_raw=1; fi',
|
|
78
|
+
' if [[ -n "${AGY_FAKE_BAD_TURN:-}" && "$turn" != "${AGY_FAKE_BAD_TURN}" ]]; then aw_status="SUCCESS"; aw_raw=""; fi',
|
|
79
|
+
' if [[ -n "${AGY_FAKE_STDERR:-}" ]]; then printf "%s\\n" "$AGY_FAKE_STDERR" >&2; fi',
|
|
80
|
+
' if [[ -n "$aw_raw" ]]; then printf "%s" "$AGY_FAKE_RAW_STDOUT"; return 0; fi',
|
|
81
|
+
' if [[ "$aw_fmt" != "json" ]]; then printf "%s\\n" "$body"; return 0; fi',
|
|
82
|
+
` printf "%s\\n" "$body" | node -e '${FAKE_ENVELOPE_ENCODER}' \\`,
|
|
83
|
+
' "${AGY_FAKE_CONV_ID:-11111111-2222-3333-4444-555555555555}" "$aw_status" "${AGY_FAKE_CONV_SHAPE:-ok}"',
|
|
84
|
+
'}',
|
|
33
85
|
'prev=""; for a in "$@"; do',
|
|
34
86
|
' if [[ "$prev" == "--add-dir" ]]; then',
|
|
35
87
|
' printf "%s" "$a" > "${AGY_FAKE_ADDDIR:-/dev/null}"',
|
|
@@ -49,14 +101,6 @@ const FAKE_AGY = [
|
|
|
49
101
|
' printf "%s" "$prompt" > "${AGY_FAKE_PROMPT}.$turn"',
|
|
50
102
|
' { for a in "$@"; do printf "%s\\n" "$a"; done; } > "${AGY_FAKE_ARGV}.$turn"',
|
|
51
103
|
'fi',
|
|
52
|
-
// agy writes the conversation id into its --log-file; AGY_FAKE_BAD_CONV_LOG=1 writes a log the
|
|
53
|
-
// wrapper cannot parse (the D9 degrade arm).
|
|
54
|
-
'prev=""; for a in "$@"; do',
|
|
55
|
-
' if [[ "$prev" == "--log-file" ]]; then',
|
|
56
|
-
' if [[ "${AGY_FAKE_BAD_CONV_LOG:-}" == "1" ]]; then printf "no conversation marker here\\n" > "$a"',
|
|
57
|
-
' else printf "Starting new conversation %s\\n" "${AGY_FAKE_CONV_ID:-11111111-2222-3333-4444-555555555555}" > "$a"; fi',
|
|
58
|
-
' fi; prev="$a"',
|
|
59
|
-
'done',
|
|
60
104
|
'if [[ -n "${AGY_FAKE_FAIL_TURN:-}" && "$turn" == "${AGY_FAKE_FAIL_TURN}" ]]; then',
|
|
61
105
|
' printf "FAKE_TURN_FAILURE\\n" >&2; exit 3',
|
|
62
106
|
'fi',
|
|
@@ -64,7 +108,7 @@ const FAKE_AGY = [
|
|
|
64
108
|
// PREMATURE verdict — so the isolation invariant (feed output never reaches stdout or the parsed
|
|
65
109
|
// capture) is proven against the worst case, not the polite one.
|
|
66
110
|
'if [[ -z "${AGY_FAKE_OUTPUT+x}" && "$prompt" == *"--- BEGIN CHANGE-SET PART "* && "$prompt" != *"Requested addresses"* ]]; then',
|
|
67
|
-
' printf "PREMATURE_FEED_CHATTER\\n### Verdict\\nREWORK
|
|
111
|
+
' aw_fake_emit "$(printf "PREMATURE_FEED_CHATTER\\n### Verdict\\nREWORK")"; exit 0',
|
|
68
112
|
'fi',
|
|
69
113
|
// The FINAL turn carries the delivery-proof request. The fake answers it the only honest way:
|
|
70
114
|
// by reading the bodies it was actually fed, turn by turn — so a wrapper that never delivered a
|
|
@@ -93,20 +137,23 @@ const FAKE_AGY = [
|
|
|
93
137
|
' done',
|
|
94
138
|
' if [[ "${AGY_FAKE_PROOF_EXTRA:-}" == "1" ]]; then entries+=("part 99 line 1: an address nobody asked for"); fi',
|
|
95
139
|
' if [[ "${AGY_FAKE_PROOF_HUGE_EXTRA:-}" == "1" ]]; then entries+=("part 99999999999999999999 line 1: an invented giant address"); fi',
|
|
96
|
-
'
|
|
97
|
-
'
|
|
98
|
-
'
|
|
99
|
-
'
|
|
100
|
-
'
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
'
|
|
104
|
-
'
|
|
140
|
+
' out="$( {',
|
|
141
|
+
' if [[ "${AGY_FAKE_PROOF_LATE:-}" == "1" ]]; then printf "### Verdict\\nSHIP\\n"; fi',
|
|
142
|
+
' if [[ "${AGY_FAKE_PROOF_CASE:-}" == "1" ]]; then printf "### Delivery Proof\\n"; else printf "### Delivery proof\\n"; fi',
|
|
143
|
+
' if [[ "${AGY_FAKE_PROOF_OUTSIDE:-}" == "1" ]]; then',
|
|
144
|
+
' printf "(nothing here)\\n### Verdict\\nSHIP\\n"',
|
|
145
|
+
' if (( ${#entries[@]} > 0 )); then printf "%s\\n" "${entries[@]}"; fi',
|
|
146
|
+
' else',
|
|
147
|
+
' if (( ${#entries[@]} > 0 )); then printf "%s\\n" "${entries[@]}"; fi',
|
|
148
|
+
' printf "### Verdict\\nSHIP\\n"',
|
|
149
|
+
' fi',
|
|
150
|
+
' } )"',
|
|
151
|
+
' aw_fake_emit "$out"',
|
|
105
152
|
' exit 0',
|
|
106
153
|
'fi',
|
|
107
154
|
// Unset AGY_FAKE_OUTPUT → a verdict-carrying default (D4: a verdict-less run is a FAILURE, so
|
|
108
155
|
// the success-path tests need one); an EXPLICIT empty value exercises the empty-output failure.
|
|
109
|
-
'if [[ -z "${AGY_FAKE_OUTPUT+x}" ]]; then printf "FAKE_AGY_REVIEW_OUTPUT\\n### Verdict\\nSHIP
|
|
156
|
+
'if [[ -z "${AGY_FAKE_OUTPUT+x}" ]]; then aw_fake_emit "$(printf "FAKE_AGY_REVIEW_OUTPUT\\n### Verdict\\nSHIP")"; else aw_fake_emit "$AGY_FAKE_OUTPUT"; fi',
|
|
110
157
|
'exit "${AGY_FAKE_EXIT:-0}"',
|
|
111
158
|
'',
|
|
112
159
|
].join('\n');
|
|
@@ -176,7 +223,7 @@ const makeSandbox = ({ clean = false } = {}) => {
|
|
|
176
223
|
// Capture files are per-INVOCATION: a second run() on the same sandbox must not inherit the first
|
|
177
224
|
// run's turn counter or per-turn prompt files (the fed lane reads them back by turn index).
|
|
178
225
|
let runSeq = 0;
|
|
179
|
-
const run = (sb, { args, env = {}, cwd } = {}) => {
|
|
226
|
+
const run = (sb, { args, env = {}, cwd, wrapper } = {}) => {
|
|
180
227
|
const { home, bin, repo } = sb;
|
|
181
228
|
const farm = farmFor(['agy', 'agy-run']);
|
|
182
229
|
const tag = `cap-${++runSeq}`;
|
|
@@ -185,8 +232,9 @@ const run = (sb, { args, env = {}, cwd } = {}) => {
|
|
|
185
232
|
sentinel: join(home, `${tag}-sentinel`), adddir: join(home, `${tag}-adddir`),
|
|
186
233
|
adddirMode: join(home, `${tag}-adddir-mode`), artifactMode: join(home, `${tag}-artifact-mode`),
|
|
187
234
|
artifactCopy: join(home, `${tag}-artifact-copy`), turns: join(home, `${tag}-turns`),
|
|
235
|
+
dispatchCwd: join(home, `${tag}-dispatch-cwd`),
|
|
188
236
|
};
|
|
189
|
-
const r = spawnSync('bash', [WRAPPER, ...args], {
|
|
237
|
+
const r = spawnSync('bash', [wrapper || WRAPPER, ...args], {
|
|
190
238
|
cwd: cwd || repo,
|
|
191
239
|
encoding: 'utf8',
|
|
192
240
|
timeout: 30000,
|
|
@@ -199,7 +247,7 @@ const run = (sb, { args, env = {}, cwd } = {}) => {
|
|
|
199
247
|
AGY_FAKE_ARGV: cap.argv, AGY_FAKE_ENV: cap.env, AGY_FAKE_PROMPT: cap.prompt,
|
|
200
248
|
AGY_FAKE_SENTINEL: cap.sentinel, AGY_FAKE_ADDDIR: cap.adddir, AGY_FAKE_ADDDIR_MODE: cap.adddirMode,
|
|
201
249
|
AGY_FAKE_ARTIFACT_MODE: cap.artifactMode, AGY_FAKE_ARTIFACT_COPY: cap.artifactCopy,
|
|
202
|
-
AGY_FAKE_TURNS: cap.turns,
|
|
250
|
+
AGY_FAKE_TURNS: cap.turns, AGY_FAKE_CWD: cap.dispatchCwd,
|
|
203
251
|
...env,
|
|
204
252
|
},
|
|
205
253
|
});
|
|
@@ -218,7 +266,7 @@ const run = (sb, { args, env = {}, cwd } = {}) => {
|
|
|
218
266
|
argv: readIf(cap.argv), capEnv: readIf(cap.env), prompt: readIf(cap.prompt),
|
|
219
267
|
adddir: readIf(cap.adddir).trim(), adddirMode: readIf(cap.adddirMode).trim(),
|
|
220
268
|
artifactMode: readIf(cap.artifactMode).trim(), artifactCopy: readIf(cap.artifactCopy),
|
|
221
|
-
turns, prompts, argvs,
|
|
269
|
+
dispatchCwd: readIf(cap.dispatchCwd).trim(), turns, prompts, argvs,
|
|
222
270
|
};
|
|
223
271
|
};
|
|
224
272
|
|
|
@@ -298,6 +346,355 @@ describe('agy-review.sh — model policy advisory (1)', () => {
|
|
|
298
346
|
});
|
|
299
347
|
});
|
|
300
348
|
|
|
349
|
+
// ── the pre-spend capability door (AGY-1.1.13, Decision 2) ───────────────────────────────────────
|
|
350
|
+
// The wrapper drives agy in --output-format json and reads the returned envelope in node, so a host
|
|
351
|
+
// that cannot do that must refuse BEFORE a subscription turn is spent. The door probes CAPABILITY,
|
|
352
|
+
// never a version floor — the release that introduced the flag is unmeasurable from one installed
|
|
353
|
+
// build. Every case asserts ZERO paid dispatch: the fake answers --help/--version without touching
|
|
354
|
+
// the invocation sentinel, so a probe can never read as a spent run.
|
|
355
|
+
const FAKE_OLD_NODE = '#!/usr/bin/env bash\nprintf "v18.20.0\\n"\n';
|
|
356
|
+
|
|
357
|
+
describe('agy-review.sh — the pre-spend capability door (Decision 2)', () => {
|
|
358
|
+
for (const flag of ['--output-format', '--disable-slash-commands']) {
|
|
359
|
+
it(`a --help that does not advertise ${flag} refuses pre-spend and names it`, () => {
|
|
360
|
+
const sb = makeSandbox();
|
|
361
|
+
const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_FAKE_HELP_OMIT: flag } });
|
|
362
|
+
const receipts = readReceipts(sb.repo);
|
|
363
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
364
|
+
assert.notEqual(r.status, 0, r.stderr);
|
|
365
|
+
assert.equal(r.invoked, false, 'the door refuses BEFORE any paid dispatch');
|
|
366
|
+
assert.match(r.stderr, /does not advertise the flag/);
|
|
367
|
+
assert.ok(r.stderr.includes(flag), `the refusal names the missing flag: ${r.stderr}`);
|
|
368
|
+
assert.equal(receipts.length, 0);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Each required flag buys something DIFFERENT, so the refusal must name the cost of the flag that
|
|
373
|
+
// is actually missing: a build lacking --disable-slash-commands answers perfectly readably and
|
|
374
|
+
// corrupts the delivered BODY instead, which the envelope explanation would send an operator
|
|
375
|
+
// hunting the wrong bug.
|
|
376
|
+
it('the refusal names what the MISSING flag buys, not one blanket envelope explanation', () => {
|
|
377
|
+
const sb = makeSandbox();
|
|
378
|
+
const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_FAKE_HELP_OMIT: '--disable-slash-commands' } });
|
|
379
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
380
|
+
assert.notEqual(r.status, 0, r.stderr);
|
|
381
|
+
assert.equal(r.invoked, false);
|
|
382
|
+
assert.match(r.stderr, /slash command/i, 'the stated cost is command expansion of the change-set body');
|
|
383
|
+
assert.doesNotMatch(r.stderr, /cannot read/, 'the unreadable-answer cost belongs to --output-format alone');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('`agy --help` exiting non-zero refuses with a DISTINCT cause — never read as "capability present"', () => {
|
|
387
|
+
const sb = makeSandbox();
|
|
388
|
+
const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_FAKE_HELP_EXIT: '3' } });
|
|
389
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
390
|
+
assert.notEqual(r.status, 0, r.stderr);
|
|
391
|
+
assert.equal(r.invoked, false, 'a failed probe never becomes a paid dispatch');
|
|
392
|
+
assert.match(r.stderr, /'agy --help' exited 3/, 'the failed probe is the stated cause');
|
|
393
|
+
assert.doesNotMatch(r.stderr, /does not advertise the flag/, 'a failed probe is not a missing-flag verdict');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('node ABSENT refuses pre-spend, naming Node and the floor', () => {
|
|
397
|
+
const sb = makeSandbox();
|
|
398
|
+
const r = run(sb, {
|
|
399
|
+
args: ['code', '--facts', 'f'],
|
|
400
|
+
env: { PATH: `${sb.bin}:${farmFor(['agy', 'agy-run', 'node'])}` },
|
|
401
|
+
});
|
|
402
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
403
|
+
assert.notEqual(r.status, 0, r.stderr);
|
|
404
|
+
assert.equal(r.invoked, false);
|
|
405
|
+
assert.match(r.stderr, /'node' is not on PATH/);
|
|
406
|
+
assert.match(r.stderr, /Node >= 22/);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('node PRESENT but BELOW the floor refuses pre-spend — the version comparison really runs', () => {
|
|
410
|
+
const sb = makeSandbox();
|
|
411
|
+
// Into $HOME/.local/bin, which the wrapper itself prepends — so the fake wins over the real node.
|
|
412
|
+
writeFileSync(join(sb.bin, 'node'), FAKE_OLD_NODE, { mode: 0o755 });
|
|
413
|
+
const r = run(sb, { args: ['code', '--facts', 'f'] });
|
|
414
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
415
|
+
assert.notEqual(r.status, 0, r.stderr);
|
|
416
|
+
assert.equal(r.invoked, false, 'an existence-only check would have dispatched here');
|
|
417
|
+
assert.match(r.stderr, /v18\.20\.0/, 'the refusal quotes the version it read');
|
|
418
|
+
assert.match(r.stderr, /below the family floor \(Node >= 22\)/);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// The door reads option DECLARATIONS, not substrings. A near-miss flag and a prose mention are the
|
|
422
|
+
// two ways a substring search opens the door to a build that cannot honour the flag — and the
|
|
423
|
+
// failure would then surface only AFTER a subscription turn was already spent.
|
|
424
|
+
for (const [name, extraHelpLine] of [
|
|
425
|
+
['a NEAR-MISS flag name', ' --output-formatting a longer flag that is not the one we pass'],
|
|
426
|
+
['a prose mention outside any declaration', 'Note: pass --output-format json to get structured output.'],
|
|
427
|
+
['the flag named inside ANOTHER option`s description', ' --json-schema enforce structured output; see --output-format'],
|
|
428
|
+
['another option whose description BEGINS with the flag name', ' --other --output-format is unsupported on this build'],
|
|
429
|
+
]) {
|
|
430
|
+
it(`${name} does NOT open the door — an option declaration is required`, () => {
|
|
431
|
+
const sb = makeSandbox();
|
|
432
|
+
const r = run(sb, {
|
|
433
|
+
args: ['code', '--facts', 'f'],
|
|
434
|
+
env: { AGY_FAKE_HELP_OMIT: '--output-format', AGY_FAKE_HELP_EXTRA: extraHelpLine },
|
|
435
|
+
});
|
|
436
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
437
|
+
assert.notEqual(r.status, 0, r.stderr);
|
|
438
|
+
assert.equal(r.invoked, false, 'a false-positive door would have spent a subscription turn here');
|
|
439
|
+
assert.match(r.stderr, /does not advertise the flag/);
|
|
440
|
+
assert.ok(r.stderr.includes('--output-format '), `the refusal still names the flag really missing: ${r.stderr}`);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// The other direction: a declaration the parser fails to READ refuses a build that CAN honour the
|
|
445
|
+
// flag — the false refusal Decision 2 exists to avoid. Both common clustered renderings count.
|
|
446
|
+
for (const [name, extraHelpLine] of [
|
|
447
|
+
['a comma-clustered alias', ' -o, --output-format the clustered rendering'],
|
|
448
|
+
['a metavar rendering', ' -o FORMAT, --output-format FORMAT the metavar rendering'],
|
|
449
|
+
['an =<value> rendering', ' --output-format=<fmt> the joined-value rendering'],
|
|
450
|
+
['a declaration with no description at all', ' --output-format'],
|
|
451
|
+
]) {
|
|
452
|
+
it(`${name} READS as declared — a capable build is never falsely refused`, () => {
|
|
453
|
+
const sb = makeSandbox();
|
|
454
|
+
const r = run(sb, {
|
|
455
|
+
args: ['code', '--facts', 'f'],
|
|
456
|
+
env: { AGY_FAKE_HELP_OMIT: '--output-format', AGY_FAKE_HELP_EXTRA: extraHelpLine },
|
|
457
|
+
});
|
|
458
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
459
|
+
assert.equal(r.status, 0, `${name} must read as a declaration: ${r.stderr}`);
|
|
460
|
+
assert.equal(r.invoked, true);
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
it('the agy-capability refusal carries the INSTALLED version and the upgrade command', () => {
|
|
465
|
+
const sb = makeSandbox();
|
|
466
|
+
const r = run(sb, {
|
|
467
|
+
args: ['code', '--facts', 'f'],
|
|
468
|
+
env: { AGY_FAKE_HELP_OMIT: '--output-format', AGY_FAKE_VERSION: '1.0.9' },
|
|
469
|
+
});
|
|
470
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
471
|
+
assert.notEqual(r.status, 0, r.stderr);
|
|
472
|
+
assert.match(r.stderr, /Installed agy version: 1\.0\.9/, 'the operator is told what they actually have');
|
|
473
|
+
assert.match(r.stderr, /agy update/, 'and the command that fixes it');
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('a capable host passes the door and still dispatches (the door is not a blanket refusal)', () => {
|
|
477
|
+
const sb = makeSandbox();
|
|
478
|
+
const r = run(sb, { args: ['code', '--facts', 'f'] });
|
|
479
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
480
|
+
assert.equal(r.status, 0, r.stderr);
|
|
481
|
+
assert.equal(r.invoked, true);
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// ── characterize-first: what the operator sees TODAY, pinned before the transport moves ──────────
|
|
486
|
+
// Phase 4.1 of the envelope plan. The switch from raw stdout to the CLI's JSON envelope must not
|
|
487
|
+
// change one byte of this: stdout carries the model's answer and NOTHING else (the posture banner
|
|
488
|
+
// is stderr), and the receipt records the verdict parsed out of that same answer. Green before the
|
|
489
|
+
// switch, green after — anything the transport quietly rewrites fails here.
|
|
490
|
+
describe('agy-review.sh — the operator-visible answer, byte-for-byte (characterization)', () => {
|
|
491
|
+
it('stdout is EXACTLY the model answer, and the receipt records the verdict parsed from it', () => {
|
|
492
|
+
const sb = makeSandbox();
|
|
493
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
|
|
494
|
+
const receipts = readReceipts(sb.repo);
|
|
495
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
496
|
+
assert.equal(r.status, 0, r.stderr);
|
|
497
|
+
assert.equal(r.stdout, `${VERDICT_OUTPUT}\n`, 'the answer reaches the operator with nothing added and nothing dropped');
|
|
498
|
+
assert.equal(receipts[0].verdict, 'SHIP WITH NITS', 'and the receipt verdict comes from that same answer');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// The bytes a transport is most likely to mangle: blank lines, non-ASCII, quotes, a backslash, a
|
|
502
|
+
// tab. If the answer ever rides a JSON field, this is the test that catches a lossy round-trip.
|
|
503
|
+
it('blank lines, multibyte, quotes, backslashes and tabs all survive to stdout unchanged', () => {
|
|
504
|
+
const answer = [
|
|
505
|
+
'### Verdict',
|
|
506
|
+
'SHIP — «clean», no nits.',
|
|
507
|
+
'',
|
|
508
|
+
'### Blocking',
|
|
509
|
+
'none',
|
|
510
|
+
'',
|
|
511
|
+
'### Non-blocking',
|
|
512
|
+
'1. a "quoted" path C:\\tmp\\x and a\ttab',
|
|
513
|
+
'',
|
|
514
|
+
'### Questions',
|
|
515
|
+
'none',
|
|
516
|
+
].join('\n');
|
|
517
|
+
const sb = makeSandbox();
|
|
518
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: answer } });
|
|
519
|
+
const receipts = readReceipts(sb.repo);
|
|
520
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
521
|
+
assert.equal(r.status, 0, r.stderr);
|
|
522
|
+
assert.equal(r.stdout, `${answer}\n`, 'the answer is transported verbatim');
|
|
523
|
+
assert.equal(receipts[0].verdict, 'SHIP');
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// ── the dispatch rides agy's structured envelope (Phase 4.3) ─────────────────────────────────────
|
|
528
|
+
// Every lane runs `--output-format json` and the wrapper reads the envelope's `response` instead of
|
|
529
|
+
// raw stdout. The review CONTRACT does not move: the same prompt, the same mandated shape, the same
|
|
530
|
+
// D4 arm. What changes is that a fact the wrapper used to guess at now arrives named.
|
|
531
|
+
describe('agy-review.sh — the dispatch rides the JSON envelope (Phase 4.3)', () => {
|
|
532
|
+
// The transport is BOTH flags: the envelope carries the answer, and --disable-slash-commands keeps
|
|
533
|
+
// a change-set line that happens to begin with a slash command as BODY rather than an instruction
|
|
534
|
+
// the CLI expands. The pre-spend door requires both, so the dispatch must pass both — a door that
|
|
535
|
+
// demands a capability the wrapper never uses refuses working installs for nothing.
|
|
536
|
+
const argvCarriesTransport = (argv) => {
|
|
537
|
+
const tokens = argv.split('\n');
|
|
538
|
+
const at = tokens.indexOf('--output-format');
|
|
539
|
+
return at !== -1 && tokens[at + 1] === 'json' && tokens.includes('--disable-slash-commands');
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
it('all three lanes carry the transport flags (single, fed, resume)', () => {
|
|
543
|
+
const single = makeSandbox();
|
|
544
|
+
const one = run(single, { args: ['code', '--facts', 'f'] });
|
|
545
|
+
rmSync(single.home, { recursive: true, force: true });
|
|
546
|
+
assert.equal(one.status, 0, one.stderr);
|
|
547
|
+
assert.ok(argvCarriesTransport(one.argv), `single lane argv: ${one.argv}`);
|
|
548
|
+
|
|
549
|
+
const fedSb = makeSandbox();
|
|
550
|
+
seedFedChangeSet(fedSb);
|
|
551
|
+
const fed = fedRun(fedSb);
|
|
552
|
+
rmSync(fedSb.home, { recursive: true, force: true });
|
|
553
|
+
assert.equal(fed.status, 0, fed.stderr);
|
|
554
|
+
assert.ok(fed.argvs.length >= 3, 'the fixture really chunks');
|
|
555
|
+
for (const [i, argv] of fed.argvs.entries()) {
|
|
556
|
+
assert.ok(argvCarriesTransport(argv), `fed turn ${i + 1} argv: ${argv}`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const resumeSb = makeSandbox();
|
|
560
|
+
const resumed = run(resumeSb, { args: ['--continue'] });
|
|
561
|
+
rmSync(resumeSb.home, { recursive: true, force: true });
|
|
562
|
+
assert.equal(resumed.status, 0, resumed.stderr);
|
|
563
|
+
assert.ok(argvCarriesTransport(resumed.argv), `resume lane argv: ${resumed.argv}`);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('operator-visible stdout IS the envelope response — and a feed turn`s response is neither published nor parsed', () => {
|
|
567
|
+
const sb = makeSandbox();
|
|
568
|
+
seedFedChangeSet(sb);
|
|
569
|
+
const fed = fedRun(sb);
|
|
570
|
+
const receipts = readReceipts(sb.repo);
|
|
571
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
572
|
+
assert.equal(fed.status, 0, fed.stderr);
|
|
573
|
+
assert.ok(fed.stdout.startsWith('### Delivery proof\n'), `stdout must be the FINAL response: ${fed.stdout.slice(0, 80)}`);
|
|
574
|
+
assert.ok(fed.stdout.endsWith('### Verdict\nSHIP\n'), 'and nothing is appended after it');
|
|
575
|
+
assert.ok(!fed.stdout.includes('PREMATURE_FEED_CHATTER'), 'a feed turn never publishes');
|
|
576
|
+
assert.ok(!fed.stdout.includes('conversation_id'), 'the raw envelope never reaches the operator');
|
|
577
|
+
assert.equal(receipts[0].verdict, 'SHIP', 'only the FINAL response is parsed');
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('a non-JSON blob on a ZERO exit is an UNREADABLE review: distinct exit, NO receipt, the cause names the envelope', () => {
|
|
581
|
+
const sb = makeSandbox();
|
|
582
|
+
const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_FAKE_RAW_STDOUT: 'jetski: not an envelope at all\n' } });
|
|
583
|
+
const receipts = readReceipts(sb.repo);
|
|
584
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
585
|
+
assert.equal(r.status, 5, `an unreadable envelope has its OWN code, not the D4 4: ${r.stderr}`);
|
|
586
|
+
assert.equal(receipts.length, 0);
|
|
587
|
+
assert.match(r.stderr, /agy-envelope: not-json/, 'the module names the cause');
|
|
588
|
+
assert.match(r.stderr, /not a readable agy JSON envelope/, 'and the wrapper says what that means');
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('a non-SUCCESS status is unreadable too — its own named cause, NO receipt', () => {
|
|
592
|
+
const sb = makeSandbox();
|
|
593
|
+
const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_FAKE_STATUS: 'ERROR' } });
|
|
594
|
+
const receipts = readReceipts(sb.repo);
|
|
595
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
596
|
+
assert.equal(r.status, 5, r.stderr);
|
|
597
|
+
assert.equal(receipts.length, 0);
|
|
598
|
+
assert.match(r.stderr, /agy-envelope: status/);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// Decision 4: the CLI's own failure WINS. The envelope is parsed only on a zero exit, so a
|
|
602
|
+
// non-zero run keeps its code and its message and never has a parse error layered over it.
|
|
603
|
+
it('a non-zero CLI exit keeps its code and message on the SINGLE lane — no envelope error replaces them', () => {
|
|
604
|
+
const sb = makeSandbox();
|
|
605
|
+
const r = run(sb, {
|
|
606
|
+
args: ['code', '--facts', 'f'],
|
|
607
|
+
env: { AGY_FAKE_EXIT: '7', AGY_FAKE_RAW_STDOUT: 'partial plain text\n', AGY_FAKE_STDERR: 'AGY_CLI_OWN_FAILURE' },
|
|
608
|
+
});
|
|
609
|
+
const receipts = readReceipts(sb.repo);
|
|
610
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
611
|
+
assert.equal(r.status, 7, 'the CLI exit code survives verbatim');
|
|
612
|
+
assert.match(r.stderr, /AGY_CLI_OWN_FAILURE/, 'and so does its own message');
|
|
613
|
+
assert.doesNotMatch(r.stderr, /agy-envelope:/, 'the envelope is never parsed on a non-zero exit');
|
|
614
|
+
assert.match(r.stdout, /partial plain text/, 'whatever the CLI printed still reaches the operator');
|
|
615
|
+
assert.equal(receipts.length, 0);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('a non-zero CLI exit keeps its code and message on the FED lane too', () => {
|
|
619
|
+
const sb = makeSandbox();
|
|
620
|
+
seedFedChangeSet(sb);
|
|
621
|
+
const fed = fedRun(sb, { AGY_FAKE_FAIL_TURN: '2' });
|
|
622
|
+
const receipts = readReceipts(sb.repo);
|
|
623
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
624
|
+
assert.equal(fed.status, 3, 'the failing turn`s own exit code survives');
|
|
625
|
+
assert.match(fed.stderr, /FAKE_TURN_FAILURE/);
|
|
626
|
+
assert.doesNotMatch(fed.stderr, /agy-envelope:/, 'no parse is attempted on a failed turn');
|
|
627
|
+
assert.equal(fed.turns, 2, 'no later turn is spent');
|
|
628
|
+
assert.equal(receipts.length, 0);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// The switch must not move WHICH arm fires: a readable envelope carrying a verdict-less answer is
|
|
632
|
+
// still the D4 failed review, exit 4 — not the transport failure.
|
|
633
|
+
it('a READABLE envelope with no verdict still exits 4 through the EXISTING D4 arm', () => {
|
|
634
|
+
const sb = makeSandbox();
|
|
635
|
+
const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_FAKE_OUTPUT: 'prose without the mandated section' } });
|
|
636
|
+
const receipts = readReceipts(sb.repo);
|
|
637
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
638
|
+
assert.equal(r.status, 4, 'the D4 arm owns this, not the envelope arm');
|
|
639
|
+
assert.equal(receipts.length, 0);
|
|
640
|
+
assert.match(r.stderr, /no recognized '### Verdict' section/);
|
|
641
|
+
assert.doesNotMatch(r.stderr, /agy-envelope:/, 'the envelope was perfectly readable');
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
// The MANAGED INSTALL shape: `agy-review` on PATH is a SYMLINK into the placed skill
|
|
645
|
+
// (~/.local/bin/agy-review -> <placed>/bin/agy-review.sh). An unresolved BASH_SOURCE names the
|
|
646
|
+
// LINK's directory, so every sibling payload — the envelope reader above all — would be looked
|
|
647
|
+
// for beside the link and EVERY review would refuse pre-spend on a correct install.
|
|
648
|
+
it('a review launched through a managed SYMLINK resolves its own sibling payload and still attests', () => {
|
|
649
|
+
const sb = makeSandbox();
|
|
650
|
+
const linked = join(sb.bin, 'agy-review');
|
|
651
|
+
symlinkSync(WRAPPER, linked);
|
|
652
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], wrapper: linked });
|
|
653
|
+
const receipts = readReceipts(sb.repo);
|
|
654
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
655
|
+
assert.equal(r.status, 0, `a symlinked launch must behave identically: ${r.stderr}`);
|
|
656
|
+
assert.equal(r.invoked, true, 'the dispatch really happened');
|
|
657
|
+
assert.doesNotMatch(r.stderr, /envelope reader .* is missing/, 'the reader is found beside the SCRIPT, not beside the link');
|
|
658
|
+
assert.equal(receipts.length, 1, 'and the review still attests');
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// The walk that resolves the wrapper's own directory must TERMINATE. A cycle cannot be reached
|
|
662
|
+
// through exec (the kernel resolves the path first, and would fail with ELOOP), so the reachable
|
|
663
|
+
// shape is an absurd CHAIN — and it is testable exactly because the kernel tolerates a far longer
|
|
664
|
+
// chain than this bound. An unbounded walk spins until the harness kills it and prints nothing,
|
|
665
|
+
// so the assertion is on the MESSAGE: an infinite loop can never produce it.
|
|
666
|
+
it('a symlink chain past the hop bound refuses LOUDLY and spends nothing', () => {
|
|
667
|
+
const sb = makeSandbox();
|
|
668
|
+
const chainDir = join(sb.home, 'link-chain');
|
|
669
|
+
mkdirSync(chainDir, { recursive: true });
|
|
670
|
+
const chainTip = Array.from({ length: 24 }).reduce((previous, _unused, i) => {
|
|
671
|
+
const link = join(chainDir, `hop-${i}`);
|
|
672
|
+
symlinkSync(previous, link);
|
|
673
|
+
return link;
|
|
674
|
+
}, WRAPPER);
|
|
675
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], wrapper: chainTip });
|
|
676
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
677
|
+
assert.equal(r.status, 127, `the walk must refuse, not spin: ${r.stderr}`);
|
|
678
|
+
assert.match(r.stderr, /exceeded 16 symlink hops/, 'the refusal names the bound it hit');
|
|
679
|
+
assert.match(r.stderr, /could not resolve its OWN directory/, 'and what that costs');
|
|
680
|
+
assert.equal(r.invoked, false, 'nothing is spent');
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it('the resume lanes parse the envelope and mint the continuation receipt exactly as before', () => {
|
|
684
|
+
for (const args of [['--continue'], ['--conversation', 'conv-xyz']]) {
|
|
685
|
+
const sb = makeSandbox();
|
|
686
|
+
const r = run(sb, { args, env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
|
|
687
|
+
const receipts = readReceipts(sb.repo);
|
|
688
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
689
|
+
assert.equal(r.status, 0, `${args[0]}: ${r.stderr}`);
|
|
690
|
+
assert.equal(r.stdout, `${VERDICT_OUTPUT}\n`, `${args[0]}: stdout is the envelope response`);
|
|
691
|
+
assert.equal(receipts.length, 1);
|
|
692
|
+
assert.equal(receipts[0].fresh, false, `${args[0]}: a continuation still cannot attest`);
|
|
693
|
+
assert.equal(receipts[0].verdict, 'SHIP WITH NITS');
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
|
|
301
698
|
describe('agy-review.sh — guard + grounding (2, 3)', () => {
|
|
302
699
|
it('the model/cutoff GUARD line is in the captured prompt', () => {
|
|
303
700
|
const sb = makeSandbox();
|
|
@@ -573,7 +970,7 @@ const FED_CAP = 6000;
|
|
|
573
970
|
const seedFedChangeSet = (sb, { lines = 400, multibyte = false } = {}) => {
|
|
574
971
|
const body = Array.from({ length: lines }, (_, i) =>
|
|
575
972
|
multibyte
|
|
576
|
-
?
|
|
973
|
+
? `line ${String(i).padStart(4, '0')} — multibyte marker ${'\u044e'.repeat(20)}`
|
|
577
974
|
: `unique change-set line ${String(i).padStart(4, '0')} — a distinctive body marker ${'x'.repeat(20)}`).join('\n');
|
|
578
975
|
writeFileSync(join(sb.repo, 'oversized.txt'), `${body}\n`);
|
|
579
976
|
};
|
|
@@ -943,29 +1340,77 @@ describe('agy-review.sh — the agy permission fact is surfaced, never applied',
|
|
|
943
1340
|
});
|
|
944
1341
|
});
|
|
945
1342
|
|
|
1343
|
+
// ── fed lane: the conversation id comes from the ENVELOPE (Phase 5.1) ────────────────────────────
|
|
1344
|
+
// The id used to be scraped out of agy's own run log — a format that is agy's to change, degrading
|
|
1345
|
+
// to `--continue` when it changed. It is now a NAMED envelope field the reader validates against the
|
|
1346
|
+
// UUID grammar, so the pin that used to rot SILENTLY now fails LOUDLY: there is no `--continue`
|
|
1347
|
+
// fallback left in this lane and no later turn is spent when turn 1 cannot name its conversation.
|
|
946
1348
|
describe('agy-review.sh — fed lane: turn targeting (D9)', () => {
|
|
947
|
-
it('every turn after the first
|
|
1349
|
+
it('every turn after the first is ROUTED at the id turn 1`s envelope named', () => {
|
|
948
1350
|
const sb = makeSandbox();
|
|
949
1351
|
seedFedChangeSet(sb);
|
|
950
1352
|
const fed = fedRun(sb, { AGY_FAKE_CONV_ID: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' });
|
|
951
1353
|
rmSync(sb.home, { recursive: true, force: true });
|
|
952
1354
|
assert.equal(fed.status, 0, fed.stderr);
|
|
953
|
-
assert.match(fed.argvs[0], /(^|\n)--log-file(\n|$)/, 'turn 1 asks agy for its run log');
|
|
954
1355
|
assert.ok(!fed.argvs[0].includes('--conversation'), 'turn 1 is fresh');
|
|
1356
|
+
for (const [i, argv] of fed.argvs.entries()) {
|
|
1357
|
+
assert.ok(!argv.includes('--log-file'), `turn ${i + 1} must not ask for a run log any more: ${argv}`);
|
|
1358
|
+
assert.ok(!argv.includes('--continue'), `turn ${i + 1} must never ride the guessing lane: ${argv}`);
|
|
1359
|
+
}
|
|
955
1360
|
for (const argv of fed.argvs.slice(1)) {
|
|
956
1361
|
assert.match(argv, /(^|\n)--conversation(\n|$)/);
|
|
957
1362
|
assert.match(argv, /(^|\n)aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee(\n|$)/);
|
|
958
1363
|
}
|
|
959
1364
|
});
|
|
960
1365
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1366
|
+
// The three ways a NAMED field can still be unusable. Each one would otherwise route every later
|
|
1367
|
+
// turn at an arbitrary conversation, so each stops the run before turn 2 is spent.
|
|
1368
|
+
for (const [name, env] of [
|
|
1369
|
+
['absent', { AGY_FAKE_CONV_SHAPE: 'missing' }],
|
|
1370
|
+
['not a string', { AGY_FAKE_CONV_SHAPE: 'number' }],
|
|
1371
|
+
['failing the UUID grammar', { AGY_FAKE_CONV_ID: 'not-a-conversation-id' }],
|
|
1372
|
+
]) {
|
|
1373
|
+
it(`a turn-1 conversation id ${name} fails LOUDLY before turn 2, with NO receipt`, () => {
|
|
1374
|
+
const sb = makeSandbox();
|
|
1375
|
+
seedFedChangeSet(sb);
|
|
1376
|
+
const fed = fedRun(sb, env);
|
|
1377
|
+
const receipts = readReceipts(sb.repo);
|
|
1378
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1379
|
+
assert.equal(fed.status, 5, `an unusable envelope has the transport code: ${fed.stderr}`);
|
|
1380
|
+
assert.equal(fed.turns, 1, 'no later turn is spent');
|
|
1381
|
+
assert.equal(receipts.length, 0);
|
|
1382
|
+
assert.match(fed.stderr, /agy-envelope: conversation-id/, 'the reader names the field');
|
|
1383
|
+
assert.doesNotMatch(fed.stderr, /--continue/, 'there is no degrade lane to fall back to');
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// A LATER turn breaking is a different arm from turn 1 breaking: turn 1 already answered, so the
|
|
1388
|
+
// run has spent real subscription turns and must still stop without attesting anything.
|
|
1389
|
+
for (const [name, env, cause] of [
|
|
1390
|
+
['whose payload is not an envelope', { AGY_FAKE_BAD_TURN: '2', AGY_FAKE_RAW_STDOUT: 'jetski: not an envelope\n' }, /agy-envelope: not-json/],
|
|
1391
|
+
['reporting a non-SUCCESS status', { AGY_FAKE_BAD_TURN: '2', AGY_FAKE_STATUS: 'ERROR' }, /agy-envelope: status/],
|
|
1392
|
+
]) {
|
|
1393
|
+
it(`a fed turn 2 ${name} stops the run: NO receipt, no turn 3`, () => {
|
|
1394
|
+
const sb = makeSandbox();
|
|
1395
|
+
seedFedChangeSet(sb);
|
|
1396
|
+
const fed = fedRun(sb, env);
|
|
1397
|
+
const receipts = readReceipts(sb.repo);
|
|
1398
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1399
|
+
assert.equal(fed.status, 5, fed.stderr);
|
|
1400
|
+
assert.equal(fed.turns, 2, 'the failing turn is the last one spent');
|
|
1401
|
+
assert.equal(receipts.length, 0);
|
|
1402
|
+
assert.match(fed.stderr, cause, 'the reader names the cause');
|
|
1403
|
+
assert.match(fed.stderr, /feed turn 2 of/, 'and the wrapper names which turn');
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// The doc must not describe BOTH routings at once: a residual recording the retired log scrape
|
|
1408
|
+
// beside the envelope statement leaves an operator unable to tell which lane their build runs.
|
|
1409
|
+
it('the fed-lane doc states envelope routing and keeps no log-scrape residual (doc contract)', () => {
|
|
1410
|
+
const prompt = readFileSync(resolve(HERE, '..', 'references', 'review-prompt.md'), 'utf8');
|
|
1411
|
+
assert.match(prompt, /Later turns are routed by a NAMED field/, 'the live routing is stated');
|
|
1412
|
+
assert.doesNotMatch(prompt, /conversation id is parsed from/i, 'the retired scrape is no longer described as live');
|
|
1413
|
+
assert.doesNotMatch(prompt, /degrades[^\n]*--continue/, 'and neither is its degrade lane');
|
|
969
1414
|
});
|
|
970
1415
|
});
|
|
971
1416
|
|
|
@@ -1289,9 +1734,8 @@ describe('agy-review.sh — size ceiling + gated --add-dir escape (6)', () => {
|
|
|
1289
1734
|
const sb = makeSandbox();
|
|
1290
1735
|
seedFedChangeSet(sb);
|
|
1291
1736
|
const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_MAX_PROMPT_BYTES: String(FED_CAP) } });
|
|
1292
|
-
//
|
|
1293
|
-
const
|
|
1294
|
-
const stagingPath = logFile ? dirname(logFile) : '';
|
|
1737
|
+
// The fed lane dispatches every turn FROM the staging dir, so the fake's own cwd names it.
|
|
1738
|
+
const stagingPath = r.dispatchCwd;
|
|
1295
1739
|
const stillThere = stagingPath ? existsSync(stagingPath) : false;
|
|
1296
1740
|
rmSync(sb.home, { recursive: true, force: true });
|
|
1297
1741
|
assert.ok(stagingPath, 'a staging path was captured from the run');
|
|
@@ -1984,7 +2428,7 @@ describe('agy-review.sh — review receipts (AD-038)', () => {
|
|
|
1984
2428
|
// The grammar ENUMERATES the ASCII set (no ranges): a locale-collated [A-Za-z] could admit a
|
|
1985
2429
|
// non-ASCII letter the kit's JS reader then refuses, breaking correlation after a paid run.
|
|
1986
2430
|
const utf8 = makeSandbox();
|
|
1987
|
-
const r2 = run(utf8, { args: ['code', '--facts', 'a tiny fact'], env: { AW_REVIEW_NONCE: '
|
|
2431
|
+
const r2 = run(utf8, { args: ['code', '--facts', 'a tiny fact'], env: { AW_REVIEW_NONCE: 'r\u00e91', LC_ALL: 'en_US.UTF-8', LANG: 'en_US.UTF-8' } });
|
|
1988
2432
|
rmSync(utf8.home, { recursive: true, force: true });
|
|
1989
2433
|
assert.equal(r2.status, 2, 'a non-ASCII nonce letter refuses whatever the locale collation says');
|
|
1990
2434
|
assert.match(r2.stderr, /safe nonce grammar/);
|