@sabaiway/agent-workflow-kit 5.0.0 → 5.2.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 +84 -0
- package/SKILL.md +13 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +14 -3
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +220 -30
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +264 -8
- package/bridges/antigravity-cli-bridge/bin/agy.sh +12 -2
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +18 -0
- package/bridges/antigravity-cli-bridge/capability.json +19 -13
- package/bridges/antigravity-cli-bridge/references/driving-agy.md +3 -2
- package/bridges/codex-cli-bridge/SKILL.md +8 -5
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +3 -2
- package/bridges/codex-cli-bridge/bin/codex-review.sh +205 -34
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +276 -5
- package/bridges/codex-cli-bridge/capability.json +8 -6
- package/bridges/codex-cli-bridge/references/driving-codex.md +2 -2
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +2 -2
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/flow-writer.md +37 -0
- package/references/modes/gates.md +4 -4
- package/references/modes/procedures.md +4 -2
- package/references/modes/receipt-deadline.md +16 -0
- package/references/modes/review-state.md +1 -1
- package/references/modes/set-flow.md +22 -0
- package/tools/cheap-agents.mjs +8 -2
- package/tools/commands.mjs +24 -2
- package/tools/commit-guard.mjs +44 -9
- package/tools/core-evidence.mjs +25 -22
- package/tools/detect-backends.mjs +32 -11
- package/tools/doc-parity.mjs +29 -2
- package/tools/flow-check.mjs +806 -0
- package/tools/flow-record.mjs +795 -0
- package/tools/flow-store-read.mjs +114 -0
- package/tools/flow-store.mjs +1178 -0
- package/tools/flow-writer.mjs +1265 -0
- package/tools/fs-read-nofollow.mjs +128 -0
- package/tools/gates-declaration.mjs +184 -0
- package/tools/gates-init.mjs +59 -17
- package/tools/orchestration-config.mjs +105 -4
- package/tools/orchestration-write.mjs +3 -3
- package/tools/plan-files.mjs +35 -0
- package/tools/procedures.mjs +75 -11
- package/tools/receipt-deadline.mjs +242 -0
- package/tools/recipes.mjs +21 -0
- package/tools/repo-lex.mjs +22 -0
- package/tools/review-state.mjs +240 -80
- package/tools/run-gates.mjs +361 -139
- package/tools/set-flow.mjs +465 -0
- package/tools/velocity-profile.mjs +8 -2
|
@@ -1880,6 +1880,198 @@ describe('agy-review.sh — review receipts (AD-038)', () => {
|
|
|
1880
1880
|
}
|
|
1881
1881
|
});
|
|
1882
1882
|
|
|
1883
|
+
// The wrapper-minted finding manifest (flow-orchestration Phase 4.2, Decision 2/P5/P24-25):
|
|
1884
|
+
// nonce-supplied dispatches mint {schema, backend, nonce, fingerprint, findings} beside the
|
|
1885
|
+
// receipt, atomic + no-clobber + ORDERED — a failed mint EXCLUDES the receipt append.
|
|
1886
|
+
describe('finding manifest (AW_REVIEW_NONCE)', () => {
|
|
1887
|
+
const manifestPath = (repo, nonce) => join(repo, '.git', `agent-workflow-finding-manifest-agy-${nonce}.json`);
|
|
1888
|
+
|
|
1889
|
+
it('a nonce-supplied grounded code dispatch mints the {backend, nonce}-named manifest carrying the captured findings + the receipt fingerprint', () => {
|
|
1890
|
+
const sb = makeSandbox();
|
|
1891
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT, AW_REVIEW_NONCE: 'r1-d2' } });
|
|
1892
|
+
const receipts = readReceipts(sb.repo);
|
|
1893
|
+
const manifest = JSON.parse(readFileSync(manifestPath(sb.repo, 'r1-d2'), 'utf8'));
|
|
1894
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1895
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1896
|
+
assert.equal(receipts.length, 1, 'the receipt landed beside the manifest');
|
|
1897
|
+
assert.deepEqual(Object.keys(manifest), ['schema', 'backend', 'nonce', 'fingerprint', 'findings'], 'the closed manifest key set, in order');
|
|
1898
|
+
assert.equal(manifest.schema, 1);
|
|
1899
|
+
assert.equal(manifest.backend, 'agy');
|
|
1900
|
+
assert.equal(manifest.nonce, 'r1-d2');
|
|
1901
|
+
assert.equal(manifest.fingerprint, receipts[0].fingerprint, 'the manifest binds the SAME reviewed-tree fingerprint as the receipt');
|
|
1902
|
+
assert.equal(manifest.findings, `${VERDICT_OUTPUT}\n`, 'findings = the captured review output VERBATIM');
|
|
1903
|
+
assert.equal(receipts[0].nonce, 'r1-d2', 'a nonce-supplied receipt carries the dispatch nonce — the flow round-land matcher requires exact equality (dispatch identity end-to-end)');
|
|
1904
|
+
});
|
|
1905
|
+
|
|
1906
|
+
it('a nonce-less invocation mints NO manifest and adds NO nonce field — the receipt field set is unchanged (Decision 2 both branches)', () => {
|
|
1907
|
+
const sb = makeSandbox();
|
|
1908
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
|
|
1909
|
+
const receipts = readReceipts(sb.repo);
|
|
1910
|
+
const gitEntries = readdirSync(join(sb.repo, '.git')).filter((n) => n.startsWith('agent-workflow-finding-manifest-'));
|
|
1911
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1912
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1913
|
+
assert.equal(gitEntries.length, 0, 'no nonce, no manifest');
|
|
1914
|
+
assert.deepEqual(Object.keys(receipts[0]), Object.keys(RECEIPT_FIXTURE), 'the receipt line field set is unchanged');
|
|
1915
|
+
});
|
|
1916
|
+
|
|
1917
|
+
it('DIFFERENT bytes at the derived name refuse loudly AND EXCLUDE the receipt append (ordering, behaviorally)', () => {
|
|
1918
|
+
const sb = makeSandbox();
|
|
1919
|
+
writeFileSync(manifestPath(sb.repo, 'r1-d2'), '{"schema":1,"backend":"agy","nonce":"r1-d2","fingerprint":null,"findings":"other bytes"}\n');
|
|
1920
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT, AW_REVIEW_NONCE: 'r1-d2' } });
|
|
1921
|
+
const receipts = readReceipts(sb.repo);
|
|
1922
|
+
const manifest = readFileSync(manifestPath(sb.repo, 'r1-d2'), 'utf8');
|
|
1923
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1924
|
+
assert.equal(r.status, 0, 'the review itself still succeeds (the artifact lane failed loudly)');
|
|
1925
|
+
assert.match(r.stderr, /DIFFERENT bytes or is not a regular file — no-clobber refuses loudly/);
|
|
1926
|
+
assert.match(r.stderr, /receipt append is EXCLUDED/);
|
|
1927
|
+
assert.equal(receipts.length, 0, 'a nonce-supplied dispatch never lands a receipt without its manifest');
|
|
1928
|
+
assert.match(manifest, /other bytes/, 'the pre-existing manifest is never clobbered');
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1931
|
+
it('a SYMLINK at the derived manifest path refuses and EXCLUDES the receipt — even when its target is byte-identical', () => {
|
|
1932
|
+
const sb = makeSandbox();
|
|
1933
|
+
assert.equal(run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT, AW_REVIEW_NONCE: 'sym2' } }).status, 0);
|
|
1934
|
+
const mPath = manifestPath(sb.repo, 'sym2');
|
|
1935
|
+
const target = join(sb.repo, '.git', 'manifest-target-copy.json');
|
|
1936
|
+
writeFileSync(target, readFileSync(mPath));
|
|
1937
|
+
rmSync(mPath);
|
|
1938
|
+
symlinkSync(target, mPath);
|
|
1939
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT, AW_REVIEW_NONCE: 'sym2' } });
|
|
1940
|
+
const receipts = readReceipts(sb.repo);
|
|
1941
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1942
|
+
assert.equal(r.status, 0, 'the review itself still succeeds (the artifact lane failed loudly)');
|
|
1943
|
+
assert.match(r.stderr, /receipt append is EXCLUDED/);
|
|
1944
|
+
assert.equal(receipts.length, 1, 'the second receipt is EXCLUDED — a symlinked manifest is never read through as the idempotent no-op');
|
|
1945
|
+
});
|
|
1946
|
+
|
|
1947
|
+
it('a FIFO at the derived manifest path refuses fast and EXCLUDES the receipt (O_NONBLOCK — no hang; fix characterization)', () => {
|
|
1948
|
+
const sb = makeSandbox();
|
|
1949
|
+
const mPath = manifestPath(sb.repo, 'fifo2');
|
|
1950
|
+
assert.equal(spawnSync('mkfifo', [mPath], { encoding: 'utf8' }).status, 0, 'mkfifo fixture');
|
|
1951
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT, AW_REVIEW_NONCE: 'fifo2' } });
|
|
1952
|
+
const receipts = readReceipts(sb.repo);
|
|
1953
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1954
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1955
|
+
assert.match(r.stderr, /receipt append is EXCLUDED/);
|
|
1956
|
+
assert.equal(receipts.length, 0, 'a FIFO manifest is never read (fstat-first) and the receipt is excluded');
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1959
|
+
it('a BOM-prefixed captured output round-trips VERBATIM into the manifest (U+FEFF preserved)', () => {
|
|
1960
|
+
const sb = makeSandbox();
|
|
1961
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: `\uFEFFpreamble\n${VERDICT_OUTPUT}`, AW_REVIEW_NONCE: 'b2' } });
|
|
1962
|
+
const manifest = JSON.parse(readFileSync(manifestPath(sb.repo, 'b2'), 'utf8'));
|
|
1963
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1964
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1965
|
+
assert.equal(manifest.findings, `\uFEFFpreamble\n${VERDICT_OUTPUT}\n`, 'the captured output is VERBATIM — a stripped BOM would move the findingDigest');
|
|
1966
|
+
});
|
|
1967
|
+
|
|
1968
|
+
it('an unsafe nonce refuses PRE-SPEND (exit 2, agy never runs) — a non-ASCII letter refuses under a UTF-8 locale too', () => {
|
|
1969
|
+
const sb = makeSandbox();
|
|
1970
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AW_REVIEW_NONCE: 'a/b' } });
|
|
1971
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1972
|
+
assert.equal(r.status, 2);
|
|
1973
|
+
assert.match(r.stderr, /safe nonce grammar/);
|
|
1974
|
+
assert.equal(r.invoked, false, 'the containment refusal fires before any CLI spend');
|
|
1975
|
+
// The grammar ENUMERATES the ASCII set (no ranges): a locale-collated [A-Za-z] could admit a
|
|
1976
|
+
// non-ASCII letter the kit's JS reader then refuses, breaking correlation after a paid run.
|
|
1977
|
+
const utf8 = makeSandbox();
|
|
1978
|
+
const r2 = run(utf8, { args: ['code', '--facts', 'a tiny fact'], env: { AW_REVIEW_NONCE: 'ré1', LC_ALL: 'en_US.UTF-8', LANG: 'en_US.UTF-8' } });
|
|
1979
|
+
rmSync(utf8.home, { recursive: true, force: true });
|
|
1980
|
+
assert.equal(r2.status, 2, 'a non-ASCII nonce letter refuses whatever the locale collation says');
|
|
1981
|
+
assert.match(r2.stderr, /safe nonce grammar/);
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
it('a nonce-supplied CONTINUATION mints its manifest with fingerprint null (the receipt identity is null too)', () => {
|
|
1985
|
+
const sb = makeSandbox();
|
|
1986
|
+
const r = run(sb, { args: ['--continue'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT, AW_REVIEW_NONCE: 'r2-c1' } });
|
|
1987
|
+
const receipts = readReceipts(sb.repo);
|
|
1988
|
+
const manifest = JSON.parse(readFileSync(manifestPath(sb.repo, 'r2-c1'), 'utf8'));
|
|
1989
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1990
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1991
|
+
assert.equal(receipts.length, 1);
|
|
1992
|
+
assert.equal(manifest.fingerprint, null, 'a continuation carries no tree identity — the manifest says so honestly');
|
|
1993
|
+
assert.equal(manifest.findings, `${VERDICT_OUTPUT}\n`);
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
// The --nonce flag (FLOW-NONCE-DISPATCH-LANE): the plain-argument lane onto the SAME seam —
|
|
1997
|
+
// for hosts whose dispatch policy has no env-prefix form.
|
|
1998
|
+
it('--nonce rides the AW_REVIEW_NONCE seam: manifest minted, receipt nonce-stamped (flag form ≡ env form)', () => {
|
|
1999
|
+
const sb = makeSandbox();
|
|
2000
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact', '--nonce', 'f2-d2'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
|
|
2001
|
+
const receipts = readReceipts(sb.repo);
|
|
2002
|
+
const manifest = JSON.parse(readFileSync(manifestPath(sb.repo, 'f2-d2'), 'utf8'));
|
|
2003
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
2004
|
+
assert.equal(r.status, 0, r.stderr);
|
|
2005
|
+
assert.equal(manifest.nonce, 'f2-d2');
|
|
2006
|
+
assert.equal(receipts[0].nonce, 'f2-d2', 'the flag stamps the receipt exactly like the env form');
|
|
2007
|
+
});
|
|
2008
|
+
|
|
2009
|
+
it('an unsafe --nonce value refuses PRE-SPEND (exit 2, agy never runs) — same grammar as the env screen', () => {
|
|
2010
|
+
const sb = makeSandbox();
|
|
2011
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact', '--nonce', 'a/b'] });
|
|
2012
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
2013
|
+
assert.equal(r.status, 2);
|
|
2014
|
+
assert.match(r.stderr, /safe nonce grammar/);
|
|
2015
|
+
assert.equal(r.invoked, false, 'the containment refusal fires before any CLI spend');
|
|
2016
|
+
});
|
|
2017
|
+
|
|
2018
|
+
it('a missing value, a duplicate flag, and a disagreeing env+flag pair each refuse (exit 2); an agreeing pair proceeds', () => {
|
|
2019
|
+
const sb = makeSandbox();
|
|
2020
|
+
const missing = run(sb, { args: ['code', '--facts', 'a tiny fact', '--nonce'] });
|
|
2021
|
+
assert.equal(missing.status, 2);
|
|
2022
|
+
assert.match(missing.stderr, /--nonce needs a value/);
|
|
2023
|
+
const dup = run(sb, { args: ['code', '--facts', 'a tiny fact', '--nonce', 'n1', '--nonce', 'n2'] });
|
|
2024
|
+
assert.equal(dup.status, 2);
|
|
2025
|
+
assert.match(dup.stderr, /duplicate --nonce/);
|
|
2026
|
+
const clash = run(sb, { args: ['code', '--facts', 'a tiny fact', '--nonce', 'n1'], env: { AW_REVIEW_NONCE: 'n2' } });
|
|
2027
|
+
assert.equal(clash.status, 2);
|
|
2028
|
+
assert.match(clash.stderr, /disagrees with the AW_REVIEW_NONCE environment value/);
|
|
2029
|
+
const agree = run(sb, { args: ['code', '--facts', 'a tiny fact', '--nonce', 'n3'], env: { AW_REVIEW_NONCE: 'n3', AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
|
|
2030
|
+
const exists = existsSync(manifestPath(sb.repo, 'n3'));
|
|
2031
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
2032
|
+
assert.equal(agree.status, 0, agree.stderr);
|
|
2033
|
+
assert.equal(exists, true, 'an agreeing pair is ONE seam value — the dispatch proceeds');
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
it('--nonce is valid on a CONTINUATION too — the flag lane covers every seam-honoring form', () => {
|
|
2037
|
+
const sb = makeSandbox();
|
|
2038
|
+
const r = run(sb, { args: ['--continue', '--nonce', 'r2-c2'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
|
|
2039
|
+
const manifest = JSON.parse(readFileSync(manifestPath(sb.repo, 'r2-c2'), 'utf8'));
|
|
2040
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
2041
|
+
assert.equal(r.status, 0, r.stderr);
|
|
2042
|
+
assert.equal(manifest.fingerprint, null, 'a continuation manifest carries no tree identity');
|
|
2043
|
+
});
|
|
2044
|
+
|
|
2045
|
+
it('a grammar-valid leading-dash --nonce value is ACCEPTED — the flag lane spans the whole declared grammar', () => {
|
|
2046
|
+
const sb = makeSandbox();
|
|
2047
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact', '--nonce', '--n1'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
|
|
2048
|
+
const receipts = readReceipts(sb.repo);
|
|
2049
|
+
const exists = existsSync(manifestPath(sb.repo, '--n1'));
|
|
2050
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
2051
|
+
assert.equal(r.status, 0, r.stderr);
|
|
2052
|
+
assert.equal(exists, true, 'the {backend, nonce}-named manifest lands under the leading-dash nonce');
|
|
2053
|
+
assert.equal(receipts[0].nonce, '--n1', 'the receipt carries the exact grammar-valid value — flag lane ≡ env lane');
|
|
2054
|
+
});
|
|
2055
|
+
|
|
2056
|
+
it('an EMPTY --nonce value refuses pre-spend under the grammar screen (presence is tracked separately from the value)', () => {
|
|
2057
|
+
const sb = makeSandbox();
|
|
2058
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact', '--nonce', ''] });
|
|
2059
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
2060
|
+
assert.equal(r.status, 2);
|
|
2061
|
+
assert.match(r.stderr, /safe nonce grammar/);
|
|
2062
|
+
assert.equal(r.invoked, false, 'the refusal fires before any CLI spend');
|
|
2063
|
+
});
|
|
2064
|
+
|
|
2065
|
+
it('a duplicate --nonce after an EMPTY first value still refuses as a duplicate — an empty value never erases presence', () => {
|
|
2066
|
+
const sb = makeSandbox();
|
|
2067
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact', '--nonce', '', '--nonce', 'n2'] });
|
|
2068
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
2069
|
+
assert.equal(r.status, 2);
|
|
2070
|
+
assert.match(r.stderr, /duplicate --nonce/);
|
|
2071
|
+
assert.equal(r.invoked, false, 'the refusal fires before any CLI spend');
|
|
2072
|
+
});
|
|
2073
|
+
});
|
|
2074
|
+
|
|
1883
2075
|
it('a continuation receipt is fresh:false with null identity fields, and the wrapper prints the fresh-run notice', () => {
|
|
1884
2076
|
const sb = makeSandbox();
|
|
1885
2077
|
const r = run(sb, { args: ['--continue', '--decided', 'already folded'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
|
|
@@ -1966,6 +2158,45 @@ describe('agy-review.sh — review receipts (AD-038)', () => {
|
|
|
1966
2158
|
});
|
|
1967
2159
|
});
|
|
1968
2160
|
|
|
2161
|
+
// MANIFEST-TMP-ORPHAN-ON-FAILURE: on a FAILURE exit of the finding-manifest mint whose temp
|
|
2162
|
+
// unlink ALSO fails, the error names the orphan path — parameterized over BOTH failure codes
|
|
2163
|
+
// (rc 3 no-clobber, rc 1 fs failure); a failure whose temp IS removable stays orphan-silent.
|
|
2164
|
+
describe('agy-review.sh — finding-manifest failure branches name the orphan (MANIFEST-TMP-ORPHAN-ON-FAILURE)', () => {
|
|
2165
|
+
const manifestPath = (repo, nonce) => join(repo, '.git', `agent-workflow-finding-manifest-agy-${nonce}.json`);
|
|
2166
|
+
const UNLINK_FAIL = "const fs = require('node:fs');\nconst real = fs.unlinkSync;\nfs.unlinkSync = (p) => { if (String(p).includes('.tmp')) { const e = new Error('EPERM'); e.code = 'EPERM'; throw e; } return real(p); };\n";
|
|
2167
|
+
const LINK_FAIL = "const fsLink = require('node:fs');\nfsLink.linkSync = () => { const e = new Error('EPERM'); e.code = 'EPERM'; throw e; };\n";
|
|
2168
|
+
for (const failure of [
|
|
2169
|
+
{ rc: 3, name: 'no-clobber (rc 3)', preload: UNLINK_FAIL, plant: true, errRe: /DIFFERENT bytes or is not a regular file/ },
|
|
2170
|
+
{ rc: 1, name: 'fs failure (rc 1)', preload: UNLINK_FAIL + LINK_FAIL, plant: false, errRe: /could not compose or write the finding manifest/ },
|
|
2171
|
+
]) {
|
|
2172
|
+
it(`a FAILURE exit (${failure.name}) whose temp unlink also fails names the ORPHAN PATH in the error`, () => {
|
|
2173
|
+
const sb = makeSandbox();
|
|
2174
|
+
if (failure.plant) writeFileSync(manifestPath(sb.repo, 'orphf1'), 'planted different bytes\n');
|
|
2175
|
+
const preload = join(sb.home, 'orphan-fail-preload.cjs');
|
|
2176
|
+
writeFileSync(preload, failure.preload);
|
|
2177
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT, AW_REVIEW_NONCE: 'orphf1', NODE_OPTIONS: `--require ${preload}` } });
|
|
2178
|
+
const receipts = readReceipts(sb.repo);
|
|
2179
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
2180
|
+
assert.equal(r.status, 0, 'the review itself still succeeds (the artifact lane failed loudly)');
|
|
2181
|
+
assert.match(r.stderr, failure.errRe);
|
|
2182
|
+
assert.match(r.stderr, /orphan left at: \S*\.tmp/, 'the failure branch names the exact orphan path — never a silent leftover');
|
|
2183
|
+
assert.equal(receipts.length, 0, 'a failed mint still EXCLUDES the receipt');
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
it('a FAILURE exit whose temp IS removable stays orphan-silent (no leftover, no orphan line)', () => {
|
|
2188
|
+
const sb = makeSandbox();
|
|
2189
|
+
writeFileSync(manifestPath(sb.repo, 'orphf2'), 'planted different bytes\n');
|
|
2190
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT, AW_REVIEW_NONCE: 'orphf2' } });
|
|
2191
|
+
const leftovers = readdirSync(join(sb.repo, '.git')).filter((n) => n.endsWith('.tmp'));
|
|
2192
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
2193
|
+
assert.equal(r.status, 0, 'the review itself still succeeds (the artifact lane failed loudly)');
|
|
2194
|
+
assert.match(r.stderr, /DIFFERENT bytes or is not a regular file/);
|
|
2195
|
+
assert.doesNotMatch(r.stderr, /orphan left at:/, 'a removed temp is not an orphan — the line would train readers to ignore it');
|
|
2196
|
+
assert.deepEqual(leftovers, [], 'the temp really was removed');
|
|
2197
|
+
});
|
|
2198
|
+
});
|
|
2199
|
+
|
|
1969
2200
|
// ── bridge settings file (bridges 2.3.0) ─────────────────────────────────────────
|
|
1970
2201
|
// ${XDG_CONFIG_HOME:-$HOME/.config}/agent-workflow/bridge-settings.conf holds KEY=VALUE
|
|
1971
2202
|
// lines, PARSED (never sourced). Precedence: explicit env (even empty: KEY= disables the
|
|
@@ -2311,18 +2542,43 @@ describe('agy-review.sh — dispatch-posture labeling (D5)', () => {
|
|
|
2311
2542
|
assert.match(r.stderr, /control/i);
|
|
2312
2543
|
});
|
|
2313
2544
|
|
|
2314
|
-
|
|
2545
|
+
// The two-stage cap guarantee: the parent preflight fails closed, and the seam
|
|
2546
|
+
// (AGY_REQUIRE_TIMEOUT_BIN=1) makes the CHILD's missing-binary lane refuse too — but only a
|
|
2547
|
+
// child that HONORS the seam. A stale installed agy-run that never reads it could run uncapped
|
|
2548
|
+
// past the parent preflight, so the parent verifies the resolved child carries the seam token
|
|
2549
|
+
// and refuses loudly naming the refresh recovery otherwise.
|
|
2550
|
+
it('a STALE agy-run child that does not honor the timeout seam refuses fail-closed (never a silently uncapped dispatch)', () => {
|
|
2551
|
+
const sb = makeSandbox();
|
|
2552
|
+
const staleDir = join(sb.home, 'stale-bin');
|
|
2553
|
+
mkdirSync(staleDir, { recursive: true });
|
|
2554
|
+
writeFileSync(join(staleDir, 'agy-run'), '#!/usr/bin/env bash\nprintf "FAKE_AGY_REVIEW_OUTPUT\\n### Verdict\\nSHIP\\n"\n', { mode: 0o755 });
|
|
2555
|
+
const r = run(sb, {
|
|
2556
|
+
args: ['code', '--facts', 'a tiny fact'],
|
|
2557
|
+
env: { PATH: `${staleDir}:${sb.bin}:${farmFor(['agy-run'])}` },
|
|
2558
|
+
});
|
|
2559
|
+
const receipts = readReceipts(sb.repo);
|
|
2560
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
2561
|
+
assert.equal(r.status, 127, 'a seam-blind child is a stale bridge install — refuse, never dispatch uncapped');
|
|
2562
|
+
assert.match(r.stderr, /does not honor the AGY_REQUIRE_TIMEOUT_BIN seam/);
|
|
2563
|
+
assert.equal(receipts.length, 0, 'no dispatch, no receipt');
|
|
2564
|
+
});
|
|
2565
|
+
|
|
2566
|
+
// Flow-orchestration Phase 4.2 (#26): the uncapped lane is CLOSED — without a capping binary the
|
|
2567
|
+
// preflight refuses by name BEFORE any CLI run (the pre-fix wrapper printed timeout=uncapped and
|
|
2568
|
+
// ran anyway). The shadow-proof resolver discipline now surfaces as the REFUSAL, not a banner.
|
|
2569
|
+
it('fails CLOSED when no timeout/gtimeout is on PATH — refuses by name, agy never runs', () => {
|
|
2315
2570
|
const sb = makeSandbox();
|
|
2316
2571
|
const r = run(sb, {
|
|
2317
2572
|
args: ['code', '--facts', 'a tiny fact'],
|
|
2318
2573
|
env: { PATH: `${sb.bin}:${farmFor(['agy', 'agy-run', 'timeout', 'gtimeout'])}` },
|
|
2319
2574
|
});
|
|
2320
2575
|
rmSync(sb.home, { recursive: true, force: true });
|
|
2321
|
-
assert.equal(r.status,
|
|
2322
|
-
assert.match(r.stderr,
|
|
2576
|
+
assert.equal(r.status, 127, 'the hard-timeout preflight is a refusal, never a warned uncapped run');
|
|
2577
|
+
assert.match(r.stderr, /hard-timeout preflight fails CLOSED/);
|
|
2578
|
+
assert.equal(r.invoked, false, 'agy must NOT be invoked when the preflight refuses');
|
|
2323
2579
|
});
|
|
2324
2580
|
|
|
2325
|
-
it('an EXPORTED shell function shadowing timeout never fools the
|
|
2581
|
+
it('an EXPORTED shell function shadowing timeout never fools the preflight (type -P discipline)', () => {
|
|
2326
2582
|
const sb = makeSandbox();
|
|
2327
2583
|
const r = run(sb, {
|
|
2328
2584
|
args: ['code', '--facts', 'a tiny fact'],
|
|
@@ -2332,8 +2588,8 @@ describe('agy-review.sh — dispatch-posture labeling (D5)', () => {
|
|
|
2332
2588
|
},
|
|
2333
2589
|
});
|
|
2334
2590
|
rmSync(sb.home, { recursive: true, force: true });
|
|
2335
|
-
assert.equal(r.status,
|
|
2336
|
-
assert.match(r.stderr,
|
|
2591
|
+
assert.equal(r.status, 127, 'a shell function is not a capping binary — the preflight still refuses');
|
|
2592
|
+
assert.match(r.stderr, /hard-timeout preflight fails CLOSED/);
|
|
2337
2593
|
});
|
|
2338
2594
|
|
|
2339
2595
|
it('an EXPORTED `type` function faking a path never fools the resolver (builtin type discipline)', () => {
|
|
@@ -2346,8 +2602,8 @@ describe('agy-review.sh — dispatch-posture labeling (D5)', () => {
|
|
|
2346
2602
|
},
|
|
2347
2603
|
});
|
|
2348
2604
|
rmSync(sb.home, { recursive: true, force: true });
|
|
2349
|
-
assert.equal(r.status,
|
|
2350
|
-
assert.match(r.stderr,
|
|
2605
|
+
assert.equal(r.status, 127, 'builtin type bypasses an exported type function — the preflight still refuses');
|
|
2606
|
+
assert.match(r.stderr, /hard-timeout preflight fails CLOSED/);
|
|
2351
2607
|
});
|
|
2352
2608
|
|
|
2353
2609
|
it('a DEL (0x7f) byte in a banner field refuses pre-spend like the C0 range', () => {
|
|
@@ -183,8 +183,9 @@ aw_apply_settings
|
|
|
183
183
|
|
|
184
184
|
# --- Effective-timeout resolver (D5 banner honesty; AD-061) --------------------
|
|
185
185
|
# ONE rule, both bridges: the posture banner prints EXACTLY the duration handed to timeout(1) —
|
|
186
|
-
# an integer-seconds value rendered with the `s` suffix, a duration string verbatim
|
|
187
|
-
#
|
|
186
|
+
# an integer-seconds value rendered with the `s` suffix, a duration string verbatim; without a
|
|
187
|
+
# capping binary the EXEC wrappers print `timeout=uncapped` and run, while the REVIEW wrappers
|
|
188
|
+
# refuse pre-spend (fail-closed preflight) — never a fabricated number.
|
|
188
189
|
# The EFFECTIVE value (env included — closing the aw_settings_valid env bypass) is validated by
|
|
189
190
|
# the same per-key rule as the settings file, plus a 7-digit integer-part bound (overflow); an
|
|
190
191
|
# invalid value warns + falls back to the built-in default — a typo never silently masquerades
|
|
@@ -331,6 +332,15 @@ agy_cmd=(agy "${model_flag[@]}" --print-timeout "$AGY_TIMEOUT" "${passthrough[@]
|
|
|
331
332
|
timeout_bin="$(aw_resolve_timeout_bin)"
|
|
332
333
|
|
|
333
334
|
if [[ -z "$timeout_bin" ]]; then
|
|
335
|
+
# The review child seam (flow-orchestration Phase 4): agy-review fails CLOSED at its own
|
|
336
|
+
# preflight and exports this seam so a delete-between race can never void the cap through the
|
|
337
|
+
# child's re-resolution; the legacy warn+uncapped lane stays for direct agy-run use.
|
|
338
|
+
if [[ "${AGY_REQUIRE_TIMEOUT_BIN:-}" == "1" ]]; then
|
|
339
|
+
echo "error: no 'timeout'/'gtimeout' binary on PATH — the hard-timeout preflight fails CLOSED:" >&2
|
|
340
|
+
echo " the invoking review wrapper requires a capped run (AGY_REQUIRE_TIMEOUT_BIN=1)." >&2
|
|
341
|
+
echo " Install coreutils (timeout; on macOS: brew install coreutils), then re-run." >&2
|
|
342
|
+
exit 127
|
|
343
|
+
fi
|
|
334
344
|
echo "warning: no 'timeout'/'gtimeout' on PATH — running agy WITHOUT a hard wall-clock cap" >&2
|
|
335
345
|
echo " (install coreutils to enable AGY_HARD_TIMEOUT=$AGY_HARD_TIMEOUT)." >&2
|
|
336
346
|
exec "${agy_cmd[@]}"
|
|
@@ -70,6 +70,24 @@ describe('agy.sh — hard wall-clock cap (timeout(1))', { concurrency: true }, (
|
|
|
70
70
|
assert.equal(r.status, 3, 'a genuine agy failure code must pass through');
|
|
71
71
|
assert.doesNotMatch(r.stderr, /exceeded the hard cap/, 'must not mislabel a non-timeout failure');
|
|
72
72
|
});
|
|
73
|
+
|
|
74
|
+
// The review child seam (flow-orchestration Phase 4): agy-review fails CLOSED at ITS preflight,
|
|
75
|
+
// but this child re-resolves the binary — the seam closes the delete-between window; the legacy
|
|
76
|
+
// warn+uncapped lane stays for direct agy-run use.
|
|
77
|
+
it('AGY_REQUIRE_TIMEOUT_BIN=1 fails CLOSED without a capping binary; the legacy lane stays without the seam', async () => {
|
|
78
|
+
const home = makeSandbox('#!/usr/bin/env bash\necho "OK reply"\nexit 0\n');
|
|
79
|
+
const farm = makePathWithout(FARM_ROOT, ['agy', 'timeout', 'gtimeout']);
|
|
80
|
+
const path = `${join(home, '.local', 'bin')}:${farm}`;
|
|
81
|
+
const sealed = await runWrapperAsync(home, { PATH: path, AGY_MODEL: '', AGY_REQUIRE_TIMEOUT_BIN: '1' });
|
|
82
|
+
assert.equal(sealed.status, 127, 'the seam turns the uncapped lane into a refusal');
|
|
83
|
+
assert.match(sealed.stderr, /fails CLOSED/);
|
|
84
|
+
assert.doesNotMatch(sealed.stdout, /OK reply/, 'agy never runs under the sealed lane');
|
|
85
|
+
const legacy = await runWrapperAsync(home, { PATH: path, AGY_MODEL: '' });
|
|
86
|
+
rmSync(home, { recursive: true, force: true });
|
|
87
|
+
assert.equal(legacy.status, 0, 'without the seam the legacy warn+uncapped lane is unchanged');
|
|
88
|
+
assert.match(legacy.stderr, /WITHOUT a hard wall-clock cap/);
|
|
89
|
+
assert.match(legacy.stdout, /OK reply/);
|
|
90
|
+
});
|
|
73
91
|
});
|
|
74
92
|
|
|
75
93
|
// A stub that records (via a SENTINEL file) whether agy was actually invoked, so a
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"schema": 1,
|
|
4
4
|
"name": "antigravity-cli-bridge",
|
|
5
5
|
"kind": "execution-backend",
|
|
6
|
-
"version": "5.
|
|
6
|
+
"version": "5.1.0",
|
|
7
7
|
"provides": ["review", "probe"],
|
|
8
8
|
"posture": { "model": "Gemini 3.1 Pro (High)" },
|
|
9
9
|
"roles": {
|
|
@@ -15,25 +15,26 @@
|
|
|
15
15
|
"output": "advisory",
|
|
16
16
|
"contract": {
|
|
17
17
|
"invocations": [
|
|
18
|
-
"agy-review code [--facts @f] [--ungrounded] [--decided @f] [--focus \"…\"] [extra focus…]",
|
|
19
|
-
"agy-review plan <plan-file> [--facts @f] [--decided @f] [--focus \"…\"]",
|
|
20
|
-
"agy-review diff <diff-file> [--facts @f] [--decided @f] [--focus \"…\"]"
|
|
18
|
+
"agy-review code [--facts @f] [--ungrounded] [--decided @f] [--focus \"…\"] [--nonce <n>] [extra focus…]",
|
|
19
|
+
"agy-review plan <plan-file> [--facts @f] [--decided @f] [--focus \"…\"] [--nonce <n>]",
|
|
20
|
+
"agy-review diff <diff-file> [--facts @f] [--decided @f] [--focus \"…\"] [--nonce <n>]"
|
|
21
21
|
],
|
|
22
22
|
"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",
|
|
23
23
|
"flags": [
|
|
24
24
|
"--facts @f — verified facts the review runs AGAINST (code mode REQUIRES a non-empty payload; plan/diff warn loudly when omitted)",
|
|
25
25
|
"--ungrounded — deliberately ungrounded CODE review, a throwaway opinion (code mode only, contradicts --facts; the receipt records grounded:false and never attests)",
|
|
26
26
|
"--decided @f — already-decided / already-addressed list; do NOT re-raise (anti-circling; the round-2 payload)",
|
|
27
|
-
"--focus \"…\" — extra focus (repeatable; code mode also takes trailing focus words)"
|
|
27
|
+
"--focus \"…\" — extra focus (repeatable; code mode also takes trailing focus words)",
|
|
28
|
+
"--nonce <n> — the flow dispatch nonce, the plain-argument lane onto the AW_REVIEW_NONCE seam (one seam: flag and a non-empty env must agree; a disagreeing pair refuses pre-spend)"
|
|
28
29
|
],
|
|
29
30
|
"continue": [
|
|
30
|
-
"agy-review --continue [--decided @f] [--focus \"…\"]",
|
|
31
|
-
"agy-review --conversation <id> [--decided @f] [--focus \"…\"]"
|
|
31
|
+
"agy-review --continue [--decided @f] [--focus \"…\"] [--nonce <n>]",
|
|
32
|
+
"agy-review --conversation <id> [--decided @f] [--focus \"…\"] [--nonce <n>]"
|
|
32
33
|
],
|
|
33
|
-
"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); posture = the ACTUAL run posture {model} (agy has no tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, an ATTESTING review with AGY_MODEL explicitly emptied refuses pre-spend, and a model string carrying control bytes refuses pre-spend in every mode; delivery = how the change set REACHED the model, currently emitted as 'inline' (the whole set rode one prompt — proven by construction) or 'fed' (a chunked feed whose per-part echo proof verified); REQUIRED on every agy code receipt and its ABSENCE is what stops a pre-fed-lane receipt attesting, while the gate accepts any well-formed declaration rather than a particular value; absent by construction on plan/diff/continuation receipts, which carry no change set; a run whose output carries NO recognized '### Verdict' section — empty output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); a write failure warns, never fails the review",
|
|
34
|
+
"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); posture = the ACTUAL run posture {model} (agy has no tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, an ATTESTING review with AGY_MODEL explicitly emptied refuses pre-spend, and a model string carrying control bytes refuses pre-spend in every mode; delivery = how the change set REACHED the model, currently emitted as 'inline' (the whole set rode one prompt — proven by construction) or 'fed' (a chunked feed whose per-part echo proof verified); REQUIRED on every agy code receipt and its ABSENCE is what stops a pre-fed-lane receipt attesting, while the gate accepts any well-formed declaration rather than a particular value; absent by construction on plan/diff/continuation receipts, which carry no change set; a run whose output carries NO recognized '### Verdict' section — empty output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); when the dispatch nonce seam is supplied — the AW_REVIEW_NONCE environment value or its plain-argument equivalent --nonce <n> (one seam: the flag assigns the same value; supplying both with different values refuses pre-spend) — under the safe grammar [A-Za-z0-9._-]{1,64} (anything else refuses pre-spend), the wrapper first mints the finding MANIFEST {schema, backend, nonce, fingerprint, findings} beside the receipts file (agent-workflow-finding-manifest-<backend>-<nonce>.json; atomic, no-clobber — a byte-identical rewrite is an idempotent no-op, different bytes refuse loudly) ORDERED before the receipt append — a failed manifest write EXCLUDES the receipt append, so a nonce-supplied dispatch can never land a receipt without its readable manifest; a nonce-less invocation adds NO nonce field and mints NO finding manifest (the existing wrapperVersion field still changes with each bridge release); a write failure warns, never fails the review",
|
|
34
35
|
"notes": [
|
|
35
36
|
"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",
|
|
36
|
-
"the review posture banner appends a banner-only timeout=<duration
|
|
37
|
+
"the review posture banner appends a banner-only timeout=<duration> field — exactly the duration agy-run hands to timeout(1); the hard-timeout preflight fails CLOSED when no timeout/gtimeout binary exists (the wrapper refuses by name before any CLI run, so an uncapped review run can no longer happen), and the field never enters the receipt posture or the D5 banner↔receipt parity",
|
|
37
38
|
"quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts"
|
|
38
39
|
]
|
|
39
40
|
}
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
{ "slot": "[--ungrounded]", "required": false, "description": "deliberately ungrounded code review — a throwaway opinion; the receipt records grounded:false and never attests" },
|
|
59
60
|
{ "slot": "[--decided @f]", "required": false, "description": "decisions already made; the reviewer must not re-raise them (anti-circling)" },
|
|
60
61
|
{ "slot": "[--focus \"…\"]", "required": false, "description": "what this review must look at (repeatable)" },
|
|
62
|
+
{ "slot": "[--nonce <n>]", "required": false, "description": "the flow dispatch nonce — the plain-argument lane onto the AW_REVIEW_NONCE seam (round-open prints the value; flag and a non-empty env must agree)" },
|
|
61
63
|
{ "slot": "[extra focus…]", "required": false, "description": "extra focus words appended to the review directive" }
|
|
62
64
|
],
|
|
63
65
|
"guardrails": [
|
|
@@ -86,7 +88,8 @@
|
|
|
86
88
|
{ "slot": "<plan-file>", "required": true, "description": "the plan file under review" },
|
|
87
89
|
{ "slot": "[--facts @f]", "required": false, "description": "the verified facts to review AGAINST; omitting it warns loudly and records the review ungrounded" },
|
|
88
90
|
{ "slot": "[--decided @f]", "required": false, "description": "decisions already made; the reviewer must not re-raise them (anti-circling)" },
|
|
89
|
-
{ "slot": "[--focus \"…\"]", "required": false, "description": "what this review must look at (repeatable)" }
|
|
91
|
+
{ "slot": "[--focus \"…\"]", "required": false, "description": "what this review must look at (repeatable)" },
|
|
92
|
+
{ "slot": "[--nonce <n>]", "required": false, "description": "the flow dispatch nonce — the plain-argument lane onto the AW_REVIEW_NONCE seam (round-open prints the value; flag and a non-empty env must agree)" }
|
|
90
93
|
],
|
|
91
94
|
"guardrails": [
|
|
92
95
|
{ "value": "read-only posture — the prompt forbids edits, commands and git writes", "enforcement": "advisory", "source": "bin/agy-review.sh" },
|
|
@@ -110,7 +113,8 @@
|
|
|
110
113
|
{ "slot": "<diff-file>", "required": true, "description": "the diff file under review" },
|
|
111
114
|
{ "slot": "[--facts @f]", "required": false, "description": "the verified facts to review AGAINST; omitting it warns loudly and records the review ungrounded" },
|
|
112
115
|
{ "slot": "[--decided @f]", "required": false, "description": "decisions already made; the reviewer must not re-raise them (anti-circling)" },
|
|
113
|
-
{ "slot": "[--focus \"…\"]", "required": false, "description": "what this review must look at (repeatable)" }
|
|
116
|
+
{ "slot": "[--focus \"…\"]", "required": false, "description": "what this review must look at (repeatable)" },
|
|
117
|
+
{ "slot": "[--nonce <n>]", "required": false, "description": "the flow dispatch nonce — the plain-argument lane onto the AW_REVIEW_NONCE seam (round-open prints the value; flag and a non-empty env must agree)" }
|
|
114
118
|
],
|
|
115
119
|
"guardrails": [
|
|
116
120
|
{ "value": "read-only posture — the prompt forbids edits, commands and git writes", "enforcement": "advisory", "source": "bin/agy-review.sh" },
|
|
@@ -131,7 +135,8 @@
|
|
|
131
135
|
"invocationRefs": [{ "contractField": "continue", "index": 0 }],
|
|
132
136
|
"operands": [
|
|
133
137
|
{ "slot": "[--decided @f]", "required": false, "description": "decisions already made; the reviewer must not re-raise them (anti-circling)" },
|
|
134
|
-
{ "slot": "[--focus \"…\"]", "required": false, "description": "what this round must look at (repeatable)" }
|
|
138
|
+
{ "slot": "[--focus \"…\"]", "required": false, "description": "what this round must look at (repeatable)" },
|
|
139
|
+
{ "slot": "[--nonce <n>]", "required": false, "description": "the flow dispatch nonce — the plain-argument lane onto the AW_REVIEW_NONCE seam (a continuation manifest carries fingerprint null)" }
|
|
135
140
|
],
|
|
136
141
|
"guardrails": [
|
|
137
142
|
{ "value": "--facts is rejected on a continuation — the facts are already in the conversation", "enforcement": "enforced", "source": "bin/agy-review.sh" },
|
|
@@ -149,7 +154,8 @@
|
|
|
149
154
|
"operands": [
|
|
150
155
|
{ "slot": "<id>", "required": true, "description": "the conversation id of the review to resume" },
|
|
151
156
|
{ "slot": "[--decided @f]", "required": false, "description": "decisions already made; the reviewer must not re-raise them (anti-circling)" },
|
|
152
|
-
{ "slot": "[--focus \"…\"]", "required": false, "description": "what this round must look at (repeatable)" }
|
|
157
|
+
{ "slot": "[--focus \"…\"]", "required": false, "description": "what this round must look at (repeatable)" },
|
|
158
|
+
{ "slot": "[--nonce <n>]", "required": false, "description": "the flow dispatch nonce — the plain-argument lane onto the AW_REVIEW_NONCE seam (a continuation manifest carries fingerprint null)" }
|
|
153
159
|
],
|
|
154
160
|
"guardrails": [
|
|
155
161
|
{ "value": "a continuation receipt is fresh:false — informational only, never a gate pass", "enforcement": "enforced", "source": "capability.json roles.review.contract.receipt" }
|
|
@@ -100,8 +100,9 @@ What it does for you, and what YOU must supply:
|
|
|
100
100
|
- **Posture banner — quote it verbatim.** Every review states its ACTUAL posture on ONE stderr line
|
|
101
101
|
(`review posture: model=… timeout=…`). When you label a dispatch for a user or a record, **quote
|
|
102
102
|
the posture banner verbatim** — the banner is the machine-stated posture; a prose re-type drifts.
|
|
103
|
-
The `timeout=` field is **banner-only** (exactly the duration `agy-run` hands to `timeout(1)
|
|
104
|
-
|
|
103
|
+
The `timeout=` field is **banner-only** (exactly the duration `agy-run` hands to `timeout(1)`;
|
|
104
|
+
without a capping binary `agy-review` fails CLOSED pre-spend, so an uncapped review banner no
|
|
105
|
+
longer exists) — informational, never part of the receipt posture.
|
|
105
106
|
|
|
106
107
|
## Escalation policy (edits, network, git)
|
|
107
108
|
|
|
@@ -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: '3.
|
|
5
|
+
version: '3.3.0'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# codex-cli-bridge
|
|
@@ -104,8 +104,9 @@ records the same `posture {model, effort, tier}` (tier `null` on the standard ti
|
|
|
104
104
|
in a posture value refuse pre-spend in every mode. `codex-exec` states its posture the same way —
|
|
105
105
|
ONE `exec posture: model=… effort=… tier=… sandbox=workspace-write session=fresh|resume:<id>
|
|
106
106
|
timeout=…` stderr line before dispatch (the resume id validated pre-spend). The `timeout=` field
|
|
107
|
-
is **banner-only** (exactly the duration handed to `timeout(1)
|
|
108
|
-
|
|
107
|
+
is **banner-only** (exactly the duration handed to `timeout(1)`; on exec `uncapped` without a
|
|
108
|
+
capping binary, while `codex-review` **fails CLOSED pre-spend** there) — informational, never a
|
|
109
|
+
receipt field. **Quote the posture banner verbatim** when labeling a dispatch.
|
|
109
110
|
|
|
110
111
|
`codex exec` is headless: there is **no TTY**, so `approval_policy=never` — anything needing
|
|
111
112
|
escalation is refused and reported, never interactively approved. The wrappers capture only codex's
|
|
@@ -126,10 +127,11 @@ defeat a policy is guarded — see [§ Models](#models-quality-first-pinned).
|
|
|
126
127
|
|---|---|---|
|
|
127
128
|
| `CODEX_MODEL` | `gpt-5.6-sol` (pinned) | model; non-default REFUSED unless `CODEX_PROBE=1` |
|
|
128
129
|
| `CODEX_EFFORT` | `xhigh` (pinned) | reasoning effort; non-default REFUSED unless `CODEX_PROBE=1` |
|
|
129
|
-
| `CODEX_HARD_TIMEOUT` | `3600` (exec) / `1800` (review) | hard wall-clock cap (seconds) via `timeout`/`gtimeout`; exit 124/137 ⇒ "exceeded hard cap". No `timeout` binary ⇒
|
|
130
|
+
| `CODEX_HARD_TIMEOUT` | `3600` (exec) / `1800` (review) | hard wall-clock cap (seconds) via `timeout`/`gtimeout`; exit 124/137 ⇒ "exceeded hard cap". No `timeout` binary ⇒ exec warns loudly + runs uncapped; `codex-review` REFUSES pre-spend (fail-closed preflight). |
|
|
130
131
|
| `CODEX_SERVICE_TIER` | unset (standard tier) | **SPEND knob**: `priority` (catalog name "Fast") = ~1.5× token speed at a **2.5× credit rate** on gpt-5.6-sol — quality-neutral (same model). codex accepts any `-c service_tier` string silently (probe-pinned 2026-07-05), so the wrapper validates: an unsupported value warns and runs standard. Env or settings file. |
|
|
131
132
|
| `CODEX_SESSION_FILE` | `./.codex-last-session` | where `codex-exec` records the session id and where `--resume-last` reads it |
|
|
132
133
|
| `CODEX_REVIEW_MAX_TOTAL_BYTES` | `1500000` | `codex-review code`: above this the assembled diff goes via a git-dir temp file instead of inline — never truncated |
|
|
134
|
+
| `AW_REVIEW_NONCE` | unset | the flow dispatch nonce (safe grammar `[A-Za-z0-9._-]{1,64}` — anything else refuses pre-spend). `codex-review … --nonce <n>` is the plain-argument equivalent (one seam; flag and a non-empty env must agree, a disagreeing pair refuses pre-spend) — the lane for hosts whose dispatch policy has no env-prefix form. When supplied, a successful review first mints the finding MANIFEST `agent-workflow-finding-manifest-codex-<nonce>.json` beside the receipts file (atomic, no-clobber, ORDERED before the receipt append) — a failed mint EXCLUDES the receipt, so a nonce-supplied dispatch never lands a receipt without its readable manifest; nonce-less runs add no nonce field and mint nothing (the `wrapperVersion` field every receipt carries moves with each release) |
|
|
133
135
|
| `CODEX_REVIEW_SCHEMA` | unset | `codex-review`: `=1` returns findings as a validated JSON object (`--output-schema`), with a raw-text fallback. Default off. |
|
|
134
136
|
| `CODEX_PROBE` | unset | `=1` ⇒ throwaway-probe mode: relaxes the model/effort guard AND the tier-2 passthrough guard (echoed loudly). Never for real work. |
|
|
135
137
|
|
|
@@ -223,7 +225,8 @@ The wrappers work in any git repo where `codex` is installed and authenticated.
|
|
|
223
225
|
restates it via `-c`; only a *raw* `codex exec resume` (bypassing the wrapper) loses the posture.
|
|
224
226
|
- **Hard timeout** — a hung run is killed at `CODEX_HARD_TIMEOUT` (exec 3600s / review 1800s) and
|
|
225
227
|
reported (exit 124/137); raise it for a known-healthy slow run. If neither `timeout` nor `gtimeout`
|
|
226
|
-
is on `PATH`
|
|
228
|
+
is on `PATH`, `codex-exec` warns loudly and runs uncapped; `codex-review` refuses pre-spend
|
|
229
|
+
(the fail-closed preflight — an uncapped review run no longer exists).
|
|
227
230
|
- **Native `codex review` is out of scope** — it rejects `--ignore-user-config` (would load a personal
|
|
228
231
|
`config.toml` and break the subscription/config-isolation invariant) and can't be cleanly captured;
|
|
229
232
|
`codex-review` runs `codex exec` over a precomputed diff instead.
|
|
@@ -210,8 +210,9 @@ aw_apply_settings
|
|
|
210
210
|
|
|
211
211
|
# --- Effective-timeout resolver (D5 banner honesty; AD-061) --------------------
|
|
212
212
|
# ONE rule, both bridges: the posture banner prints EXACTLY the duration handed to timeout(1) —
|
|
213
|
-
# an integer-seconds value rendered with the `s` suffix, a duration string verbatim
|
|
214
|
-
#
|
|
213
|
+
# an integer-seconds value rendered with the `s` suffix, a duration string verbatim; without a
|
|
214
|
+
# capping binary the EXEC wrappers print `timeout=uncapped` and run, while the REVIEW wrappers
|
|
215
|
+
# refuse pre-spend (fail-closed preflight) — never a fabricated number.
|
|
215
216
|
# The EFFECTIVE value (env included — closing the aw_settings_valid env bypass) is validated by
|
|
216
217
|
# the same per-key rule as the settings file, plus a 7-digit integer-part bound (overflow); an
|
|
217
218
|
# invalid value warns + falls back to the built-in default — a typo never silently masquerades
|