@sabaiway/agent-workflow-kit 1.24.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/README.md +12 -8
- package/SKILL.md +53 -25
- package/bin/install.mjs +74 -29
- package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +35 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +186 -0
- package/bridges/antigravity-cli-bridge/bin/agy.sh +23 -0
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +58 -2
- package/bridges/antigravity-cli-bridge/capability.json +25 -2
- package/bridges/codex-cli-bridge/SKILL.md +2 -2
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +34 -0
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +160 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +28 -0
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +127 -0
- package/bridges/codex-cli-bridge/capability.json +36 -3
- package/bridges/codex-cli-bridge/references/driving-codex.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/tools/commands.mjs +1 -1
- package/tools/detect-backends.mjs +59 -0
- package/tools/family-members.mjs +2 -1
- package/tools/family-registry.mjs +111 -13
- package/tools/labels.mjs +13 -0
- package/tools/manifest/schema.md +15 -0
- package/tools/procedures.mjs +37 -6
- package/tools/recipes.mjs +56 -17
- package/tools/renderers.mjs +12 -1
- package/tools/semver-lite.mjs +23 -0
- package/tools/setup-backends.mjs +143 -6
- package/tools/view-model.mjs +11 -1
|
@@ -439,3 +439,189 @@ describe('agy-review.sh — subdir invocation is repo-complete (13)', () => {
|
|
|
439
439
|
assert.match(r.prompt, /SUBDIR_RELATIVE_FACT/, 'a relative --facts path resolves against the invocation cwd, before the cd');
|
|
440
440
|
});
|
|
441
441
|
});
|
|
442
|
+
|
|
443
|
+
// ── driving contract: --help ⟷ manifest ⟷ real arg-parsing (drift-guarded) ─────
|
|
444
|
+
// The manifest roles.review.contract is the single machine-readable source of the
|
|
445
|
+
// driving contract; these suites pin (a) --help renders it verbatim (set-EQUALITY,
|
|
446
|
+
// both directions), (b) the wrapper's REAL parser arms equal the declared sets
|
|
447
|
+
// (source-level reverse guard), (c) each declared mode/flag is really accepted and
|
|
448
|
+
// the CLOSED grammar really rejects an invented flag. Helpers are inline — each
|
|
449
|
+
// bridge test file stays standalone (mirror byte-equality).
|
|
450
|
+
|
|
451
|
+
const MANIFEST = JSON.parse(readFileSync(join(HERE, '..', 'capability.json'), 'utf8'));
|
|
452
|
+
const REVIEW_CONTRACT = MANIFEST.roles.review.contract;
|
|
453
|
+
const norm = (s) => s.replace(/\s+/g, ' ').trim();
|
|
454
|
+
const setEq = (got, want, msg) => assert.deepEqual([...got].sort(), [...want].sort(), msg);
|
|
455
|
+
const leadingFlag = (descriptor) => {
|
|
456
|
+
const m = norm(descriptor).match(/(^|\s)(--[a-z-]+)/);
|
|
457
|
+
assert.ok(m, `descriptor "${descriptor}" carries no --flag token`);
|
|
458
|
+
return m[2];
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// Run `--help`/-h with PATH stripped of codex/agy/git, from a non-git cwd —
|
|
462
|
+
// proving the short-circuit fires BEFORE every preflight.
|
|
463
|
+
const runHelp = (arg) => {
|
|
464
|
+
const root = mkdtempSync(join(tmpdir(), 'agy-review-help-'));
|
|
465
|
+
const nongit = join(root, 'nongit');
|
|
466
|
+
mkdirSync(nongit, { recursive: true });
|
|
467
|
+
const path = makePathWithout(root, ['codex', 'agy', 'git']);
|
|
468
|
+
const r = spawnSync('bash', [WRAPPER, arg], {
|
|
469
|
+
cwd: nongit, encoding: 'utf8', timeout: 15000, env: { HOME: root, PATH: path },
|
|
470
|
+
});
|
|
471
|
+
rmSync(root, { recursive: true, force: true });
|
|
472
|
+
return r;
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// The lines of a labelled --help section (header line → the next blank line).
|
|
476
|
+
const helpSection = (text, header) => {
|
|
477
|
+
const lines = text.split('\n');
|
|
478
|
+
const i = lines.findIndex((l) => l.trim() === header);
|
|
479
|
+
assert.notEqual(i, -1, `--help must carry a "${header}" section`);
|
|
480
|
+
const out = [];
|
|
481
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
482
|
+
if (lines[j].trim() === '') break;
|
|
483
|
+
out.push(lines[j].trim());
|
|
484
|
+
}
|
|
485
|
+
return out;
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// Source-level parser-arm extractor — the reverse drift guard. Scans ONLY `case`
|
|
489
|
+
// statements whose SUBJECT is a CLI-argument variable (allowlisted), skipping
|
|
490
|
+
// heredoc bodies (a heredoc may carry non-CLI `case` arms — e.g. codex-exec's
|
|
491
|
+
// git-shim). Returns Map(subject → [raw arm label, …]) in source order.
|
|
492
|
+
const ARG_SUBJECTS = new Set(['"$mode"', '"${1:-}"', '"$1"', '"$_arg"']);
|
|
493
|
+
const extractArgCaseArms = (source) => {
|
|
494
|
+
const arms = new Map();
|
|
495
|
+
const stack = [];
|
|
496
|
+
let heredoc = null;
|
|
497
|
+
for (const raw of source.split('\n')) {
|
|
498
|
+
if (heredoc) {
|
|
499
|
+
if (raw.trim() === heredoc) heredoc = null;
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (raw.trimStart().startsWith('#')) continue; // a comment line may carry a stray ')'
|
|
503
|
+
const hd = raw.match(/<<-?\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?/);
|
|
504
|
+
if (hd) { heredoc = hd[1]; continue; }
|
|
505
|
+
const cs = raw.match(/^\s*case\s+(\S+)\s+in\b/);
|
|
506
|
+
if (cs) { stack.push(cs[1]); continue; }
|
|
507
|
+
if (/^\s*esac\b/.test(raw)) { stack.pop(); continue; }
|
|
508
|
+
if (stack.length && ARG_SUBJECTS.has(stack[stack.length - 1])) {
|
|
509
|
+
const arm = raw.match(/^\s*([^)(\s][^)(]*)\)/);
|
|
510
|
+
if (arm) {
|
|
511
|
+
const subject = stack[stack.length - 1];
|
|
512
|
+
if (!arms.has(subject)) arms.set(subject, []);
|
|
513
|
+
arms.get(subject).push(arm[1].trim());
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return arms;
|
|
518
|
+
};
|
|
519
|
+
const splitArms = (labels) => (labels ?? []).flatMap((l) => l.split('|'));
|
|
520
|
+
|
|
521
|
+
describe('agy-review.sh — --help contract (manifest-pinned)', () => {
|
|
522
|
+
it('--help and -h exit 0 pre-preflight (no agy, no git)', () => {
|
|
523
|
+
for (const arg of ['--help', '-h']) {
|
|
524
|
+
const r = runHelp(arg);
|
|
525
|
+
assert.equal(r.status, 0, `${arg}: ${r.stderr}`);
|
|
526
|
+
assert.match(r.stdout, /Usage:/, `${arg} prints the contract to stdout`);
|
|
527
|
+
assert.equal(r.stderr, '', `${arg} prints nothing to stderr`);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('Usage set-EQUALS the manifest invocation descriptors (both directions)', () => {
|
|
532
|
+
const help = runHelp('--help').stdout;
|
|
533
|
+
const got = helpSection(help, 'Usage:').filter((l) => l.startsWith('agy-review')).map(norm);
|
|
534
|
+
assert.ok(REVIEW_CONTRACT.invocations.length > 0, 'manifest invocations must be non-empty');
|
|
535
|
+
setEq(got, REVIEW_CONTRACT.invocations.map(norm), 'help Usage ⟷ manifest invocations');
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('Flags set-EQUALS the manifest flag descriptors (both directions)', () => {
|
|
539
|
+
const help = runHelp('--help').stdout;
|
|
540
|
+
const got = helpSection(help, 'Flags:').filter((l) => l.startsWith('--')).map(norm);
|
|
541
|
+
assert.ok(REVIEW_CONTRACT.flags.length > 0, 'manifest flags must be non-empty');
|
|
542
|
+
setEq(got, REVIEW_CONTRACT.flags.map(norm), 'help Flags ⟷ manifest flags');
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('Grounding renders the manifest grounding note verbatim', () => {
|
|
546
|
+
const help = runHelp('--help').stdout;
|
|
547
|
+
assert.equal(norm(helpSection(help, 'Grounding:').join(' ')), norm(REVIEW_CONTRACT.grounding));
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('Round-2 / resume set-EQUALS the manifest continue descriptors', () => {
|
|
551
|
+
const help = runHelp('--help').stdout;
|
|
552
|
+
const got = helpSection(help, 'Round-2 / resume:').filter((l) => l.startsWith('agy-review')).map(norm);
|
|
553
|
+
assert.ok(REVIEW_CONTRACT.continue.length > 0, 'manifest continue must be non-empty');
|
|
554
|
+
setEq(got, REVIEW_CONTRACT.continue.map(norm), 'help continue ⟷ manifest continue');
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
describe('agy-review.sh — source-level reverse guard (parser arms ⟷ manifest)', () => {
|
|
559
|
+
const arms = extractArgCaseArms(readFileSync(WRAPPER, 'utf8'));
|
|
560
|
+
|
|
561
|
+
it('the real mode arms equal the manifest modes (adding a mode without the manifest fails here)', () => {
|
|
562
|
+
// Deliberately a UNION over every `case "$mode"` in the wrapper (the CLI dispatch AND the
|
|
563
|
+
// emit_artifact renderer): the union can only be conservative — a mode added to EITHER case
|
|
564
|
+
// without the manifest goes red; no renderer-only arm can make a missing manifest entry green.
|
|
565
|
+
const modes = splitArms(arms.get('"$mode"')).filter((a) => a !== '*');
|
|
566
|
+
assert.ok(MANIFEST.roles.review.modes.length > 0, 'manifest modes must be non-empty');
|
|
567
|
+
setEq(new Set(modes), MANIFEST.roles.review.modes, 'parser mode arms ⟷ manifest modes');
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('the real flag arms equal the manifest flag set (closed grammar; catch-alls excluded)', () => {
|
|
571
|
+
const flagArms = splitArms(arms.get('"$1"')).filter((a) => !['--', '--*', '*'].includes(a));
|
|
572
|
+
const declared = REVIEW_CONTRACT.flags.map(leadingFlag);
|
|
573
|
+
assert.ok(declared.length > 0, 'manifest flag set must be non-empty');
|
|
574
|
+
setEq(new Set(flagArms), new Set(declared), 'parser flag arms ⟷ manifest flags');
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('the first-arg entrypoints are exactly --help/-h + the manifest continue flags', () => {
|
|
578
|
+
const declared = REVIEW_CONTRACT.continue.map(leadingFlag);
|
|
579
|
+
assert.ok(declared.length > 0, 'manifest continue set must be non-empty');
|
|
580
|
+
setEq(new Set(splitArms(arms.get('"${1:-}"'))), new Set(['--help', '-h', ...declared]));
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
describe('agy-review.sh — declared contract is really accepted (forward guard)', () => {
|
|
585
|
+
it('every manifest mode runs green', () => {
|
|
586
|
+
const drive = {
|
|
587
|
+
code: () => ['code', '--facts', 'f'],
|
|
588
|
+
plan: (sb) => { writeFileSync(join(sb.repo, 'p.md'), '# p\n'); return ['plan', 'p.md', '--facts', 'f']; },
|
|
589
|
+
diff: (sb) => { writeFileSync(join(sb.repo, 'c.diff'), 'diff body\n'); return ['diff', 'c.diff', '--facts', 'f']; },
|
|
590
|
+
};
|
|
591
|
+
for (const mode of MANIFEST.roles.review.modes) {
|
|
592
|
+
assert.ok(drive[mode], `no test drive for manifest mode "${mode}" — add one`);
|
|
593
|
+
const sb = makeSandbox();
|
|
594
|
+
const r = run(sb, { args: drive[mode](sb) });
|
|
595
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
596
|
+
assert.equal(r.status, 0, `mode ${mode}: ${r.stderr}`);
|
|
597
|
+
assert.equal(r.invoked, true, `mode ${mode} must reach agy`);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('every manifest flag is accepted in code mode', () => {
|
|
602
|
+
for (const descriptor of REVIEW_CONTRACT.flags) {
|
|
603
|
+
const flag = leadingFlag(descriptor);
|
|
604
|
+
const sb = makeSandbox();
|
|
605
|
+
const r = run(sb, { args: ['code', flag, 'f'] });
|
|
606
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
607
|
+
assert.equal(r.status, 0, `${flag}: ${r.stderr}`);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('an invented flag is rejected (closed grammar negative)', () => {
|
|
612
|
+
const sb = makeSandbox();
|
|
613
|
+
const r = run(sb, { args: ['code', '--facts', 'f', '--bogus-flag'] });
|
|
614
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
615
|
+
assert.equal(r.status, 2);
|
|
616
|
+
assert.match(r.stderr, /unknown flag '--bogus-flag'/);
|
|
617
|
+
assert.equal(r.invoked, false, 'an unknown flag must not spend a run');
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('--help NOT in first position is an unknown flag, never an intercepted help', () => {
|
|
621
|
+
const sb = makeSandbox();
|
|
622
|
+
const r = run(sb, { args: ['code', '--facts', 'f', '--help'] });
|
|
623
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
624
|
+
assert.equal(r.status, 2, 'help is keyed on the FIRST argument only');
|
|
625
|
+
assert.doesNotMatch(r.stdout, /Usage:/);
|
|
626
|
+
});
|
|
627
|
+
});
|
|
@@ -34,6 +34,29 @@
|
|
|
34
34
|
# # passthrough agy flags (future flows)
|
|
35
35
|
set -euo pipefail
|
|
36
36
|
|
|
37
|
+
# --- --help / -h (pre-preflight: no agy, no login needed) ----------------------
|
|
38
|
+
# Keyed ONLY on the FIRST argument — never a scan of all args, else a passthrough
|
|
39
|
+
# payload like `agy-run "prompt" -- --help` would be intercepted. agy-run is the
|
|
40
|
+
# probe role (not dispatched by any activity slot), so this help is authored here
|
|
41
|
+
# — not manifest-pinned (candidate C only).
|
|
42
|
+
case "${1:-}" in
|
|
43
|
+
--help|-h)
|
|
44
|
+
cat <<'HELP'
|
|
45
|
+
agy-run — thin, flow-agnostic wrapper around Google's Antigravity CLI (agy; subscription-only, hard wall-clock cap).
|
|
46
|
+
|
|
47
|
+
Usage:
|
|
48
|
+
agy-run "your prompt"
|
|
49
|
+
echo "your prompt" | agy-run -
|
|
50
|
+
agy-run @path/to/prompt.md
|
|
51
|
+
agy-run <prompt|-|@file> -- <extra agy flags...>
|
|
52
|
+
|
|
53
|
+
Environment: AGY_MODEL (exact display string from `agy models`; empty ⇒ agy's settings.json), AGY_TIMEOUT / AGY_HARD_TIMEOUT (duration strings), AGY_MAX_PROMPT_BYTES (single-argv byte ceiling; the override only lowers it).
|
|
54
|
+
Requires at run time: the agy CLI on PATH + a Google AI subscription login (--help needs neither).
|
|
55
|
+
HELP
|
|
56
|
+
exit 0
|
|
57
|
+
;;
|
|
58
|
+
esac
|
|
59
|
+
|
|
37
60
|
# 1. Make `agy` findable even when ~/.bashrc was not sourced.
|
|
38
61
|
export PATH="$HOME/.local/bin:$PATH"
|
|
39
62
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdtempSync, mkdirSync, writeFileSync, chmodSync, rmSync, existsSync } from 'node:fs';
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync, chmodSync, rmSync, existsSync, readdirSync, symlinkSync } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
-
import { join, dirname } from 'node:path';
|
|
5
|
+
import { join, dirname, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { spawnSync } from 'node:child_process';
|
|
8
8
|
|
|
@@ -152,3 +152,59 @@ describe('agy.sh — argv byte-ceiling guard (AGY_MAX_PROMPT_BYTES)', () => {
|
|
|
152
152
|
assert.equal(invoked, false, 'raising the ceiling past the OS limit must fail loud, not pass through to E2BIG');
|
|
153
153
|
});
|
|
154
154
|
});
|
|
155
|
+
|
|
156
|
+
// ── --help (candidate C only — the probe role is not dispatched by any activity
|
|
157
|
+
// slot, so this help is authored in the wrapper, NOT manifest-pinned; the lighter
|
|
158
|
+
// guard pins pre-preflight reachability + the documented usage forms). ──────────
|
|
159
|
+
|
|
160
|
+
// A PATH farm mirroring the real one MINUS the named binaries — so the reachability claim
|
|
161
|
+
// ("--help needs no agy/git") holds even on a host that has the real CLIs installed.
|
|
162
|
+
// Ported from agy-review.test.mjs (inline: each bridge test file stays standalone).
|
|
163
|
+
const makePathWithout = (root, exclude = []) => {
|
|
164
|
+
const skip = new Set(exclude);
|
|
165
|
+
const dir = mkdtempSync(join(root, 'nobin-'));
|
|
166
|
+
for (const d of (process.env.PATH || '').split(':').filter(Boolean)) {
|
|
167
|
+
let names;
|
|
168
|
+
try { names = readdirSync(d); } catch { continue; }
|
|
169
|
+
for (const name of names) {
|
|
170
|
+
if (skip.has(name)) continue;
|
|
171
|
+
const link = join(dir, name);
|
|
172
|
+
if (existsSync(link)) continue;
|
|
173
|
+
try { symlinkSync(resolve(d, name), link); } catch { /* dup / race — ignore */ }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return dir;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
describe('agy.sh — --help (pre-preflight, candidate C)', () => {
|
|
180
|
+
it('--help and -h exit 0 with NO agy on PATH and name the documented usage', () => {
|
|
181
|
+
for (const arg of ['--help', '-h']) {
|
|
182
|
+
// A bare HOME with no ~/.local/bin/agy stub AND a PATH farm stripped of agy/git —
|
|
183
|
+
// the help must not need the CLI even when the host has a real agy installed.
|
|
184
|
+
const home = mkdtempSync(join(tmpdir(), 'agy-help-'));
|
|
185
|
+
const r = spawnSync('bash', [WRAPPER, arg], {
|
|
186
|
+
env: { HOME: home, PATH: makePathWithout(home, ['agy', 'git']) },
|
|
187
|
+
encoding: 'utf8',
|
|
188
|
+
timeout: 15000,
|
|
189
|
+
});
|
|
190
|
+
rmSync(home, { recursive: true, force: true });
|
|
191
|
+
assert.equal(r.status, 0, `${arg}: ${r.stderr}`);
|
|
192
|
+
assert.equal(r.stderr, '', `${arg} prints nothing to stderr`);
|
|
193
|
+
assert.match(r.stdout, /Usage:/);
|
|
194
|
+
assert.match(r.stdout, /agy-run "your prompt"/);
|
|
195
|
+
assert.match(r.stdout, /agy-run @path\/to\/prompt\.md/);
|
|
196
|
+
assert.match(r.stdout, /agy-run <prompt\|-\|@file> -- <extra agy flags\.\.\.>/);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('--help after the -- separator is passthrough payload, never intercepted', () => {
|
|
201
|
+
const home = makeSandbox(RECORDING_STUB);
|
|
202
|
+
const sentinel = join(home, 'sentinel');
|
|
203
|
+
const r = runArgs(home, { args: ['prompt', '--', '--help'], env: { AGY_STUB_SENTINEL: sentinel } });
|
|
204
|
+
const invoked = existsSync(sentinel);
|
|
205
|
+
rmSync(home, { recursive: true, force: true });
|
|
206
|
+
assert.equal(r.status, 0, r.stderr);
|
|
207
|
+
assert.doesNotMatch(r.stdout, /Usage:/, 'help is keyed on the FIRST argument only');
|
|
208
|
+
assert.equal(invoked, true, 'the run must proceed to agy with the payload');
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -3,10 +3,33 @@
|
|
|
3
3
|
"schema": 1,
|
|
4
4
|
"name": "antigravity-cli-bridge",
|
|
5
5
|
"kind": "execution-backend",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.1.0",
|
|
7
7
|
"provides": ["review", "probe"],
|
|
8
8
|
"roles": {
|
|
9
|
-
"review": {
|
|
9
|
+
"review": {
|
|
10
|
+
"cmd": "agy-review",
|
|
11
|
+
"source": "bin/agy-review.sh",
|
|
12
|
+
"template": "references/review-prompt.md",
|
|
13
|
+
"modes": ["code", "plan", "diff"],
|
|
14
|
+
"output": "advisory",
|
|
15
|
+
"contract": {
|
|
16
|
+
"invocations": [
|
|
17
|
+
"agy-review code [--facts @f] [--decided @f] [--focus \"…\"] [extra focus…]",
|
|
18
|
+
"agy-review plan <plan-file> [--facts @f] [--decided @f] [--focus \"…\"]",
|
|
19
|
+
"agy-review diff <diff-file> [--facts @f] [--decided @f] [--focus \"…\"]"
|
|
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)",
|
|
22
|
+
"flags": [
|
|
23
|
+
"--facts @f — verified facts the review runs AGAINST (omit ⇒ loud ungrounded-review warning)",
|
|
24
|
+
"--decided @f — already-decided / already-addressed list; do NOT re-raise (anti-circling; the round-2 payload)",
|
|
25
|
+
"--focus \"…\" — extra focus (repeatable; code mode also takes trailing focus words)"
|
|
26
|
+
],
|
|
27
|
+
"continue": [
|
|
28
|
+
"agy-review --continue [--decided @f] [--focus \"…\"]",
|
|
29
|
+
"agy-review --conversation <id> [--decided @f] [--focus \"…\"]"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
},
|
|
10
33
|
"probe": { "cmd": "agy-run", "source": "bin/agy.sh", "output": "advisory" }
|
|
11
34
|
},
|
|
12
35
|
"detect": {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: codex-cli-bridge
|
|
3
3
|
description: Delegate work to the OpenAI Codex CLI (`codex`) under a ChatGPT subscription — run plan/instruction EXECUTION in a sandboxed workspace, or get a read-only ADVISORY review of a plan or working-tree diff — as a second delegated-execution backend beside Antigravity. Use when the user wants to hand a bounded coding task or plan to `codex exec`, get a second-opinion review from codex, install or authenticate Codex CLI, understand its sandbox/network/approval policy, drive codex efficiently from the main agent (exec vs review, resume, the commit boundary), bridge project context (`AGENTS.md`) into codex, or troubleshoot codex flags, models, auth, or its no-TTY headless behaviour.
|
|
4
4
|
metadata:
|
|
5
|
-
version: '2.
|
|
5
|
+
version: '2.1.0'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# codex-cli-bridge
|
|
@@ -77,7 +77,7 @@ Drive codex only through the two wrappers (installed on `PATH`), run from the ta
|
|
|
77
77
|
# EXECUTION (workspace-write sandbox, network OFF, never prompts):
|
|
78
78
|
codex-exec docs/plans/<slug>.md # drive a plan file
|
|
79
79
|
echo "apply review fix: ..." | codex-exec - # ad-hoc instruction from stdin
|
|
80
|
-
codex-exec <file|-> -- <
|
|
80
|
+
codex-exec <file|-> -- <extra codex flags...> # GUARDED passthrough after `--` (policy/model/capture flags rejected; some relaxed only under CODEX_PROBE=1)
|
|
81
81
|
|
|
82
82
|
# RESUME (iterate on the SAME session without re-sending context):
|
|
83
83
|
codex-exec --resume-last docs/plans/<slug>.md # continue the last session (id from the sidecar)
|
|
@@ -40,6 +40,40 @@
|
|
|
40
40
|
# CODEX_PROBE=1 CODEX_MODEL=<slug> codex-exec <file> # throwaway probe (relaxes the guard)
|
|
41
41
|
set -euo pipefail
|
|
42
42
|
|
|
43
|
+
# --- --help / -h (pre-preflight: no codex, no login, no git tree needed) -------
|
|
44
|
+
# Keyed ONLY on the FIRST argument — never a scan of all args, else a passthrough
|
|
45
|
+
# payload like `codex-exec f - -- --help` would be intercepted.
|
|
46
|
+
# The contract below is drift-guarded against capability.json roles.execute.contract.
|
|
47
|
+
case "${1:-}" in
|
|
48
|
+
--help|-h)
|
|
49
|
+
cat <<'HELP'
|
|
50
|
+
codex-exec — delegate plan/instruction EXECUTION to the OpenAI Codex CLI (subscription-only; workspace-write sandbox, network OFF, git writes blocked — the orchestrator commits).
|
|
51
|
+
|
|
52
|
+
Usage:
|
|
53
|
+
codex-exec <plan-file|->
|
|
54
|
+
codex-exec <plan-file|-> -- <extra codex flags...>
|
|
55
|
+
|
|
56
|
+
Grounding:
|
|
57
|
+
automatic — the root AGENTS.md (Hard Constraints) is auto-merged into codex's
|
|
58
|
+
context and the wrapper prepends the orchestrator execution contract; no
|
|
59
|
+
grounding flags
|
|
60
|
+
|
|
61
|
+
Round-2 / resume:
|
|
62
|
+
codex-exec --resume-last <plan-file|->
|
|
63
|
+
codex-exec --resume <session-id> <plan-file|->
|
|
64
|
+
(resume continues the recorded session without re-sending context; takes no '--' passthrough)
|
|
65
|
+
|
|
66
|
+
Guarded passthrough after '--':
|
|
67
|
+
blocked always: -c* --config* -s* --sandbox* --dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust --full-auto --oss --local-provider* -p* --profile* -m* --model* -o* --output-last-message* --json* --color* --output-schema* --ephemeral*
|
|
68
|
+
relaxed only under CODEX_PROBE=1: --add-dir* -C* --cd* --skip-git-repo-check --ignore-rules --enable* --disable*
|
|
69
|
+
|
|
70
|
+
Environment: CODEX_HARD_TIMEOUT (seconds, default 3600), CODEX_PROBE=1 (throwaway probe only).
|
|
71
|
+
Requires at run time: the codex CLI on PATH, a ChatGPT-subscription login, a git work tree with a root AGENTS.md (--help needs none of these).
|
|
72
|
+
HELP
|
|
73
|
+
exit 0
|
|
74
|
+
;;
|
|
75
|
+
esac
|
|
76
|
+
|
|
43
77
|
DEFAULT_CODEX_MODEL="gpt-5.5" # frontier coding model (verified locally) — pinned
|
|
44
78
|
DEFAULT_CODEX_EFFORT="xhigh" # maximum reasoning effort — pinned
|
|
45
79
|
CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
|
|
@@ -149,7 +149,8 @@ describe('codex-exec.sh — quality-first model/effort guard (1.1)', () => {
|
|
|
149
149
|
// capture flags: rejected ALWAYS, even under CODEX_PROBE=1.
|
|
150
150
|
const ALWAYS_BLOCKED = [
|
|
151
151
|
['-s', 'read-only'], ['--sandbox', 'danger-full-access'], ['-c', 'k=v'], ['--config', 'k=v'],
|
|
152
|
-
['--full-auto'], ['--dangerously-bypass-approvals-and-sandbox'], ['--
|
|
152
|
+
['--full-auto'], ['--dangerously-bypass-approvals-and-sandbox'], ['--dangerously-bypass-hook-trust'],
|
|
153
|
+
['--oss'], ['--local-provider', 'x'],
|
|
153
154
|
['-p', 'prof'], ['--profile', 'prof'], ['-m', 'gpt-5.5'], ['--model', 'gpt-5.5'],
|
|
154
155
|
['-o', '/x'], ['--output-last-message', '/x'], ['--json'], ['--color', 'always'],
|
|
155
156
|
['--output-schema', '/x'], ['--ephemeral'],
|
|
@@ -591,3 +592,161 @@ describe('codex-exec.sh — session id absent', () => {
|
|
|
591
592
|
assert.match(r.stdout, /FAKE_FINAL_MESSAGE/, 'the run still succeeds');
|
|
592
593
|
});
|
|
593
594
|
});
|
|
595
|
+
|
|
596
|
+
// ── driving contract: --help ⟷ manifest ⟷ real arg-parsing (drift-guarded) ─────
|
|
597
|
+
// The manifest roles.execute.contract is the single machine-readable source of the
|
|
598
|
+
// driving contract; these suites pin (a) --help renders it verbatim (set-EQUALITY,
|
|
599
|
+
// both directions, incl. the TIERED guarded-passthrough sets), (b) the wrapper's
|
|
600
|
+
// REAL parser arms equal the declared sets (source-level reverse guard — the
|
|
601
|
+
// git-shim heredoc's own `case` arms are NOT CLI modes and must be skipped).
|
|
602
|
+
// Helpers are inline — each bridge test file stays standalone (mirror byte-equality).
|
|
603
|
+
|
|
604
|
+
const MANIFEST = JSON.parse(readFileSync(join(HERE, '..', 'capability.json'), 'utf8'));
|
|
605
|
+
const EXEC_CONTRACT = MANIFEST.roles.execute.contract;
|
|
606
|
+
const norm = (s) => s.replace(/\s+/g, ' ').trim();
|
|
607
|
+
const setEq = (got, want, msg) => assert.deepEqual([...got].sort(), [...want].sort(), msg);
|
|
608
|
+
const leadingFlag = (descriptor) => {
|
|
609
|
+
const m = norm(descriptor).match(/(^|\s)(--[a-z-]+)/);
|
|
610
|
+
assert.ok(m, `descriptor "${descriptor}" carries no --flag token`);
|
|
611
|
+
return m[2];
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
// Run `--help`/-h with PATH stripped of codex/agy/git, from a non-git cwd with no
|
|
615
|
+
// AGENTS.md — proving the short-circuit fires BEFORE every preflight.
|
|
616
|
+
const runHelp = (arg) => {
|
|
617
|
+
const root = mkdtempSync(join(tmpdir(), 'codex-exec-help-'));
|
|
618
|
+
const nongit = join(root, 'nongit');
|
|
619
|
+
mkdirSync(nongit, { recursive: true });
|
|
620
|
+
const path = makePathWithout(root, ['codex', 'agy', 'git']);
|
|
621
|
+
const r = spawnSync('bash', [WRAPPER, arg], {
|
|
622
|
+
cwd: nongit, encoding: 'utf8', timeout: 15000, env: { HOME: root, PATH: path },
|
|
623
|
+
});
|
|
624
|
+
rmSync(root, { recursive: true, force: true });
|
|
625
|
+
return r;
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
// The lines of a labelled --help section (header line → the next blank line).
|
|
629
|
+
const helpSection = (text, header) => {
|
|
630
|
+
const lines = text.split('\n');
|
|
631
|
+
const i = lines.findIndex((l) => l.trim() === header);
|
|
632
|
+
assert.notEqual(i, -1, `--help must carry a "${header}" section`);
|
|
633
|
+
const out = [];
|
|
634
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
635
|
+
if (lines[j].trim() === '') break;
|
|
636
|
+
out.push(lines[j].trim());
|
|
637
|
+
}
|
|
638
|
+
return out;
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
// Source-level parser-arm extractor — the reverse drift guard. Scans ONLY `case`
|
|
642
|
+
// statements whose SUBJECT is a CLI-argument variable (allowlisted), skipping
|
|
643
|
+
// heredoc bodies (codex-exec's git-shim heredoc carries its own `case "$verb"`
|
|
644
|
+
// git-verb arms that are NOT CLI modes). Returns Map(subject → [raw arm label, …]).
|
|
645
|
+
const ARG_SUBJECTS = new Set(['"$mode"', '"${1:-}"', '"$1"', '"$_arg"']);
|
|
646
|
+
const extractArgCaseArms = (source) => {
|
|
647
|
+
const arms = new Map();
|
|
648
|
+
const stack = [];
|
|
649
|
+
let heredoc = null;
|
|
650
|
+
for (const raw of source.split('\n')) {
|
|
651
|
+
if (heredoc) {
|
|
652
|
+
if (raw.trim() === heredoc) heredoc = null;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (raw.trimStart().startsWith('#')) continue; // a comment line may carry a stray ')'
|
|
656
|
+
const hd = raw.match(/<<-?\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?/);
|
|
657
|
+
if (hd) { heredoc = hd[1]; continue; }
|
|
658
|
+
const cs = raw.match(/^\s*case\s+(\S+)\s+in\b/);
|
|
659
|
+
if (cs) { stack.push(cs[1]); continue; }
|
|
660
|
+
if (/^\s*esac\b/.test(raw)) { stack.pop(); continue; }
|
|
661
|
+
if (stack.length && ARG_SUBJECTS.has(stack[stack.length - 1])) {
|
|
662
|
+
const arm = raw.match(/^\s*([^)(\s][^)(]*)\)/);
|
|
663
|
+
if (arm) {
|
|
664
|
+
const subject = stack[stack.length - 1];
|
|
665
|
+
if (!arms.has(subject)) arms.set(subject, []);
|
|
666
|
+
arms.get(subject).push(arm[1].trim());
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return arms;
|
|
671
|
+
};
|
|
672
|
+
const splitArms = (labels) => (labels ?? []).flatMap((l) => l.split('|'));
|
|
673
|
+
|
|
674
|
+
describe('codex-exec.sh — --help contract (manifest-pinned)', () => {
|
|
675
|
+
it('--help and -h exit 0 pre-preflight (no codex, no git, no AGENTS.md)', () => {
|
|
676
|
+
for (const arg of ['--help', '-h']) {
|
|
677
|
+
const r = runHelp(arg);
|
|
678
|
+
assert.equal(r.status, 0, `${arg}: ${r.stderr}`);
|
|
679
|
+
assert.match(r.stdout, /Usage:/, `${arg} prints the contract to stdout`);
|
|
680
|
+
assert.equal(r.stderr, '', `${arg} prints nothing to stderr`);
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it('Usage set-EQUALS the manifest invocation descriptors (both directions)', () => {
|
|
685
|
+
const help = runHelp('--help').stdout;
|
|
686
|
+
const got = helpSection(help, 'Usage:').filter((l) => l.startsWith('codex-exec')).map(norm);
|
|
687
|
+
assert.ok(EXEC_CONTRACT.invocations.length > 0, 'manifest invocations must be non-empty');
|
|
688
|
+
setEq(got, EXEC_CONTRACT.invocations.map(norm), 'help Usage ⟷ manifest invocations');
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('Grounding renders the manifest grounding note verbatim', () => {
|
|
692
|
+
const help = runHelp('--help').stdout;
|
|
693
|
+
assert.equal(norm(helpSection(help, 'Grounding:').join(' ')), norm(EXEC_CONTRACT.grounding));
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it('Round-2 / resume set-EQUALS the manifest continue descriptors', () => {
|
|
697
|
+
const help = runHelp('--help').stdout;
|
|
698
|
+
const got = helpSection(help, 'Round-2 / resume:').filter((l) => l.startsWith('codex-exec')).map(norm);
|
|
699
|
+
assert.ok(EXEC_CONTRACT.continue.length > 0, 'manifest continue must be non-empty');
|
|
700
|
+
setEq(got, EXEC_CONTRACT.continue.map(norm), 'help continue ⟷ manifest continue');
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('the guarded-passthrough TIERS set-EQUAL the manifest tiers (never a flat set)', () => {
|
|
704
|
+
const help = runHelp('--help').stdout;
|
|
705
|
+
const section = helpSection(help, "Guarded passthrough after '--':");
|
|
706
|
+
const tier = (prefix) => {
|
|
707
|
+
const line = section.find((l) => l.startsWith(prefix));
|
|
708
|
+
assert.ok(line, `passthrough section must carry a "${prefix}" line`);
|
|
709
|
+
return line.slice(prefix.length).trim().split(/\s+/);
|
|
710
|
+
};
|
|
711
|
+
assert.ok(EXEC_CONTRACT.passthrough.blocked.length > 0, 'manifest blocked tier must be non-empty');
|
|
712
|
+
assert.ok(EXEC_CONTRACT.passthrough.probeRelaxed.length > 0, 'manifest probe tier must be non-empty');
|
|
713
|
+
setEq(tier('blocked always:'), EXEC_CONTRACT.passthrough.blocked, 'help tier-1 ⟷ manifest blocked');
|
|
714
|
+
setEq(tier('relaxed only under CODEX_PROBE=1:'), EXEC_CONTRACT.passthrough.probeRelaxed, 'help tier-2 ⟷ manifest probeRelaxed');
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it('--help after the -- separator is passthrough payload, never intercepted', () => {
|
|
718
|
+
const sb = makeSandbox();
|
|
719
|
+
const r = run(sb, { args: ['-', '--', '--help'], input: 'go' });
|
|
720
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
721
|
+
assert.equal(r.status, 0, r.stderr);
|
|
722
|
+
assert.doesNotMatch(r.stdout, /Usage:/, 'help is keyed on the FIRST argument only');
|
|
723
|
+
assert.match(r.argv, /(^|\n)--help(\n|$)/, 'the payload --help reaches codex argv');
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
describe('codex-exec.sh — source-level reverse guard (parser arms ⟷ manifest)', () => {
|
|
728
|
+
const arms = extractArgCaseArms(readFileSync(WRAPPER, 'utf8'));
|
|
729
|
+
|
|
730
|
+
it('the first-arg entrypoints are exactly --help/-h + the manifest resume flags', () => {
|
|
731
|
+
const declared = EXEC_CONTRACT.continue.map(leadingFlag);
|
|
732
|
+
assert.ok(declared.length > 0, 'manifest resume set must be non-empty');
|
|
733
|
+
setEq(new Set(splitArms(arms.get('"${1:-}"'))), new Set(['--help', '-h', ...declared]));
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it('the real passthrough tier arms equal the manifest tiers (git-shim heredoc excluded)', () => {
|
|
737
|
+
const tierArms = arms.get('"$_arg"') ?? [];
|
|
738
|
+
assert.equal(tierArms.length, 2, 'exactly two passthrough tiers: always-blocked, probe-relaxed');
|
|
739
|
+
setEq(tierArms[0].split('|'), EXEC_CONTRACT.passthrough.blocked, 'tier-1 arm ⟷ manifest blocked');
|
|
740
|
+
setEq(tierArms[1].split('|'), EXEC_CONTRACT.passthrough.probeRelaxed, 'tier-2 arm ⟷ manifest probeRelaxed');
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it('the in-test tier samples cover every manifest tier pattern (behavioural forward guard)', () => {
|
|
744
|
+
// ALWAYS_BLOCKED / PROBE_RELAXABLE drive the real behaviour suite above; pin them
|
|
745
|
+
// to the manifest so a tier edit cannot leave the behavioural samples stale.
|
|
746
|
+
const sample = (patterns) => patterns.map((p) => p.replace(/\*$/, ''));
|
|
747
|
+
const covered = (flags, patterns) =>
|
|
748
|
+
sample(patterns).every((p) => flags.some(([f]) => f === p || f.startsWith(p)));
|
|
749
|
+
assert.ok(covered(ALWAYS_BLOCKED, EXEC_CONTRACT.passthrough.blocked), 'every blocked pattern has a behavioural sample');
|
|
750
|
+
assert.ok(covered(PROBE_RELAXABLE, EXEC_CONTRACT.passthrough.probeRelaxed), 'every probe pattern has a behavioural sample');
|
|
751
|
+
});
|
|
752
|
+
});
|
|
@@ -31,6 +31,34 @@
|
|
|
31
31
|
# on disk remain readable under read-only — read-scoping is a prompt concern).
|
|
32
32
|
set -euo pipefail
|
|
33
33
|
|
|
34
|
+
# --- --help / -h (pre-preflight: no codex, no login, no git tree needed) -------
|
|
35
|
+
# Keyed ONLY on the FIRST argument — never a scan of all args (uniform rule across
|
|
36
|
+
# the four wrappers, so an open wrapper's passthrough payload is never intercepted).
|
|
37
|
+
# The contract below is drift-guarded against capability.json roles.review.contract.
|
|
38
|
+
case "${1:-}" in
|
|
39
|
+
--help|-h)
|
|
40
|
+
cat <<'HELP'
|
|
41
|
+
codex-review — read-only ADVISORY review by the OpenAI Codex CLI (subscription-only; frontier model at max effort).
|
|
42
|
+
|
|
43
|
+
Usage:
|
|
44
|
+
codex-review plan <plan-file>
|
|
45
|
+
codex-review code [extra focus...]
|
|
46
|
+
|
|
47
|
+
Grounding:
|
|
48
|
+
automatic — the wrapper precomputes the full working-tree change set (repo map,
|
|
49
|
+
status, diffs, untracked contents) and codex auto-merges the root AGENTS.md;
|
|
50
|
+
no grounding flags
|
|
51
|
+
|
|
52
|
+
Round-2 / resume:
|
|
53
|
+
(none — one-shot; a follow-up review is a fresh run)
|
|
54
|
+
|
|
55
|
+
Environment: CODEX_REVIEW_SCHEMA=1 (structured JSON findings), CODEX_HARD_TIMEOUT (seconds, default 1800), CODEX_PROBE=1 (throwaway probe only).
|
|
56
|
+
Requires at run time: the codex CLI on PATH, a ChatGPT-subscription login, a git work tree with a root AGENTS.md (--help needs none of these).
|
|
57
|
+
HELP
|
|
58
|
+
exit 0
|
|
59
|
+
;;
|
|
60
|
+
esac
|
|
61
|
+
|
|
34
62
|
DEFAULT_CODEX_MODEL="gpt-5.5"
|
|
35
63
|
DEFAULT_CODEX_EFFORT="xhigh"
|
|
36
64
|
CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
|