@sabaiway/agent-workflow-kit 2.1.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +94 -1
  2. package/README.md +7 -5
  3. package/SKILL.md +16 -8
  4. package/bin/install.mjs +11 -2
  5. package/bridges/antigravity-cli-bridge/SKILL.md +8 -1
  6. package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +163 -0
  7. package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +76 -0
  8. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +80 -15
  9. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +171 -22
  10. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +59 -41
  11. package/bridges/antigravity-cli-bridge/capability.json +3 -2
  12. package/bridges/codex-cli-bridge/SKILL.md +8 -1
  13. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +67 -22
  14. package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +156 -0
  15. package/bridges/codex-cli-bridge/bin/codex-review.sh +71 -10
  16. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +158 -27
  17. package/bridges/codex-cli-bridge/capability.json +3 -2
  18. package/capability.json +1 -1
  19. package/migrations/3.0.0-hardened-core-loop.md +44 -0
  20. package/migrations/README.md +1 -1
  21. package/package.json +2 -2
  22. package/references/hooks/gate-approve.mjs +1 -1
  23. package/references/modes/bootstrap.md +3 -3
  24. package/references/modes/commit-guard.md +19 -0
  25. package/references/modes/core-evidence.md +15 -0
  26. package/references/modes/coverage-check.md +12 -0
  27. package/references/modes/doc-parity.md +7 -6
  28. package/references/modes/gates.md +7 -6
  29. package/references/modes/grounding.md +1 -2
  30. package/references/modes/hook.md +1 -1
  31. package/references/modes/recommendations.md +1 -0
  32. package/references/modes/review-state.md +3 -3
  33. package/references/modes/upgrade.md +6 -6
  34. package/references/modes/velocity.md +2 -2
  35. package/references/modes/worktrees.md +120 -0
  36. package/references/scripts/archive-decisions.mjs +3 -3
  37. package/references/scripts/archive-decisions.test.mjs +5 -5
  38. package/references/scripts/check-docs-size-cli.test.mjs +41 -0
  39. package/references/scripts/check-docs-size.mjs +46 -29
  40. package/references/scripts/install-git-hooks-repo-exec.test.mjs +82 -0
  41. package/references/scripts/install-git-hooks.mjs +90 -18
  42. package/references/scripts/install-git-hooks.test.mjs +102 -0
  43. package/references/scripts/migrate-gates-branches.test.mjs +157 -0
  44. package/references/scripts/migrate-gates.mjs +395 -0
  45. package/references/scripts/migrate-gates.test.mjs +284 -0
  46. package/references/templates/agent_rules.md +2 -2
  47. package/tools/ack-write.mjs +1 -1
  48. package/tools/atomic-write.mjs +2 -2
  49. package/tools/autonomy-config.mjs +1 -1
  50. package/tools/autonomy-doctor.mjs +2 -2
  51. package/tools/autonomy-write.mjs +1 -1
  52. package/tools/bridge-settings-read.mjs +1 -1
  53. package/tools/bridge-settings.mjs +1 -1
  54. package/tools/changed-surface.mjs +16 -77
  55. package/tools/cheap-agents.mjs +2 -2
  56. package/tools/commands.mjs +26 -10
  57. package/tools/commit-guard.mjs +167 -0
  58. package/tools/core-evidence.mjs +914 -0
  59. package/tools/coverage-check.mjs +260 -0
  60. package/tools/delegation.mjs +1 -1
  61. package/tools/detect-backends.mjs +3 -3
  62. package/tools/doc-parity.mjs +11 -27
  63. package/tools/engine-source.mjs +1 -1
  64. package/tools/family-members.mjs +1 -1
  65. package/tools/family-registry.mjs +1 -1
  66. package/tools/fs-safe.mjs +1 -1
  67. package/tools/gate-hook.mjs +1 -1
  68. package/tools/{seed-gates.mjs → gates-init.mjs} +58 -52
  69. package/tools/grounding.mjs +19 -96
  70. package/tools/hide-footprint.mjs +1 -1
  71. package/tools/inject-methodology.mjs +72 -41
  72. package/tools/known-footprint.mjs +1 -1
  73. package/tools/labels.mjs +1 -1
  74. package/tools/lcov.mjs +6 -10
  75. package/tools/lens-region.mjs +1 -1
  76. package/tools/manifest/validate.mjs +19 -0
  77. package/tools/migrate-adr-store.mjs +5 -5
  78. package/tools/orchestration-config.mjs +1 -1
  79. package/tools/orchestration-write.mjs +1 -1
  80. package/tools/presentation.mjs +1 -1
  81. package/tools/procedures.mjs +3 -3
  82. package/tools/recipes.mjs +62 -11
  83. package/tools/recommendations.mjs +97 -14
  84. package/tools/release-scan.mjs +10 -3
  85. package/tools/renderers.mjs +1 -1
  86. package/tools/review-state.mjs +291 -343
  87. package/tools/run-gates.mjs +216 -92
  88. package/tools/sandbox-masks.mjs +14 -14
  89. package/tools/semver-lite.mjs +1 -1
  90. package/tools/set-autonomy.mjs +1 -1
  91. package/tools/set-recipe.mjs +2 -2
  92. package/tools/setup-backends.mjs +1 -1
  93. package/tools/surface.mjs +1 -1
  94. package/tools/uninstall.mjs +1 -1
  95. package/tools/velocity-profile.mjs +9 -9
  96. package/tools/view-model.mjs +1 -1
  97. package/tools/worktrees.mjs +2292 -0
  98. package/references/modes/fold-completeness.md +0 -30
  99. package/references/modes/review-ledger.md +0 -34
  100. package/references/templates/verification-profile.json +0 -10
  101. package/tools/fold-completeness-run.mjs +0 -1120
  102. package/tools/fold-completeness.mjs +0 -672
  103. package/tools/review-ledger-core.mjs +0 -428
  104. package/tools/review-ledger-write.mjs +0 -647
  105. package/tools/review-ledger.mjs +0 -630
  106. package/tools/sarif.mjs +0 -52
  107. package/tools/verification-profile.mjs +0 -219
