@sabaiway/agent-workflow-kit 3.0.0 → 3.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 +82 -1
- package/README.md +1 -0
- package/SKILL.md +7 -3
- package/bin/install.mjs +10 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +6 -4
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +20 -7
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +84 -7
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +189 -18
- package/bridges/antigravity-cli-bridge/bin/agy.sh +55 -6
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +77 -41
- package/bridges/antigravity-cli-bridge/capability.json +4 -2
- package/bridges/antigravity-cli-bridge/references/driving-agy.md +5 -0
- package/bridges/codex-cli-bridge/SKILL.md +8 -4
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +129 -11
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +278 -23
- package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +20 -7
- package/bridges/codex-cli-bridge/bin/codex-review.sh +76 -13
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +112 -17
- package/bridges/codex-cli-bridge/capability.json +19 -4
- package/bridges/codex-cli-bridge/references/driving-codex.md +11 -0
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/bootstrap.md +3 -2
- package/references/modes/recommendations.md +1 -0
- package/references/modes/upgrade.md +9 -4
- package/references/modes/velocity.md +2 -0
- package/references/modes/worktrees.md +120 -0
- package/references/scripts/archive-decisions.mjs +2 -2
- package/references/scripts/archive-decisions.test.mjs +5 -5
- package/references/scripts/check-docs-size-cli.test.mjs +41 -0
- package/references/scripts/check-docs-size.mjs +46 -29
- package/references/shared/command-shapes.md +22 -0
- package/references/shared/composition-handoff.md +7 -2
- package/references/templates/agent_rules.md +10 -1
- package/tools/autonomy-doctor.mjs +1 -1
- package/tools/bridge-settings-read.mjs +25 -5
- package/tools/changed-surface.mjs +8 -8
- package/tools/commands.mjs +9 -0
- package/tools/delegation.mjs +2 -2
- package/tools/detect-backends.mjs +10 -0
- package/tools/grounding.mjs +13 -13
- package/tools/inject-methodology.mjs +71 -40
- package/tools/lens-region.mjs +113 -36
- package/tools/migrate-adr-store.mjs +3 -3
- package/tools/procedures.mjs +1 -1
- package/tools/recipes.mjs +2 -2
- package/tools/recommendations.mjs +34 -7
- package/tools/release-scan.mjs +12 -2
- package/tools/review-state.mjs +3 -3
- package/tools/sandbox-masks.mjs +13 -13
- package/tools/set-recipe.mjs +1 -1
- package/tools/velocity-profile.mjs +7 -7
- package/tools/worktrees.mjs +2292 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
1
|
+
import { describe, it, after } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import {
|
|
4
4
|
mkdtempSync, mkdirSync, writeFileSync, chmodSync, rmSync, readFileSync,
|
|
5
|
-
existsSync, readdirSync, symlinkSync,
|
|
5
|
+
existsSync, readdirSync, symlinkSync, cpSync,
|
|
6
6
|
} from 'node:fs';
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
8
|
import { join, dirname, resolve } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { spawnSync, execFile } from 'node:child_process';
|
|
11
11
|
|
|
12
12
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const WRAPPER = join(HERE, 'agy-review.sh');
|
|
@@ -65,15 +65,23 @@ const makePathWithout = (root, exclude = []) => {
|
|
|
65
65
|
return dir;
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
// The PATH farms and the sandbox base are READ-ONLY per invocation, so both are built ONCE and
|
|
69
|
+
// shared: a per-run farm rebuild (thousands of symlinks) plus a per-test `git init`+commit were
|
|
70
|
+
// the suite's dominant wall cost, not the wrapper under test.
|
|
71
|
+
const SHARED_ROOT = mkdtempSync(join(tmpdir(), 'agy-review-shared-'));
|
|
72
|
+
after(() => rmSync(SHARED_ROOT, { recursive: true, force: true }));
|
|
73
|
+
const farms = new Map();
|
|
74
|
+
const farmFor = (exclude) => {
|
|
75
|
+
const key = exclude.join('|');
|
|
76
|
+
if (!farms.has(key)) farms.set(key, makePathWithout(SHARED_ROOT, exclude));
|
|
77
|
+
return farms.get(key);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const TEMPLATE_HOME = (() => {
|
|
81
|
+
const home = join(SHARED_ROOT, 'template-home');
|
|
72
82
|
const bin = join(home, '.local', 'bin');
|
|
73
83
|
mkdirSync(bin, { recursive: true });
|
|
74
|
-
|
|
75
|
-
writeFileSync(stub, FAKE_AGY, { mode: 0o755 });
|
|
76
|
-
chmodSync(stub, 0o755);
|
|
84
|
+
writeFileSync(join(bin, 'agy'), FAKE_AGY, { mode: 0o755 });
|
|
77
85
|
const repo = join(home, 'repo');
|
|
78
86
|
mkdirSync(repo);
|
|
79
87
|
const g = (...args) => spawnSync('git', args, { cwd: repo, encoding: 'utf8' });
|
|
@@ -83,13 +91,25 @@ const makeSandbox = ({ clean = false } = {}) => {
|
|
|
83
91
|
writeFileSync(join(repo, 'base.txt'), 'committed base\n');
|
|
84
92
|
g('add', '-A');
|
|
85
93
|
g('commit', '-qm', 'base');
|
|
94
|
+
return home;
|
|
95
|
+
})();
|
|
96
|
+
|
|
97
|
+
// `clean: true` leaves a pristine committed tree (for the no-diff preflight); the default leaves one
|
|
98
|
+
// untracked file so `code` mode has a diff to review.
|
|
99
|
+
const makeSandbox = ({ clean = false } = {}) => {
|
|
100
|
+
const home = mkdtempSync(join(tmpdir(), 'agy-review-test-'));
|
|
101
|
+
cpSync(TEMPLATE_HOME, home, { recursive: true });
|
|
102
|
+
const bin = join(home, '.local', 'bin');
|
|
103
|
+
chmodSync(join(bin, 'agy'), 0o755);
|
|
104
|
+
const repo = join(home, 'repo');
|
|
105
|
+
const g = (...args) => spawnSync('git', args, { cwd: repo, encoding: 'utf8' });
|
|
86
106
|
if (!clean) writeFileSync(join(repo, 'pending.txt'), 'PENDING_UNTRACKED_BODY\n');
|
|
87
107
|
return { home, bin, repo, g };
|
|
88
108
|
};
|
|
89
109
|
|
|
90
110
|
const run = (sb, { args, env = {}, cwd } = {}) => {
|
|
91
111
|
const { home, bin, repo } = sb;
|
|
92
|
-
const farm =
|
|
112
|
+
const farm = farmFor(['agy', 'agy-run']);
|
|
93
113
|
const cap = {
|
|
94
114
|
argv: join(home, 'cap-argv'), env: join(home, 'cap-env'), prompt: join(home, 'cap-prompt'),
|
|
95
115
|
sentinel: join(home, 'cap-sentinel'), adddir: join(home, 'cap-adddir'),
|
|
@@ -122,6 +142,44 @@ const run = (sb, { args, env = {}, cwd } = {}) => {
|
|
|
122
142
|
};
|
|
123
143
|
};
|
|
124
144
|
|
|
145
|
+
// Async twin of run() for the two sleep-bound timeout tests: spawnSync blocks the event loop
|
|
146
|
+
// for the whole deliberate wait, so a concurrent describe could not overlap them. Same spawn
|
|
147
|
+
// contract and captures.
|
|
148
|
+
const runAsync = (sb, { args, env = {}, cwd } = {}) =>
|
|
149
|
+
new Promise((done) => {
|
|
150
|
+
const { home, bin, repo } = sb;
|
|
151
|
+
const cap = {
|
|
152
|
+
argv: join(home, 'cap-argv'), env: join(home, 'cap-env'), prompt: join(home, 'cap-prompt'),
|
|
153
|
+
sentinel: join(home, 'cap-sentinel'), adddir: join(home, 'cap-adddir'),
|
|
154
|
+
adddirMode: join(home, 'cap-adddir-mode'), artifactMode: join(home, 'cap-artifact-mode'),
|
|
155
|
+
artifactCopy: join(home, 'cap-artifact-copy'),
|
|
156
|
+
};
|
|
157
|
+
const child = execFile('bash', [WRAPPER, ...args], {
|
|
158
|
+
cwd: cwd || repo,
|
|
159
|
+
encoding: 'utf8',
|
|
160
|
+
timeout: 30000,
|
|
161
|
+
env: {
|
|
162
|
+
HOME: home,
|
|
163
|
+
PATH: `${bin}:${farmFor(['agy', 'agy-run'])}`,
|
|
164
|
+
TMPDIR: process.env.TMPDIR ?? '/tmp',
|
|
165
|
+
AGY_FAKE_ARGV: cap.argv, AGY_FAKE_ENV: cap.env, AGY_FAKE_PROMPT: cap.prompt,
|
|
166
|
+
AGY_FAKE_SENTINEL: cap.sentinel, AGY_FAKE_ADDDIR: cap.adddir, AGY_FAKE_ADDDIR_MODE: cap.adddirMode,
|
|
167
|
+
AGY_FAKE_ARTIFACT_MODE: cap.artifactMode, AGY_FAKE_ARTIFACT_COPY: cap.artifactCopy,
|
|
168
|
+
...env,
|
|
169
|
+
},
|
|
170
|
+
}, (error, stdout, stderr) => {
|
|
171
|
+
const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '');
|
|
172
|
+
done({
|
|
173
|
+
status: error ? (error.code ?? 1) : 0, stdout, stderr,
|
|
174
|
+
invoked: existsSync(cap.sentinel),
|
|
175
|
+
argv: readIf(cap.argv), capEnv: readIf(cap.env), prompt: readIf(cap.prompt),
|
|
176
|
+
adddir: readIf(cap.adddir).trim(), adddirMode: readIf(cap.adddirMode).trim(),
|
|
177
|
+
artifactMode: readIf(cap.artifactMode).trim(), artifactCopy: readIf(cap.artifactCopy),
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
child.stdin.end();
|
|
181
|
+
});
|
|
182
|
+
|
|
125
183
|
describe('agy-review.sh — model policy advisory (1)', () => {
|
|
126
184
|
it('warns for a non-frontier model but still runs', () => {
|
|
127
185
|
const sb = makeSandbox();
|
|
@@ -424,11 +482,11 @@ describe('agy-review.sh — resume / round-2 delta (7)', () => {
|
|
|
424
482
|
});
|
|
425
483
|
});
|
|
426
484
|
|
|
427
|
-
describe('agy-review.sh — delegated guards inherited via agy-run (9, 10)', () => {
|
|
428
|
-
it('hard timeout: a sleeping stub is killed at AGY_HARD_TIMEOUT', () => {
|
|
485
|
+
describe('agy-review.sh — delegated guards inherited via agy-run (9, 10)', { concurrency: true }, () => {
|
|
486
|
+
it('hard timeout: a sleeping stub is killed at AGY_HARD_TIMEOUT', async () => {
|
|
429
487
|
const sb = makeSandbox();
|
|
430
488
|
const started = Date.now();
|
|
431
|
-
const r =
|
|
489
|
+
const r = await runAsync(sb, { args: ['code', '--facts', 'f'], env: { AGY_FAKE_SLEEP: '30', AGY_HARD_TIMEOUT: '2s', AGY_TIMEOUT: '2s' } });
|
|
432
490
|
const elapsed = Date.now() - started;
|
|
433
491
|
rmSync(sb.home, { recursive: true, force: true });
|
|
434
492
|
assert.ok(elapsed < 20000, `must return well under the kill-after window, took ${elapsed}ms`);
|
|
@@ -552,7 +610,7 @@ const runHelp = (arg) => {
|
|
|
552
610
|
const root = mkdtempSync(join(tmpdir(), 'agy-review-help-'));
|
|
553
611
|
const nongit = join(root, 'nongit');
|
|
554
612
|
mkdirSync(nongit, { recursive: true });
|
|
555
|
-
const path =
|
|
613
|
+
const path = farmFor(['codex', 'agy', 'git']);
|
|
556
614
|
const r = spawnSync('bash', [WRAPPER, arg], {
|
|
557
615
|
cwd: nongit, encoding: 'utf8', timeout: 15000, env: { HOME: root, PATH: path },
|
|
558
616
|
});
|
|
@@ -1078,7 +1136,7 @@ const writeSettings = (sb, text) => {
|
|
|
1078
1136
|
};
|
|
1079
1137
|
const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
|
|
1080
1138
|
|
|
1081
|
-
describe('agy-review.sh — bridge settings file (bridges 2.3.0)', () => {
|
|
1139
|
+
describe('agy-review.sh — bridge settings file (bridges 2.3.0)', { concurrency: true }, () => {
|
|
1082
1140
|
it('a file-set AGY_REVIEW_ALLOW_ADDDIR=1 arms the oversized --add-dir escape', () => {
|
|
1083
1141
|
const sb = makeSandbox();
|
|
1084
1142
|
writeFileSync(join(sb.repo, 'unique.txt'), `OVERSIZE_UNIQUE_MARKER\n${'x'.repeat(8000)}\n`);
|
|
@@ -1123,10 +1181,10 @@ describe('agy-review.sh — bridge settings file (bridges 2.3.0)', () => {
|
|
|
1123
1181
|
assert.equal(r.invoked, false);
|
|
1124
1182
|
});
|
|
1125
1183
|
|
|
1126
|
-
it('a file-set AGY_HARD_TIMEOUT flows through the agy-run delegation (killed at the file cap)', () => {
|
|
1184
|
+
it('a file-set AGY_HARD_TIMEOUT flows through the agy-run delegation (killed at the file cap)', async () => {
|
|
1127
1185
|
const sb = makeSandbox();
|
|
1128
1186
|
writeSettings(sb, 'AGY_HARD_TIMEOUT=2s\n');
|
|
1129
|
-
const r =
|
|
1187
|
+
const r = await runAsync(sb, { args: ['code', '--facts', 'f'], env: { AGY_FAKE_SLEEP: '5' } });
|
|
1130
1188
|
rmSync(sb.home, { recursive: true, force: true });
|
|
1131
1189
|
assert.notEqual(r.status, 0, 'the file cap must apply end-to-end (reader → agy-run → timeout)');
|
|
1132
1190
|
assert.match(r.stderr, /exceeded the hard cap AGY_HARD_TIMEOUT=2s/);
|
|
@@ -1354,4 +1412,117 @@ describe('agy-review.sh — dispatch-posture labeling (D5)', () => {
|
|
|
1354
1412
|
assert.equal(receipts.length, 0);
|
|
1355
1413
|
assert.match(r.stderr, /control/i);
|
|
1356
1414
|
});
|
|
1415
|
+
|
|
1416
|
+
it('the banner appends the RESOLVED hard timeout verbatim — banner-only, never in the receipt (AD-061)', () => {
|
|
1417
|
+
const sb = makeSandbox();
|
|
1418
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'] });
|
|
1419
|
+
const receipts = readReceipts(sb.repo);
|
|
1420
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1421
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1422
|
+
assert.match(r.stderr, /^review posture: model=Gemini 3\.1 Pro \(High\) timeout=30m$/m);
|
|
1423
|
+
assert.deepEqual(Object.keys(receipts[0].posture), ['model'], 'timeout never enters the receipt posture');
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
it('the banner prints the EFFECTIVE hard cap: an env override and a fractional duration ride verbatim', () => {
|
|
1427
|
+
for (const [envValue, want] of [['90s', '90s'], ['1.5m', '1\\.5m']]) {
|
|
1428
|
+
const sb = makeSandbox();
|
|
1429
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_HARD_TIMEOUT: envValue } });
|
|
1430
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1431
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1432
|
+
assert.match(r.stderr, new RegExp(`^review posture: .* timeout=${want}$`, 'm'));
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
it('AGY_TIMEOUT (soft print-timeout) alone never moves the banner — the hard cap governs (precedence pin)', () => {
|
|
1437
|
+
const sb = makeSandbox();
|
|
1438
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_TIMEOUT: '5m' } });
|
|
1439
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1440
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1441
|
+
assert.match(r.stderr, /^review posture: .* timeout=30m$/m, 'the soft print-timeout is not the banner value');
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
it('an INVALID / EMPTY / OVERFLOW effective AGY_HARD_TIMEOUT falls back to the built-in default (loud on invalid)', () => {
|
|
1445
|
+
for (const [bad, wantWarn] of [['10x', true], ['', false], ['99999999m', true]]) {
|
|
1446
|
+
const sb = makeSandbox();
|
|
1447
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_HARD_TIMEOUT: bad } });
|
|
1448
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1449
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1450
|
+
assert.match(r.stderr, /^review posture: .* timeout=30m$/m, `default must stand for ${JSON.stringify(bad)}`);
|
|
1451
|
+
if (wantWarn) assert.match(r.stderr, new RegExp(`invalid value '${bad}' for AGY_HARD_TIMEOUT`));
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
it('a timeout value carrying CONTROL BYTES refuses pre-spend (the banner-field screen)', () => {
|
|
1456
|
+
const sb = makeSandbox();
|
|
1457
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_HARD_TIMEOUT: `30m${String.fromCharCode(1)}` } });
|
|
1458
|
+
const receipts = readReceipts(sb.repo);
|
|
1459
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1460
|
+
assert.notEqual(r.status, 0);
|
|
1461
|
+
assert.equal(r.invoked, false);
|
|
1462
|
+
assert.equal(receipts.length, 0);
|
|
1463
|
+
assert.match(r.stderr, /control/i);
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
it('timeout honesty: no timeout/gtimeout on PATH → timeout=uncapped, never a fabricated number', () => {
|
|
1467
|
+
const sb = makeSandbox();
|
|
1468
|
+
const r = run(sb, {
|
|
1469
|
+
args: ['code', '--facts', 'a tiny fact'],
|
|
1470
|
+
env: { PATH: `${sb.bin}:${farmFor(['agy', 'agy-run', 'timeout', 'gtimeout'])}` },
|
|
1471
|
+
});
|
|
1472
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1473
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1474
|
+
assert.match(r.stderr, /^review posture: .* timeout=uncapped$/m);
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
it('an EXPORTED shell function shadowing timeout never fools the banner (type -P discipline)', () => {
|
|
1478
|
+
const sb = makeSandbox();
|
|
1479
|
+
const r = run(sb, {
|
|
1480
|
+
args: ['code', '--facts', 'a tiny fact'],
|
|
1481
|
+
env: {
|
|
1482
|
+
PATH: `${sb.bin}:${farmFor(['agy', 'agy-run', 'timeout', 'gtimeout'])}`,
|
|
1483
|
+
'BASH_FUNC_timeout%%': '() { return 0; }',
|
|
1484
|
+
},
|
|
1485
|
+
});
|
|
1486
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1487
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1488
|
+
assert.match(r.stderr, /^review posture: .* timeout=uncapped$/m, 'a shell function is not a capping binary');
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
it('an EXPORTED `type` function faking a path never fools the resolver (builtin type discipline)', () => {
|
|
1492
|
+
const sb = makeSandbox();
|
|
1493
|
+
const r = run(sb, {
|
|
1494
|
+
args: ['code', '--facts', 'a tiny fact'],
|
|
1495
|
+
env: {
|
|
1496
|
+
PATH: `${sb.bin}:${farmFor(['agy', 'agy-run', 'timeout', 'gtimeout'])}`,
|
|
1497
|
+
'BASH_FUNC_type%%': '() { echo /fake/timeout; }',
|
|
1498
|
+
},
|
|
1499
|
+
});
|
|
1500
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1501
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1502
|
+
assert.match(r.stderr, /^review posture: .* timeout=uncapped$/m, 'builtin type bypasses an exported type function');
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
it('a DEL (0x7f) byte in a banner field refuses pre-spend like the C0 range', () => {
|
|
1506
|
+
const sb = makeSandbox();
|
|
1507
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_MODEL: `bad${String.fromCharCode(127)}model` } });
|
|
1508
|
+
const receipts = readReceipts(sb.repo);
|
|
1509
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1510
|
+
assert.notEqual(r.status, 0);
|
|
1511
|
+
assert.equal(r.invoked, false);
|
|
1512
|
+
assert.equal(receipts.length, 0);
|
|
1513
|
+
assert.match(r.stderr, /control/i);
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1516
|
+
it('a control byte in AGY_TIMEOUT refuses pre-spawn — agy-review forwards it to the child agy-run', () => {
|
|
1517
|
+
for (const c of [1, 127]) {
|
|
1518
|
+
const sb = makeSandbox();
|
|
1519
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_TIMEOUT: `30m${String.fromCharCode(c)}` } });
|
|
1520
|
+
const receipts = readReceipts(sb.repo);
|
|
1521
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
1522
|
+
assert.notEqual(r.status, 0, `must refuse control byte ${c}`);
|
|
1523
|
+
assert.equal(r.invoked, false, 'refused BEFORE spawning agy-run');
|
|
1524
|
+
assert.equal(receipts.length, 0);
|
|
1525
|
+
assert.match(r.stderr, /AGY_TIMEOUT/, 'names the offending knob');
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1357
1528
|
});
|
|
@@ -180,6 +180,56 @@ aw_apply_settings() {
|
|
|
180
180
|
}
|
|
181
181
|
aw_apply_settings
|
|
182
182
|
|
|
183
|
+
# --- Effective-timeout resolver (D5 banner honesty; AD-061) --------------------
|
|
184
|
+
# ONE rule, both bridges: the posture banner prints EXACTLY the duration handed to timeout(1) —
|
|
185
|
+
# an integer-seconds value rendered with the `s` suffix, a duration string verbatim — and
|
|
186
|
+
# `timeout=uncapped` when no timeout/gtimeout binary can cap the run; never a fabricated number.
|
|
187
|
+
# The EFFECTIVE value (env included — closing the aw_settings_valid env bypass) is validated by
|
|
188
|
+
# the same per-key rule as the settings file, plus a 7-digit integer-part bound (overflow); an
|
|
189
|
+
# invalid value warns + falls back to the built-in default — a typo never silently masquerades
|
|
190
|
+
# as a cap. AGY_TIMEOUT shares AGY_HARD_TIMEOUT's duration rule (it has no settings-file arm).
|
|
191
|
+
aw_effective_timeout() {
|
|
192
|
+
local key="$1" default="$2" value="${!1:-}" rule="$1" intpart
|
|
193
|
+
[[ "$rule" == "AGY_TIMEOUT" ]] && rule="AGY_HARD_TIMEOUT"
|
|
194
|
+
[[ -n "$value" ]] || { printf '%s' "$default"; return 0; }
|
|
195
|
+
intpart="${value%%[!0-9]*}"
|
|
196
|
+
if ! aw_settings_valid "$rule" "$value" || (( ${#intpart} > 7 )); then
|
|
197
|
+
# %q escapes the raw value so a control byte in it can never forge an extra diagnostic line
|
|
198
|
+
# (the direct agy-run lane has no pre-spend screen — the warning itself must be injection-proof).
|
|
199
|
+
printf "warning: invalid value '%q' for %s — using the built-in default %s.\n" "$value" "$key" "$default" >&2
|
|
200
|
+
printf '%s' "$default"
|
|
201
|
+
return 0
|
|
202
|
+
fi
|
|
203
|
+
printf '%s' "$value"
|
|
204
|
+
}
|
|
205
|
+
aw_timeout_label() {
|
|
206
|
+
local bin="$1" value="$2"
|
|
207
|
+
[[ -n "$bin" ]] || { printf 'uncapped'; return 0; }
|
|
208
|
+
case "$value" in
|
|
209
|
+
*[!0-9]*) printf '%s' "$value" ;;
|
|
210
|
+
*) printf '%ss' "$value" ;;
|
|
211
|
+
esac
|
|
212
|
+
}
|
|
213
|
+
aw_resolve_timeout_bin() {
|
|
214
|
+
local bin dir base
|
|
215
|
+
bin="$(builtin type -P timeout 2>/dev/null || true)"
|
|
216
|
+
[[ -n "$bin" ]] || bin="$(builtin type -P gtimeout 2>/dev/null || true)"
|
|
217
|
+
[[ -n "$bin" ]] || { printf ''; return 0; }
|
|
218
|
+
case "$bin" in
|
|
219
|
+
/*) ;;
|
|
220
|
+
*)
|
|
221
|
+
case "$bin" in
|
|
222
|
+
*/*) dir="${bin%/*}"; base="${bin##*/}" ;;
|
|
223
|
+
*) dir="."; base="$bin" ;;
|
|
224
|
+
esac
|
|
225
|
+
dir="$(builtin cd -- "$dir" 2>/dev/null && builtin pwd -P)" || { printf ''; return 0; }
|
|
226
|
+
bin="$dir/$base"
|
|
227
|
+
;;
|
|
228
|
+
esac
|
|
229
|
+
[[ -f "$bin" && -x "$bin" ]] || { printf ''; return 0; }
|
|
230
|
+
printf '%s' "$bin"
|
|
231
|
+
}
|
|
232
|
+
|
|
183
233
|
if ! command -v agy >/dev/null 2>&1; then
|
|
184
234
|
echo "error: 'agy' (Antigravity CLI) not found on PATH. Install it and run 'agy' once to sign in." >&2
|
|
185
235
|
exit 127
|
|
@@ -188,11 +238,13 @@ fi
|
|
|
188
238
|
# `-` (empty) => skip --model and let agy use settings.json; default to Pro.
|
|
189
239
|
AGY_MODEL="${AGY_MODEL-Gemini 3.1 Pro (High)}"
|
|
190
240
|
AGY_TIMEOUT="${AGY_TIMEOUT:-5m}"
|
|
241
|
+
AGY_TIMEOUT="$(aw_effective_timeout AGY_TIMEOUT 5m)"
|
|
191
242
|
# Hard wall-clock cap (defaults to AGY_TIMEOUT). agy's own --print-timeout is NOT a reliable
|
|
192
243
|
# wall-clock kill — a run was observed surviving 32 min past a 10m --print-timeout — so we also wrap
|
|
193
244
|
# agy in timeout(1). A heavy `--add-dir` agentic prompt on the slowest model can otherwise run
|
|
194
245
|
# unbounded, and once a caller backgrounds it nothing kills it. Raise only for a known-healthy run.
|
|
195
246
|
AGY_HARD_TIMEOUT="${AGY_HARD_TIMEOUT:-$AGY_TIMEOUT}"
|
|
247
|
+
AGY_HARD_TIMEOUT="$(aw_effective_timeout AGY_HARD_TIMEOUT "$AGY_TIMEOUT")"
|
|
196
248
|
|
|
197
249
|
if [[ $# -lt 1 ]]; then
|
|
198
250
|
echo "usage: $0 <prompt | - | @file> [-- extra agy flags...]" >&2
|
|
@@ -273,12 +325,9 @@ agy_cmd=(agy "${model_flag[@]}" --print-timeout "$AGY_TIMEOUT" "${passthrough[@]
|
|
|
273
325
|
|
|
274
326
|
# Hard wall-clock cap via timeout(1) (GNU `timeout` on Linux, `gtimeout` from coreutils on macOS).
|
|
275
327
|
# This is the real guard — a backgrounded, hung agy survives its own --print-timeout otherwise.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
elif command -v gtimeout >/dev/null 2>&1; then
|
|
280
|
-
timeout_bin="gtimeout"
|
|
281
|
-
fi
|
|
328
|
+
# aw_resolve_timeout_bin: builtin type -P (an exported function can shadow neither `timeout` nor
|
|
329
|
+
# `type` itself), normalized to an ABSOLUTE path fail-closed — nothing can masquerade as the cap.
|
|
330
|
+
timeout_bin="$(aw_resolve_timeout_bin)"
|
|
282
331
|
|
|
283
332
|
if [[ -z "$timeout_bin" ]]; then
|
|
284
333
|
echo "warning: no 'timeout'/'gtimeout' on PATH — running agy WITHOUT a hard wall-clock cap" >&2
|