@sabaiway/agent-workflow-kit 3.15.0 → 4.1.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 (65) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +4 -4
  3. package/SKILL.md +1 -1
  4. package/bridges/antigravity-cli-bridge/SKILL.md +12 -8
  5. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +735 -55
  6. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +899 -51
  7. package/bridges/antigravity-cli-bridge/bin/agy.sh +4 -3
  8. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +23 -0
  9. package/bridges/antigravity-cli-bridge/capability.json +14 -4
  10. package/bridges/antigravity-cli-bridge/references/driving-agy.md +12 -4
  11. package/bridges/antigravity-cli-bridge/references/models-and-flags.md +4 -3
  12. package/bridges/antigravity-cli-bridge/references/review-prompt.md +65 -2
  13. package/bridges/codex-cli-bridge/SKILL.md +1 -1
  14. package/bridges/codex-cli-bridge/bin/codex-exec.sh +2 -1
  15. package/bridges/codex-cli-bridge/bin/codex-review.sh +63 -13
  16. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +38 -0
  17. package/bridges/codex-cli-bridge/capability.json +1 -1
  18. package/capability.json +1 -1
  19. package/package.json +1 -1
  20. package/references/agents/review-lens.md +39 -0
  21. package/references/hooks/gate-approve.mjs +54 -7
  22. package/references/modes/agents.md +11 -2
  23. package/references/modes/autonomy-doctor.md +2 -0
  24. package/references/modes/backends.md +2 -0
  25. package/references/modes/bootstrap.md +2 -0
  26. package/references/modes/bridge-settings.md +4 -1
  27. package/references/modes/commit-guard.md +2 -0
  28. package/references/modes/core-evidence.md +2 -0
  29. package/references/modes/coverage-check.md +2 -0
  30. package/references/modes/doc-parity.md +2 -0
  31. package/references/modes/gates.md +2 -0
  32. package/references/modes/grounding.md +2 -0
  33. package/references/modes/help.md +2 -0
  34. package/references/modes/hook.md +13 -2
  35. package/references/modes/migrate-adr-store.md +2 -0
  36. package/references/modes/procedures.md +2 -0
  37. package/references/modes/recipes.md +2 -0
  38. package/references/modes/recommendations.md +4 -3
  39. package/references/modes/review-state.md +2 -0
  40. package/references/modes/sandbox-masks.md +2 -0
  41. package/references/modes/set-autonomy.md +2 -0
  42. package/references/modes/set-recipe.md +3 -0
  43. package/references/modes/setup.md +2 -0
  44. package/references/modes/state-block-guard.md +2 -0
  45. package/references/modes/status.md +4 -1
  46. package/references/modes/uninstall.md +2 -0
  47. package/references/modes/upgrade.md +2 -0
  48. package/references/modes/velocity.md +7 -0
  49. package/references/modes/worktrees.md +2 -0
  50. package/tools/bridge-settings-read.mjs +40 -10
  51. package/tools/bridge-settings.mjs +22 -7
  52. package/tools/cheap-agents.mjs +15 -5
  53. package/tools/commands.mjs +2 -2
  54. package/tools/core-evidence.mjs +29 -2
  55. package/tools/detect-backends.mjs +1 -1
  56. package/tools/manifest/schema.md +7 -0
  57. package/tools/manifest/validate.mjs +8 -0
  58. package/tools/presentation.mjs +1 -1
  59. package/tools/procedures.mjs +9 -2
  60. package/tools/recipes.mjs +4 -1
  61. package/tools/recommendations.mjs +110 -59
  62. package/tools/renderers.mjs +10 -1
  63. package/tools/review-state.mjs +4 -0
  64. package/tools/velocity-profile.mjs +6 -3
  65. package/tools/view-model.mjs +3 -1
@@ -28,7 +28,8 @@ const FAKE_AGY = [
28
28
  'printf invoked > "$AGY_FAKE_SENTINEL"',
29
29
  '{ for a in "$@"; do printf "%s\\n" "$a"; done; } > "$AGY_FAKE_ARGV"',
30
30
  '{ echo "FOO_API_KEY=${FOO_API_KEY:-<unset>}"; echo "ANTIGRAVITY_API_KEY=${ANTIGRAVITY_API_KEY:-<unset>}"; } > "$AGY_FAKE_ENV"',
31
- 'prev=""; for a in "$@"; do [[ "$prev" == "-p" ]] && printf "%s" "$a" > "$AGY_FAKE_PROMPT"; prev="$a"; done',
31
+ 'prompt=""',
32
+ 'prev=""; for a in "$@"; do if [[ "$prev" == "-p" ]]; then prompt="$a"; printf "%s" "$a" > "$AGY_FAKE_PROMPT"; fi; prev="$a"; done',
32
33
  'prev=""; for a in "$@"; do',
33
34
  ' if [[ "$prev" == "--add-dir" ]]; then',
34
35
  ' printf "%s" "$a" > "${AGY_FAKE_ADDDIR:-/dev/null}"',
@@ -38,6 +39,71 @@ const FAKE_AGY = [
38
39
  ' fi; prev="$a"',
39
40
  'done',
40
41
  'if [[ -n "${AGY_FAKE_SLEEP:-}" ]]; then sleep "$AGY_FAKE_SLEEP"; fi',
42
+ // ── multi-turn support (the fed lane) ──────────────────────────────────────────────────────────
43
+ // The single-file captures above record the LAST invocation; a chunked feed needs a PER-TURN
44
+ // record, so each invocation also writes prompt/argv to "<file>.<turn>" and bumps a counter file.
45
+ 'turn=1',
46
+ 'if [[ -n "${AGY_FAKE_TURNS:-}" ]]; then',
47
+ ' if [[ -s "$AGY_FAKE_TURNS" ]]; then turn=$(( $(cat "$AGY_FAKE_TURNS") + 1 )); fi',
48
+ ' printf "%s" "$turn" > "$AGY_FAKE_TURNS"',
49
+ ' printf "%s" "$prompt" > "${AGY_FAKE_PROMPT}.$turn"',
50
+ ' { for a in "$@"; do printf "%s\\n" "$a"; done; } > "${AGY_FAKE_ARGV}.$turn"',
51
+ '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
+ 'if [[ -n "${AGY_FAKE_FAIL_TURN:-}" && "$turn" == "${AGY_FAKE_FAIL_TURN}" ]]; then',
61
+ ' printf "FAKE_TURN_FAILURE\\n" >&2; exit 3',
62
+ 'fi',
63
+ // A FEED turn: the model was told to reply OK only. This fake deliberately misbehaves — it emits a
64
+ // PREMATURE verdict — so the isolation invariant (feed output never reaches stdout or the parsed
65
+ // capture) is proven against the worst case, not the polite one.
66
+ 'if [[ -z "${AGY_FAKE_OUTPUT+x}" && "$prompt" == *"--- BEGIN CHANGE-SET PART "* && "$prompt" != *"Requested addresses"* ]]; then',
67
+ ' printf "PREMATURE_FEED_CHATTER\\n### Verdict\\nREWORK\\n"; exit 0',
68
+ 'fi',
69
+ // The FINAL turn carries the delivery-proof request. The fake answers it the only honest way:
70
+ // by reading the bodies it was actually fed, turn by turn — so a wrapper that never delivered a
71
+ // part cannot be satisfied by this stub either.
72
+ 'if [[ -z "${AGY_FAKE_OUTPUT+x}" && "$prompt" == *"Requested addresses"* ]]; then',
73
+ ' req="$(printf "%s" "$prompt" | awk "/^Requested addresses/{f=1; next} f && /^###/{exit} f{print}")"',
74
+ ' entries=()',
75
+ ' mapfile -t _items <<< "$req"',
76
+ ' for _it in "${_items[@]}"; do',
77
+ ' [[ -n "$_it" ]] || continue',
78
+ ' k="$(printf "%s" "$_it" | awk "{print \\$2}")"; l="$(printf "%s" "$_it" | awk "{print \\$4}")"',
79
+ ' src="$k"',
80
+ ' if [[ "${AGY_FAKE_PROOF_DUP:-}" == "1" ]]; then src=1; fi',
81
+ ' if [[ "${AGY_FAKE_PROOF_OMIT:-}" == "$k" ]]; then continue; fi',
82
+ ' body="$(awk -v want="$l" "f && /^--- END CHANGE-SET PART /{exit} f{c++; if (c==want) {print; exit}} /^--- BEGIN CHANGE-SET PART /{f=1}" "${AGY_FAKE_PROMPT}.$src")"',
83
+ ' if [[ "${AGY_FAKE_PROOF_CORRUPT:-}" == "$k" ]]; then body="${body}X"; fi',
84
+ ' entry="$(printf "part %s line %s: %s" "$k" "$l" "$body")"',
85
+ // Shape knobs the grammar must survive (a bullet) or reject (everything else).
86
+ ' if [[ "${AGY_FAKE_PROOF_BULLET:-}" == "1" ]]; then entry="- $entry"; fi',
87
+ ' if [[ "${AGY_FAKE_PROOF_NESTED:-}" == "$k" ]]; then entry="note: I believe $entry"; fi',
88
+ ' if [[ "${AGY_FAKE_PROOF_PAD:-}" == "1" ]]; then entry="$(printf "part %02d line %04d: %s" "$k" "$l" "$body")"; fi',
89
+ ' if [[ "${AGY_FAKE_PROOF_CASE:-}" == "1" ]]; then entry="$(printf "Part %s Line %s: %s" "$k" "$l" "$body")"; fi',
90
+ ' if [[ "${AGY_FAKE_PROOF_HUGE:-}" == "$k" ]]; then entry="$(printf "part %s line 99999999999999999999: %s" "$k" "$body")"; fi',
91
+ ' entries+=("$entry")',
92
+ ' if [[ "${AGY_FAKE_PROOF_TWICE:-}" == "1" ]]; then entries+=("$entry"); fi',
93
+ ' done',
94
+ ' if [[ "${AGY_FAKE_PROOF_EXTRA:-}" == "1" ]]; then entries+=("part 99 line 1: an address nobody asked for"); fi',
95
+ ' if [[ "${AGY_FAKE_PROOF_HUGE_EXTRA:-}" == "1" ]]; then entries+=("part 99999999999999999999 line 1: an invented giant address"); fi',
96
+ ' if [[ "${AGY_FAKE_PROOF_LATE:-}" == "1" ]]; then printf "### Verdict\\nSHIP\\n"; fi',
97
+ ' if [[ "${AGY_FAKE_PROOF_CASE:-}" == "1" ]]; then printf "### Delivery Proof\\n"; else printf "### Delivery proof\\n"; fi',
98
+ ' if [[ "${AGY_FAKE_PROOF_OUTSIDE:-}" == "1" ]]; then',
99
+ ' printf "(nothing here)\\n### Verdict\\nSHIP\\n"',
100
+ ' if (( ${#entries[@]} > 0 )); then printf "%s\\n" "${entries[@]}"; fi',
101
+ ' else',
102
+ ' if (( ${#entries[@]} > 0 )); then printf "%s\\n" "${entries[@]}"; fi',
103
+ ' printf "### Verdict\\nSHIP\\n"',
104
+ ' fi',
105
+ ' exit 0',
106
+ 'fi',
41
107
  // Unset AGY_FAKE_OUTPUT → a verdict-carrying default (D4: a verdict-less run is a FAILURE, so
42
108
  // the success-path tests need one); an EXPLICIT empty value exercises the empty-output failure.
43
109
  'if [[ -z "${AGY_FAKE_OUTPUT+x}" ]]; then printf "FAKE_AGY_REVIEW_OUTPUT\\n### Verdict\\nSHIP\\n"; else printf "%s\\n" "$AGY_FAKE_OUTPUT"; fi',
@@ -107,14 +173,18 @@ const makeSandbox = ({ clean = false } = {}) => {
107
173
  return { home, bin, repo, g };
108
174
  };
109
175
 
176
+ // Capture files are per-INVOCATION: a second run() on the same sandbox must not inherit the first
177
+ // run's turn counter or per-turn prompt files (the fed lane reads them back by turn index).
178
+ let runSeq = 0;
110
179
  const run = (sb, { args, env = {}, cwd } = {}) => {
111
180
  const { home, bin, repo } = sb;
112
181
  const farm = farmFor(['agy', 'agy-run']);
182
+ const tag = `cap-${++runSeq}`;
113
183
  const cap = {
114
- argv: join(home, 'cap-argv'), env: join(home, 'cap-env'), prompt: join(home, 'cap-prompt'),
115
- sentinel: join(home, 'cap-sentinel'), adddir: join(home, 'cap-adddir'),
116
- adddirMode: join(home, 'cap-adddir-mode'), artifactMode: join(home, 'cap-artifact-mode'),
117
- artifactCopy: join(home, 'cap-artifact-copy'),
184
+ argv: join(home, `${tag}-argv`), env: join(home, `${tag}-env`), prompt: join(home, `${tag}-prompt`),
185
+ sentinel: join(home, `${tag}-sentinel`), adddir: join(home, `${tag}-adddir`),
186
+ adddirMode: join(home, `${tag}-adddir-mode`), artifactMode: join(home, `${tag}-artifact-mode`),
187
+ artifactCopy: join(home, `${tag}-artifact-copy`), turns: join(home, `${tag}-turns`),
118
188
  };
119
189
  const r = spawnSync('bash', [WRAPPER, ...args], {
120
190
  cwd: cwd || repo,
@@ -129,16 +199,26 @@ const run = (sb, { args, env = {}, cwd } = {}) => {
129
199
  AGY_FAKE_ARGV: cap.argv, AGY_FAKE_ENV: cap.env, AGY_FAKE_PROMPT: cap.prompt,
130
200
  AGY_FAKE_SENTINEL: cap.sentinel, AGY_FAKE_ADDDIR: cap.adddir, AGY_FAKE_ADDDIR_MODE: cap.adddirMode,
131
201
  AGY_FAKE_ARTIFACT_MODE: cap.artifactMode, AGY_FAKE_ARTIFACT_COPY: cap.artifactCopy,
202
+ AGY_FAKE_TURNS: cap.turns,
132
203
  ...env,
133
204
  },
134
205
  });
135
206
  const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '');
207
+ // Per-turn captures are read EAGERLY: callers rmSync the sandbox before asserting.
208
+ const turns = existsSync(cap.turns) ? Number(readFileSync(cap.turns, 'utf8')) : 0;
209
+ const prompts = [];
210
+ const argvs = [];
211
+ for (let i = 1; i <= turns; i += 1) {
212
+ prompts.push(readIf(`${cap.prompt}.${i}`));
213
+ argvs.push(readIf(`${cap.argv}.${i}`));
214
+ }
136
215
  return {
137
216
  ...r,
138
217
  invoked: existsSync(cap.sentinel),
139
218
  argv: readIf(cap.argv), capEnv: readIf(cap.env), prompt: readIf(cap.prompt),
140
219
  adddir: readIf(cap.adddir).trim(), adddirMode: readIf(cap.adddirMode).trim(),
141
220
  artifactMode: readIf(cap.artifactMode).trim(), artifactCopy: readIf(cap.artifactCopy),
221
+ turns, prompts, argvs,
142
222
  };
143
223
  };
144
224
 
@@ -403,45 +483,809 @@ describe('agy-review.sh — code-mode precomputed diff (4, 5, 8)', () => {
403
483
  });
404
484
  });
