@sabaiway/agent-workflow-kit 1.49.0 → 2.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.
@@ -190,17 +190,100 @@ describe('agy-review.sh — guard + grounding (2, 3)', () => {
190
190
  assert.match(r.prompt, /## Focus\nfirst second third/);
191
191
  });
192
192
 
193
- it('warns LOUDLY when --facts is omitted, and proceeds', () => {
193
+ it('plan mode with no --facts keeps the warning and proceeds (unchanged contract)', () => {
194
194
  const sb = makeSandbox();
195
- const r = run(sb, { args: ['code'] });
195
+ writeFileSync(join(sb.repo, 'p.md'), '# plan body\n');
196
+ const r = run(sb, { args: ['plan', 'p.md'] });
196
197
  rmSync(sb.home, { recursive: true, force: true });
197
198
  assert.equal(r.status, 0, r.stderr);
198
199
  assert.match(r.stderr, /no --facts supplied/);
199
- assert.equal(r.invoked, true, 'an ungrounded review still proceeds (warn, not block)');
200
+ assert.equal(r.invoked, true, 'an ungrounded plan review still proceeds (warn, not block)');
200
201
  assert.match(r.prompt, /none supplied/, 'the prompt notes the missing facts in-band');
201
202
  });
202
203
  });
203
204
 
205
+ // ── code mode fails CLOSED without grounded facts (D4) ───────────────────────────
206
+ // An ungrounded CODE receipt records grounded:false, which the kit's review-state gate rejects —
207
+ // the run would be paid for and attest nothing. The wrapper refuses BEFORE the spend, keyed on the
208
+ // resolved CONTENT (an empty --facts payload refuses identically). Escapes: the explicit
209
+ // --ungrounded flag (throwaway opinion) and AGY_PROBE=1 (a probe receipt never attests anyway).
210
+ describe('agy-review.sh — code mode fails CLOSED without grounded facts (D4)', () => {
211
+ it('code mode with no --facts exits 2 before any agy invocation', () => {
212
+ const sb = makeSandbox();
213
+ const r = run(sb, { args: ['code'] });
214
+ rmSync(sb.home, { recursive: true, force: true });
215
+ assert.equal(r.status, 2, r.stderr);
216
+ assert.equal(r.invoked, false, 'the refusal must fire before any agy invocation — zero runs spent');
217
+ assert.match(r.stderr, /grounding\.mjs/, 'the refusal names the facts assembler');
218
+ assert.match(r.stderr, /agy-review code --facts @/, 'the refusal prints the exact re-run line');
219
+ const hint = r.stderr.match(/node "([^"]+grounding\.mjs)"/);
220
+ assert.ok(hint, 'the recovery hint resolves and QUOTES a real grounding.mjs path (an install path may carry spaces)');
221
+ assert.ok(existsSync(hint[1]), 'the resolved hint path exists on this layout');
222
+ });
223
+
224
+ it('code mode with --facts naming an EMPTY payload exits 2 before any agy invocation', () => {
225
+ const sb = makeSandbox();
226
+ writeFileSync(join(sb.repo, 'empty-facts.md'), '');
227
+ const r = run(sb, { args: ['code', '--facts', '@empty-facts.md'] });
228
+ rmSync(sb.home, { recursive: true, force: true });
229
+ assert.equal(r.status, 2, 'the refusal keys on the CONTENT, not the flag');
230
+ assert.equal(r.invoked, false, 'an empty payload must not spend a run');
231
+ assert.match(r.stderr, /agy-review code --facts @/);
232
+ });
233
+
234
+ it('code --ungrounded proceeds and the receipt records grounded:false', () => {
235
+ const sb = makeSandbox();
236
+ const r = run(sb, { args: ['code', '--ungrounded'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
237
+ const receipts = readReceipts(sb.repo);
238
+ rmSync(sb.home, { recursive: true, force: true });
239
+ assert.equal(r.status, 0, r.stderr);
240
+ assert.equal(r.invoked, true, 'the explicit escape lets the run proceed');
241
+ assert.equal(receipts.length, 1);
242
+ assert.equal(receipts[0].grounded, false, 'an --ungrounded run still records grounded:false');
243
+ assert.equal(receipts[0].factsHash, null);
244
+ assert.match(r.stderr, /no --facts supplied/, 'the escape path stays loud, never silent');
245
+ });
246
+
247
+ it('AGY_PROBE=1 code with no --facts proceeds and the receipt records probe:true', () => {
248
+ const sb = makeSandbox();
249
+ const r = run(sb, { args: ['code'], env: { AGY_PROBE: '1', AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
250
+ const receipts = readReceipts(sb.repo);
251
+ rmSync(sb.home, { recursive: true, force: true });
252
+ assert.equal(r.status, 0, r.stderr);
253
+ assert.equal(r.invoked, true, 'an ungrounded probe is coherent — a probe receipt never attests');
254
+ assert.equal(receipts[0].probe, true);
255
+ assert.equal(receipts[0].grounded, false);
256
+ });
257
+
258
+ it('--ungrounded with --facts is a refusal (contradiction)', () => {
259
+ const sb = makeSandbox();
260
+ const r = run(sb, { args: ['code', '--ungrounded', '--facts', 'f'] });
261
+ rmSync(sb.home, { recursive: true, force: true });
262
+ assert.equal(r.status, 2);
263
+ assert.equal(r.invoked, false);
264
+ assert.match(r.stderr, /--ungrounded contradicts --facts/);
265
+ });
266
+
267
+ it('--ungrounded outside code mode is a refusal', () => {
268
+ const sb = makeSandbox();
269
+ writeFileSync(join(sb.repo, 'p.md'), '# p\n');
270
+ const r = run(sb, { args: ['plan', 'p.md', '--ungrounded'] });
271
+ rmSync(sb.home, { recursive: true, force: true });
272
+ assert.equal(r.status, 2);
273
+ assert.equal(r.invoked, false);
274
+ assert.match(r.stderr, /--ungrounded is only valid in code mode/);
275
+ });
276
+
277
+ it('--ungrounded on a continuation is a refusal', () => {
278
+ const sb = makeSandbox();
279
+ const r = run(sb, { args: ['--continue', '--ungrounded'] });
280
+ rmSync(sb.home, { recursive: true, force: true });
281
+ assert.equal(r.status, 2);
282
+ assert.equal(r.invoked, false);
283
+ assert.match(r.stderr, /--ungrounded is not valid on a continuation/);
284
+ });
285
+ });
286
+
204
287
  describe('agy-review.sh — code-mode precomputed diff (4, 5, 8)', () => {
205
288
  it('assembles repo map + status + untracked CONTENTS', () => {
206
289
  const sb = makeSandbox();
@@ -521,6 +604,32 @@ const extractArgCaseArms = (source) => {
521
604
  };
522
605
  const splitArms = (labels) => (labels ?? []).flatMap((l) => l.split('|'));
523
606
 
607
+ // The source lines that really EXECUTE: a heredoc body (the --help text) and a comment both carry
608
+ // names without carrying logic, so a bare name-grep over the whole source stays green after the
609
+ // logic is deleted. Reuses the same heredoc discipline as extractArgCaseArms.
610
+ const executableLines = (source) => {
611
+ const out = [];
612
+ let heredoc = null;
613
+ for (const raw of source.split('\n')) {
614
+ if (heredoc) {
615
+ if (raw.trim() === heredoc) heredoc = null;
616
+ continue;
617
+ }
618
+ const hd = raw.match(/<<-?\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?/);
619
+ if (hd) { heredoc = hd[1]; continue; }
620
+ if (raw.trimStart().startsWith('#')) continue;
621
+ out.push(raw);
622
+ }
623
+ return out;
624
+ };
625
+ // An env var is really CONSULTED when an executable test compares it: [[ "$NAME" == … ]].
626
+ const consultsEnv = (source, name) =>
627
+ executableLines(source).some((l) => new RegExp(`\\[\\[[^\\]]*\\$\\{?${name}\\b[^\\]]*(==|!=)`).test(l));
628
+ // The operand slots a rendered invocation form really carries: <angle> and [bracket] placeholders.
629
+ // The optional `@` prefix rides WITH the slot (`@<facts-file>` is one operand, not a bare
630
+ // `<facts-file>` behind a stray character) — the catalog declares the whole token a user types.
631
+ const SLOT_RE = /@?<[^<>]+>|\[[^[\]]*\]/g;
632
+
524
633
  describe('agy-review.sh — --help contract (manifest-pinned)', () => {
525
634
  it('--help and -h exit 0 pre-preflight (no agy, no git)', () => {
526
635
  for (const arg of ['--help', '-h']) {
@@ -597,6 +706,116 @@ describe('agy-review.sh — source-level reverse guard (parser arms ⟷ manifest
597
706
  });
598
707
  });
599
708
 
709
+ // ── mode catalog ⟷ wrapper reality (BRIDGE-MODES-CATALOG) ─────────────────────────
710
+ // The kit validator owns the catalog's INTERNAL shape; these arms pin the half only this wrapper's
711
+ // source can settle — the cataloged review modes ARE the real parser arms, every declared contract
712
+ // invocation is cataloged, and the env-hook the catalog aims at review is a real env var.
713
+ describe('agy-review.sh — mode catalog ⟷ wrapper reality (manifest-pinned)', () => {
714
+ const source = readFileSync(WRAPPER, 'utf8');
715
+ const arms = extractArgCaseArms(source);
716
+ const catalog = MANIFEST.modeCatalog ?? [];
717
+ const reviewEntries = catalog.filter((e) => e.role === 'review');
718
+ const reviewPrimaries = reviewEntries.filter((e) => e.kind === 'primary');
719
+
720
+ it('the catalog submodes ARE the wrapper\'s real parser mode arms (both directions)', () => {
721
+ const modes = splitArms(arms.get('"$mode"')).filter((a) => a !== '*');
722
+ assert.ok(reviewPrimaries.length > 0, 'the manifest must catalog its review modes');
723
+ setEq(new Set(reviewPrimaries.map((e) => e.submode)), new Set(modes), 'catalog submodes ⟷ real parser mode arms');
724
+ });
725
+
726
+ it('every review entry composes BY REFERENCE and every reference resolves', () => {
727
+ for (const entry of reviewEntries) {
728
+ assert.ok(
729
+ Array.isArray(entry.invocationRefs) && entry.invocationRefs.length > 0,
730
+ `${entry.key}: a contract-backed entry references at least one contract descriptor`,
731
+ );
732
+ assert.ok(!Object.hasOwn(entry, 'descriptor'), `${entry.key}: a contract-backed entry never restates a literal descriptor`);
733
+ for (const ref of entry.invocationRefs) {
734
+ assert.equal(
735
+ typeof REVIEW_CONTRACT[ref.contractField]?.[ref.index], 'string',
736
+ `${entry.key}: ref ${ref.contractField}[${ref.index}] does not resolve into the manifest contract`,
737
+ );
738
+ }
739
+ }
740
+ });
741
+
742
+ it('every review contract invocation is claimed by exactly ONE catalog entry (no uncataloged mode)', () => {
743
+ const claims = reviewEntries.flatMap((e) => e.invocationRefs.map((r) => `${r.contractField}[${r.index}]`));
744
+ assert.equal(new Set(claims).size, claims.length, 'a contract invocation is claimed at most once');
745
+ const declared = [
746
+ ...REVIEW_CONTRACT.invocations.map((_, i) => `invocations[${i}]`),
747
+ ...REVIEW_CONTRACT.continue.map((_, i) => `continue[${i}]`),
748
+ ];
749
+ setEq(new Set(claims), declared, 'catalog claims ⟷ declared contract invocations');
750
+ });
751
+
752
+ it('every env-hook the catalog aims at a review mode is a real EXECUTABLE guard, not a mention', () => {
753
+ const hooks = catalog.filter((e) => e.kind === 'env-hook' && e.parents.some((p) => reviewPrimaries.some((r) => r.key === p)));
754
+ assert.ok(hooks.length > 0, 'AGY_PROBE must be cataloged as an env-hook over the review modes');
755
+ for (const hook of hooks) {
756
+ assert.ok(
757
+ consultsEnv(source, hook.key),
758
+ `env-hook ${hook.key} is named in the source but never TESTED in an executable condition — a help/comment mention would keep a name-grep green after the logic is deleted`,
759
+ );
760
+ }
761
+ });
762
+
763
+ it('the catalog operand slots set-EQUAL the slots its rendered forms really carry (both directions)', () => {
764
+ for (const entry of reviewEntries) {
765
+ const forms = entry.invocationRefs.map((r) => REVIEW_CONTRACT[r.contractField][r.index]);
766
+ // The DEDUPLICATED UNION over every resolved form: a plural-ref entry legitimately spreads its
767
+ // slots across forms, so per-form equality would false-fail a correct catalog.
768
+ const realSlots = new Set(forms.flatMap((f) => f.match(SLOT_RE) ?? []));
769
+ setEq(new Set((entry.operands ?? []).map((o) => o.slot)), realSlots, `${entry.key}: catalog operands ⟷ the slots its forms really carry`);
770
+ }
771
+ });
772
+
773
+ it('an entry rendering a LITERAL descriptor is slot-checked too (env-hooks have no role to filter on)', () => {
774
+ // The contract-backed arm above filters by role — and an env-hook HAS no role, so its descriptor
775
+ // was never slot-checked at all. That is exactly how a hardcoded dead path can reach the
776
+ // discovery surface looking ready-to-run. Every literal-descriptor kind is covered here:
777
+ // env-hooks and contract-free primaries.
778
+ const literalEntries = catalog.filter((e) => typeof e.descriptor === 'string');
779
+ assert.ok(literalEntries.length > 0, 'AGY_PROBE must be cataloged with a literal descriptor');
780
+ for (const entry of literalEntries) {
781
+ const realSlots = new Set(entry.descriptor.match(SLOT_RE) ?? []);
782
+ setEq(new Set((entry.operands ?? []).map((o) => o.slot)), realSlots, `${entry.key}: catalog operands ⟷ the slots its descriptor really carries`);
783
+ }
784
+ });
785
+
786
+ it('AGY_PROBE really silences the advisory on EVERY review parent the catalog claims (behavioural)', () => {
787
+ // The catalog CLAIMS these modes are modified by the hook; prove it per parent rather than
788
+ // trusting a source scan: the off-frontier advisory fires without it, is silent with it.
789
+ const hook = catalog.find((e) => e.key === 'AGY_PROBE');
790
+ const drive = {
791
+ 'review.code': () => ['code', '--facts', 'f'],
792
+ 'review.plan': (sb) => { writeFileSync(join(sb.repo, 'p.md'), '# p\n'); return ['plan', 'p.md', '--facts', 'f']; },
793
+ 'review.diff': (sb) => { writeFileSync(join(sb.repo, 'c.diff'), 'diff body\n'); return ['diff', 'c.diff', '--facts', 'f']; },
794
+ 'review.continue': () => ['--continue'],
795
+ 'review.conversation': () => ['--conversation', 'conv-1'],
796
+ };
797
+ assert.ok(hook.parents.length > 0, 'AGY_PROBE must claim at least one parent');
798
+ for (const parent of hook.parents) {
799
+ assert.ok(drive[parent], `no behavioural drive for claimed parent "${parent}" — add one`);
800
+ // Both runs must really REACH agy: asserting the diagnostic text alone would let an early
801
+ // failure that never dispatched pass the probe-on branch (its stderr simply lacks the string).
802
+ const noisy = makeSandbox();
803
+ const off = run(noisy, { args: drive[parent](noisy), env: { AGY_MODEL: 'Some Weak Model' } });
804
+ rmSync(noisy.home, { recursive: true, force: true });
805
+ assert.equal(off.status, 0, `${parent}: ${off.stderr}`);
806
+ assert.equal(off.invoked, true, `${parent}: the control run must reach agy`);
807
+ assert.match(off.stderr, /non-frontier model/, `${parent}: the advisory must fire without the hook`);
808
+
809
+ const quiet = makeSandbox();
810
+ const on = run(quiet, { args: drive[parent](quiet), env: { AGY_MODEL: 'Some Weak Model', AGY_PROBE: '1' } });
811
+ rmSync(quiet.home, { recursive: true, force: true });
812
+ assert.equal(on.status, 0, `${parent}: ${on.stderr}`);
813
+ assert.equal(on.invoked, true, `${parent}: AGY_PROBE=1 must still reach agy — silence must come from the hook, not from an early exit`);
814
+ assert.doesNotMatch(on.stderr, /non-frontier model/, `${parent}: AGY_PROBE=1 must really silence it — the catalog claims it does`);
815
+ }
816
+ });
817
+ });
818
+
600
819
  describe('agy-review.sh — declared contract is really accepted (forward guard)', () => {
601
820
  it('every manifest mode runs green', () => {
602
821
  const drive = {
@@ -618,7 +837,12 @@ describe('agy-review.sh — declared contract is really accepted (forward guard)
618
837
  for (const descriptor of REVIEW_CONTRACT.flags) {
619
838
  const flag = leadingFlag(descriptor);
620
839
  const sb = makeSandbox();
621
- const r = run(sb, { args: ['code', flag, 'f'] });
840
+ // D4: code mode refuses without grounded facts, so a non-facts value flag is driven on a
841
+ // grounded run; --ungrounded takes no value, contradicts --facts, and is driven alone.
842
+ const args = flag === '--facts' ? ['code', '--facts', 'f']
843
+ : flag === '--ungrounded' ? ['code', '--ungrounded']
844
+ : ['code', '--facts', 'f', flag, 'f'];
845
+ const r = run(sb, { args });
622
846
  rmSync(sb.home, { recursive: true, force: true });
623
847
  assert.equal(r.status, 0, `${flag}: ${r.stderr}`);
624
848
  }
@@ -643,10 +867,10 @@ describe('agy-review.sh — declared contract is really accepted (forward guard)
643
867
  });
644
868
 
645
869
  // ── review receipts (AD-038) ─────────────────────────────────────────────────────
646
- // The normative fixture (docs: the AD-038 plan Decisions copied verbatim; backend/verdict here
870
+ // The normative fixture: the AD-038 shape + the D3 self-declaring probe marker (backend/verdict here
647
871
  // carry this bridge's vocabulary; dynamic values are asserted by shape):
648
872
  const RECEIPT_FIXTURE = JSON.parse(
649
- '{"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"}',
873
+ '{"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}',
650
874
  );
651
875
  const RECEIPTS_REL = join('.git', 'agent-workflow-review-receipts.jsonl');
652
876
  const readReceipts = (repo) => {
@@ -682,9 +906,44 @@ describe('agy-review.sh — review receipts (AD-038)', () => {
682
906
  assert.match(receipt.timestamp, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
683
907
  });
684
908
 
685
- it('an ungrounded fresh run records grounded:false + factsHash null (the vacuous-grounding hole stays visible)', () => {
909
+ // The probe marker (BRIDGE-MODES-CATALOG, D3) the twin of the sibling bridge's arm: an
910
+ // AGY_PROBE=1 review runs with the frontier-model advisory silenced, so its receipt is marked and
911
+ // the kit's review-state gate rejects it. EVERY receipt carries the marker (true or false): it
912
+ // self-declares, so the gate reads the fact rather than inferring it from a version string that
913
+ // bumps in a different release phase. Silence is not a declaration.
914
+ it('AGY_PROBE=1 stamps probe:true — a throwaway probe can never attest a tree (D3)', () => {
686
915
  const sb = makeSandbox();
687
- const r = run(sb, { args: ['code'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
916
+ const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_PROBE: '1', AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
917
+ const receipts = readReceipts(sb.repo);
918
+ rmSync(sb.home, { recursive: true, force: true });
919
+ assert.equal(r.status, 0, r.stderr);
920
+ assert.equal(receipts[0].probe, true, 'a probe-relaxed run marks its own receipt');
921
+ assert.deepEqual(Object.keys(receipts[0]), Object.keys(RECEIPT_FIXTURE), 'fixture key set + order');
922
+ });
923
+
924
+ // Every receipt SELF-DECLARES: the kit's gate reads the marker, never the wrapper version — so
925
+ // the marker must not depend on a version bump landing in the same release phase.
926
+ it('a normal review self-declares probe:false — the receipt states the fact, not a version', () => {
927
+ const sb = makeSandbox();
928
+ run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
929
+ const receipts = readReceipts(sb.repo);
930
+ rmSync(sb.home, { recursive: true, force: true });
931
+ assert.equal(receipts[0].probe, false, 'silence is not a declaration — the gate rejects an unmarked receipt');
932
+ });
933
+
934
+ it('a probe CONTINUATION is marked too (it is doubly unable to attest — fresh:false AND probe)', () => {
935
+ const sb = makeSandbox();
936
+ const r = run(sb, { args: ['--continue'], env: { AGY_PROBE: '1', AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
937
+ const receipts = readReceipts(sb.repo);
938
+ rmSync(sb.home, { recursive: true, force: true });
939
+ assert.equal(r.status, 0, r.stderr);
940
+ assert.equal(receipts[0].fresh, false);
941
+ assert.equal(receipts[0].probe, true, 'both write paths carry the marker — no unmarked probe lane');
942
+ });
943
+
944
+ it('an --ungrounded fresh run records grounded:false + factsHash null (the vacuous-grounding hole stays visible)', () => {
945
+ const sb = makeSandbox();
946
+ const r = run(sb, { args: ['code', '--ungrounded'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
688
947
  const receipts = readReceipts(sb.repo);
689
948
  rmSync(sb.home, { recursive: true, force: true });
690
949
  assert.equal(r.status, 0, r.stderr);
@@ -692,16 +951,15 @@ describe('agy-review.sh — review receipts (AD-038)', () => {
692
951
  assert.equal(receipts[0].factsHash, null);
693
952
  });
694
953
 
695
- it('an EMPTY --facts file records grounded:false (fail-closedvacuous grounding never satisfies the gate) and still warns', () => {
954
+ it('an EMPTY --facts file in code mode refuses pre-spendno run, no receipt (D4 fail-closed)', () => {
696
955
  const sb = makeSandbox();
697
956
  writeFileSync(join(sb.home, 'empty-facts.md'), '');
698
957
  const r = run(sb, { args: ['code', '--facts', `@${join(sb.home, 'empty-facts.md')}`], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
699
958
  const receipts = readReceipts(sb.repo);
700
959
  rmSync(sb.home, { recursive: true, force: true });
701
- assert.equal(r.status, 0, r.stderr);
702
- assert.equal(receipts[0].grounded, false, 'an empty payload is recorded as ungrounded, never as grounded-by-flag');
703
- assert.equal(receipts[0].factsHash, null);
704
- assert.match(r.stderr, /no --facts supplied|ungrounded review GUESSES/, 'the ungrounded warning fires for an empty payload');
960
+ assert.equal(r.status, 2, 'vacuous grounding no longer spends a run');
961
+ assert.equal(r.invoked, false);
962
+ assert.equal(receipts.length, 0, 'no run — no receipt');
705
963
  });
706
964
 
707
965
  it('parses REWORK and plain SHIP; records "unknown" when the mandated section is absent', () => {
@@ -455,3 +455,79 @@ describe('agy.sh — settings surface ⟷ manifest (D6, manifest-pinned)', () =>
455
455
  }
456
456
  });
457
457
  });
458
+
459
+ // ── mode catalog: the contract-free raw mode ⟷ this wrapper (BRIDGE-MODES-CATALOG) ─────
460
+ // agy-run is the probe role: it is dispatched by no activity slot and so carries NO manifest
461
+ // driving contract (`wrapperContractFor(_, 'probe')` stays null). Its catalog entry therefore
462
+ // carries a LITERAL descriptor — the stated exception to no-duplication, because for a
463
+ // contract-free mode the catalog IS canonical. That makes the catalog the only thing that can
464
+ // drift from this wrapper, so these arms pin it in BOTH directions and then really drive it.
465
+ const USAGE_HEADER = 'Usage:';
466
+ const PROBE_CMD = 'agy-run';
467
+ // The operand slots a rendered invocation form really carries: <angle> and [bracket] placeholders.
468
+ // The optional `@` prefix rides WITH the slot — the catalog declares the whole token a user types.
469
+ const SLOT_RE = /@?<[^<>]+>|\[[^[\]]*\]/g;
470
+
471
+ describe('agy.sh — mode catalog ⟷ wrapper reality (the contract-free raw mode)', () => {
472
+ const catalog = MANIFEST.modeCatalog ?? [];
473
+ const probeEntries = catalog.filter((e) => e.role === 'probe');
474
+
475
+ const helpText = () => {
476
+ const home = mkdtempSync(join(tmpdir(), 'agy-catalog-help-'));
477
+ const r = spawnSync('bash', [WRAPPER, '--help'], {
478
+ env: { HOME: home, PATH: makePathWithout(home, ['agy', 'git']) },
479
+ encoding: 'utf8',
480
+ timeout: 15000,
481
+ });
482
+ rmSync(home, { recursive: true, force: true });
483
+ assert.equal(r.status, 0, r.stderr);
484
+ return r.stdout;
485
+ };
486
+
487
+ it('the probe role is cataloged as exactly ONE contract-free primary carrying a literal descriptor', () => {
488
+ assert.equal(probeEntries.length, 1, 'agy-run is one mode — a raw prompt');
489
+ const [entry] = probeEntries;
490
+ assert.equal(entry.kind, 'primary');
491
+ assert.equal(MANIFEST.roles.probe.contract, undefined, 'the probe role carries no driving contract — the catalog must not invent one');
492
+ assert.ok(!Object.hasOwn(entry, 'invocationRefs'), 'a contract-free entry has nothing to reference');
493
+ assert.equal(typeof entry.descriptor, 'string', 'a contract-free entry is canonical for its own form');
494
+ });
495
+
496
+ it('the catalog descriptor IS a real --help Usage line (catalog → wrapper: no invented form)', () => {
497
+ const usage = helpSection(helpText(), USAGE_HEADER);
498
+ assert.ok(usage.includes(probeEntries[0].descriptor), `the cataloged form is in no Usage line: ${probeEntries[0].descriptor}`);
499
+ });
500
+
501
+ it('every --help Usage line belongs to the cataloged mode (wrapper → catalog: no uncataloged form)', () => {
502
+ const usage = helpSection(helpText(), USAGE_HEADER);
503
+ assert.ok(usage.length > 0, 'the wrapper must document its usage');
504
+ for (const line of usage) {
505
+ assert.ok(line.includes(PROBE_CMD), `Usage line "${line}" invokes something other than the one cataloged mode`);
506
+ }
507
+ });
508
+
509
+ it('the catalog operand slots set-EQUAL the slots the cataloged form really carries (both directions)', () => {
510
+ const [entry] = probeEntries;
511
+ const realSlots = new Set(entry.descriptor.match(SLOT_RE) ?? []);
512
+ setEq(new Set((entry.operands ?? []).map((o) => o.slot)), realSlots, 'catalog operands ⟷ the slots the descriptor really carries');
513
+ });
514
+
515
+ it('the required operand really accepts every form the catalog says it takes (forward drive)', () => {
516
+ // <prompt|-|@file> is ONE slot with three real spellings — drive all three against the stub.
517
+ const home = makeSandbox('#!/usr/bin/env bash\nprintf "%s" "$*" >"$HOME/argv"\necho FAKE_REPLY\n');
518
+ const promptFile = join(home, 'p.md');
519
+ writeFileSync(promptFile, 'from a file\n');
520
+ const env = { HOME: home, PATH: `${join(home, '.local', 'bin')}:${process.env.PATH}`, TMPDIR: process.env.TMPDIR ?? '/tmp' };
521
+ const drives = [
522
+ { label: 'literal prompt', args: ['inline prompt'], stdin: undefined },
523
+ { label: 'stdin (-)', args: ['-'], stdin: 'from stdin\n' },
524
+ { label: '@file', args: [`@${promptFile}`], stdin: undefined },
525
+ ];
526
+ for (const drive of drives) {
527
+ const r = spawnSync('bash', [WRAPPER, ...drive.args], { env, encoding: 'utf8', timeout: 20000, input: drive.stdin });
528
+ assert.equal(r.status, 0, `${drive.label}: ${r.stderr}`);
529
+ assert.match(r.stdout, /FAKE_REPLY/, `${drive.label} never reached agy`);
530
+ }
531
+ rmSync(home, { recursive: true, force: true });
532
+ });
533
+ });
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "antigravity-cli-bridge",
5
5
  "kind": "execution-backend",
6
- "version": "2.6.1",
6
+ "version": "3.0.0",
7
7
  "provides": ["review", "probe"],
8
8
  "roles": {
9
9
  "review": {
@@ -14,13 +14,14 @@
14
14
  "output": "advisory",
15
15
  "contract": {
16
16
  "invocations": [
17
- "agy-review code [--facts @f] [--decided @f] [--focus \"…\"] [extra focus…]",
17
+ "agy-review code [--facts @f] [--ungrounded] [--decided @f] [--focus \"…\"] [extra focus…]",
18
18
  "agy-review plan <plan-file> [--facts @f] [--decided @f] [--focus \"…\"]",
19
19
  "agy-review diff <diff-file> [--facts @f] [--decided @f] [--focus \"…\"]"
20
20
  ],
21
- "grounding": "grounded review — agy reads NOTHING by default, an ungrounded review GUESSES: --facts @f = the verified facts to review AGAINST; --decided @f = decisions already made, do NOT re-raise (anti-circling)",
21
+ "grounding": "grounded review — agy reads NOTHING by default, an ungrounded review GUESSES: --facts @f = the verified facts to review AGAINST; --decided @f = decisions already made, do NOT re-raise (anti-circling). code mode REQUIRES a non-empty --facts payload and refuses BEFORE spending a run (escapes: --ungrounded, AGY_PROBE=1); plan/diff proceed with a loud warning",
22
22
  "flags": [
23
- "--facts @f — verified facts the review runs AGAINST (omit loud ungrounded-review warning)",
23
+ "--facts @f — verified facts the review runs AGAINST (code mode REQUIRES a non-empty payload; plan/diff warn loudly when omitted)",
24
+ "--ungrounded — deliberately ungrounded CODE review, a throwaway opinion (code mode only, contradicts --facts; the receipt records grounded:false and never attests)",
24
25
  "--decided @f — already-decided / already-addressed list; do NOT re-raise (anti-circling; the round-2 payload)",
25
26
  "--focus \"…\" — extra focus (repeatable; code mode also takes trailing focus words)"
26
27
  ],
@@ -28,7 +29,7 @@
28
29
  "agy-review --continue [--decided @f] [--focus \"…\"]",
29
30
  "agy-review --conversation <id> [--decided @f] [--focus \"…\"]"
30
31
  ],
31
- "receipt": "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides; plan/diff outside a git tree: warn + skip unless overridden): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan/diff mode; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (an empty payload records grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts payload; a continuation receipt is fresh:false (informational-only — it cannot attest the folded tree); a write failure warns, never fails the review",
32
+ "receipt": "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides; plan/diff outside a git tree: warn + skip unless overridden): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan/diff mode; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (code mode refuses pre-spend without one — no run, no receipt — unless --ungrounded/AGY_PROBE=1; in plan/diff an empty payload records grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts payload; a continuation receipt is fresh:false (informational-only — it cannot attest the folded tree); probe = whether the run relaxed the quality guards (AGY_PROBE=1), written on EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); a write failure warns, never fails the review",
32
33
  "notes": [
33
34
  "pre-dispatch host-diff: before the FIRST dispatch of this bridge, diff its declared networkHosts against the live sandbox allow-list — a missing host is surfaced to the maintainer BEFORE dispatching, never fired into a known prompt"
34
35
  ]
@@ -36,6 +37,166 @@
36
37
  },
37
38
  "probe": { "cmd": "agy-run", "source": "bin/agy.sh", "output": "advisory" }
38
39
  },
40
+ "modeCatalog": [
41
+ {
42
+ "key": "review.code",
43
+ "kind": "primary",
44
+ "role": "review",
45
+ "submode": "code",
46
+ "purpose": "Review the uncommitted working-tree change set against facts you supply.",
47
+ "whenToUse": [
48
+ "a finished segment heading into its review round",
49
+ "a second opinion on real code before the commit ask"
50
+ ],
51
+ "whenNotTo": ["a clean tree — the wrapper exits before spending a run"],
52
+ "invocationRefs": [{ "contractField": "invocations", "index": 0 }],
53
+ "operands": [
54
+ { "slot": "[--facts @f]", "required": false, "description": "the verified facts to review AGAINST; code mode refuses pre-spend without a non-empty payload (escapes: --ungrounded, AGY_PROBE=1)" },
55
+ { "slot": "[--ungrounded]", "required": false, "description": "deliberately ungrounded code review — a throwaway opinion; the receipt records grounded:false and never attests" },
56
+ { "slot": "[--decided @f]", "required": false, "description": "decisions already made; the reviewer must not re-raise them (anti-circling)" },
57
+ { "slot": "[--focus \"…\"]", "required": false, "description": "what this review must look at (repeatable)" },
58
+ { "slot": "[extra focus…]", "required": false, "description": "extra focus words appended to the review directive" }
59
+ ],
60
+ "guardrails": [
61
+ { "value": "read-only posture — the prompt forbids edits, commands and git writes", "enforcement": "advisory", "source": "bin/agy-review.sh" },
62
+ { "value": "receives no ambient repo/file context by default — the wrapper passes no --add-dir", "enforcement": "enforced", "source": "bin/agy-review.sh" },
63
+ { "value": "an ungrounded review guesses — stale-model and partial-diff false positives", "enforcement": "advisory", "source": "bin/agy-review.sh" },
64
+ { "value": "without a non-empty --facts payload the run refuses BEFORE the spend (exit 2) — the only escapes are --ungrounded and AGY_PROBE=1", "enforcement": "enforced", "source": "bin/agy-review.sh" },
65
+ { "value": "an ungrounded run records grounded:false and the review-state gate rejects it", "enforcement": "enforced", "source": "capability.json roles.review.contract.receipt" },
66
+ { "value": "an oversized prompt refuses rather than truncate", "enforcement": "enforced", "condition": "unless AGY_REVIEW_ALLOW_ADDDIR=1 offloads it to a private --add-dir staging dir", "source": "capability.json settings.AGY_REVIEW_ALLOW_ADDDIR" }
67
+ ],
68
+ "customHooks": ["AGY_PROBE"]
69
+ },
70
+ {
71
+ "key": "review.plan",
72
+ "kind": "primary",
73
+ "role": "review",
74
+ "submode": "plan",
75
+ "purpose": "Critique an implementation plan before any of its code exists.",
76
+ "whenToUse": [
77
+ "a plan draft heading into its review council",
78
+ "checking whether a cold executor could really run the plan"
79
+ ],
80
+ "whenNotTo": ["a working-tree change set — that is review.code"],
81
+ "invocationRefs": [{ "contractField": "invocations", "index": 1 }],
82
+ "operands": [
83
+ { "slot": "<plan-file>", "required": true, "description": "the plan file under review" },
84
+ { "slot": "[--facts @f]", "required": false, "description": "the verified facts to review AGAINST; omitting it warns loudly and records the review ungrounded" },
85
+ { "slot": "[--decided @f]", "required": false, "description": "decisions already made; the reviewer must not re-raise them (anti-circling)" },
86
+ { "slot": "[--focus \"…\"]", "required": false, "description": "what this review must look at (repeatable)" }
87
+ ],
88
+ "guardrails": [
89
+ { "value": "read-only posture — the prompt forbids edits, commands and git writes", "enforcement": "advisory", "source": "bin/agy-review.sh" },
90
+ { "value": "outside a git tree the receipt is skipped with a warning", "enforcement": "enforced", "condition": "unless AW_REVIEW_RECEIPTS names a destination", "source": "capability.json roles.review.contract.receipt" }
91
+ ],
92
+ "customHooks": ["AGY_PROBE"]
93
+ },
94
+ {
95
+ "key": "review.diff",
96
+ "kind": "primary",
97
+ "role": "review",
98
+ "submode": "diff",
99
+ "purpose": "Review a diff file you assembled yourself, rather than the live working tree.",
100
+ "whenToUse": [
101
+ "reviewing a diff that is not the current tree — a patch, a range, another branch",
102
+ "reviewing a change set trimmed to the hunks that matter"
103
+ ],
104
+ "whenNotTo": ["the current uncommitted tree — review.code assembles that for you"],
105
+ "invocationRefs": [{ "contractField": "invocations", "index": 2 }],
106
+ "operands": [
107
+ { "slot": "<diff-file>", "required": true, "description": "the diff file under review" },
108
+ { "slot": "[--facts @f]", "required": false, "description": "the verified facts to review AGAINST; omitting it warns loudly and records the review ungrounded" },
109
+ { "slot": "[--decided @f]", "required": false, "description": "decisions already made; the reviewer must not re-raise them (anti-circling)" },
110
+ { "slot": "[--focus \"…\"]", "required": false, "description": "what this review must look at (repeatable)" }
111
+ ],
112
+ "guardrails": [
113
+ { "value": "read-only posture — the prompt forbids edits, commands and git writes", "enforcement": "advisory", "source": "bin/agy-review.sh" },
114
+ { "value": "the receipt fingerprints the DIFF FILE, never the tree — it cannot attest the tree", "enforcement": "enforced", "source": "capability.json roles.review.contract.receipt" }
115
+ ],
116
+ "customHooks": ["AGY_PROBE"]
117
+ },
118
+ {
119
+ "key": "review.continue",
120
+ "kind": "continuation",
121
+ "role": "review",
122
+ "purpose": "Send a round-2 delta into the review already open in this conversation.",
123
+ "whenToUse": [
124
+ "asking the same reviewer whether a fold really solves its own finding",
125
+ "a follow-up round that must not re-read the whole artifact"
126
+ ],
127
+ "whenNotTo": ["attesting a folded tree — a continuation records fresh:false and never satisfies the gate"],
128
+ "invocationRefs": [{ "contractField": "continue", "index": 0 }],
129
+ "operands": [
130
+ { "slot": "[--decided @f]", "required": false, "description": "decisions already made; the reviewer must not re-raise them (anti-circling)" },
131
+ { "slot": "[--focus \"…\"]", "required": false, "description": "what this round must look at (repeatable)" }
132
+ ],
133
+ "guardrails": [
134
+ { "value": "--facts is rejected on a continuation — the facts are already in the conversation", "enforcement": "enforced", "source": "bin/agy-review.sh" },
135
+ { "value": "a continuation receipt is fresh:false — informational only, never a gate pass", "enforcement": "enforced", "source": "capability.json roles.review.contract.receipt" }
136
+ ],
137
+ "customHooks": ["AGY_PROBE"]
138
+ },
139
+ {
140
+ "key": "review.conversation",
141
+ "kind": "continuation",
142
+ "role": "review",
143
+ "purpose": "Resume a NAMED review conversation by its id, rather than the most recent one.",
144
+ "whenToUse": ["returning to a specific earlier review after other agy runs happened in between"],
145
+ "invocationRefs": [{ "contractField": "continue", "index": 1 }],
146
+ "operands": [
147
+ { "slot": "<id>", "required": true, "description": "the conversation id of the review to resume" },
148
+ { "slot": "[--decided @f]", "required": false, "description": "decisions already made; the reviewer must not re-raise them (anti-circling)" },
149
+ { "slot": "[--focus \"…\"]", "required": false, "description": "what this round must look at (repeatable)" }
150
+ ],
151
+ "guardrails": [
152
+ { "value": "a continuation receipt is fresh:false — informational only, never a gate pass", "enforcement": "enforced", "source": "capability.json roles.review.contract.receipt" }
153
+ ],
154
+ "customHooks": ["AGY_PROBE"]
155
+ },
156
+ {
157
+ "key": "run",
158
+ "kind": "primary",
159
+ "role": "probe",
160
+ "purpose": "Send a raw advisory prompt to a selectable model and print the reply.",
161
+ "whenToUse": [
162
+ "a one-off question that needs no repo grounding",
163
+ "reaching a specific model (Gemini, Claude, GPT-OSS) from the terminal",
164
+ "a throwaway probe of the CLI, a model, or the wrapper plumbing"
165
+ ],
166
+ "whenNotTo": ["a code or plan review — agy-review owns the grounded posture and the receipt"],
167
+ "descriptor": "agy-run <prompt|-|@file> -- <extra agy flags...>",
168
+ "operands": [
169
+ { "slot": "<prompt|-|@file>", "required": true, "description": "the prompt text, - to read it from stdin, or @path to read it from a file" },
170
+ { "slot": "<extra agy flags...>", "required": false, "description": "raw agy flags passed straight through after --" }
171
+ ],
172
+ "guardrails": [
173
+ { "value": "subscription-only — every *_API_KEY env var is unset before the run", "enforcement": "enforced", "source": "bin/agy.sh" },
174
+ { "value": "any model is selectable via AGY_MODEL; the default is Gemini 3.1 Pro (High)", "enforcement": "advisory", "source": "bin/agy.sh" },
175
+ { "value": "hard wall-clock cap AGY_HARD_TIMEOUT (built-in default 5m)", "enforcement": "enforced", "condition": "only while timeout(1)/gtimeout is on PATH — otherwise the wrapper warns and runs uncapped", "source": "capability.json settings.AGY_HARD_TIMEOUT" },
176
+ { "value": "the prompt rides a single argv — over AGY_MAX_PROMPT_BYTES (120000) the run refuses", "enforcement": "enforced", "source": "bin/agy.sh" },
177
+ { "value": "no review posture, no grounding, no receipt — the reply is raw model output", "enforcement": "advisory", "source": "bin/agy.sh" }
178
+ ],
179
+ "customHooks": ["run"]
180
+ },
181
+ {
182
+ "key": "AGY_PROBE",
183
+ "kind": "env-hook",
184
+ "parents": ["review.code", "review.plan", "review.diff", "review.continue", "review.conversation"],
185
+ "purpose": "Silence the off-frontier model advisory for a THROWAWAY probe review.",
186
+ "whenToUse": [
187
+ "a probe whose answer cannot depend on the model",
188
+ "deliberately reviewing with a cheaper non-frontier model"
189
+ ],
190
+ "whenNotTo": ["any run whose output informs what ships — a probe never attests"],
191
+ "descriptor": "AGY_PROBE=1 agy-review code [--facts @<facts-file>]",
192
+ "operands": [
193
+ { "slot": "[--facts @<facts-file>]", "required": false, "description": "the verified facts the review runs AGAINST — a probe may run ungrounded (its receipt never attests either way)" }
194
+ ],
195
+ "guardrails": [
196
+ { "value": "a probe review mints a probe-marked receipt the review-state gate rejects", "enforcement": "enforced", "source": "agent-workflow-kit tools/review-state.mjs" }
197
+ ]
198
+ }
199
+ ],
39
200
  "settings": [
40
201
  {
41
202
  "key": "AGY_HARD_TIMEOUT",