@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,10 +1,10 @@
|
|
|
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 { mkdtempSync, mkdirSync, writeFileSync, chmodSync, rmSync, existsSync, readdirSync, symlinkSync, readFileSync } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join, dirname, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { spawnSync, execFile } from 'node:child_process';
|
|
8
8
|
|
|
9
9
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const WRAPPER = join(HERE, 'agy.sh');
|
|
@@ -31,11 +31,23 @@ const runWrapper = (home, env, prompt = 'hello') =>
|
|
|
31
31
|
timeout: 20000,
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// Async twin of runWrapper for the sleep-bound suites below: spawnSync BLOCKS the event loop
|
|
35
|
+
// and would serialize the deliberate timeout waits; the async form lets a concurrent describe
|
|
36
|
+
// overlap them (the waiting children are idle, not CPU-bound). Same spawn contract.
|
|
37
|
+
const runWrapperAsync = (home, env, prompt = 'hello') =>
|
|
38
|
+
new Promise((done) => {
|
|
39
|
+
execFile('bash', [WRAPPER, prompt], {
|
|
40
|
+
env: { HOME: home, PATH: `${join(home, '.local', 'bin')}:${process.env.PATH}`, TMPDIR: process.env.TMPDIR ?? '/tmp', ...env },
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
timeout: 20000,
|
|
43
|
+
}, (error, stdout, stderr) => done({ status: error ? (error.code ?? 1) : 0, stdout, stderr }));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('agy.sh — hard wall-clock cap (timeout(1))', { concurrency: true }, () => {
|
|
47
|
+
it('kills a hung agy at AGY_HARD_TIMEOUT and reports it (non-zero + actionable guidance)', async () => {
|
|
36
48
|
const home = makeSandbox('#!/usr/bin/env bash\nsleep 30\n');
|
|
37
49
|
const started = Date.now();
|
|
38
|
-
const r =
|
|
50
|
+
const r = await runWrapperAsync(home, { AGY_HARD_TIMEOUT: '2s', AGY_TIMEOUT: '2s', AGY_MODEL: '' });
|
|
39
51
|
const elapsed = Date.now() - started;
|
|
40
52
|
rmSync(home, { recursive: true, force: true });
|
|
41
53
|
assert.ok(elapsed < 13000, `wrapper must return well under the kill-after window, took ${elapsed}ms`);
|
|
@@ -43,17 +55,17 @@ describe('agy.sh — hard wall-clock cap (timeout(1))', () => {
|
|
|
43
55
|
assert.match(r.stderr, /exceeded the hard cap/, 'must explain the hard-cap kill');
|
|
44
56
|
});
|
|
45
57
|
|
|
46
|
-
it('passes a fast agy run through unchanged (exit 0, stdout preserved)', () => {
|
|
58
|
+
it('passes a fast agy run through unchanged (exit 0, stdout preserved)', async () => {
|
|
47
59
|
const home = makeSandbox('#!/usr/bin/env bash\necho "OK reply"\nexit 0\n');
|
|
48
|
-
const r =
|
|
60
|
+
const r = await runWrapperAsync(home, { AGY_HARD_TIMEOUT: '10s', AGY_TIMEOUT: '10s', AGY_MODEL: '' });
|
|
49
61
|
rmSync(home, { recursive: true, force: true });
|
|
50
62
|
assert.equal(r.status, 0, `expected clean exit, got ${r.status}; stderr=${r.stderr}`);
|
|
51
63
|
assert.match(r.stdout, /OK reply/);
|
|
52
64
|
});
|
|
53
65
|
|
|
54
|
-
it('propagates a non-timeout agy failure code verbatim (no false hard-cap message)', () => {
|
|
66
|
+
it('propagates a non-timeout agy failure code verbatim (no false hard-cap message)', async () => {
|
|
55
67
|
const home = makeSandbox('#!/usr/bin/env bash\necho "boom" >&2\nexit 3\n');
|
|
56
|
-
const r =
|
|
68
|
+
const r = await runWrapperAsync(home, { AGY_HARD_TIMEOUT: '10s', AGY_TIMEOUT: '10s', AGY_MODEL: '' });
|
|
57
69
|
rmSync(home, { recursive: true, force: true });
|
|
58
70
|
assert.equal(r.status, 3, 'a genuine agy failure code must pass through');
|
|
59
71
|
assert.doesNotMatch(r.stderr, /exceeded the hard cap/, 'must not mislabel a non-timeout failure');
|
|
@@ -178,6 +190,12 @@ const makePathWithout = (root, exclude = []) => {
|
|
|
178
190
|
return dir;
|
|
179
191
|
};
|
|
180
192
|
|
|
193
|
+
// The farm is READ-ONLY per invocation — built ONCE and shared (a per-call rebuild is thousands
|
|
194
|
+
// of symlinks).
|
|
195
|
+
const FARM_ROOT = mkdtempSync(join(tmpdir(), 'agy-farm-'));
|
|
196
|
+
after(() => rmSync(FARM_ROOT, { recursive: true, force: true }));
|
|
197
|
+
const FARM = makePathWithout(FARM_ROOT, ['agy', 'git']);
|
|
198
|
+
|
|
181
199
|
describe('agy.sh — --help (pre-preflight, candidate C)', () => {
|
|
182
200
|
it('--help and -h exit 0 with NO agy on PATH and name the documented usage', () => {
|
|
183
201
|
for (const arg of ['--help', '-h']) {
|
|
@@ -185,7 +203,7 @@ describe('agy.sh — --help (pre-preflight, candidate C)', () => {
|
|
|
185
203
|
// the help must not need the CLI even when the host has a real agy installed.
|
|
186
204
|
const home = mkdtempSync(join(tmpdir(), 'agy-help-'));
|
|
187
205
|
const r = spawnSync('bash', [WRAPPER, arg], {
|
|
188
|
-
env: { HOME: home, PATH:
|
|
206
|
+
env: { HOME: home, PATH: FARM },
|
|
189
207
|
encoding: 'utf8',
|
|
190
208
|
timeout: 15000,
|
|
191
209
|
});
|
|
@@ -226,77 +244,95 @@ const writeSettings = (home, text) => {
|
|
|
226
244
|
};
|
|
227
245
|
const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
|
|
228
246
|
|
|
229
|
-
describe('agy.sh — bridge settings file (bridges 2.3.0)', () => {
|
|
230
|
-
it('a file-set AGY_HARD_TIMEOUT is effective (killed at the file cap)', () => {
|
|
247
|
+
describe('agy.sh — bridge settings file (bridges 2.3.0)', { concurrency: true }, () => {
|
|
248
|
+
it('a file-set AGY_HARD_TIMEOUT is effective (killed at the file cap)', async () => {
|
|
231
249
|
const home = makeSandbox('#!/usr/bin/env bash\nsleep 5\n');
|
|
232
250
|
writeSettings(home, 'AGY_HARD_TIMEOUT=2s\n');
|
|
233
|
-
const r =
|
|
251
|
+
const r = await runWrapperAsync(home, { AGY_MODEL: '' });
|
|
234
252
|
rmSync(home, { recursive: true, force: true });
|
|
235
253
|
assert.notEqual(r.status, 0, 'the file cap must apply when the env is unset');
|
|
236
254
|
assert.match(r.stderr, /exceeded the hard cap AGY_HARD_TIMEOUT=2s/);
|
|
237
255
|
});
|
|
238
256
|
|
|
239
|
-
it('env overrides file: env=2s file=10m → killed at the env cap', () => {
|
|
257
|
+
it('env overrides file: env=2s file=10m → killed at the env cap', async () => {
|
|
240
258
|
const home = makeSandbox('#!/usr/bin/env bash\nsleep 5\n');
|
|
241
259
|
writeSettings(home, 'AGY_HARD_TIMEOUT=10m\n');
|
|
242
|
-
const r =
|
|
260
|
+
const r = await runWrapperAsync(home, { AGY_HARD_TIMEOUT: '2s', AGY_TIMEOUT: '2s', AGY_MODEL: '' });
|
|
243
261
|
rmSync(home, { recursive: true, force: true });
|
|
244
262
|
assert.notEqual(r.status, 0, 'the env cap (2s) must win over the file cap (10m)');
|
|
245
263
|
assert.match(r.stderr, /exceeded the hard cap AGY_HARD_TIMEOUT=2s/);
|
|
246
264
|
});
|
|
247
265
|
|
|
248
|
-
it('an EXPLICITLY EMPTY env (AGY_HARD_TIMEOUT=) disables the file knob for one run', () => {
|
|
266
|
+
it('an EXPLICITLY EMPTY env (AGY_HARD_TIMEOUT=) disables the file knob for one run', async () => {
|
|
249
267
|
const home = makeSandbox('#!/usr/bin/env bash\nsleep 3\necho "OK reply"\n');
|
|
250
268
|
writeSettings(home, 'AGY_HARD_TIMEOUT=2s\n');
|
|
251
|
-
const r =
|
|
269
|
+
const r = await runWrapperAsync(home, { AGY_HARD_TIMEOUT: '', AGY_MODEL: '' });
|
|
252
270
|
rmSync(home, { recursive: true, force: true });
|
|
253
271
|
assert.equal(r.status, 0, `built-in default must apply (not the 2s file cap): ${r.stderr}`);
|
|
254
272
|
assert.match(r.stdout, /OK reply/);
|
|
255
273
|
});
|
|
256
274
|
|
|
257
|
-
it('duplicate key → the LAST occurrence wins (10m then 2s → killed at 2s)', () => {
|
|
275
|
+
it('duplicate key → the LAST occurrence wins (10m then 2s → killed at 2s)', async () => {
|
|
258
276
|
const home = makeSandbox('#!/usr/bin/env bash\nsleep 5\n');
|
|
259
277
|
writeSettings(home, 'AGY_HARD_TIMEOUT=10m\nAGY_HARD_TIMEOUT=2s\n');
|
|
260
|
-
const r =
|
|
278
|
+
const r = await runWrapperAsync(home, { AGY_MODEL: '' });
|
|
261
279
|
rmSync(home, { recursive: true, force: true });
|
|
262
280
|
assert.notEqual(r.status, 0);
|
|
263
281
|
assert.match(r.stderr, /exceeded the hard cap AGY_HARD_TIMEOUT=2s/);
|
|
264
282
|
});
|
|
265
283
|
|
|
266
|
-
it('an invalid duration warns and falls back to the built-in default', () => {
|
|
284
|
+
it('an invalid duration warns and falls back to the built-in default', async () => {
|
|
267
285
|
const home = makeSandbox(RECORDING_STUB);
|
|
268
286
|
writeSettings(home, 'AGY_HARD_TIMEOUT=abc\n');
|
|
269
|
-
const r =
|
|
287
|
+
const r = await runWrapperAsync(home, { AGY_MODEL: '' });
|
|
270
288
|
rmSync(home, { recursive: true, force: true });
|
|
271
289
|
assert.equal(r.status, 0, r.stderr);
|
|
272
290
|
assert.match(r.stderr, /invalid value 'abc'/);
|
|
273
291
|
assert.match(r.stdout, /OK reply/);
|
|
274
292
|
});
|
|
275
293
|
|
|
276
|
-
it('a bare-integer duration (no unit suffix) is invalid → warn + built-in default', () => {
|
|
294
|
+
it('a bare-integer duration (no unit suffix) is invalid → warn + built-in default', async () => {
|
|
277
295
|
const home = makeSandbox(RECORDING_STUB);
|
|
278
296
|
writeSettings(home, 'AGY_HARD_TIMEOUT=90\n');
|
|
279
|
-
const r =
|
|
297
|
+
const r = await runWrapperAsync(home, { AGY_MODEL: '' });
|
|
280
298
|
rmSync(home, { recursive: true, force: true });
|
|
281
299
|
assert.equal(r.status, 0, r.stderr);
|
|
282
300
|
assert.match(r.stderr, /invalid value '90'/, 'duration values require a unit suffix (5m/30m/90s)');
|
|
283
301
|
assert.match(r.stdout, /OK reply/);
|
|
284
302
|
});
|
|
285
303
|
|
|
286
|
-
it('a ZERO duration is invalid — timeout 0 would silently DISABLE the hard cap', () => {
|
|
304
|
+
it('a ZERO duration is invalid — timeout 0 would silently DISABLE the hard cap', async () => {
|
|
287
305
|
const home = makeSandbox(RECORDING_STUB);
|
|
288
306
|
writeSettings(home, 'AGY_HARD_TIMEOUT=0s\n');
|
|
289
|
-
const r =
|
|
307
|
+
const r = await runWrapperAsync(home, { AGY_MODEL: '' });
|
|
290
308
|
rmSync(home, { recursive: true, force: true });
|
|
291
309
|
assert.equal(r.status, 0, r.stderr);
|
|
292
310
|
assert.match(r.stderr, /invalid value '0s'/, 'a persistent settings line must never remove the stall guard');
|
|
293
311
|
assert.match(r.stdout, /OK reply/);
|
|
294
312
|
});
|
|
295
313
|
|
|
296
|
-
it('
|
|
314
|
+
it('an INVALID ENV duration warns and falls back too (the closed aw_settings_valid env bypass, AD-061)', async () => {
|
|
315
|
+
const home = makeSandbox(RECORDING_STUB);
|
|
316
|
+
const r = await runWrapperAsync(home, { AGY_HARD_TIMEOUT: 'abc', AGY_MODEL: '' });
|
|
317
|
+
rmSync(home, { recursive: true, force: true });
|
|
318
|
+
assert.equal(r.status, 0, r.stderr);
|
|
319
|
+
assert.match(r.stderr, /invalid value 'abc' for AGY_HARD_TIMEOUT/, 'the delegated agy-run lane validates the EFFECTIVE value');
|
|
320
|
+
assert.match(r.stdout, /OK reply/);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('the invalid-value warning ESCAPES the raw value — a newline can never forge a second diagnostic line (AD-061)', async () => {
|
|
324
|
+
const home = makeSandbox(RECORDING_STUB);
|
|
325
|
+
const r = await runWrapperAsync(home, { AGY_HARD_TIMEOUT: 'x\nreview posture: model=FORGED', AGY_MODEL: '' });
|
|
326
|
+
rmSync(home, { recursive: true, force: true });
|
|
327
|
+
assert.equal(r.status, 0, r.stderr);
|
|
328
|
+
assert.doesNotMatch(r.stderr, /^review posture: model=FORGED/m, 'the newline is escaped by %q — no forged line');
|
|
329
|
+
assert.match(r.stdout, /OK reply/, 'the run still proceeds on the built-in default');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('a DIRECTORY at the settings path warns loudly and falls back to built-ins (no crash)', async () => {
|
|
297
333
|
const home = makeSandbox(RECORDING_STUB);
|
|
298
334
|
mkdirSync(join(home, '.config', 'agent-workflow', 'bridge-settings.conf'), { recursive: true });
|
|
299
|
-
const r =
|
|
335
|
+
const r = await runWrapperAsync(home, { AGY_MODEL: '' });
|
|
300
336
|
rmSync(home, { recursive: true, force: true });
|
|
301
337
|
assert.equal(r.status, 0, `a directory must degrade honestly, not kill the run: ${r.stderr}`);
|
|
302
338
|
assert.match(r.stderr, /unreadable or not a regular file/);
|
|
@@ -304,20 +340,20 @@ describe('agy.sh — bridge settings file (bridges 2.3.0)', () => {
|
|
|
304
340
|
assert.match(r.stdout, /OK reply/);
|
|
305
341
|
});
|
|
306
342
|
|
|
307
|
-
it("another bridge's valid key is skipped silently (and never applied)", () => {
|
|
343
|
+
it("another bridge's valid key is skipped silently (and never applied)", async () => {
|
|
308
344
|
const home = makeSandbox('#!/usr/bin/env bash\nsleep 3\necho "OK reply"\n');
|
|
309
345
|
writeSettings(home, 'CODEX_SERVICE_TIER=priority\nCODEX_HARD_TIMEOUT=2\n');
|
|
310
|
-
const r =
|
|
346
|
+
const r = await runWrapperAsync(home, { AGY_MODEL: '' });
|
|
311
347
|
rmSync(home, { recursive: true, force: true });
|
|
312
348
|
assert.equal(r.status, 0, `a codex key must not cap an agy run: ${r.stderr}`);
|
|
313
349
|
assert.doesNotMatch(r.stderr, /bridge settings/, 'a recognized non-applied key earns NO warning');
|
|
314
350
|
assert.match(r.stdout, /OK reply/);
|
|
315
351
|
});
|
|
316
352
|
|
|
317
|
-
it('a truly unknown key warns ONCE naming the file; the run is unaffected', () => {
|
|
353
|
+
it('a truly unknown key warns ONCE naming the file; the run is unaffected', async () => {
|
|
318
354
|
const home = makeSandbox(RECORDING_STUB);
|
|
319
355
|
writeSettings(home, 'TOTALLY_UNKNOWN=1\nTOTALLY_UNKNOWN=2\n');
|
|
320
|
-
const r =
|
|
356
|
+
const r = await runWrapperAsync(home, { AGY_MODEL: '' });
|
|
321
357
|
rmSync(home, { recursive: true, force: true });
|
|
322
358
|
assert.equal(r.status, 0, r.stderr);
|
|
323
359
|
const warns = r.stderr.match(/unknown key 'TOTALLY_UNKNOWN'/g) ?? [];
|
|
@@ -326,10 +362,10 @@ describe('agy.sh — bridge settings file (bridges 2.3.0)', () => {
|
|
|
326
362
|
assert.match(r.stdout, /OK reply/);
|
|
327
363
|
});
|
|
328
364
|
|
|
329
|
-
it('malformed lines warn and are ignored; comments and blank lines are silent', () => {
|
|
365
|
+
it('malformed lines warn and are ignored; comments and blank lines are silent', async () => {
|
|
330
366
|
const home = makeSandbox(RECORDING_STUB);
|
|
331
367
|
writeSettings(home, '# a comment\n\nNOT A KEY VALUE LINE\n');
|
|
332
|
-
const r =
|
|
368
|
+
const r = await runWrapperAsync(home, { AGY_MODEL: '' });
|
|
333
369
|
rmSync(home, { recursive: true, force: true });
|
|
334
370
|
assert.equal(r.status, 0, r.stderr);
|
|
335
371
|
const malformed = r.stderr.match(/malformed line/g) ?? [];
|
|
@@ -337,22 +373,22 @@ describe('agy.sh — bridge settings file (bridges 2.3.0)', () => {
|
|
|
337
373
|
assert.match(r.stdout, /OK reply/);
|
|
338
374
|
});
|
|
339
375
|
|
|
340
|
-
it('an existing-but-unreadable file warns loudly and falls back to built-ins', { skip: isRoot }, () => {
|
|
376
|
+
it('an existing-but-unreadable file warns loudly and falls back to built-ins', { skip: isRoot }, async () => {
|
|
341
377
|
const home = makeSandbox(RECORDING_STUB);
|
|
342
378
|
const file = writeSettings(home, 'AGY_HARD_TIMEOUT=2s\n');
|
|
343
379
|
chmodSync(file, 0o000);
|
|
344
|
-
const r =
|
|
380
|
+
const r = await runWrapperAsync(home, { AGY_MODEL: '' });
|
|
345
381
|
rmSync(home, { recursive: true, force: true });
|
|
346
382
|
assert.equal(r.status, 0, r.stderr);
|
|
347
383
|
assert.match(r.stderr, /unreadable/);
|
|
348
384
|
assert.match(r.stdout, /OK reply/);
|
|
349
385
|
});
|
|
350
386
|
|
|
351
|
-
it('a settings line can NEVER execute code (command-substitution payload inert)', () => {
|
|
387
|
+
it('a settings line can NEVER execute code (command-substitution payload inert)', async () => {
|
|
352
388
|
const home = makeSandbox(RECORDING_STUB);
|
|
353
389
|
const pwned = join(home, 'pwned');
|
|
354
390
|
writeSettings(home, `AGY_HARD_TIMEOUT=$(touch ${pwned})\nEVIL_KEY=\`touch ${pwned}2\`\n`);
|
|
355
|
-
const r =
|
|
391
|
+
const r = await runWrapperAsync(home, { AGY_MODEL: '' });
|
|
356
392
|
const executed = existsSync(pwned) || existsSync(`${pwned}2`);
|
|
357
393
|
rmSync(home, { recursive: true, force: true });
|
|
358
394
|
assert.equal(r.status, 0, r.stderr);
|
|
@@ -360,9 +396,9 @@ describe('agy.sh — bridge settings file (bridges 2.3.0)', () => {
|
|
|
360
396
|
assert.match(r.stdout, /OK reply/);
|
|
361
397
|
});
|
|
362
398
|
|
|
363
|
-
it('no file → byte-identical behaviour to today (no settings chatter)', () => {
|
|
399
|
+
it('no file → byte-identical behaviour to today (no settings chatter)', async () => {
|
|
364
400
|
const home = makeSandbox(RECORDING_STUB);
|
|
365
|
-
const r =
|
|
401
|
+
const r = await runWrapperAsync(home, { AGY_MODEL: '' });
|
|
366
402
|
rmSync(home, { recursive: true, force: true });
|
|
367
403
|
assert.equal(r.status, 0, r.stderr);
|
|
368
404
|
assert.doesNotMatch(r.stderr, /bridge settings/);
|
|
@@ -398,7 +434,7 @@ describe('agy.sh — settings surface ⟷ manifest (D6, manifest-pinned)', () =>
|
|
|
398
434
|
const runHelpText = () => {
|
|
399
435
|
const home = mkdtempSync(join(tmpdir(), 'agy-settings-help-'));
|
|
400
436
|
const r = spawnSync('bash', [WRAPPER, '--help'], {
|
|
401
|
-
env: { HOME: home, PATH:
|
|
437
|
+
env: { HOME: home, PATH: FARM },
|
|
402
438
|
encoding: 'utf8',
|
|
403
439
|
timeout: 15000,
|
|
404
440
|
});
|
|
@@ -475,7 +511,7 @@ describe('agy.sh — mode catalog ⟷ wrapper reality (the contract-free raw mod
|
|
|
475
511
|
const helpText = () => {
|
|
476
512
|
const home = mkdtempSync(join(tmpdir(), 'agy-catalog-help-'));
|
|
477
513
|
const r = spawnSync('bash', [WRAPPER, '--help'], {
|
|
478
|
-
env: { HOME: home, PATH:
|
|
514
|
+
env: { HOME: home, PATH: FARM },
|
|
479
515
|
encoding: 'utf8',
|
|
480
516
|
timeout: 15000,
|
|
481
517
|
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"schema": 1,
|
|
4
4
|
"name": "antigravity-cli-bridge",
|
|
5
5
|
"kind": "execution-backend",
|
|
6
|
-
"version": "4.
|
|
6
|
+
"version": "4.1.0",
|
|
7
7
|
"provides": ["review", "probe"],
|
|
8
8
|
"posture": { "model": "Gemini 3.1 Pro (High)" },
|
|
9
9
|
"roles": {
|
|
@@ -32,7 +32,9 @@
|
|
|
32
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; 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
34
|
"notes": [
|
|
35
|
-
"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"
|
|
35
|
+
"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|uncapped> field — exactly the duration agy-run hands to timeout(1), uncapped when no timeout/gtimeout binary caps the run; INFORMATIONAL only: it never enters the receipt posture or the D5 banner↔receipt parity",
|
|
37
|
+
"quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts"
|
|
36
38
|
]
|
|
37
39
|
}
|
|
38
40
|
},
|
|
@@ -89,6 +89,11 @@ What it does for you, and what YOU must supply:
|
|
|
89
89
|
- **Model:** frontier default `Gemini 3.1 Pro (High)`; any model is allowed (a sub-frontier one earns a
|
|
90
90
|
silenceable `AGY_PROBE=1` advisory). The service can still **stall on large/substantive prompts**
|
|
91
91
|
(Issue-001) — keep reviews **focused**; the hard timeout is the guard.
|
|
92
|
+
- **Posture banner — quote it verbatim.** Every review states its ACTUAL posture on ONE stderr line
|
|
93
|
+
(`review posture: model=… timeout=…`). When you label a dispatch for a user or a record, **quote
|
|
94
|
+
the posture banner verbatim** — the banner is the machine-stated posture; a prose re-type drifts.
|
|
95
|
+
The `timeout=` field is **banner-only** (exactly the duration `agy-run` hands to `timeout(1)`, or
|
|
96
|
+
`uncapped` when no capping binary is on PATH) — informational, never part of the receipt posture.
|
|
92
97
|
|
|
93
98
|
## Escalation policy (edits, network, git)
|
|
94
99
|
|
|
@@ -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.1.0'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# codex-cli-bridge
|
|
@@ -99,9 +99,13 @@ codex-review code "focus on the new reducer" # review with extra focus
|
|
|
99
99
|
**Honesty + posture (D4/D5):** a run whose final message has no recognized
|
|
100
100
|
`Verdict: <ship|revise|rethink>` line — empty or missing output included — **exits 4 with NO
|
|
101
101
|
receipt**: treat it as a *failed review to re-run*, never a fatal session error. One stderr banner
|
|
102
|
-
states the actual posture (`review posture: model=… effort=… tier=…`) and the receipt
|
|
103
|
-
same `posture {model, effort, tier}` (tier `null` on the standard tier); control bytes
|
|
104
|
-
posture value refuse pre-spend in every mode.
|
|
102
|
+
states the actual posture (`review posture: model=… effort=… tier=… timeout=…`) and the receipt
|
|
103
|
+
records the same `posture {model, effort, tier}` (tier `null` on the standard tier); control bytes
|
|
104
|
+
in a posture value refuse pre-spend in every mode. `codex-exec` states its posture the same way —
|
|
105
|
+
ONE `exec posture: model=… effort=… tier=… sandbox=workspace-write session=fresh|resume:<id>
|
|
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)`, or `uncapped`) — informational,
|
|
108
|
+
never a receipt field. **Quote the posture banner verbatim** when labeling a dispatch.
|
|
105
109
|
|
|
106
110
|
`codex exec` is headless: there is **no TTY**, so `approval_policy=never` — anything needing
|
|
107
111
|
escalation is refused and reported, never interactively approved. The wrappers capture only codex's
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
# codex-exec <file|-> -- <extra codex flags...> # passthrough codex flags
|
|
37
37
|
# codex-exec --resume-last <file|-> # continue the last session (iterate, no re-send)
|
|
38
38
|
# codex-exec --resume <session-id> <file|-> # continue a specific session
|
|
39
|
-
# CODEX_HARD_TIMEOUT=
|
|
39
|
+
# CODEX_HARD_TIMEOUT=7200 codex-exec <file> # raise the hard wall-clock cap (integer seconds)
|
|
40
40
|
# CODEX_PROBE=1 CODEX_MODEL=<slug> codex-exec <file> # throwaway probe (relaxes the guard)
|
|
41
41
|
set -euo pipefail
|
|
42
42
|
|
|
@@ -72,6 +72,20 @@ Notes:
|
|
|
72
72
|
nested inside a harness sandbox (the FS turns read-only) — route it OUTSIDE the harness sandbox
|
|
73
73
|
(excludedCommands / a per-run consented bypass) on the OBSERVED bwrap/EPERM failure, never a
|
|
74
74
|
preemptive blanket
|
|
75
|
+
exec posture banner: ONE stderr line before dispatch states the ACTUAL run posture —
|
|
76
|
+
exec posture: model=… effort=… tier=… sandbox=workspace-write session=fresh|resume:<id> timeout=… —
|
|
77
|
+
from RESOLVED post-validation values; the resume id is validated pre-spend, and control bytes in
|
|
78
|
+
any banner field refuse pre-spend
|
|
79
|
+
threat model: the sidecar byte and grammar screens detect corrupted input under a trusted parent
|
|
80
|
+
environment. A hostile parent environment — including exported shell functions or PATH
|
|
81
|
+
substitution of core/backend commands — is outside the threat model and can substitute the
|
|
82
|
+
backend itself. Targeted shadow-proof resolution protects banner/dispatch honesty from accidental
|
|
83
|
+
shadowing; it is not an environment security boundary
|
|
84
|
+
the exec posture banner appends a banner-only timeout=<duration|uncapped> field — exactly the
|
|
85
|
+
duration handed to timeout(1), uncapped when no timeout/gtimeout binary caps the run;
|
|
86
|
+
INFORMATIONAL only: it is never persisted in a receipt or session sidecar
|
|
87
|
+
quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated
|
|
88
|
+
posture; a prose re-type drifts
|
|
75
89
|
|
|
76
90
|
Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins over built-in default):
|
|
77
91
|
${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf
|
|
@@ -193,6 +207,56 @@ aw_apply_settings() {
|
|
|
193
207
|
}
|
|
194
208
|
aw_apply_settings
|
|
195
209
|
|
|
210
|
+
# --- Effective-timeout resolver (D5 banner honesty; AD-061) --------------------
|
|
211
|
+
# ONE rule, both bridges: the posture banner prints EXACTLY the duration handed to timeout(1) —
|
|
212
|
+
# an integer-seconds value rendered with the `s` suffix, a duration string verbatim — and
|
|
213
|
+
# `timeout=uncapped` when no timeout/gtimeout binary can cap the run; never a fabricated number.
|
|
214
|
+
# The EFFECTIVE value (env included — closing the aw_settings_valid env bypass) is validated by
|
|
215
|
+
# the same per-key rule as the settings file, plus a 7-digit integer-part bound (overflow); an
|
|
216
|
+
# invalid value warns + falls back to the built-in default — a typo never silently masquerades
|
|
217
|
+
# as a cap. AGY_TIMEOUT shares AGY_HARD_TIMEOUT's duration rule (it has no settings-file arm).
|
|
218
|
+
aw_effective_timeout() {
|
|
219
|
+
local key="$1" default="$2" value="${!1:-}" rule="$1" intpart
|
|
220
|
+
[[ "$rule" == "AGY_TIMEOUT" ]] && rule="AGY_HARD_TIMEOUT"
|
|
221
|
+
[[ -n "$value" ]] || { printf '%s' "$default"; return 0; }
|
|
222
|
+
intpart="${value%%[!0-9]*}"
|
|
223
|
+
if ! aw_settings_valid "$rule" "$value" || (( ${#intpart} > 7 )); then
|
|
224
|
+
# %q escapes the raw value so a control byte in it can never forge an extra diagnostic line
|
|
225
|
+
# (the direct agy-run lane has no pre-spend screen — the warning itself must be injection-proof).
|
|
226
|
+
printf "warning: invalid value '%q' for %s — using the built-in default %s.\n" "$value" "$key" "$default" >&2
|
|
227
|
+
printf '%s' "$default"
|
|
228
|
+
return 0
|
|
229
|
+
fi
|
|
230
|
+
printf '%s' "$value"
|
|
231
|
+
}
|
|
232
|
+
aw_timeout_label() {
|
|
233
|
+
local bin="$1" value="$2"
|
|
234
|
+
[[ -n "$bin" ]] || { printf 'uncapped'; return 0; }
|
|
235
|
+
case "$value" in
|
|
236
|
+
*[!0-9]*) printf '%s' "$value" ;;
|
|
237
|
+
*) printf '%ss' "$value" ;;
|
|
238
|
+
esac
|
|
239
|
+
}
|
|
240
|
+
aw_resolve_timeout_bin() {
|
|
241
|
+
local bin dir base
|
|
242
|
+
bin="$(builtin type -P timeout 2>/dev/null || true)"
|
|
243
|
+
[[ -n "$bin" ]] || bin="$(builtin type -P gtimeout 2>/dev/null || true)"
|
|
244
|
+
[[ -n "$bin" ]] || { printf ''; return 0; }
|
|
245
|
+
case "$bin" in
|
|
246
|
+
/*) ;;
|
|
247
|
+
*)
|
|
248
|
+
case "$bin" in
|
|
249
|
+
*/*) dir="${bin%/*}"; base="${bin##*/}" ;;
|
|
250
|
+
*) dir="."; base="$bin" ;;
|
|
251
|
+
esac
|
|
252
|
+
dir="$(builtin cd -- "$dir" 2>/dev/null && builtin pwd -P)" || { printf ''; return 0; }
|
|
253
|
+
bin="$dir/$base"
|
|
254
|
+
;;
|
|
255
|
+
esac
|
|
256
|
+
[[ -f "$bin" && -x "$bin" ]] || { printf ''; return 0; }
|
|
257
|
+
printf '%s' "$bin"
|
|
258
|
+
}
|
|
259
|
+
|
|
196
260
|
DEFAULT_CODEX_MODEL="gpt-5.6-sol" # frontier coding model (verified locally) — pinned
|
|
197
261
|
DEFAULT_CODEX_EFFORT="xhigh" # maximum reasoning effort — pinned
|
|
198
262
|
CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
|
|
@@ -209,6 +273,15 @@ CODEX_HARD_TIMEOUT="${CODEX_HARD_TIMEOUT:-3600}"
|
|
|
209
273
|
# the wrapper validates the effective value: an unsupported one warns and runs on the standard
|
|
210
274
|
# tier — a typo can never silently masquerade as Fast.
|
|
211
275
|
CODEX_SERVICE_TIER="${CODEX_SERVICE_TIER:-}"
|
|
276
|
+
# D5 pre-spend control-byte screen — BEFORE tier validation (the codex-review.sh order: a
|
|
277
|
+
# malformed value is not a policy question). Screens EVERY exec-banner field.
|
|
278
|
+
for _posture_pair in "CODEX_MODEL=$CODEX_MODEL" "CODEX_EFFORT=$CODEX_EFFORT" "CODEX_SERVICE_TIER=$CODEX_SERVICE_TIER" "CODEX_HARD_TIMEOUT=$CODEX_HARD_TIMEOUT"; do
|
|
279
|
+
if [[ "${_posture_pair#*=}" == *[$'\x01'-$'\x1f'$'\x7f']* ]]; then
|
|
280
|
+
echo "error: ${_posture_pair%%=*} contains control bytes — fix the setting (env or bridge-settings.conf) and re-run." >&2
|
|
281
|
+
exit 2
|
|
282
|
+
fi
|
|
283
|
+
done
|
|
284
|
+
CODEX_HARD_TIMEOUT="$(aw_effective_timeout CODEX_HARD_TIMEOUT 3600)"
|
|
212
285
|
if [[ -n "$CODEX_SERVICE_TIER" ]] && ! aw_settings_valid CODEX_SERVICE_TIER "$CODEX_SERVICE_TIER"; then
|
|
213
286
|
echo "warning: CODEX_SERVICE_TIER='$CODEX_SERVICE_TIER' is not a supported service tier ('priority') — running on the standard tier." >&2
|
|
214
287
|
CODEX_SERVICE_TIER=""
|
|
@@ -257,14 +330,27 @@ fi
|
|
|
257
330
|
# Resolve the real git to an ABSOLUTE executable, ignoring shell functions/aliases
|
|
258
331
|
# (`type -P` forces a PATH lookup). The shim embeds this path so codex cannot recurse
|
|
259
332
|
# into the shim or delegate to the wrong binary.
|
|
260
|
-
real_git="$(type -P git 2>/dev/null || true)"
|
|
333
|
+
real_git="$(builtin type -P git 2>/dev/null || true)"
|
|
261
334
|
if [[ -z "$real_git" ]]; then
|
|
262
335
|
echo "error: 'git' not found on PATH (needed for the work tree and the git-write boundary shim)." >&2
|
|
263
336
|
exit 127
|
|
264
337
|
fi
|
|
338
|
+
# Normalize a relative-PATH hit to an ABSOLUTE path with the same shadow-proof discipline as
|
|
339
|
+
# aw_resolve_timeout_bin: parameter-expansion split (no external dirname/basename an exported
|
|
340
|
+
# function could shadow), builtin cd/pwd -P, fail-closed to empty (the -x check below STOPs).
|
|
265
341
|
case "$real_git" in
|
|
266
342
|
/*) ;;
|
|
267
|
-
*)
|
|
343
|
+
*)
|
|
344
|
+
case "$real_git" in
|
|
345
|
+
*/*) _git_dir="${real_git%/*}"; _git_base="${real_git##*/}" ;;
|
|
346
|
+
*) _git_dir="."; _git_base="$real_git" ;;
|
|
347
|
+
esac
|
|
348
|
+
if _git_dir="$(builtin cd -- "$_git_dir" 2>/dev/null && builtin pwd -P)"; then
|
|
349
|
+
real_git="$_git_dir/$_git_base"
|
|
350
|
+
else
|
|
351
|
+
real_git=""
|
|
352
|
+
fi
|
|
353
|
+
;;
|
|
268
354
|
esac
|
|
269
355
|
if [[ ! -x "$real_git" ]]; then
|
|
270
356
|
echo "error: resolved git path '$real_git' is not an executable." >&2
|
|
@@ -363,12 +449,37 @@ if [[ -n "$resume_mode" ]]; then
|
|
|
363
449
|
echo " Run a normal 'codex-exec' once (it records the session id there) before resuming." >&2
|
|
364
450
|
exit 2
|
|
365
451
|
fi
|
|
366
|
-
|
|
367
|
-
|
|
452
|
+
# RAW-byte NUL screen BEFORE the shell variable: bash command substitution silently DROPS
|
|
453
|
+
# NUL bytes, so a hostile `sess-\0target` would otherwise be repaired into a valid id.
|
|
454
|
+
nul_count="$(head -n1 -- "$sidecar" | LC_ALL=C tr -cd '\000' | wc -c)"
|
|
455
|
+
if (( nul_count != 0 )); then
|
|
456
|
+
echo "error: the session sidecar '$sidecar' carries NUL bytes — refusing before any spend; delete it or pass --resume <session-id>." >&2
|
|
457
|
+
exit 2
|
|
458
|
+
fi
|
|
459
|
+
# Read the first line WITHOUT content mutation — only a CRLF terminator is stripped. A
|
|
460
|
+
# whitespace-ONLY line is an empty sidecar; inner whitespace beside real content hits the
|
|
461
|
+
# session-id grammar below and REFUSES — it is never silently stripped into a different
|
|
462
|
+
# (accidentally valid) id.
|
|
463
|
+
resume_id="$(head -n1 -- "$sidecar")"
|
|
464
|
+
resume_id="${resume_id%$'\r'}"
|
|
465
|
+
if [[ -z "${resume_id//[[:space:]]/}" ]]; then
|
|
368
466
|
echo "error: the session sidecar '$sidecar' is empty — no session id to resume." >&2
|
|
369
467
|
exit 2
|
|
370
468
|
fi
|
|
371
469
|
fi
|
|
470
|
+
# Resume-id grammar (AD-061, derived from live codex ids — UUID-like and sess-* forms alike):
|
|
471
|
+
# a safe charset + length bound, FIRST char alphanumeric (a leading dash would reach
|
|
472
|
+
# `codex exec resume` as an OPTION, bypassing the guarded passthrough). A hostile/malformed
|
|
473
|
+
# id — explicit OR sidecar-read — refuses BEFORE any spend and before the id can reach the
|
|
474
|
+
# codex argv; the raw value is never echoed (it could carry control bytes).
|
|
475
|
+
aw_session_id_re='^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$'
|
|
476
|
+
if [[ ! "$resume_id" =~ $aw_session_id_re ]]; then
|
|
477
|
+
echo "error: the resolved session id is not a valid codex session id (letters, digits, dot," >&2
|
|
478
|
+
echo " underscore, hyphen; must START with a letter or digit; 1-128 chars) — refusing" >&2
|
|
479
|
+
echo " before any spend. If it came from the sidecar, the file may be corrupted —" >&2
|
|
480
|
+
echo " delete it or pass --resume <session-id>." >&2
|
|
481
|
+
exit 2
|
|
482
|
+
fi
|
|
372
483
|
else
|
|
373
484
|
# Normal mode: split off passthrough codex flags after a literal `--`. Extra args
|
|
374
485
|
# WITHOUT the `--` separator are a mistake — fail loudly rather than drop them.
|
|
@@ -541,17 +652,24 @@ run_env=(env "PATH=$shim_dir:$PATH")
|
|
|
541
652
|
# after the initial TERM if codex ignores it (a live probe confirmed plain
|
|
542
653
|
# `timeout` reaps the whole codex child tree — no --foreground needed). If neither
|
|
543
654
|
# binary exists we warn loudly and run uncapped rather than fail silently.
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
timeout_bin="gtimeout"
|
|
549
|
-
fi
|
|
655
|
+
# aw_resolve_timeout_bin: builtin type -P (an exported function can shadow neither `timeout` nor
|
|
656
|
+
# `type` itself), normalized to an ABSOLUTE path fail-closed — the dispatch invokes the same
|
|
657
|
+
# absolute path the banner rendered from (banner and run never make independent conclusions).
|
|
658
|
+
timeout_bin="$(aw_resolve_timeout_bin)"
|
|
550
659
|
if [[ -z "$timeout_bin" ]]; then
|
|
551
660
|
echo "warning: no 'timeout'/'gtimeout' on PATH — running codex WITHOUT a hard wall-clock cap" >&2
|
|
552
661
|
echo " (install coreutils to enable CODEX_HARD_TIMEOUT=$CODEX_HARD_TIMEOUT)." >&2
|
|
553
662
|
fi
|
|
554
663
|
|
|
664
|
+
# --- D5 exec banner (one line, the ACTUAL run posture; AD-061) -----------------
|
|
665
|
+
# Emitted from RESOLVED post-validation values, AFTER the resume id is resolved and validated,
|
|
666
|
+
# BEFORE the dispatch. The timeout field is banner-only (never a receipt/sidecar field): it
|
|
667
|
+
# prints exactly the duration handed to timeout(1), or `uncapped` without a capping binary.
|
|
668
|
+
aw_timeout_banner="$(aw_timeout_label "$timeout_bin" "$CODEX_HARD_TIMEOUT")"
|
|
669
|
+
aw_session_label="fresh"
|
|
670
|
+
[[ -n "$resume_mode" ]] && aw_session_label="resume:$resume_id"
|
|
671
|
+
echo "exec posture: model=$CODEX_MODEL effort=$CODEX_EFFORT tier=${CODEX_SERVICE_TIER:-standard} sandbox=workspace-write session=$aw_session_label timeout=$aw_timeout_banner" >&2
|
|
672
|
+
|
|
555
673
|
# Normal mode: -o writes $out, the JSON stream + logs go to $trace. Resume mode: the
|
|
556
674
|
# final message is codex's stdout → $out, logs → $trace. Either way the final lands
|
|
557
675
|
# in $out and diagnostics in $trace, so the post-processing below is shared.
|