405
485
 
486
+ // ── the repo file map budget (Phase 2) ───────────────────────────────────────────────────────────
487
+ // The kit's extracted-helper parity test proves the SHARED emit_repo_file_map bounds the map; it
488
+ // cannot prove this REAL wrapper sets the budget. These cases run the wrapper end to end.
489
+ const MAP_DIR = 'deeply/nested/fixture/directory/for/the/repo/file/map/budget';
490
+ const AGY_MAP_BUDGET_BYTES = 8192;
491
+ const MAP_HEADER = '=== repo file map (git ls-files) ===\n';
492
+ const untouchedPath = (i) => `${MAP_DIR}/aa-untouched-file-${String(i).padStart(3, '0')}.txt`;
493
+ const modifiedPath = (i) => `${MAP_DIR}/zz-modified-file-${String(i).padStart(3, '0')}.txt`;
494
+
495
+ // A tracked map far past the budget (~200 long paths ≈ 17 KB) whose CHANGED subset is ALSO past it
496
+ // (~100 paths ≈ 8.7 KB) — the fixture Invariant B needs: a change touching very many long paths.
497
+ const seedOversizedMap = (sb, count = 100) => {
498
+ mkdirSync(join(sb.repo, MAP_DIR), { recursive: true });
499
+ for (let i = 0; i < count; i += 1) {
500
+ writeFileSync(join(sb.repo, untouchedPath(i)), `untouched ${i}\n`);
501
+ writeFileSync(join(sb.repo, modifiedPath(i)), `body ${i} v1\n`);
502
+ }
503
+ sb.g('add', '-A');
504
+ sb.g('commit', '-qm', 'map fixture');
505
+ for (let i = 0; i < count; i += 1) writeFileSync(join(sb.repo, modifiedPath(i)), `body ${i} v2 — changed\n`);
506
+ return count;
507
+ };
508
+ const mapSectionOf = (prompt) =>
509
+ prompt.slice(prompt.indexOf(MAP_HEADER) + MAP_HEADER.length, prompt.indexOf('\n\n=== git status (porcelain) ==='));
510
+
511
+ describe('agy-review.sh — repo file map budget (Phase 2)', () => {
512
+ it('agy-review sets the map budget and degrades an over-budget map', () => {
513
+ const sb = makeSandbox();
514
+ const count = seedOversizedMap(sb);
515
+ const r = run(sb, { args: ['code', '--facts', 'f'] });
516
+ rmSync(sb.home, { recursive: true, force: true });
517
+ assert.equal(r.status, 0, r.stderr);
518
+ const note = r.prompt.match(new RegExp(`=== repo file map TRUNCATED to the changed-path subset: (\\d+) of (\\d+) tracked paths shown, (\\d+) omitted \\(map budget ${AGY_MAP_BUDGET_BYTES} bytes\\) ===`));
519
+ assert.ok(note, 'the wrapper must set the budget and state the truncation with its counts');
520
+ const [, shown, total, omitted] = note.map(Number);
521
+ assert.ok(total >= 2 * count, 'the fixture map really carries every seeded path');
522
+ assert.equal(shown + omitted, total, 'the stated counts add up — a truncation-with-count, never a silent cut');
523
+ assert.ok(shown < total, 'the map really degraded');
524
+ assert.ok(r.prompt.includes(modifiedPath(0)), 'the degraded map keeps the CHANGED paths');
525
+ assert.ok(!r.prompt.includes(untouchedPath(0)), 'an untouched path is dropped, and it appears nowhere else in the payload');
526
+ });
527
+
528
+ it('the degraded changed-path subset itself stays inside the wrapper budget', () => {
529
+ const sb = makeSandbox();
530
+ seedOversizedMap(sb);
531
+ const r = run(sb, { args: ['code', '--facts', 'f'] });
532
+ rmSync(sb.home, { recursive: true, force: true });
533
+ assert.equal(r.status, 0, r.stderr);
534
+ const lines = mapSectionOf(r.prompt).split('\n');
535
+ const noteAt = lines.findIndex((l) => l.startsWith('=== repo file map TRUNCATED'));
536
+ assert.notEqual(noteAt, -1, 'the note closes the degraded section');
537
+ const pathBytes = Buffer.byteLength(lines.slice(0, noteAt).join('\n'), 'utf8');
538
+ assert.ok(pathBytes > 0, 'the subset is non-empty');
539
+ assert.ok(pathBytes <= AGY_MAP_BUDGET_BYTES, `the subset (${pathBytes} bytes) must stay inside the ${AGY_MAP_BUDGET_BYTES}-byte budget`);
540
+ });
541
+
542
+ it('an ordinary in-budget repo keeps the whole map, unnoted', () => {
543
+ const sb = makeSandbox();
544
+ const r = run(sb, { args: ['code', '--facts', 'f'] });
545
+ rmSync(sb.home, { recursive: true, force: true });
546
+ assert.equal(r.status, 0, r.stderr);
547
+ assert.match(r.prompt, /=== repo file map \(git ls-files\) ===\nbase\.txt\n/);
548
+ assert.doesNotMatch(r.prompt, /TRUNCATED/, 'a fitting map is untouched by the bound');
549
+ });
550
+ });
551
+
552
+ // ── the chunked-feed code review with PROVEN delivery (Phase 3) ──────────────────────────────────
553
+ // agy takes its prompt as ONE argv, and this host AUTO-DENIES agy's native read_file tool — so an
554
+ // over-cap change set can never be FETCHED by the model. It is DELIVERED instead: partitioned into
555
+ // under-cap parts, fed over continuation turns, then reviewed in a final turn. Delivery is PROVEN,
556
+ // never assumed: the wrapper picks a line from each part's body AFTER assembly and the final answer
557
+ // must reproduce every picked line verbatim. Envelope and body are formally separate — only BODIES
558
+ // concatenate, and they concatenate to the change set byte-for-byte.
559
+ const ARTIFACT_HEADER = '## The change set under review (assembled working-tree diff — repo-complete)';
560
+ const SHAPE_HEADER = '\n## Output — Markdown, this exact shape, nothing else';
561
+ const FED_CAP = 6000;
562
+
563
+ // A change set big enough to need several parts under FED_CAP.
564
+ const seedFedChangeSet = (sb, { lines = 400, multibyte = false } = {}) => {
565
+ const body = Array.from({ length: lines }, (_, i) =>
566
+ multibyte
567
+ ? `строка ${String(i).padStart(4, '0')} — многобайтовый маркер ${'ю'.repeat(20)}`
568
+ : `unique change-set line ${String(i).padStart(4, '0')} — a distinctive body marker ${'x'.repeat(20)}`).join('\n');
569
+ writeFileSync(join(sb.repo, 'oversized.txt'), `${body}\n`);
570
+ };
571
+
572
+ const inlineArtifactOf = (prompt) => prompt.slice(prompt.indexOf(ARTIFACT_HEADER), prompt.indexOf(SHAPE_HEADER));
573
+ const bodyOf = (turnPrompt) => {
574
+ const begin = turnPrompt.match(/--- BEGIN CHANGE-SET PART \d+ OF \d+ ---\n/);
575
+ if (!begin) return null;
576
+ const start = begin.index + begin[0].length;
577
+ return turnPrompt.slice(start, turnPrompt.indexOf('\n--- END CHANGE-SET PART ', start));
578
+ };
579
+ // The addresses ride ONE PER LINE — that format is what makes a collision with a proof candidate
580
+ // constructively impossible, so the parser reads lines, never a delimiter-joined field.
581
+ const requestedBlockOf = (finalPrompt) => {
582
+ const start = finalPrompt.indexOf('Requested addresses');
583
+ assert.notEqual(start, -1, 'the final turn states which lines it requires');
584
+ const after = finalPrompt.slice(finalPrompt.indexOf('\n', start) + 1);
585
+ return after.slice(0, after.indexOf('\n###')).split('\n').filter(Boolean);
586
+ };
587
+ const requestedOf = (finalPrompt) => requestedBlockOf(finalPrompt).map((item) => {
588
+ const [, part, line] = item.match(/^part (\d+) line (\d+)$/);
589
+ return { part: Number(part), line: Number(line) };
590
+ });
591
+ const fedRun = (sb, extraEnv = {}) =>
592
+ run(sb, { args: ['code', '--facts', 'grounded fact'], env: { AGY_MAX_PROMPT_BYTES: String(FED_CAP), ...extraEnv } });
593
+
594
+ describe('agy-review.sh — chunked feed: the change set is DELIVERED (Phase 3)', () => {
595
+ it('an over-cap code review feeds every part and the concatenated BODIES reproduce the change set exactly', () => {
596
+ const sb = makeSandbox();
597
+ seedFedChangeSet(sb);
598
+ const inline = run(sb, { args: ['code', '--facts', 'grounded fact'], env: { AGY_MAX_PROMPT_BYTES: '130000' } });
599
+ const fed = fedRun(sb);
600
+ rmSync(sb.home, { recursive: true, force: true });
601
+ assert.equal(inline.status, 0, inline.stderr);
602
+ assert.equal(fed.status, 0, fed.stderr);
603
+ assert.ok(fed.turns >= 3, `the fixture must really chunk (got ${fed.turns} turns)`);
604
+ const bodies = fed.prompts.slice(0, -1).map(bodyOf);
605
+ assert.ok(bodies.every((b) => b !== null), 'every turn but the last carries exactly one body');
606
+ assert.equal(bodies.join(''), inlineArtifactOf(inline.prompt), 'the bodies concatenate to the change set byte-for-byte');
607
+ });
608
+
609
+ it('no envelope text appears in the reconstructed review artifact', () => {
610
+ const sb = makeSandbox();
611
+ seedFedChangeSet(sb);
612
+ const fed = fedRun(sb);
613
+ rmSync(sb.home, { recursive: true, force: true });
614
+ const reconstructed = fed.prompts.slice(0, -1).map(bodyOf).join('');
615
+ for (const envelope of ['--- BEGIN CHANGE-SET PART', '--- END CHANGE-SET PART', 'Chunked delivery', 'Reply with exactly OK', 'Grounded facts', 'Requested:']) {
616
+ assert.ok(!reconstructed.includes(envelope), `envelope text leaked into the artifact: ${envelope}`);
617
+ }
618
+ });
619
+
620
+ it('every fed turn prompt is under AGY_MAX_PROMPT_BYTES', () => {
621
+ const sb = makeSandbox();
622
+ seedFedChangeSet(sb);
623
+ const fed = fedRun(sb);
624
+ rmSync(sb.home, { recursive: true, force: true });
625
+ assert.equal(fed.status, 0, fed.stderr);
626
+ for (const [i, p] of fed.prompts.entries()) {
627
+ assert.ok(Buffer.byteLength(p, 'utf8') <= FED_CAP, `turn ${i + 1} is ${Buffer.byteLength(p, 'utf8')} bytes, over the ${FED_CAP} ceiling`);
628
+ }
629
+ });
630
+
631
+ it('the shape block appears only on the final turn, and every feed turn carries the acknowledge-only instruction', () => {
632
+ const sb = makeSandbox();
633
+ seedFedChangeSet(sb);
634
+ const fed = fedRun(sb);
635
+ rmSync(sb.home, { recursive: true, force: true });
636
+ const feed = fed.prompts.slice(0, -1);
637
+ const final = fed.prompts[fed.prompts.length - 1];
638
+ for (const [i, p] of feed.entries()) {
639
+ assert.ok(!p.includes('## Output — Markdown'), `feed turn ${i + 1} must not carry the output shape`);
640
+ assert.ok(!p.includes('### Verdict'), `feed turn ${i + 1} must not ask for a verdict`);
641
+ assert.match(p, /Reply with exactly OK and NOTHING else/, `feed turn ${i + 1} must be acknowledge-only`);
642
+ }
643
+ assert.match(final, /## Output — Markdown/);
644
+ assert.match(final, /### Delivery proof/);
645
+ assert.ok(final.indexOf('### Delivery proof') < final.indexOf('### Verdict'), 'the proof section is FIRST in the mandated shape');
646
+ assert.ok(bodyOf(final) === null, 'the final turn carries no body — the change set is already delivered');
647
+ });
648
+
649
+ // Observed LIVE on the first real over-cap dispatch: the final turn reached for a tool to count
650
+ // lines, headless agy auto-denied it ("no output produced — a tool required the \"command\"
651
+ // permission"), and the whole answer was lost. Every turn must forbid tool use outright — the
652
+ // change set is already IN the conversation, so no tool can add anything.
653
+ it('every turn forbids tool use — a denied tool loses the whole answer on this host', () => {
654
+ const sb = makeSandbox();
655
+ seedFedChangeSet(sb);
656
+ const fed = fedRun(sb);
657
+ rmSync(sb.home, { recursive: true, force: true });
658
+ assert.equal(fed.status, 0, fed.stderr);
659
+ for (const [i, p] of fed.prompts.entries()) {
660
+ assert.match(p, /do NOT use any tool/i, `turn ${i + 1} must forbid tool use`);
661
+ assert.match(p, /already in this conversation|from THIS CONVERSATION only/i, `turn ${i + 1} must say why no tool is needed`);
662
+ }
663
+ });
664
+
665
+ // The delivery verdict must not blame delivery for a run that produced no answer at all: the parts
666
+ // WERE fed, the model was blocked from replying. Reporting it as "the change set never arrived"
667
+ // sends the reader hunting the wrong bug.
668
+ it('a final turn that produced NO answer reports that cause, never a delivery failure', () => {
669
+ const sb = makeSandbox();
670
+ seedFedChangeSet(sb);
671
+ const fed = fedRun(sb, { AGY_FAKE_OUTPUT: 'jetski: no output produced — a tool required the "command" permission that headless mode cannot prompt for, so it was auto-denied.' });
672
+ const receipts = readReceipts(sb.repo);
673
+ rmSync(sb.home, { recursive: true, force: true });
674
+ assert.equal(fed.status, 4, fed.stderr);
675
+ assert.match(fed.stderr, /produced no review/i, 'the cause is the empty answer');
676
+ assert.doesNotMatch(fed.stderr, /never received it/, 'never the delivery accusation');
677
+ assert.match(fed.stderr, /SENT every one of/, 'it claims only what it can: the parts were sent');
678
+ assert.doesNotMatch(fed.stderr, /delivery was proven|WAS delivered/, 'retention is exactly what the unanswered proof leaves unknown');
679
+ assert.match(fed.stderr, /CAUSE \(named by agy itself\)/, 'the KNOWN denial signature is recognized, not guessed');
680
+ assert.equal(receipts.length, 0, 'still no receipt — an unanswered review attests nothing');
681
+ });
682
+
683
+ it('an answerless final turn with NO recognizable diagnostic reports the cause as unknown', () => {
684
+ const sb = makeSandbox();
685
+ seedFedChangeSet(sb);
686
+ const fed = fedRun(sb, { AGY_FAKE_OUTPUT: 'something the wrapper has never seen before' });
687
+ rmSync(sb.home, { recursive: true, force: true });
688
+ assert.equal(fed.status, 4, fed.stderr);
689
+ assert.match(fed.stderr, /CAUSE: unknown/, 'an unrecognized failure is never dressed up as a known one');
690
+ assert.doesNotMatch(fed.stderr, /named by agy itself/);
691
+ });
692
+
693
+ it('the grounding rides turn 1 only', () => {
694
+ const sb = makeSandbox();
695
+ seedFedChangeSet(sb);
696
+ const fed = fedRun(sb, { AGY_MAX_PROMPT_BYTES: String(FED_CAP) });
697
+ rmSync(sb.home, { recursive: true, force: true });
698
+ assert.match(fed.prompts[0], /## Grounded facts — review AGAINST these/);
699
+ assert.match(fed.prompts[0], /grounded fact/);
700
+ for (const p of fed.prompts.slice(1)) assert.ok(!p.includes('## Grounded facts'), 'the grounding is not re-sent');
701
+ });
702
+
703
+ it('a multibyte body is cut at LINE boundaries — every part decodes cleanly and concatenation stays byte-exact', () => {
704
+ const sb = makeSandbox();
705
+ seedFedChangeSet(sb, { multibyte: true, lines: 400 });
706
+ const inline = run(sb, { args: ['code', '--facts', 'grounded fact'], env: { AGY_MAX_PROMPT_BYTES: '130000' } });
707
+ const fed = fedRun(sb);
708
+ rmSync(sb.home, { recursive: true, force: true });
709
+ assert.equal(fed.status, 0, fed.stderr);
710
+ assert.ok(fed.turns >= 3, 'the multibyte fixture really chunks');
711
+ const bodies = fed.prompts.slice(0, -1).map(bodyOf);
712
+ for (const [i, b] of bodies.entries()) {
713
+ assert.equal(Buffer.from(b, 'utf8').toString('utf8'), b, `part ${i + 1} carries no split code point`);
714
+ if (i < bodies.length - 1) assert.ok(b.endsWith('\n'), `part ${i + 1} ends on a line boundary`);
715
+ }
716
+ assert.equal(bodies.join(''), inlineArtifactOf(inline.prompt), 'byte-exact concatenation survives multibyte content');
717
+ });
718
+
719
+ // The partitioner's two boundary defects, both caught at review: an invented separator byte on an
720
+ // unterminated last line (an extra part and an extra TURN), and an over-eager refusal for a line
721
+ // that is merely longer than the FIRST part's smaller budget.
722
+ it('an artifact with NO trailing newline yields no extra or empty part, and reassembles byte-exactly', () => {
723
+ const sb = makeSandbox();
724
+ // An untracked file with no final newline: the assembled change set ends without one too.
725
+ writeFileSync(join(sb.repo, 'oversized.txt'), `${Array.from({ length: 400 }, (_, i) => `unique change-set line ${String(i).padStart(4, '0')} — a distinctive body marker ${'x'.repeat(20)}`).join('\n')}`);
726
+ const inline = run(sb, { args: ['code', '--facts', 'grounded fact'], env: { AGY_MAX_PROMPT_BYTES: '130000' } });
727
+ const fed = fedRun(sb);
728
+ rmSync(sb.home, { recursive: true, force: true });
729
+ assert.equal(fed.status, 0, fed.stderr);
730
+ const bodies = fed.prompts.slice(0, -1).map(bodyOf);
731
+ for (const [i, b] of bodies.entries()) assert.ok(b.length > 0, `part ${i + 1} is non-empty`);
732
+ assert.equal(bodies.join(''), inlineArtifactOf(inline.prompt), 'byte-exact reassembly without a final newline');
733
+ const announced = fed.stderr.match(/feeding the change set in (\d+) part\(s\) over (\d+) subscription turns/);
734
+ assert.equal(Number(announced[1]), bodies.length, 'the announced part count is the count really sent — no phantom part');
735
+ assert.equal(Number(announced[2]), fed.turns, 'and no phantom turn');
736
+ });
737
+
738
+ it('a line longer than the FIRST part budget but not the later one is placed, not refused', () => {
739
+ const sb = makeSandbox();
740
+ // Turn 1 carries the grounding too, so its body budget is SMALLER by exactly the grounding size.
741
+ // A fat grounding opens a real window between the two budgets; a line inside that window must be
742
+ // moved to a later part, not made to fail the whole run.
743
+ const facts = `grounded fact ${'g'.repeat(2000)}`;
744
+ // Every filler line must be GLOBALLY UNIQUE — the proof selector rejects a repeated line, so a
745
+ // restarted counter would starve the part of candidates and hide what this case is testing.
746
+ const filler = (from, n) => Array.from({ length: n }, (_, i) => `unique change-set line ${String(from + i).padStart(4, '0')} — a distinctive body marker ${'x'.repeat(20)}`).join('\n');
747
+ const longLine = `unique long marker line ${'y'.repeat(4200)}`;
748
+ writeFileSync(join(sb.repo, 'oversized.txt'), `${filler(0, 20)}\n${longLine}\n${filler(20, 60)}\n`);
749
+ const fed = run(sb, { args: ['code', '--facts', facts], env: { AGY_MAX_PROMPT_BYTES: String(FED_CAP) } });
750
+ rmSync(sb.home, { recursive: true, force: true });
751
+ assert.equal(fed.status, 0, `a placeable long line must not refuse the run: ${fed.stderr}`);
752
+ const bodies = fed.prompts.slice(0, -1).map(bodyOf);
753
+ assert.ok(bodies.some((b) => b.includes(longLine)), 'the long line rode a part whole');
754
+ assert.ok(!bodies[0].includes(longLine), 'and it was moved OFF the smaller first part');
755
+ });
756
+
757
+ it('a line that fits NO part refuses before any turn is spent', () => {
758
+ const sb = makeSandbox();
759
+ writeFileSync(join(sb.repo, 'oversized.txt'), `head\n${'z'.repeat(FED_CAP * 2)}\ntail\n`);
760
+ const fed = fedRun(sb);
761
+ rmSync(sb.home, { recursive: true, force: true });
762
+ assert.equal(fed.status, 2, fed.stderr);
763
+ assert.equal(fed.invoked, false, 'not one turn is spent');
764
+ assert.match(fed.stderr, /does not fit even an EMPTY fed part/);
765
+ });
766
+
767
+ it('feed-turn output never reaches stdout or the parsed capture (a premature verdict is discarded)', () => {
768
+ const sb = makeSandbox();
769
+ seedFedChangeSet(sb);
770
+ const fed = fedRun(sb);
771
+ const receipts = readReceipts(sb.repo);
772
+ rmSync(sb.home, { recursive: true, force: true });
773
+ assert.equal(fed.status, 0, fed.stderr);
774
+ assert.ok(!fed.stdout.includes('PREMATURE_FEED_CHATTER'), 'a feed turn never publishes to stdout');
775
+ assert.ok(!fed.stdout.includes('REWORK'), 'a feed turn`s premature verdict never reaches the reader');
776
+ assert.equal(receipts.length, 1);
777
+ assert.equal(receipts[0].verdict, 'SHIP', 'only the FINAL turn is parsed into the receipt');
778
+ });
779
+
780
+ it('a non-zero feed turn stops the run, spends no later turn, and writes NO receipt', () => {
781
+ const sb = makeSandbox();
782
+ seedFedChangeSet(sb);
783
+ const fed = fedRun(sb, { AGY_FAKE_FAIL_TURN: '2' });
784
+ const receipts = readReceipts(sb.repo);
785
+ rmSync(sb.home, { recursive: true, force: true });
786
+ assert.notEqual(fed.status, 0, 'a failed feed turn is a failed review');
787
+ assert.equal(fed.turns, 2, 'the run stops at the first failure — no later turn is spent');
788
+ assert.equal(receipts.length, 0, 'a run whose delivery never completed mints nothing');
789
+ });
790
+
791
+ // The hard cap is ONE wall-clock budget for the whole review. Handing each of the N+1 calls the
792
+ // full AGY_HARD_TIMEOUT multiplied the stated guarantee by the turn count — a 30m cap could run
793
+ // for hours. Each turn now gets only what is LEFT of a shared deadline.
794
+ it('the hard cap is ONE budget for the whole review, not one per turn', () => {
795
+ const sb = makeSandbox();
796
+ seedFedChangeSet(sb, { lines: 150 });
797
+ const fed = run(sb, {
798
+ args: ['code', '--facts', 'grounded fact'],
799
+ env: { AGY_MAX_PROMPT_BYTES: String(FED_CAP), AGY_HARD_TIMEOUT: '600s', AGY_TIMEOUT: '600s', AGY_FAKE_SLEEP: '1' },
800
+ });
801
+ rmSync(sb.home, { recursive: true, force: true });
802
+ assert.equal(fed.status, 0, fed.stderr);
803
+ const timeouts = fed.argvs.map((argv) => {
804
+ const tokens = argv.split('\n');
805
+ return Number((tokens[tokens.indexOf('--print-timeout') + 1] ?? '').replace(/s$/, ''));
806
+ });
807
+ assert.ok(timeouts.length >= 3, `the fixture must really chunk (got ${timeouts.length} turns)`);
808
+ for (const [i, t] of timeouts.entries()) {
809
+ assert.ok(t > 0 && t <= 600, `turn ${i + 1} asked for ${t}s, outside the shared 600s budget`);
810
+ }
811
+ assert.ok(timeouts[timeouts.length - 1] < timeouts[0], 'the budget SHRINKS across turns — a per-turn cap would keep handing out the full 600s');
812
+ // council R1-M5: agy.sh hands timeout(1) `--kill-after=10s`, so a turn given the FULL remaining
813
+ // time can outlive the shared deadline by that grace when it ignores TERM. Every turn must
814
+ // therefore be handed strictly less than what is left.
815
+ assert.ok(timeouts[0] <= 600 - 10, `turn 1 asked for ${timeouts[0]}s — the SIGKILL grace is not reserved`);
816
+ });
817
+
818
+ // council R2-M2: shrinking a turn to 1s does not save the cap — a TERM-ignoring process still runs
819
+ // for the SIGKILL grace on top. Too little budget left is a REFUSAL, never a tiny turn.
820
+ it('a cap smaller than the SIGKILL grace refuses BEFORE spending a single turn', () => {
821
+ const sb = makeSandbox();
822
+ seedFedChangeSet(sb, { lines: 150 });
823
+ const fed = run(sb, {
824
+ args: ['code', '--facts', 'grounded fact'],
825
+ env: { AGY_MAX_PROMPT_BYTES: String(FED_CAP), AGY_HARD_TIMEOUT: '5s', AGY_TIMEOUT: '5s' },
826
+ });
827
+ const receipts = readReceipts(sb.repo);
828
+ rmSync(sb.home, { recursive: true, force: true });
829
+ assert.equal(fed.status, 124, fed.stderr);
830
+ assert.equal(fed.turns, 0, 'not one turn is dispatched — the refusal is pre-spend');
831
+ assert.match(fed.stderr, /SIGKILL grace/u, 'the refusal names the real cause');
832
+ assert.equal(receipts.length, 0, 'a refused review mints NO receipt');
833
+ });
834
+
835
+ it('the fed lane announces part and turn counts before the first dispatch (D5 quota honesty)', () => {
836
+ const sb = makeSandbox();
837
+ seedFedChangeSet(sb);
838
+ const fed = fedRun(sb);
839
+ rmSync(sb.home, { recursive: true, force: true });
840
+ const announce = fed.stderr.match(/feeding the change set in (\d+) part\(s\) over (\d+) subscription turns/);
841
+ assert.ok(announce, `the cost must be stated before it is spent: ${fed.stderr}`);
842
+ assert.equal(Number(announce[1]) + 1, Number(announce[2]), 'N parts cost N+1 turns');
843
+ assert.equal(Number(announce[2]), fed.turns, 'the announced turn count is the count really spent');
844
+ });
845
+
846
+ // The ceiling is a SPENDING guard, so a value the operator sets and the wrapper cannot honour must
847
+ // never be silently ignored. `008000` used to make bash evaluate an invalid octal constant: both
848
+ // range tests errored to false and the ceiling simply stopped existing.
849
+ it('a leading-zero ceiling is canonicalized, not read as octal — and it still refuses', () => {
850
+ const sb = makeSandbox();
851
+ seedFedChangeSet(sb);
852
+ const fed = fedRun(sb, { AGY_REVIEW_MAX_TOTAL_BYTES: '008000' });
853
+ rmSync(sb.home, { recursive: true, force: true });
854
+ assert.equal(fed.status, 2, fed.stderr);
855
+ assert.equal(fed.invoked, false, 'the ceiling really bound — not one turn was spent');
856
+ assert.match(fed.stderr, /over AGY_REVIEW_MAX_TOTAL_BYTES=8000\b/, 'canonicalized to 8000, and enforced at that value');
857
+ assert.doesNotMatch(fed.stderr, /value too great for base|invalid arithmetic/, 'no octal diagnostic anywhere');
858
+ });
859
+
860
+ it('an explicit env ceiling the wrapper cannot honour REFUSES, never silently defaults', () => {
861
+ const sb = makeSandbox();
862
+ seedFedChangeSet(sb);
863
+ for (const bad of ['999999999999999999999', '200000000', 'lots']) {
864
+ const fed = fedRun(sb, { AGY_REVIEW_MAX_TOTAL_BYTES: bad });
865
+ assert.equal(fed.status, 2, `${bad}: ${fed.stderr}`);
866
+ assert.equal(fed.invoked, false, `${bad}: no run is spent under an unhonoured ceiling`);
867
+ assert.match(fed.stderr, /not a valid byte ceiling/, `${bad}: the refusal names the cause`);
868
+ }
869
+ rmSync(sb.home, { recursive: true, force: true });
870
+ });
871
+
872
+ it('the DEFAULT ceiling still lets an ordinary fed review through', () => {
873
+ const sb = makeSandbox();
874
+ seedFedChangeSet(sb);
875
+ const fed = fedRun(sb, { AGY_REVIEW_MAX_TOTAL_BYTES: '240000' });
876
+ const receipts = readReceipts(sb.repo);
877
+ rmSync(sb.home, { recursive: true, force: true });
878
+ assert.equal(fed.status, 0, fed.stderr);
879
+ assert.equal(receipts[0].delivery, 'fed');
880
+ });
881
+
882
+ it('a change set whose total outgoing prompt bytes exceed AGY_REVIEW_MAX_TOTAL_BYTES refuses before the first turn is spent', () => {
883
+ const sb = makeSandbox();
884
+ seedFedChangeSet(sb);
885
+ const fed = fedRun(sb, { AGY_REVIEW_MAX_TOTAL_BYTES: '9000' });
886
+ const receipts = readReceipts(sb.repo);
887
+ rmSync(sb.home, { recursive: true, force: true });
888
+ assert.equal(fed.status, 2, fed.stderr);
889
+ assert.equal(fed.invoked, false, 'not one subscription turn is spent');
890
+ assert.match(fed.stderr, /over AGY_REVIEW_MAX_TOTAL_BYTES=9000/);
891
+ assert.equal(receipts.length, 0);
892
+ });
893
+
894
+ it('a fixed overhead that cannot fit refuses rather than emitting an empty body', () => {
895
+ const sb = makeSandbox();
896
+ seedFedChangeSet(sb);
897
+ const fed = fedRun(sb, { AGY_MAX_PROMPT_BYTES: '900' });
898
+ rmSync(sb.home, { recursive: true, force: true });
899
+ assert.equal(fed.status, 2, fed.stderr);
900
+ assert.equal(fed.invoked, false);
901
+ assert.match(fed.stderr, /leaves no room/);
902
+ });
903
+ });
904
+
905
+ // 4.3: agy's own denial names the permission rule it wants. The kit SURFACES that fact and never
906
+ // applies it — granting read_file would widen a boundary for ALL agy use on the machine to re-arm
907
+ // the one lane whose failure mode is undetectable by construction.
908
+ describe('agy-review.sh — the agy permission fact is surfaced, never applied', () => {
909
+ const BRIDGE_ROOT = resolve(HERE, '..');
910
+
911
+ it('the over-cap path states why the change set is delivered rather than read', () => {
912
+ const sb = makeSandbox();
913
+ seedFedChangeSet(sb);
914
+ const fed = fedRun(sb);
915
+ rmSync(sb.home, { recursive: true, force: true });
916
+ assert.equal(fed.status, 0, fed.stderr);
917
+ assert.match(fed.stderr, /read_file/, 'the notice names the denied tool');
918
+ assert.match(fed.stderr, /never (grants|writes)/, 'and states that the kit does not grant it');
919
+ });
920
+
921
+ it('the bridge docs state the never-applied posture (doc contract)', () => {
922
+ const prompt = readFileSync(join(BRIDGE_ROOT, 'references', 'review-prompt.md'), 'utf8');
923
+ assert.match(prompt, /read_file/, 'the denial is named');
924
+ assert.match(prompt, /never writes it|never applied/i, 'the never-applied posture is stated');
925
+ assert.match(prompt, /dangerously-skip-permissions/, 'and the strictly-worse alternative is named as not offered');
926
+ assert.doesNotMatch(prompt, /grant (the )?read_file permission to (fix|enable)/i, 'the docs never RECOMMEND granting it');
927
+ });
928
+
929
+ it('no wrapper or doc surface ever writes an agy permission rule', () => {
930
+ for (const rel of [join('bin', 'agy-review.sh'), join('bin', 'agy.sh')]) {
931
+ const text = readFileSync(join(BRIDGE_ROOT, rel), 'utf8');
932
+ assert.doesNotMatch(text, /--dangerously-skip-permissions/, `${rel} must never pass the blanket-permission flag`);
933
+ }
934
+ });
935
+ });
936
+
937
+ describe('agy-review.sh — fed lane: turn targeting (D9)', () => {
938
+ it('every turn after the first pins the captured conversation id', () => {
939
+ const sb = makeSandbox();
940
+ seedFedChangeSet(sb);
941
+ const fed = fedRun(sb, { AGY_FAKE_CONV_ID: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' });
942
+ rmSync(sb.home, { recursive: true, force: true });
943
+ assert.equal(fed.status, 0, fed.stderr);
944
+ assert.match(fed.argvs[0], /(^|\n)--log-file(\n|$)/, 'turn 1 asks agy for its run log');
945
+ assert.ok(!fed.argvs[0].includes('--conversation'), 'turn 1 is fresh');
946
+ for (const argv of fed.argvs.slice(1)) {
947
+ assert.match(argv, /(^|\n)--conversation(\n|$)/);
948
+ assert.match(argv, /(^|\n)aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee(\n|$)/);
949
+ }
950
+ });
951
+
952
+ it('an unparseable log degrades to --continue with a stated notice, not silently', () => {
953
+ const sb = makeSandbox();
954
+ seedFedChangeSet(sb);
955
+ const fed = fedRun(sb, { AGY_FAKE_BAD_CONV_LOG: '1' });
956
+ rmSync(sb.home, { recursive: true, force: true });
957
+ assert.equal(fed.status, 0, fed.stderr);
958
+ assert.match(fed.stderr, /could not capture the conversation id/, 'the degrade is STATED');
959
+ for (const argv of fed.argvs.slice(1)) assert.match(argv, /(^|\n)--continue(\n|$)/);
960
+ });
961
+ });
962
+
963
+ describe('agy-review.sh — fed lane: delivery is PROVEN or the review FAILS (D1, D7)', () => {
964
+ it('a fed review reproducing every selected line writes a fresh code receipt at the tree fingerprint', () => {
965
+ const sb = makeSandbox();
966
+ seedFedChangeSet(sb);
967
+ const fed = fedRun(sb);
968
+ const receipts = readReceipts(sb.repo);
969
+ rmSync(sb.home, { recursive: true, force: true });
970
+ assert.equal(fed.status, 0, fed.stderr);
971
+ assert.equal(receipts.length, 1);
972
+ assert.equal(receipts[0].artifact, 'code');
973
+ assert.equal(receipts[0].fresh, true);
974
+ assert.match(receipts[0].fingerprint, /^[0-9a-f]{64}$/);
975
+ assert.equal(receipts[0].delivery, 'fed', 'the receipt declares HOW delivery was established');
976
+ });
977
+
978
+ it('a fed review whose output omits a part`s echo exits 4 and writes NO receipt', () => {
979
+ const sb = makeSandbox();
980
+ seedFedChangeSet(sb);
981
+ const fed = fedRun(sb, { AGY_FAKE_PROOF_OMIT: '2' });
982
+ const receipts = readReceipts(sb.repo);
983
+ rmSync(sb.home, { recursive: true, force: true });
984
+ assert.equal(fed.status, 4, fed.stderr);
985
+ assert.match(fed.stderr, /delivery/i, 'the cause names delivery, not a generic missing verdict');
986
+ assert.ok(!/no recognized '### Verdict' section/.test(fed.stderr), 'never the generic verdict-less message');
987
+ assert.equal(receipts.length, 0);
988
+ });
989
+
990
+ it('a fed review whose echo differs from the recorded line exits 4 and writes NO receipt', () => {
991
+ const sb = makeSandbox();
992
+ seedFedChangeSet(sb);
993
+ const fed = fedRun(sb, { AGY_FAKE_PROOF_CORRUPT: '2' });
994
+ const receipts = readReceipts(sb.repo);
995
+ rmSync(sb.home, { recursive: true, force: true });
996
+ assert.equal(fed.status, 4, fed.stderr);
997
+ assert.match(fed.stderr, /delivery/i);
998
+ assert.equal(receipts.length, 0);
999
+ });
1000
+
1001
+ it('a fed review echoing one part`s line for two parts exits 4 and writes NO receipt', () => {
1002
+ const sb = makeSandbox();
1003
+ seedFedChangeSet(sb);
1004
+ const fed = fedRun(sb, { AGY_FAKE_PROOF_DUP: '1' });
1005
+ const receipts = readReceipts(sb.repo);
1006
+ rmSync(sb.home, { recursive: true, force: true });
1007
+ assert.equal(fed.status, 4, fed.stderr);
1008
+ assert.equal(receipts.length, 0);
1009
+ });
1010
+
1011
+ // The proof GRAMMAR, pinned red→green (Test-as-spec). A substring search over the whole answer
1012
+ // accepted every shape below except the bullet — which is the one shape that should pass.
1013
+ it('an echo placed OUTSIDE the proof section does not count', () => {
1014
+ const sb = makeSandbox();
1015
+ seedFedChangeSet(sb);
1016
+ const fed = fedRun(sb, { AGY_FAKE_PROOF_OUTSIDE: '1' });
1017
+ const receipts = readReceipts(sb.repo);
1018
+ rmSync(sb.home, { recursive: true, force: true });
1019
+ assert.equal(fed.status, 4, fed.stderr);
1020
+ assert.equal(receipts.length, 0);
1021
+ });
1022
+
1023
+ // The proof comes FIRST so output truncation can never silently drop it. A block that arrives
1024
+ // after a verdict is not that shape, so it is not searched for — otherwise the "first" in the
1025
+ // contract would be decoration.
1026
+ it('a proof block placed AFTER the verdict does not count — the proof must be the first section', () => {
1027
+ const sb = makeSandbox();
1028
+ seedFedChangeSet(sb);
1029
+ const fed = fedRun(sb, { AGY_FAKE_PROOF_LATE: '1' });
1030
+ const receipts = readReceipts(sb.repo);
1031
+ rmSync(sb.home, { recursive: true, force: true });
1032
+ assert.equal(fed.status, 4, fed.stderr);
1033
+ assert.equal(receipts.length, 0);
1034
+ });
1035
+
1036
+ it('an address echoed TWICE fails — one address, one line', () => {
1037
+ const sb = makeSandbox();
1038
+ seedFedChangeSet(sb);
1039
+ const fed = fedRun(sb, { AGY_FAKE_PROOF_TWICE: '1' });
1040
+ const receipts = readReceipts(sb.repo);
1041
+ rmSync(sb.home, { recursive: true, force: true });
1042
+ assert.equal(fed.status, 4, fed.stderr);
1043
+ assert.match(fed.stderr, /more than once/i);
1044
+ assert.equal(receipts.length, 0);
1045
+ });
1046
+
1047
+ it('an UNREQUESTED address in the proof section fails', () => {
1048
+ const sb = makeSandbox();
1049
+ seedFedChangeSet(sb);
1050
+ const fed = fedRun(sb, { AGY_FAKE_PROOF_EXTRA: '1' });
1051
+ const receipts = readReceipts(sb.repo);
1052
+ rmSync(sb.home, { recursive: true, force: true });
1053
+ assert.equal(fed.status, 4, fed.stderr);
1054
+ assert.match(fed.stderr, /was never requested/i);
1055
+ assert.equal(receipts.length, 0);
1056
+ });
1057
+
1058
+ it('a marker BURIED inside a sentence is not an echo (the anchor is real)', () => {
1059
+ const sb = makeSandbox();
1060
+ seedFedChangeSet(sb);
1061
+ const fed = fedRun(sb, { AGY_FAKE_PROOF_NESTED: '2' });
1062
+ const receipts = readReceipts(sb.repo);
1063
+ rmSync(sb.home, { recursive: true, force: true });
1064
+ assert.equal(fed.status, 4, fed.stderr);
1065
+ assert.equal(receipts.length, 0);
1066
+ });
1067
+
1068
+ // Numbers arriving from the MODEL are untrusted input. `part 08 line 09` reached bash arithmetic
1069
+ // and array indexing as `08`, which bash reads as OCTAL — the wrapper crashed with `value too
1070
+ // great for base` instead of the contracted clean refusal, and a safely padded `01` also mismatched
1071
+ // the unpadded `1`. Both die at the PARSE boundary now: awk hands bash plain decimals.
1072
+ it('a zero-padded proof address is normalized, never an octal crash and never a false refusal', () => {
1073
+ const sb = makeSandbox();
1074
+ seedFedChangeSet(sb);
1075
+ const fed = fedRun(sb, { AGY_FAKE_PROOF_PAD: '1' });
1076
+ const receipts = readReceipts(sb.repo);
1077
+ rmSync(sb.home, { recursive: true, force: true });
1078
+ assert.equal(fed.status, 0, `a padded address must pass, not crash: ${fed.stderr}`);
1079
+ assert.doesNotMatch(fed.stderr, /value too great for base/, 'never bash octal arithmetic on model input');
1080
+ assert.equal(receipts.length, 1);
1081
+ assert.equal(receipts[0].delivery, 'fed');
1082
+ });
1083
+
1084
+ it('a capitalized heading and anchor still count — and the payload keeps its own case', () => {
1085
+ const sb = makeSandbox();
1086
+ seedFedChangeSet(sb);
1087
+ const fed = fedRun(sb, { AGY_FAKE_PROOF_CASE: '1' });
1088
+ const receipts = readReceipts(sb.repo);
1089
+ rmSync(sb.home, { recursive: true, force: true });
1090
+ assert.equal(fed.status, 0, `a capitalization must not fail a real delivery: ${fed.stderr}`);
1091
+ assert.equal(receipts.length, 1, 'the review attests');
1092
+ });
1093
+
1094
+ it('an absurd proof address never reaches bash arithmetic — no crash, no impersonated address', () => {
1095
+ const sb = makeSandbox();
1096
+ seedFedChangeSet(sb);
1097
+ const fed = fedRun(sb, { AGY_FAKE_PROOF_HUGE: '2' });
1098
+ const receipts = readReceipts(sb.repo);
1099
+ rmSync(sb.home, { recursive: true, force: true });
1100
+ assert.equal(fed.status, 4, fed.stderr);
1101
+ assert.doesNotMatch(fed.stderr, /value too great for base|syntax error/, 'an absurd number never reaches bash arithmetic');
1102
+ assert.equal(receipts.length, 0);
1103
+ });
1104
+
1105
+ // Dropping an out-of-range address made it INVISIBLE: an answer with every correct echo plus one
1106
+ // invented giant address then satisfied a grammar whose whole point is that it is closed.
1107
+ it('a VALID proof carrying one extra out-of-range address still fails — an invented address is never invisible', () => {
1108
+ const sb = makeSandbox();
1109
+ seedFedChangeSet(sb);
1110
+ const fed = fedRun(sb, { AGY_FAKE_PROOF_HUGE_EXTRA: '1' });
1111
+ const receipts = readReceipts(sb.repo);
1112
+ rmSync(sb.home, { recursive: true, force: true });
1113
+ assert.equal(fed.status, 4, fed.stderr);
1114
+ assert.match(fed.stderr, /out of range/, 'the refusal names what was wrong with it');
1115
+ assert.equal(receipts.length, 0, 'a closed grammar does not mint a receipt beside an invented address');
1116
+ });
1117
+
1118
+ // The candidate cap was 25, so a change set whose first 25 middle-nearest candidates all fail the
1119
+ // fixed-string checks earned a false "no usable candidate" refusal while candidate 26 was fine.
1120
+ it('a part whose first 25 candidates are unusable still finds the one after them', () => {
1121
+ const sb = makeSandbox();
1122
+ // Each decoy is a unique WHOLE line (so it survives the cheap prefilter) that also occurs as a
1123
+ // SUBSTRING of a longer line — exactly the case the exact occurrence check must reject.
1124
+ const decoys = Array.from({ length: 30 }, (_, i) => `decoy candidate ${String(i).padStart(3, '0')} — appears twice as a substring`);
1125
+ const echoes = decoys.map((d) => `carrier line wrapping ${d} inside a longer line`);
1126
+ const filler = (from, n) => Array.from({ length: n }, (_, i) => `unique change-set line ${String(from + i).padStart(4, '0')} — a distinctive body marker ${'x'.repeat(20)}`);
1127
+ const body = [...filler(0, 40), ...decoys, ...filler(40, 40), ...echoes, ...filler(80, 60)];
1128
+ writeFileSync(join(sb.repo, 'oversized.txt'), `${body.join('\n')}\n`);
1129
+ const fed = fedRun(sb);
1130
+ rmSync(sb.home, { recursive: true, force: true });
1131
+ assert.equal(fed.status, 0, `a usable candidate past position 25 must be found: ${fed.stderr}`);
1132
+ const final = fed.prompts[fed.prompts.length - 1];
1133
+ const bodies = fed.prompts.slice(0, -1).map(bodyOf);
1134
+ for (const { part, line } of requestedOf(final)) {
1135
+ const chosen = bodies[part - 1].split('\n')[line - 1].trim();
1136
+ assert.ok(!decoys.includes(chosen), `a twice-occurring decoy was chosen for part ${part}: ${chosen}`);
1137
+ }
1138
+ });
1139
+
1140
+ it('a harmless `- ` bullet still counts — the anchor is strict, not brittle', () => {
1141
+ const sb = makeSandbox();
1142
+ seedFedChangeSet(sb);
1143
+ const fed = fedRun(sb, { AGY_FAKE_PROOF_BULLET: '1' });
1144
+ const receipts = readReceipts(sb.repo);
1145
+ rmSync(sb.home, { recursive: true, force: true });
1146
+ assert.equal(fed.status, 0, `a bulleted proof must not be a false refusal: ${fed.stderr}`);
1147
+ assert.equal(receipts.length, 1);
1148
+ assert.equal(receipts[0].delivery, 'fed');
1149
+ });
1150
+
1151
+ it('the selected lines never appear in any envelope the wrapper sends', () => {
1152
+ const sb = makeSandbox();
1153
+ seedFedChangeSet(sb);
1154
+ const fed = fedRun(sb);
1155
+ rmSync(sb.home, { recursive: true, force: true });
1156
+ assert.equal(fed.status, 0, fed.stderr);
1157
+ const final = fed.prompts[fed.prompts.length - 1];
1158
+ const bodies = fed.prompts.slice(0, -1).map(bodyOf);
1159
+ const requested = requestedOf(final);
1160
+ assert.equal(requested.length, bodies.length, 'one requested line per fed part');
1161
+ for (const { part, line } of requested) {
1162
+ const expected = bodies[part - 1].split('\n')[line - 1];
1163
+ assert.ok(expected && expected.trim().length > 0, `part ${part} line ${line} resolves to a real body line`);
1164
+ assert.ok(!final.includes(expected), 'the final turn asks for the line by ADDRESS — it never reveals it');
1165
+ for (const [i, p] of fed.prompts.entries()) {
1166
+ if (i === part - 1) continue;
1167
+ const envelope = p.replace(bodies[i] ?? '', '');
1168
+ assert.ok(!envelope.includes(expected), `part ${part}'s expected line leaked into turn ${i + 1}'s envelope`);
1169
+ }
1170
+ }
1171
+ });
1172
+
1173
+ // The blocker, both halves. A candidate is only sound if the model CANNOT have seen its text
1174
+ // anywhere but the body it is being asked to prove — so it must occur exactly once across the
1175
+ // bodies AND nowhere in what the wrapper itself sends, including the request line that names the
1176
+ // addresses (which only exists once every address is chosen).
1177
+ it('a change-set line that duplicates the wrapper`s own framing is never chosen as a proof', () => {
1178
+ const sb = makeSandbox();
1179
+ // The change set contains lines copied verbatim out of the envelope the wrapper will send.
1180
+ const framing = [
1181
+ 'This is one piece of ONE change set being delivered to you in order.',
1182
+ 'Work from THIS CONVERSATION only: do NOT use any tool, do NOT run any command, do NOT read any file.',
1183
+ 'One line: SHIP / SHIP WITH NITS / REWORK, plus a one-sentence reason.',
1184
+ ];
1185
+ const filler = Array.from({ length: 400 }, (_, i) => `unique change-set line ${String(i).padStart(4, '0')} — a distinctive body marker ${'x'.repeat(20)}`);
1186
+ const woven = filler.flatMap((l, i) => (i % 40 === 20 ? [framing[(i / 40) | 0 % framing.length] ?? framing[0], l] : [l]));
1187
+ writeFileSync(join(sb.repo, 'oversized.txt'), `${woven.join('\n')}\n`);
1188
+ const fed = fedRun(sb);
1189
+ rmSync(sb.home, { recursive: true, force: true });
1190
+ assert.equal(fed.status, 0, fed.stderr);
1191
+ const final = fed.prompts[fed.prompts.length - 1];
1192
+ const bodies = fed.prompts.slice(0, -1).map(bodyOf);
1193
+ for (const { part, line } of requestedOf(final)) {
1194
+ const chosen = bodies[part - 1].split('\n')[line - 1].trim();
1195
+ assert.ok(!framing.includes(chosen), `a framing line was chosen as part ${part}'s proof: ${chosen}`);
1196
+ // The real invariant behind it: the chosen text appears nowhere the model could read it
1197
+ // except its own body — not in another body, and not in any envelope.
1198
+ const inBodies = bodies.join('\n').split(chosen).length - 1;
1199
+ assert.equal(inBodies, 1, `part ${part}'s proof text must occur exactly once across the bodies`);
1200
+ for (const [i, p] of fed.prompts.entries()) {
1201
+ const envelope = p.replace(bodies[i] ?? '', '');
1202
+ assert.ok(!envelope.includes(chosen), `part ${part}'s proof text leaked into turn ${i + 1}'s envelope`);
1203
+ }
1204
+ }
1205
+ });
1206
+
1207
+ it('a change-set line whose text IS a request address is never chosen (the request would reveal it)', () => {
1208
+ const sb = makeSandbox();
1209
+ // Seed every plausible address form the request line could carry, so a naive selector that
1210
+ // filters only against the PRE-request envelope can pick one of them.
1211
+ const addresses = Array.from({ length: 60 }, (_, i) => `part ${(i % 6) + 1} line ${i + 3}`);
1212
+ const filler = Array.from({ length: 400 }, (_, i) => `unique change-set line ${String(i).padStart(4, '0')} — a distinctive body marker ${'x'.repeat(20)}`);
1213
+ const woven = filler.flatMap((l, i) => (i % 7 === 3 && addresses[(i / 7) | 0] ? [addresses[(i / 7) | 0], l] : [l]));
1214
+ writeFileSync(join(sb.repo, 'oversized.txt'), `${woven.join('\n')}\n`);
1215
+ const fed = fedRun(sb);
1216
+ rmSync(sb.home, { recursive: true, force: true });
1217
+ assert.equal(fed.status, 0, fed.stderr);
1218
+ const final = fed.prompts[fed.prompts.length - 1];
1219
+ const addressBlock = requestedBlockOf(final);
1220
+ const bodies = fed.prompts.slice(0, -1).map(bodyOf);
1221
+ for (const address of addressBlock) {
1222
+ assert.ok(address.length < 24, `every address line must stay under the proof-candidate minimum, got ${address.length}: ${address}`);
1223
+ }
1224
+ for (const { part, line } of requestedOf(final)) {
1225
+ const chosen = bodies[part - 1].split('\n')[line - 1].trim();
1226
+ assert.ok(!addressBlock.some((a) => a.includes(chosen)), `part ${part}'s proof text is revealed by an address line: ${chosen}`);
1227
+ }
1228
+ });
1229
+
1230
+ it('an UNDER-cap single-turn review declares delivery `inline` and still attests', () => {
1231
+ const sb = makeSandbox();
1232
+ const r = run(sb, { args: ['code', '--facts', 'f'] });
1233
+ const receipts = readReceipts(sb.repo);
1234
+ rmSync(sb.home, { recursive: true, force: true });
1235
+ assert.equal(r.status, 0, r.stderr);
1236
+ assert.equal(receipts[0].delivery, 'inline', 'the single-turn path proves delivery BY CONSTRUCTION and says so');
1237
+ });
1238
+ });
1239
+
406
1240
  describe('agy-review.sh — size ceiling + gated --add-dir escape (6)', () => {
407
- it('default: oversized prompt exits 2 with guidance, agy not invoked', () => {
1241
+ // D2: chunking is CODE-mode only. A plan/diff artifact is an operator-supplied file the operator
1242
+ // can split, so those modes keep today's refuse-over-cap behaviour verbatim.
1243
+ it('plan mode: an oversized prompt exits 2 with guidance, agy not invoked (chunking is code-only)', () => {
408
1244
  const sb = makeSandbox();
409
- const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_MAX_PROMPT_BYTES: '50' } });
1245
+ writeFileSync(join(sb.repo, 'big-plan.md'), `# plan\n${'a plan line that is long enough to matter\n'.repeat(400)}`);
1246
+ const r = run(sb, { args: ['plan', 'big-plan.md', '--facts', 'f'], env: { AGY_MAX_PROMPT_BYTES: '4000' } });
410
1247
  rmSync(sb.home, { recursive: true, force: true });
411
1248
  assert.equal(r.status, 2, r.stderr);
412
- assert.match(r.stderr, /over AGY_MAX_PROMPT_BYTES=50/);
1249
+ assert.match(r.stderr, /over AGY_MAX_PROMPT_BYTES=4000/);
413
1250
  assert.match(r.stderr, /Trim to the relevant hunks/);
414
- assert.equal(r.invoked, false, 'an oversized prompt must not spend a run by default');
1251
+ assert.match(r.stderr, /split the plan into focused parts/);
1252
+ assert.equal(r.invoked, false, 'an oversized plan must not spend a run');
415
1253
  });
416
1254
 
417
- // A ceiling ABOVE the grounding-only prompt (~1.3 KB) but BELOW the full prompt (a big artifact),
418
- // so the escape can actually offload the artifact while the grounding still fits inline.
419
- it('AGY_REVIEW_ALLOW_ADDDIR=1: offloads the artifact to a 0700/0600 staging dir via --add-dir', () => {
1255
+ // D3: the offload is RETIRED, not removed. The key stays recognized (an existing settings line must
1256
+ // never start warning as unknown) but it arms nothing, and setting it says so.
1257
+ it('a set AGY_REVIEW_ALLOW_ADDDIR prints the retirement notice and does not pass --add-dir', () => {
420
1258
  const sb = makeSandbox();
421
- writeFileSync(join(sb.repo, 'unique.txt'), `OVERSIZE_UNIQUE_MARKER\n${'x'.repeat(8000)}\n`);
422
- const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_MAX_PROMPT_BYTES: '2000', AGY_REVIEW_ALLOW_ADDDIR: '1' } });
1259
+ seedFedChangeSet(sb);
1260
+ const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_MAX_PROMPT_BYTES: String(FED_CAP), AGY_REVIEW_ALLOW_ADDDIR: '1' } });
423
1261
  rmSync(sb.home, { recursive: true, force: true });
424
1262
  assert.equal(r.status, 0, r.stderr);
425
- assert.equal(r.invoked, true, 'the escape hatch lets the run proceed');
426
- assert.match(r.argv, /--add-dir/, 'agy is given --add-dir');
427
- assert.ok(r.adddir && !r.adddir.includes('.git'), `--add-dir must NOT point at .git (got ${r.adddir})`);
428
- assert.ok(r.adddir && r.adddir !== sb.repo, '--add-dir must NOT be the work tree');
429
- assert.equal(r.adddirMode, '700', 'the staging dir must be mode 0700');
430
- assert.equal(r.artifactMode, '600', 'the offloaded artifact must be mode 0600');
431
- assert.match(r.artifactCopy, /OVERSIZE_UNIQUE_MARKER/, 'the artifact file holds the full change set');
432
- assert.match(r.prompt, /Grounded facts/, 'the -p prompt STILL carries the full grounding inline');
433
- assert.doesNotMatch(r.prompt, /repo file map/, 'the artifact is offloaded, not inlined into -p');
434
- assert.match(r.stderr, /RE-ENABLES the Issue-001 stall risk/);
1263
+ assert.match(r.stderr, /AGY_REVIEW_ALLOW_ADDDIR is set \(env\) but it is RETIRED/, 'the notice names the retirement AND where the dead value came from');
1264
+ assert.match(r.stderr, /unset AGY_REVIEW_ALLOW_ADDDIR in the environment/, 'an env override gets the recovery that actually clears it');
1265
+ assert.match(r.stderr, /chunked feed/, 'and names the lane that replaced it');
1266
+ assert.ok(!r.argv.includes('--add-dir'), 'the retired knob arms NOTHING');
1267
+ assert.match(r.stderr, /feeding the change set in \d+ part\(s\)/, 'the fed lane runs regardless of the retired knob');
1268
+ });
1269
+
1270
+ it('the settings registry still recognizes the retired key (an existing line never warns as unknown)', () => {
1271
+ const sb = makeSandbox();
1272
+ writeSettings(sb, 'AGY_REVIEW_ALLOW_ADDDIR=1\n');
1273
+ const r = run(sb, { args: ['code', '--facts', 'f'] });
1274
+ rmSync(sb.home, { recursive: true, force: true });
1275
+ assert.equal(r.status, 0, r.stderr);
1276
+ assert.doesNotMatch(r.stderr, /unknown key 'AGY_REVIEW_ALLOW_ADDDIR'/);
435
1277
  });
436
1278
 
437
1279
  it('the staging dir is trap-cleaned on exit (no leftover after the run)', () => {
438
1280
  const sb = makeSandbox();
439
- writeFileSync(join(sb.repo, 'unique.txt'), `MARKER\n${'x'.repeat(8000)}\n`);
440
- const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_MAX_PROMPT_BYTES: '2000', AGY_REVIEW_ALLOW_ADDDIR: '1' } });
441
- const stagingPath = r.adddir;
1281
+ seedFedChangeSet(sb);
1282
+ const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_MAX_PROMPT_BYTES: String(FED_CAP) } });
1283
+ // Turn 1 hands agy `--log-file <staging>/turn1.log`, so the fed lane's own argv names the dir.
1284
+ const logFile = r.argvs[0].split('\n')[r.argvs[0].split('\n').indexOf('--log-file') + 1];
1285
+ const stagingPath = logFile ? dirname(logFile) : '';
442
1286
  const stillThere = stagingPath ? existsSync(stagingPath) : false;