@@ -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
- describe('agy.sh hard wall-clock cap (timeout(1))', () => {
35
- it('kills a hung agy at AGY_HARD_TIMEOUT and reports it (non-zero + actionable guidance)', () => {
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 = runWrapper(home, { AGY_HARD_TIMEOUT: '2s', AGY_TIMEOUT: '2s', AGY_MODEL: '' });
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 = runWrapper(home, { AGY_HARD_TIMEOUT: '10s', AGY_TIMEOUT: '10s', AGY_MODEL: '' });
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 = runWrapper(home, { AGY_HARD_TIMEOUT: '10s', AGY_TIMEOUT: '10s', AGY_MODEL: '' });
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: makePathWithout(home, ['agy', 'git']) },
206
+ env: { HOME: home, PATH: FARM },
189
207
  encoding: 'utf8',
190
208
  timeout: 15000,
191
209
  });
@@ -226,77 +244,77 @@ 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 = runWrapper(home, { AGY_MODEL: '' });
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 = runWrapper(home, { AGY_HARD_TIMEOUT: '2s', AGY_TIMEOUT: '2s', AGY_MODEL: '' });
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 = runWrapper(home, { AGY_HARD_TIMEOUT: '', AGY_MODEL: '' });
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 = runWrapper(home, { AGY_MODEL: '' });
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 = runWrapper(home, { AGY_MODEL: '' });
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 = runWrapper(home, { AGY_MODEL: '' });
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 = runWrapper(home, { AGY_MODEL: '' });
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('a DIRECTORY at the settings path warns loudly and falls back to built-ins (no crash)', () => {
314
+ it('a DIRECTORY at the settings path warns loudly and falls back to built-ins (no crash)', async () => {
297
315
  const home = makeSandbox(RECORDING_STUB);
298
316
  mkdirSync(join(home, '.config', 'agent-workflow', 'bridge-settings.conf'), { recursive: true });
299
- const r = runWrapper(home, { AGY_MODEL: '' });
317
+ const r = await runWrapperAsync(home, { AGY_MODEL: '' });
300
318
  rmSync(home, { recursive: true, force: true });
301
319
  assert.equal(r.status, 0, `a directory must degrade honestly, not kill the run: ${r.stderr}`);
302
320
  assert.match(r.stderr, /unreadable or not a regular file/);
@@ -304,20 +322,20 @@ describe('agy.sh — bridge settings file (bridges 2.3.0)', () => {
304
322
  assert.match(r.stdout, /OK reply/);
305
323
  });
306
324
 
307
- it("another bridge's valid key is skipped silently (and never applied)", () => {
325
+ it("another bridge's valid key is skipped silently (and never applied)", async () => {
308
326
  const home = makeSandbox('#!/usr/bin/env bash\nsleep 3\necho "OK reply"\n');
309
327
  writeSettings(home, 'CODEX_SERVICE_TIER=priority\nCODEX_HARD_TIMEOUT=2\n');
310
- const r = runWrapper(home, { AGY_MODEL: '' });
328
+ const r = await runWrapperAsync(home, { AGY_MODEL: '' });
311
329
  rmSync(home, { recursive: true, force: true });
312
330
  assert.equal(r.status, 0, `a codex key must not cap an agy run: ${r.stderr}`);
313
331
  assert.doesNotMatch(r.stderr, /bridge settings/, 'a recognized non-applied key earns NO warning');
314
332
  assert.match(r.stdout, /OK reply/);
315
333
  });
316
334
 
317
- it('a truly unknown key warns ONCE naming the file; the run is unaffected', () => {
335
+ it('a truly unknown key warns ONCE naming the file; the run is unaffected', async () => {
318
336
  const home = makeSandbox(RECORDING_STUB);
319
337
  writeSettings(home, 'TOTALLY_UNKNOWN=1\nTOTALLY_UNKNOWN=2\n');
320
- const r = runWrapper(home, { AGY_MODEL: '' });
338
+ const r = await runWrapperAsync(home, { AGY_MODEL: '' });
321
339
  rmSync(home, { recursive: true, force: true });
322
340
  assert.equal(r.status, 0, r.stderr);
323
341
  const warns = r.stderr.match(/unknown key 'TOTALLY_UNKNOWN'/g) ?? [];
@@ -326,10 +344,10 @@ describe('agy.sh — bridge settings file (bridges 2.3.0)', () => {
326
344
  assert.match(r.stdout, /OK reply/);
327
345
  });
328
346
 
329
- it('malformed lines warn and are ignored; comments and blank lines are silent', () => {
347
+ it('malformed lines warn and are ignored; comments and blank lines are silent', async () => {
330
348
  const home = makeSandbox(RECORDING_STUB);
331
349
  writeSettings(home, '# a comment\n\nNOT A KEY VALUE LINE\n');
332
- const r = runWrapper(home, { AGY_MODEL: '' });
350
+ const r = await runWrapperAsync(home, { AGY_MODEL: '' });
333
351
  rmSync(home, { recursive: true, force: true });
334
352
  assert.equal(r.status, 0, r.stderr);
335
353
  const malformed = r.stderr.match(/malformed line/g) ?? [];
@@ -337,22 +355,22 @@ describe('agy.sh — bridge settings file (bridges 2.3.0)', () => {
337
355
  assert.match(r.stdout, /OK reply/);
338
356
  });
339
357
 
340
- it('an existing-but-unreadable file warns loudly and falls back to built-ins', { skip: isRoot }, () => {
358
+ it('an existing-but-unreadable file warns loudly and falls back to built-ins', { skip: isRoot }, async () => {
341
359
  const home = makeSandbox(RECORDING_STUB);
342
360
  const file = writeSettings(home, 'AGY_HARD_TIMEOUT=2s\n');
343
361
  chmodSync(file, 0o000);
344
- const r = runWrapper(home, { AGY_MODEL: '' });
362
+ const r = await runWrapperAsync(home, { AGY_MODEL: '' });
345
363
  rmSync(home, { recursive: true, force: true });
346
364
  assert.equal(r.status, 0, r.stderr);
347
365
  assert.match(r.stderr, /unreadable/);
348
366
  assert.match(r.stdout, /OK reply/);
349
367
  });
350
368
 
351
- it('a settings line can NEVER execute code (command-substitution payload inert)', () => {
369
+ it('a settings line can NEVER execute code (command-substitution payload inert)', async () => {
352
370
  const home = makeSandbox(RECORDING_STUB);
353
371
  const pwned = join(home, 'pwned');
354
372
  writeSettings(home, `AGY_HARD_TIMEOUT=$(touch ${pwned})\nEVIL_KEY=\`touch ${pwned}2\`\n`);
355
- const r = runWrapper(home, { AGY_MODEL: '' });
373
+ const r = await runWrapperAsync(home, { AGY_MODEL: '' });
356
374
  const executed = existsSync(pwned) || existsSync(`${pwned}2`);
357
375
  rmSync(home, { recursive: true, force: true });
358
376
  assert.equal(r.status, 0, r.stderr);
@@ -360,9 +378,9 @@ describe('agy.sh — bridge settings file (bridges 2.3.0)', () => {
360
378
  assert.match(r.stdout, /OK reply/);
361
379
  });
362
380
 
363
- it('no file → byte-identical behaviour to today (no settings chatter)', () => {
381
+ it('no file → byte-identical behaviour to today (no settings chatter)', async () => {
364
382
  const home = makeSandbox(RECORDING_STUB);
365
- const r = runWrapper(home, { AGY_MODEL: '' });
383
+ const r = await runWrapperAsync(home, { AGY_MODEL: '' });
366
384
  rmSync(home, { recursive: true, force: true });
367
385
  assert.equal(r.status, 0, r.stderr);
368
386
  assert.doesNotMatch(r.stderr, /bridge settings/);
@@ -398,7 +416,7 @@ describe('agy.sh — settings surface ⟷ manifest (D6, manifest-pinned)', () =>
398
416
  const runHelpText = () => {
399
417
  const home = mkdtempSync(join(tmpdir(), 'agy-settings-help-'));
400
418
  const r = spawnSync('bash', [WRAPPER, '--help'], {
401
- env: { HOME: home, PATH: makePathWithout(home, ['agy', 'git']) },
419
+ env: { HOME: home, PATH: FARM },
402
420
  encoding: 'utf8',
403
421
  timeout: 15000,
404
422
  });
@@ -475,7 +493,7 @@ describe('agy.sh — mode catalog ⟷ wrapper reality (the contract-free raw mod
475
493
  const helpText = () => {
476
494
  const home = mkdtempSync(join(tmpdir(), 'agy-catalog-help-'));
477
495
  const r = spawnSync('bash', [WRAPPER, '--help'], {
478
- env: { HOME: home, PATH: makePathWithout(home, ['agy', 'git']) },
496
+ env: { HOME: home, PATH: FARM },
479
497
  encoding: 'utf8',
480
498
  timeout: 15000,
481
499
  });
@@ -3,8 +3,9 @@
3
3
  "schema": 1,
4
4
  "name": "antigravity-cli-bridge",
5
5
  "kind": "execution-backend",
6
- "version": "3.0.0",
6
+ "version": "4.0.0",
7
7
  "provides": ["review", "probe"],
8
+ "posture": { "model": "Gemini 3.1 Pro (High)" },
8
9
  "roles": {
9
10
  "review": {
10
11
  "cmd": "agy-review",
@@ -29,7 +30,7 @@
29
30
  "agy-review --continue [--decided @f] [--focus \"…\"]",
30
31
  "agy-review --conversation <id> [--decided @f] [--focus \"…\"]"
31
32
  ],
32
- "receipt": "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides; plan/diff outside a git tree: warn + skip unless overridden): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan/diff mode; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (code mode refuses pre-spend without one — no run, no receipt — unless --ungrounded/AGY_PROBE=1; in plan/diff an empty payload records grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts payload; a continuation receipt is fresh:false (informational-only — it cannot attest the folded tree); probe = whether the run relaxed the quality guards (AGY_PROBE=1), written on EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); a write failure warns, never fails the review",
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",
33
34
  "notes": [
34
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
36
  ]
@@ -2,7 +2,7 @@
2
2
  name: codex-cli-bridge
3
3
  description: Delegate work to the OpenAI Codex CLI (`codex`) under a ChatGPT subscription — run plan/instruction EXECUTION in a sandboxed workspace, or get a read-only ADVISORY review of a plan or working-tree diff — as a second delegated-execution backend beside Antigravity. Use when the user wants to hand a bounded coding task or plan to `codex exec`, get a second-opinion review from codex, install or authenticate Codex CLI, understand its sandbox/network/approval policy, drive codex efficiently from the main agent (exec vs review, resume, the commit boundary), bridge project context (`AGENTS.md`) into codex, or troubleshoot codex flags, models, auth, or its no-TTY headless behaviour.
4
4
  metadata:
5
- version: '2.8.0'
5
+ version: '3.0.0'
6
6
  ---
7
7
 
8
8
  # codex-cli-bridge
@@ -96,6 +96,13 @@ codex-review code # review the current working-tr
96
96
  codex-review code "focus on the new reducer" # review with extra focus
97
97
  ```
98
98
 
99
+ **Honesty + posture (D4/D5):** a run whose final message has no recognized
100
+ `Verdict: <ship|revise|rethink>` line — empty or missing output included — **exits 4 with NO
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 records the
103
+ same `posture {model, effort, tier}` (tier `null` on the standard tier); control bytes in a
104
+ posture value refuse pre-spend in every mode.
105
+
99
106
  `codex exec` is headless: there is **no TTY**, so `approval_policy=never` — anything needing
100
107
  escalation is refused and reported, never interactively approved. The wrappers capture only codex's
101
108
  **final message** (`-o`; the JSON event stream + reasoning go to a discarded trace), so output is
@@ -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, 'codex-exec.sh');
@@ -55,13 +55,16 @@ const FAKE_CODEX = [
55
55
  '',
56
56
  ].join('\n');
57
57
 
58
- const makeSandbox = () => {
59
- const root = mkdtempSync(join(tmpdir(), 'codex-exec-test-'));
58
+ // The sandbox base and the PATH farms are READ-ONLY per invocation, so both are built ONCE and
59
+ // shared (a per-test `git init`+commit and a per-call farm rebuild dominate the wall otherwise).
60
+ const SHARED_ROOT = mkdtempSync(join(tmpdir(), 'codex-exec-shared-'));
61
+ after(() => rmSync(SHARED_ROOT, { recursive: true, force: true }));
62
+
63
+ const TEMPLATE_ROOT = (() => {
64
+ const root = join(SHARED_ROOT, 'template-root');
60
65
  const bin = join(root, 'bin');
61
66
  mkdirSync(bin, { recursive: true });
62
- const stub = join(bin, 'codex');
63
- writeFileSync(stub, FAKE_CODEX, { mode: 0o755 });
64
- chmodSync(stub, 0o755);
67
+ writeFileSync(join(bin, 'codex'), FAKE_CODEX, { mode: 0o755 });
65
68
  // A git work tree with a root AGENTS.md — the wrapper preflights both.
66
69
  const repo = join(root, 'repo');
67
70
  mkdirSync(repo);
@@ -72,7 +75,15 @@ const makeSandbox = () => {
72
75
  writeFileSync(join(repo, 'AGENTS.md'), '# AGENTS\n\nHard Constraints: none (test fixture).\n');
73
76
  g('add', '-A');
74
77
  g('commit', '-qm', 'base');
75
- return { root, bin, repo };
78
+ return root;
79
+ })();
80
+
81
+ const makeSandbox = () => {
82
+ const root = mkdtempSync(join(tmpdir(), 'codex-exec-test-'));
83
+ cpSync(TEMPLATE_ROOT, root, { recursive: true });
84
+ const bin = join(root, 'bin');
85
+ chmodSync(join(bin, 'codex'), 0o755);
86
+ return { root, bin, repo: join(root, 'repo') };
76
87
  };
77
88
 
78
89
  // A PATH dir mirroring the real one MINUS the named binaries, to exercise the
@@ -96,6 +107,13 @@ const makePathWithout = (root, exclude = []) => {
96
107
  return dir;
97
108
  };
98
109
 
110
+ const farms = new Map();
111
+ const farmFor = (exclude) => {
112
+ const key = exclude.join('|');
113
+ if (!farms.has(key)) farms.set(key, makePathWithout(SHARED_ROOT, exclude));
114
+ return farms.get(key);
115
+ };
116
+
99
117
  const run = ({ repo, bin }, { args = ['-'], input = 'do the thing', env = {}, path, cwd } = {}) => {
100
118
  const argvFile = join(repo, '.cap-argv');
101
119
  const envFile = join(repo, '.cap-env');
@@ -121,6 +139,33 @@ const run = ({ repo, bin }, { args = ['-'], input = 'do the thing', env = {}, pa
121
139
  return { ...r, argv: readIf(argvFile), capEnv: readIf(envFile), capStdin: readIf(stdinFile) };
122
140
  };
123
141
 
142
+ // Async twin of run() for the sleep-bound timeout tests: spawnSync blocks the event loop for
143
+ // the whole deliberate wait, so a concurrent describe could not overlap them. Same contract.
144
+ const runAsync = ({ repo, bin }, { args = ['-'], input = 'do the thing', env = {}, path, cwd } = {}) =>
145
+ new Promise((done) => {
146
+ const argvFile = join(repo, '.cap-argv');
147
+ const envFile = join(repo, '.cap-env');
148
+ const stdinFile = join(repo, '.cap-stdin');
149
+ const child = execFile('bash', [WRAPPER, ...args], {
150
+ cwd: cwd || repo,
151
+ encoding: 'utf8',
152
+ timeout: 30000,
153
+ env: {
154
+ PATH: path || `${bin}:${process.env.PATH}`,
155
+ HOME: repo,
156
+ TMPDIR: process.env.TMPDIR ?? '/tmp',
157
+ CODEX_FAKE_ARGV: argvFile,
158
+ CODEX_FAKE_ENV: envFile,
159
+ CODEX_FAKE_STDIN: stdinFile,
160
+ ...env,
161
+ },
162
+ }, (error, stdout, stderr) => {
163
+ const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '');
164
+ done({ status: error ? (error.code ?? 1) : 0, stdout, stderr, argv: readIf(argvFile), capEnv: readIf(envFile), capStdin: readIf(stdinFile) });
165
+ });
166
+ child.stdin.end(input);
167
+ });
168
+
124
169
  describe('codex-exec.sh — quality-first model/effort guard (1.1)', () => {
125
170
  it('refuses a non-default CODEX_MODEL and never spends a run', () => {
126
171
  const sb = makeSandbox();
@@ -344,11 +389,11 @@ describe('codex-exec.sh — leaner prompt (1.4)', () => {
344
389
  });
345
390
  });
346
391
 
347
- describe('codex-exec.sh — hard timeout (1.3)', () => {
348
- it('kills a hung codex at CODEX_HARD_TIMEOUT and reports it', () => {
392
+ describe('codex-exec.sh — hard timeout (1.3)', { concurrency: true }, () => {
393
+ it('kills a hung codex at CODEX_HARD_TIMEOUT and reports it', async () => {
349
394
  const sb = makeSandbox();
350
395
  const started = Date.now();
351
- const r = run(sb, { env: { CODEX_FAKE_SLEEP: '30', CODEX_HARD_TIMEOUT: '2' } });
396
+ const r = await runAsync(sb, { env: { CODEX_FAKE_SLEEP: '30', CODEX_HARD_TIMEOUT: '2' } });
352
397
  const elapsed = Date.now() - started;
353
398
  rmSync(sb.root, { recursive: true, force: true });
354
399
  assert.ok(elapsed < 18000, `must return well under the kill-after window, took ${elapsed}ms`);
@@ -358,7 +403,7 @@ describe('codex-exec.sh — hard timeout (1.3)', () => {
358
403
 
359
404
  it('warns and runs uncapped when neither timeout nor gtimeout is on PATH', () => {
360
405
  const sb = makeSandbox();
361
- const path = `${sb.bin}:${makePathWithout(sb.root, ['timeout', 'gtimeout'])}`;
406
+ const path = `${sb.bin}:${farmFor(['timeout', 'gtimeout'])}`;
362
407
  const r = run(sb, { path });
363
408
  rmSync(sb.root, { recursive: true, force: true });
364
409
  assert.equal(r.status, 0, r.stderr);
@@ -368,7 +413,7 @@ describe('codex-exec.sh — hard timeout (1.3)', () => {
368
413
 
369
414
  it('resume runs uncapped (and warns) when no timeout binary is on PATH', () => {
370
415
  const sb = makeSandbox();
371
- const path = `${sb.bin}:${makePathWithout(sb.root, ['timeout', 'gtimeout'])}`;
416
+ const path = `${sb.bin}:${farmFor(['timeout', 'gtimeout'])}`;
372
417
  const r = run(sb, { args: ['--resume', 'sess-1', '-'], input: 'go', path });
373
418
  rmSync(sb.root, { recursive: true, force: true });
374
419
  assert.equal(r.status, 0, r.stderr);
@@ -545,7 +590,7 @@ describe('codex-exec.sh — environment preflight (fail fast, before a run)', ()
545
590
  it('STOPs with 127 when codex is not on PATH', () => {
546
591
  const sb = makeSandbox();
547
592
  // PATH WITHOUT the fake codex bin and without any real codex.
548
- const path = makePathWithout(sb.root, ['codex']);
593
+ const path = farmFor(['codex']);
549
594
  const r = run(sb, { path });
550
595
  rmSync(sb.root, { recursive: true, force: true });
551
596
  assert.equal(r.status, 127);
@@ -556,7 +601,7 @@ describe('codex-exec.sh — environment preflight (fail fast, before a run)', ()
556
601
  it('STOPs with 127 when git is not on PATH', () => {
557
602
  const sb = makeSandbox();
558
603
  // codex present (sb.bin) but git stripped — exercises the type -P git guard.
559
- const path = `${sb.bin}:${makePathWithout(sb.root, ['git'])}`;
604
+ const path = `${sb.bin}:${farmFor(['git'])}`;
560
605
  const r = run(sb, { path });
561
606
  rmSync(sb.root, { recursive: true, force: true });
562
607
  assert.equal(r.status, 127);
@@ -681,7 +726,7 @@ const runHelp = (arg) => {
681
726
  const root = mkdtempSync(join(tmpdir(), 'codex-exec-help-'));
682
727
  const nongit = join(root, 'nongit');
683
728
  mkdirSync(nongit, { recursive: true });
684
- const path = makePathWithout(root, ['codex', 'agy', 'git']);
729
+ const path = farmFor(['codex', 'agy', 'git']);
685
730
  const r = spawnSync('bash', [WRAPPER, arg], {
686
731
  cwd: nongit, encoding: 'utf8', timeout: 15000, env: { HOME: root, PATH: path },
687
732
  });
@@ -1027,20 +1072,20 @@ describe('codex-exec.sh — service tier knob (bridges 2.3.0)', () => {
1027
1072
 
1028
1073
  });
1029
1074
 
1030
- describe('codex-exec.sh — bridge settings file semantics (bridges 2.3.0)', () => {
1031
- it('env overrides file: CODEX_HARD_TIMEOUT env=2 file=9999 → killed at the env cap', () => {
1075
+ describe('codex-exec.sh — bridge settings file semantics (bridges 2.3.0)', { concurrency: true }, () => {
1076
+ it('env overrides file: CODEX_HARD_TIMEOUT env=2 file=9999 → killed at the env cap', async () => {
1032
1077
  const sb = makeSandbox();
1033
1078
  writeSettings(sb, 'CODEX_HARD_TIMEOUT=9999\n');
1034
- const r = run(sb, { env: { CODEX_FAKE_SLEEP: '5', CODEX_HARD_TIMEOUT: '2' } });
1079
+ const r = await runAsync(sb, { env: { CODEX_FAKE_SLEEP: '5', CODEX_HARD_TIMEOUT: '2' } });
1035
1080
  rmSync(sb.root, { recursive: true, force: true });
1036
1081
  assert.notEqual(r.status, 0, 'the env cap (2s) must win over the file cap (9999s)');
1037
1082
  assert.match(r.stderr, /exceeded the hard cap CODEX_HARD_TIMEOUT=2s/);
1038
1083
  });
1039
1084
 
1040
- it('a file-set CODEX_HARD_TIMEOUT is effective (killed at the file cap)', () => {
1085
+ it('a file-set CODEX_HARD_TIMEOUT is effective (killed at the file cap)', async () => {
1041
1086
  const sb = makeSandbox();
1042
1087
  writeSettings(sb, 'CODEX_HARD_TIMEOUT=2\n');
1043
- const r = run(sb, { env: { CODEX_FAKE_SLEEP: '5' } });
1088
+ const r = await runAsync(sb, { env: { CODEX_FAKE_SLEEP: '5' } });
1044
1089
  rmSync(sb.root, { recursive: true, force: true });
1045
1090
  assert.notEqual(r.status, 0, 'the file cap must apply when the env is unset');
1046
1091
  assert.match(r.stderr, /exceeded the hard cap CODEX_HARD_TIMEOUT=2s/);