443
1287
  rmSync(sb.home, { recursive: true, force: true });
444
- assert.ok(stagingPath, 'the escape must have fired (a staging path was captured)');
1288
+ assert.ok(stagingPath, 'a staging path was captured from the run');
445
1289
  assert.equal(stillThere, false, 'the private staging dir must be removed by the EXIT trap');
446
1290
  });
447
1291
  });
@@ -930,7 +1774,7 @@ describe('agy-review.sh — declared contract is really accepted (forward guard)
930
1774
  // The normative fixture: the AD-038 shape + the D3 self-declaring probe marker (backend/verdict here
931
1775
  // carry this bridge's vocabulary; dynamic values are asserted by shape):
932
1776
  const RECEIPT_FIXTURE = JSON.parse(
933
- '{"schema":1,"artifact":"code","fresh":true,"fingerprint":"<sha256hex>","backend":"codex","verdict":"revise","grounded":true,"factsHash":null,"wrapperVersion":"2.3.0","timestamp":"2026-07-03T12:00:00Z","probe":false,"posture":{"model":"<display>"}}',
1777
+ '{"schema":1,"artifact":"code","fresh":true,"fingerprint":"<sha256hex>","backend":"codex","verdict":"revise","grounded":true,"factsHash":null,"wrapperVersion":"2.3.0","timestamp":"2026-07-03T12:00:00Z","probe":false,"posture":{"model":"<display>"},"delivery":"inline"}',
934
1778
  );
935
1779
  const RECEIPTS_REL = join('.git', 'agent-workflow-review-receipts.jsonl');
936
1780
  const readReceipts = (repo) => {
@@ -1044,7 +1888,9 @@ describe('agy-review.sh — review receipts (AD-038)', () => {
1044
1888
  assert.equal(r.status, 0, r.stderr);
1045
1889
  assert.equal(receipts.length, 1);
1046
1890
  const receipt = receipts[0];
1047
- assert.deepEqual(Object.keys(receipt), Object.keys(RECEIPT_FIXTURE), 'same fixture shape');
1891
+ // A continuation delivers NOTHING (agy holds the original round server-side), so it declares no
1892
+ // delivery — the marker is a claim about a change set this receipt does not carry.
1893
+ assert.deepEqual(Object.keys(receipt), Object.keys(RECEIPT_FIXTURE).filter((k) => k !== 'delivery'), 'the fixture shape minus the delivery declaration');
1048
1894
  assert.equal(receipt.fresh, false, 'a continuation cannot attest the folded tree');
1049
1895
  assert.equal(receipt.artifact, null);
1050
1896
  assert.equal(receipt.fingerprint, null);
@@ -1137,48 +1983,50 @@ const writeSettings = (sb, text) => {
1137
1983
  const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
1138
1984
 
1139
1985
  describe('agy-review.sh — bridge settings file (bridges 2.3.0)', { concurrency: true }, () => {
1140
- it('a file-set AGY_REVIEW_ALLOW_ADDDIR=1 arms the oversized --add-dir escape', () => {
1986
+ it('a file-set AGY_REVIEW_ALLOW_ADDDIR=1 arms nothing and states its retirement', () => {
1141
1987
  const sb = makeSandbox();
1142
- writeFileSync(join(sb.repo, 'unique.txt'), `OVERSIZE_UNIQUE_MARKER\n${'x'.repeat(8000)}\n`);
1988
+ seedFedChangeSet(sb);
1143
1989
  writeSettings(sb, 'AGY_REVIEW_ALLOW_ADDDIR=1\n');
1144
- const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_MAX_PROMPT_BYTES: '2000' } });
1990
+ const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_MAX_PROMPT_BYTES: String(FED_CAP) } });
1145
1991
  rmSync(sb.home, { recursive: true, force: true });
1146
1992
  assert.equal(r.status, 0, r.stderr);
1147
- assert.equal(r.invoked, true, 'the file-armed escape lets the run proceed');
1148
- assert.match(r.argv, /--add-dir/);
1149
- assert.match(r.stderr, /RE-ENABLES the Issue-001 stall risk/);
1993
+ assert.match(r.stderr, /AGY_REVIEW_ALLOW_ADDDIR is set \(file\) but it is RETIRED/, 'a FILE-set value is named as such, not as an env override');
1994
+ assert.match(r.stderr, /bridge-settings\.mjs --unset/, 'and gets the recovery that actually clears a file line');
1995
+ assert.ok(!r.argv.includes('--add-dir'));
1150
1996
  });
1151
1997
 
1152
- it('env overrides file: AGY_REVIEW_ALLOW_ADDDIR env=0 file=1 the refusal stands', () => {
1998
+ // With the knob DISARMED an over-cap code review is no longer a refusal it is the fed lane. So
1999
+ // "env wins over file" is now proven by which LANE runs, not by which error prints.
2000
+ it('env overrides file: AGY_REVIEW_ALLOW_ADDDIR env=0 file=1 → the offload stays disarmed and the fed lane runs', () => {
1153
2001
  const sb = makeSandbox();
1154
- writeFileSync(join(sb.repo, 'unique.txt'), `MARKER\n${'x'.repeat(8000)}\n`);
2002
+ seedFedChangeSet(sb);
1155
2003
  writeSettings(sb, 'AGY_REVIEW_ALLOW_ADDDIR=1\n');
1156
- const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_MAX_PROMPT_BYTES: '2000', AGY_REVIEW_ALLOW_ADDDIR: '0' } });
2004
+ const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_MAX_PROMPT_BYTES: String(FED_CAP), AGY_REVIEW_ALLOW_ADDDIR: '0' } });
1157
2005
  rmSync(sb.home, { recursive: true, force: true });
1158
- assert.equal(r.status, 2, r.stderr);
1159
- assert.match(r.stderr, /over AGY_MAX_PROMPT_BYTES=2000/);
1160
- assert.equal(r.invoked, false);
2006
+ assert.equal(r.status, 0, r.stderr);
2007
+ assert.ok(!r.argv.includes('--add-dir'), 'the file-set knob is overridden — no offload');
2008
+ assert.match(r.stderr, /feeding the change set in \d+ part\(s\)/);
1161
2009
  });
1162
2010
 
1163
2011
  it('an EXPLICITLY EMPTY env (AGY_REVIEW_ALLOW_ADDDIR=) disables the file knob', () => {
1164
2012
  const sb = makeSandbox();
1165
- writeFileSync(join(sb.repo, 'unique.txt'), `MARKER\n${'x'.repeat(8000)}\n`);
2013
+ seedFedChangeSet(sb);
1166
2014
  writeSettings(sb, 'AGY_REVIEW_ALLOW_ADDDIR=1\n');
1167
- const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_MAX_PROMPT_BYTES: '2000', AGY_REVIEW_ALLOW_ADDDIR: '' } });
2015
+ const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_MAX_PROMPT_BYTES: String(FED_CAP), AGY_REVIEW_ALLOW_ADDDIR: '' } });
1168
2016
  rmSync(sb.home, { recursive: true, force: true });
1169
- assert.equal(r.status, 2, 'env wins over file — empty means knob off (built-in default 0)');
1170
- assert.equal(r.invoked, false);
2017
+ assert.equal(r.status, 0, 'env wins over file — empty means knob off (built-in default 0)');
2018
+ assert.ok(!r.argv.includes('--add-dir'));
1171
2019
  });
1172
2020
 
1173
- it('an invalid boolean warns and falls back to the built-in default (refusal stands)', () => {
2021
+ it('an invalid boolean warns and falls back to the built-in default (the offload stays disarmed)', () => {
1174
2022
  const sb = makeSandbox();
1175
- writeFileSync(join(sb.repo, 'unique.txt'), `MARKER\n${'x'.repeat(8000)}\n`);
2023
+ seedFedChangeSet(sb);
1176
2024
  writeSettings(sb, 'AGY_REVIEW_ALLOW_ADDDIR=yes\n');
1177
- const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_MAX_PROMPT_BYTES: '2000' } });
2025
+ const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_MAX_PROMPT_BYTES: String(FED_CAP) } });
1178
2026
  rmSync(sb.home, { recursive: true, force: true });
1179
- assert.equal(r.status, 2, r.stderr);
2027
+ assert.equal(r.status, 0, r.stderr);
1180
2028
  assert.match(r.stderr, /invalid value 'yes'/);
1181
- assert.equal(r.invoked, false);
2029
+ assert.ok(!r.argv.includes('--add-dir'));
1182
2030
  });
1183
2031
 
1184
2032
  it('a file-set AGY_HARD_TIMEOUT flows through the agy-run delegation (killed at the file cap)', async () => {