@sabaiway/agent-workflow-kit 3.0.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 (33) hide show
  1. package/CHANGELOG.md +49 -1
  2. package/README.md +1 -0
  3. package/SKILL.md +5 -1
  4. package/bin/install.mjs +10 -1
  5. package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +20 -7
  6. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +76 -18
  7. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +59 -41
  8. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +67 -22
  9. package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +20 -7
  10. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +65 -17
  11. package/capability.json +1 -1
  12. package/package.json +1 -1
  13. package/references/modes/recommendations.md +1 -0
  14. package/references/modes/worktrees.md +120 -0
  15. package/references/scripts/archive-decisions.mjs +2 -2
  16. package/references/scripts/archive-decisions.test.mjs +5 -5
  17. package/references/scripts/check-docs-size-cli.test.mjs +41 -0
  18. package/references/scripts/check-docs-size.mjs +46 -29
  19. package/tools/autonomy-doctor.mjs +1 -1
  20. package/tools/changed-surface.mjs +8 -8
  21. package/tools/commands.mjs +9 -0
  22. package/tools/grounding.mjs +13 -13
  23. package/tools/inject-methodology.mjs +71 -40
  24. package/tools/migrate-adr-store.mjs +3 -3
  25. package/tools/procedures.mjs +1 -1
  26. package/tools/recipes.mjs +2 -2
  27. package/tools/recommendations.mjs +34 -7
  28. package/tools/release-scan.mjs +9 -2
  29. package/tools/review-state.mjs +3 -3
  30. package/tools/sandbox-masks.mjs +13 -13
  31. package/tools/set-recipe.mjs +1 -1
  32. package/tools/velocity-profile.mjs +7 -7
  33. 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, '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/);
@@ -5,9 +5,9 @@
5
5
  // CODEX_SERVICE_TIER BEFORE tier validation (no multiline warning echo, no silent standard-tier
6
6
  // run). Colocated separately — codex-review.test.mjs is red-proof-frozen; standalone harness.
7
7
 
8
- import { describe, it } from 'node:test';
8
+ import { describe, it, after } from 'node:test';
9
9
  import assert from 'node:assert/strict';
10
- import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, chmodSync, existsSync, readdirSync, symlinkSync } from 'node:fs';
10
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, chmodSync, existsSync, readdirSync, symlinkSync, cpSync } from 'node:fs';
11
11
  import { tmpdir } from 'node:os';
12
12
  import { join, dirname, resolve } from 'node:path';
13
13
  import { fileURLToPath } from 'node:url';
@@ -46,12 +46,17 @@ const makePathWithout = (root, exclude = []) => {
46
46
  return dir;
47
47
  };
48
48
 
49
- const makeSandbox = () => {
50
- const root = mkdtempSync(join(tmpdir(), 'codex-honesty-'));
49
+ // Farm + sandbox base are READ-ONLY per invocation — built ONCE, shared (a per-run farm rebuild
50
+ // plus a per-test `git init`+commit dominate the wall otherwise).
51
+ const SHARED_ROOT = mkdtempSync(join(tmpdir(), 'codex-honesty-shared-'));
52
+ after(() => rmSync(SHARED_ROOT, { recursive: true, force: true }));
53
+ const FARM = makePathWithout(SHARED_ROOT, ['codex']);
54
+
55
+ const TEMPLATE_ROOT = (() => {
56
+ const root = join(SHARED_ROOT, 'template-root');
51
57
  const bin = join(root, 'bin');
52
58
  mkdirSync(bin, { recursive: true });
53
59
  writeFileSync(join(bin, 'codex'), FAKE_CODEX, { mode: 0o755 });
54
- chmodSync(join(bin, 'codex'), 0o755);
55
60
  const repo = join(root, 'repo');
56
61
  mkdirSync(repo);
57
62
  const g = (...args) => spawnSync('git', args, { cwd: repo, encoding: 'utf8' });
@@ -62,7 +67,15 @@ const makeSandbox = () => {
62
67
  g('add', '-A');
63
68
  g('commit', '-qm', 'base');
64
69
  writeFileSync(join(repo, 'pending.txt'), 'PENDING\n');
65
- return { root, bin, repo };
70
+ return root;
71
+ })();
72
+
73
+ const makeSandbox = () => {
74
+ const root = mkdtempSync(join(tmpdir(), 'codex-honesty-'));
75
+ cpSync(TEMPLATE_ROOT, root, { recursive: true });
76
+ const bin = join(root, 'bin');
77
+ chmodSync(join(bin, 'codex'), 0o755);
78
+ return { root, bin, repo: join(root, 'repo') };
66
79
  };
67
80
 
68
81
  const run = (sb, { args = ['code'], env = {} } = {}) => {
@@ -73,7 +86,7 @@ const run = (sb, { args = ['code'], env = {} } = {}) => {
73
86
  timeout: 30000,
74
87
  env: {
75
88
  HOME: sb.repo,
76
- PATH: `${sb.bin}:${makePathWithout(sb.root, ['codex'])}`,
89
+ PATH: `${sb.bin}:${FARM}`,
77
90
  TMPDIR: process.env.TMPDIR ?? '/tmp',
78
91
  CODEX_HOME: join(sb.root, 'codex-home'),
79
92
  CODEX_FAKE_SENTINEL: sentinel,
@@ -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
  import { createHash } from 'node:crypto';
12
12
 
13
13
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -54,16 +54,23 @@ const FAKE_CODEX = [
54
54
  '',
55
55
  ].join('\n');
56
56
 
57
- // `clean: true` leaves a committed, pristine tree (for the no-diff preflight);
58
- // the default leaves one uncommitted untracked file so `code` mode has a diff to
59
- // review (otherwise the new no-diff preflight short-circuits before codex runs).
60
- const makeSandbox = ({ clean = false } = {}) => {
61
- const root = mkdtempSync(join(tmpdir(), 'codex-review-test-'));
57
+ // The PATH farms and the sandbox base are READ-ONLY per invocation, so both are built ONCE and
58
+ // shared (a per-call farm rebuild is thousands of symlinks; a per-test `git init`+commit
59
+ // dominates the sandbox cost).
60
+ const SHARED_ROOT = mkdtempSync(join(tmpdir(), 'codex-review-shared-'));
61
+ after(() => rmSync(SHARED_ROOT, { recursive: true, force: true }));
62
+ const farms = new Map();
63
+ const farmFor = (exclude) => {
64
+ const key = exclude.join('|');
65
+ if (!farms.has(key)) farms.set(key, makePathWithout(SHARED_ROOT, exclude));
66
+ return farms.get(key);
67
+ };
68
+
69
+ const TEMPLATE_ROOT = (() => {
70
+ const root = join(SHARED_ROOT, 'template-root');
62
71
  const bin = join(root, 'bin');
63
72
  mkdirSync(bin, { recursive: true });
64
- const stub = join(bin, 'codex');
65
- writeFileSync(stub, FAKE_CODEX, { mode: 0o755 });
66
- chmodSync(stub, 0o755);
73
+ writeFileSync(join(bin, 'codex'), FAKE_CODEX, { mode: 0o755 });
67
74
  const repo = join(root, 'repo');
68
75
  mkdirSync(repo);
69
76
  const g = (...args) => spawnSync('git', args, { cwd: repo, encoding: 'utf8' });
@@ -74,6 +81,18 @@ const makeSandbox = ({ clean = false } = {}) => {
74
81
  writeFileSync(join(repo, 'plan.md'), '# Plan\n\nDo a thing in two steps.\n');
75
82
  g('add', '-A');
76
83
  g('commit', '-qm', 'base');
84
+ return root;
85
+ })();
86
+
87
+ // `clean: true` leaves a committed, pristine tree (for the no-diff preflight);
88
+ // the default leaves one uncommitted untracked file so `code` mode has a diff to
89
+ // review (otherwise the new no-diff preflight short-circuits before codex runs).
90
+ const makeSandbox = ({ clean = false } = {}) => {
91
+ const root = mkdtempSync(join(tmpdir(), 'codex-review-test-'));
92
+ cpSync(TEMPLATE_ROOT, root, { recursive: true });
93
+ const bin = join(root, 'bin');
94
+ chmodSync(join(bin, 'codex'), 0o755);
95
+ const repo = join(root, 'repo');
77
96
  if (!clean) writeFileSync(join(repo, 'pending.txt'), 'an uncommitted change to review\n');
78
97
  return { root, bin, repo };
79
98
  };
@@ -123,6 +142,35 @@ const run = ({ repo, bin }, { args = ['code'], env = {}, path, cwd } = {}) => {
123
142
  return { ...r, codexHome, argv: readIf(argvFile), capEnv: readIf(envFile), capStdin: readIf(stdinFile) };
124
143
  };
125
144
 
145
+ // Async twin of run() for the sleep-bound timeout test: spawnSync blocks the event loop for
146
+ // the whole deliberate wait, so a concurrent describe could not overlap it. Same contract.
147
+ const runAsync = ({ repo, bin }, { args = ['code'], env = {}, path, cwd } = {}) =>
148
+ new Promise((done) => {
149
+ const argvFile = join(repo, '.cap-argv');
150
+ const envFile = join(repo, '.cap-env');
151
+ const stdinFile = join(repo, '.cap-stdin');
152
+ const codexHome = join(repo, '..', 'codex-home');
153
+ const child = execFile('bash', [WRAPPER, ...args], {
154
+ cwd: cwd || repo,
155
+ encoding: 'utf8',
156
+ timeout: 30000,
157
+ env: {
158
+ PATH: path || `${bin}:${process.env.PATH}`,
159
+ HOME: repo,
160
+ TMPDIR: process.env.TMPDIR ?? '/tmp',
161
+ CODEX_HOME: codexHome,
162
+ CODEX_FAKE_ARGV: argvFile,
163
+ CODEX_FAKE_ENV: envFile,
164
+ CODEX_FAKE_STDIN: stdinFile,
165
+ ...env,
166
+ },
167
+ }, (error, stdout, stderr) => {
168
+ const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '');
169
+ done({ status: error ? (error.code ?? 1) : 0, stdout, stderr, codexHome, argv: readIf(argvFile), capEnv: readIf(envFile), capStdin: readIf(stdinFile) });
170
+ });
171
+ child.stdin.end();
172
+ });
173
+
126
174
  describe('codex-review.sh — quality-first model/effort guard (1.1)', () => {
127
175
  it('refuses a non-default CODEX_MODEL', () => {
128
176
  const sb = makeSandbox();
@@ -255,11 +303,11 @@ describe('codex-review.sh — subscription / config isolation (invariant)', () =
255
303
  });
256
304
  });
257
305
 
258
- describe('codex-review.sh — hard timeout (1.3)', () => {
259
- it('kills a hung review at CODEX_HARD_TIMEOUT and reports it', () => {
306
+ describe('codex-review.sh — hard timeout (1.3)', { concurrency: true }, () => {
307
+ it('kills a hung review at CODEX_HARD_TIMEOUT and reports it', async () => {
260
308
  const sb = makeSandbox();
261
309
  const started = Date.now();
262
- const r = run(sb, { env: { CODEX_FAKE_SLEEP: '30', CODEX_HARD_TIMEOUT: '2' } });
310
+ const r = await runAsync(sb, { env: { CODEX_FAKE_SLEEP: '30', CODEX_HARD_TIMEOUT: '2' } });
263
311
  const elapsed = Date.now() - started;
264
312
  rmSync(sb.root, { recursive: true, force: true });
265
313
  assert.ok(elapsed < 18000, `must return well under the kill-after window, took ${elapsed}ms`);
@@ -269,7 +317,7 @@ describe('codex-review.sh — hard timeout (1.3)', () => {
269
317
 
270
318
  it('warns and runs uncapped when neither timeout nor gtimeout is on PATH', () => {
271
319
  const sb = makeSandbox();
272
- const path = `${sb.bin}:${makePathWithout(sb.root, ['timeout', 'gtimeout'])}`;
320
+ const path = `${sb.bin}:${farmFor(['timeout', 'gtimeout'])}`;
273
321
  const r = run(sb, { path });
274
322
  rmSync(sb.root, { recursive: true, force: true });
275
323
  assert.equal(r.status, 0, r.stderr);
@@ -414,7 +462,7 @@ describe('codex-review.sh — optional structured findings (2.2)', () => {
414
462
  describe('codex-review.sh — environment preflight (fail fast, before a run)', () => {
415
463
  it('STOPs with 127 when codex is not on PATH', () => {
416
464
  const sb = makeSandbox();
417
- const path = makePathWithout(sb.root, ['codex']); // no fake codex, no real codex
465
+ const path = farmFor(['codex']); // no fake codex, no real codex
418
466
  const r = run(sb, { args: ['code'], path });
419
467
  rmSync(sb.root, { recursive: true, force: true });
420
468
  assert.equal(r.status, 127);
@@ -570,7 +618,7 @@ const runHelp = (arg) => {
570
618
  const root = mkdtempSync(join(tmpdir(), 'codex-review-help-'));
571
619
  const nongit = join(root, 'nongit');
572
620
  mkdirSync(nongit, { recursive: true });
573
- const path = makePathWithout(root, ['codex', 'agy', 'git']);
621
+ const path = farmFor(['codex', 'agy', 'git']);
574
622
  const r = spawnSync('bash', [WRAPPER, arg], {
575
623
  cwd: nongit, encoding: 'utf8', timeout: 15000, env: { HOME: root, PATH: path },
576
624
  });
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "3.0.0",
6
+ "version": "3.1.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -24,6 +24,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
24
24
  - `sandbox-lane` — surface this note TOGETHER with the sandbox-lanes ladder below (the ladder IS the practical half of the note — inline, never a pointer). Pure DISCOVERABILITY: it surfaces the manifest-declared observed session-sandbox recipe (egress hosts ∪ resolved writable state dirs — `networkHosts` ∪ `writableDirs` of the wired bridges' `capability.json`, the single documentation source) and converges on a NEUTRAL fingerprint acknowledgement recorded by the consent-gated **ack writer** into the family-owned `docs/ai/acks.json` (`sandboxLaneAck`; a changed recipe re-fires the item). The store is family-owned so no host settings validator guards it (AD-055 relocated the ack off the Claude Code settings schema, which rejected the unknown key); the legacy `"agentWorkflow": { "sandboxLaneAck": … }` settings-scope key is still READ for one deprecation window (until the next kit MAJOR). It never claims the settings security keys take effect on any host class, never recommends writing them, and the kit never seeds `sandbox.network.allowedDomains` / `sandbox.filesystem.allowWrite` (bridge council 2026-07-11, both backends concur: a network pre-allow widens egress for EVERY sandboxed command; a write allowance on CLI state dirs would expose credential dirs). Posture history: an IDE-managed session sandbox was live-observed (2026-07-11/12) ignoring hand-applied settings security keys in BOTH scopes, and codex needs a writable HOME (EROFS `~/.codex` in-sandbox); whether a session's sandbox honors the settings keys is runtime-unknowable from the advisor (a denial-only signal) — which is exactly why the item states only detectable facts and no zero-prompt promise on any host class.
25
25
 
26
26
  - `read-lane` — enabling the opt-in read-only compound lane auto-approves *compounds* (and singles) of the seeded read-only core that carry ZERO shell metaprogramming: an UNATTENDED trust extension, bounded by the audited read-only core (never a command outside it; prompt-bypass only, never a sandbox bypass) and applied regardless of which of those core commands you seeded as individual settings rules. It is a PROJECT-PERSISTENT declaration in `docs/ai/lanes.json` — every future session, subagents' Bash too where the host fires hooks on subagent Bash, and (committed) every checkout. The apply depends on state: when the lane is OFF, it is the `gate-hook --read-lane` preview (whose own currency check refuses a stale hook — a pre-1.48 hook never reads `lanes.json`); when the placed hook is STALE (an enabled lane over an old hook) or MISSING, the item instead surfaces a **delete-to-reseed** / re-place recovery (a destructive `rm` + `--apply`, an attention item — never the safe preview). Risk profile: a bounded read-only trust-posture extension — no write/exec exposure beyond the audited core.
27
+ - `worktrees-dir` — on a settings-native host that honors the key, the HAND-APPLY line widens the OS-sandbox WRITE surface to the whole worktrees parent dir: every sibling path under it (other repositories included) becomes agent-writable, and the widening persists for every later session. A harness-managed host may ignore that project setting; grant the narrow parent through host/session controls or use the provision terminal fallback instead. When that scope is wider than you want, narrow it FIRST: create a dedicated dir yourself (outside the agent's write surface), point `docs/ai/worktrees.json` `parentDir` at it, then re-run recommendations — the item re-renders with the narrowed dir. The kit never writes sandbox filesystem allowances itself; the line is always yours to paste. Risk profile: a real write-surface widening where honored — scope it deliberately.
27
28
 
28
29
  **Sandbox lanes (what to DO with the `sandbox-lane` recipe, per host class):**
29
30
 
@@ -0,0 +1,120 @@
1
+ ### Mode: worktrees
2
+
3
+ Parallel feature worktrees (v1) — several features implemented simultaneously in DIFFERENT agent
4
+ sessions on one machine/repo, zero interference on working-tree files (the ONE exception: the
5
+ default `node_modules` symlink is a shared MUTABLE dependency cache — see below), unambiguous
6
+ ownership. One thin tool over git; every verification datum is recomputed live from git, never
7
+ read from stored metadata.
8
+ The ONE stored-metadata exception is the PREPARED OID recorded in the handoff: land and cleanup read it back only for recovery.
9
+
10
+ **Run** — `node ${CLAUDE_SKILL_DIR}/tools/worktrees.mjs <subcommand> …`:
11
+
12
+ Git ≥ 2.36 is required for NUL-terminated worktree porcelain; an older Git fails closed with its
13
+ own verbatim error through the existing Git-error surface.
14
+
15
+ - `provision <slug> --plan <path> [--as <name>.md] [--dir <path>] [--branch <name>] [--include <path>]... [--install] [--resume]`
16
+ — create a feature worktree (default: the visible sibling `<repoParent>/<repoName>--<slug>`,
17
+ branch `aw/<slug>`) and populate it: the registry-derived footprint copy-if-missing (a tracked
18
+ file is NEVER overwritten), EXACTLY ONE seeded feature plan, the `handoff-<slug>.md` stub
19
+ (written at provision — the tool's own record; `list` and `cleanup` read it), a
20
+ `node_modules` symlink when main has one and the link stays ignored — a SHARED MUTABLE cache:
21
+ writes through it hit MAIN's node_modules; for isolation RUN the printed isolated-install
22
+ command (`--install` only prints it), and on `--resume` an existing symlink is kept — run the
23
+ printed unlink-first recovery first. Absolute root-pinned gate commands are rebased on UNTRACKED copies only — and only
24
+ while their bytes still equal the MAIN source (or its rebased form); user-modified copies stay
25
+ byte-untouched. `--install` only PRINTS the install command — zero spawn, zero write.
26
+ `--resume <slug>` completes a half-done provision (identity fail-closed; handoff user sections,
27
+ the seeded plan, and edited copies are preserved byte-exact; the provision-record section is
28
+ refreshed atomically; copy-if-missing everywhere).
29
+ Resume identity also binds the EXISTING handoff: at most one `handoff-<slug>.md` may exist and
30
+ its recorded slug AND branch must match the live invocation — a second handoff, a name/record
31
+ mismatch, or a handoff that is not a regular file is a typed STOP before anything is written
32
+ (the writability probe itself runs only after these checks on a resume). The provision record
33
+ is read from exactly one REQUIRED `## Provision record` section (a decoy field elsewhere cannot
34
+ hijack identity); a missing or repeated section, or a duplicated single-valued field, is a typed
35
+ STOP, never last-wins.
36
+ - `list` — read-only: every worktree of this repo with slug (from the handoff file; none →
37
+ "unknown (foreign)"), path, branch, base OID (the worktree HEAD — under the v1 no-commit bar
38
+ that IS the provision base; a manual satellite commit moves it, and land derives its own base
39
+ live), dirty flag, handoff presence, opener suggestion. Honest read errors: only a genuinely
40
+ ABSENT docs/plans under a symlink-free, present worktree dir reads as `handoff: no`; a
41
+ symlinked docs/plans (or ancestor), a handoff-named entry that is not a regular file, a
42
+ vanished worktree dir, or any other read failure renders `handoff: (unreadable)` — never a
43
+ silent "no".
44
+ - `land <slug> --prepare` — stage the satellite's finished diff onto a CLEAN main; the commit is
45
+ NEVER run by the tool — it stays a dialogue ask at the primary session. Land takes the transient
46
+ common-git-dir lock, refuses a dirty main, graph divergence, visible `docs/ai` drift, excluded
47
+ staged paths, unstaged/untracked leftovers, an empty diff, or a red satellite review-state. The
48
+ complete satellite working-tree diff versus its base is inspected (staged and unstaged); every
49
+ unstaged/untracked path is listed and refused, so an accepted transfer cannot silently omit it.
50
+ The binary/no-ext-diff/no-textconv transfer excludes exactly `docs/ai` and `docs/plans`, applies
51
+ into the index, optionally runs the porcelain-visible sync adapter, then runs the main gate
52
+ matrix. It reports main HEAD, TRANSFER, PREPARED, and sync delta OIDs/data; the primary re-attests
53
+ that prepared tree and confirms main HEAD still equals the printed OID before the commit ask.
54
+ - `cleanup <slug> [--branch <name>] [--abandon]` — take the same transient lock and remove a LANDED
55
+ worktree fail-closed after live landed-verification against main HEAD. Verification uses exactly
56
+ the land exclusions (`docs/ai`, `docs/plans`), then checks untracked and ignored content before a
57
+ plain worktree remove, branch `-d`, and prune. Provision-derived ignored containers are ephemeral;
58
+ `node_modules` of any kind is provision-derivable because provision explicitly advises installs.
59
+ Foreign content stops cleanup. `--abandon` is the ONE destructive arm: it DESTROYS unlanded work,
60
+ requires the handoff identity, and is the only path where `--force` may appear.
61
+
62
+ **Honesty:** there is NO preview step on the writers — over-warned by design. The tool never
63
+ commits, never pushes, never runs a subscription CLI. Every content read and regular-file copy
64
+ goes through its one no-follow descriptor door (identity-bound source, exclusive destination,
65
+ descriptor mode update), and tripwire tests keep them the only paths.
66
+
67
+ **Settings:** the parent dir for new worktrees is the `docs/ai/worktrees.json` `{"parentDir": …}`
68
+ project setting (hand-editable strict JSON; absent file → the sibling default; malformed → a
69
+ typed STOP, never a guess). The file must be a REGULAR, non-symlink file reached through a
70
+ symlink-free path — anything else is a typed STOP (the advisor renders the same shape as a
71
+ stated skip), and the ancestor chain is verified even when the leaf is ABSENT (a symlinked
72
+ `docs/` or `docs/ai` never reads as plain absence). `--dir` overrides per invocation.
73
+
74
+ **Host-specific consent (zero-prompt only where the host honors it):** every sibling-dir mutation
75
+ runs a REAL create+delete writability probe first. An unwritable parent prints both the
76
+ settings-native line (`sandbox.filesystem.allowWrite` in `.claude/settings.json`) and the full
77
+ terminal command. On a settings-native host that honors the key, adding the parent can make later
78
+ provision/cleanup promptless. On a harness-managed host that ignores project settings, grant the
79
+ narrow parent through the host/session controls; if that is unavailable, use the printed terminal
80
+ command for each operation. The `recommendations` mode surfaces this lane but treats write access
81
+ as unverified without a trusted host-capability signal.
82
+
83
+ **Landing flow:** provision → work → handoff → land → re-attest → commit → cleanup. Satellite
84
+ commits are outside v1: graph divergence stops land and prints cherry-pick/rebase recovery. A gate
85
+ failure keeps the prepared main tree and names both recovery lanes. A second prepare is reset-only:
86
+ the STOP prints the current staged write-tree, compares it with the PREPARED OID recorded in the
87
+ handoff, distinguishes a converged re-run from foreign staged work, and lists removal commands for
88
+ untracked crash residue before the reset-and-re-run lane. A killed process may leave
89
+ `aw-prepare-lock`; after confirming no land/cleanup process owns it, remove that directory by hand.
90
+
91
+ The optional `scripts/sync-mirrors.mjs` adapter runs as a child. Its contract is porcelain-visible
92
+ output: tracked modifications/deletions or untracked-not-ignored creations. Ignored writes and
93
+ changes inside an already-untracked file are outside observation and therefore out of contract.
94
+ Cleanup reports EBUSY as likely lingering processes/open file descriptors (including a sandbox
95
+ mount); close them and retry, outside the sandbox when needed. Hidden satellite `docs/ai` state is
96
+ ephemeral by design; durable content belongs in the handoff before landing — the handoff carries a
97
+ free-form session-records digest slot (every section outside `## Provision record` is user-owned
98
+ and byte-preserved by the tool).
99
+
100
+ **Ownership:** MAIN owns MAIN-tree files, commits, pushes, releases, the gate matrix, every
101
+ docs/ai record, `docs/plans/queue.md`, and all shared git state — stash, hooks, repo config,
102
+ `.git/info/exclude`, and every ref except the satellite's configured branch. The SATELLITE owns
103
+ that one branch (`aw/<slug>` or the `--branch` override), its feature edits, its seeded plan, and
104
+ the user-owned handoff sections; `## Provision record` remains tool-owned. Satellite forbidden
105
+ verbs (the v1 docs-only bar): no `git commit`/`push`/`tag`/`git stash`/history rewrite — the ONE
106
+ legal rewrite is the tool-printed `git reset --hard` recovery of the satellite's OWN configured
107
+ branch (`aw/<slug>` or the `--branch` override) — no kit lifecycle writers
108
+ (`init`/`upgrade`/`setup`/`hide-footprint`/`install-git-hooks`/`sandbox-masks`/`ack-write`), no
109
+ queue.md writes, no version bumps or publishes, no edits to MAIN's files from the satellite
110
+ session — divergence and the landed verification enforce the observable half; the rest is the
111
+ stated contract. A symlinked `node_modules` under npm workspaces resolves
112
+ workspace self-links to MAIN-tree sources — use the printed isolated install when that matters.
113
+
114
+ **Other harnesses:** PROVEN — the host-installed codex/agy review wrappers run with a provisioned
115
+ worktree as their cwd; the footprint carries the root `AGENTS.md` required by the codex wrappers
116
+ and available to agy as its fallback project context. The host-level `bridge-settings.conf` under
117
+ `${XDG_CONFIG_HOME:-~/.config}/agent-workflow/` is per-host, shared by every worktree, and is not
118
+ copied into the worktree. ASSUMED until probed per harness — each target harness's project-local
119
+ settings and context pickup in a fresh worktree session; treat that as unverified until the target
120
+ harness demonstrates it.
@@ -649,7 +649,7 @@ const runMigrate = (root, flags, today, deps, log, logError) => {
649
649
  verifyConservation(oldItems, newItems);
650
650
  // Integrity over the FULL final store — existing records ∪ freshly-exploded — not just the new
651
651
  // writes: a pre-existing adr/ record whose id also stays RETAINED in HOT would otherwise leave the
652
- // ADR in two places (codex R4). Same full-store pattern as runRotate.
652
+ // ADR in two places. Same full-store pattern as runRotate.
653
653
  const finalStoreById = new Map(existingStore.map((e) => [e.id, { id: e.id, idNum: e.idNum, fileName: e.fileName }]));
654
654
  for (const r of records) finalStoreById.set(r.id, { id: r.id, idNum: r.idNum, fileName: r.fileName });
655
655
  assertStoreIntegrity(retained, [...finalStoreById.values()]);
@@ -783,7 +783,7 @@ const runRotate = (root, flags, today, deps, log, logError) => {
783
783
  }
784
784
 
785
785
  const records = explode(toExplode, today);
786
- // Crash-resume (fold-induced, codex R2): a record from a prior crashed rotate may already be on
786
+ // Crash-resume (fold-induced): a record from a prior crashed rotate may already be on
787
787
  // disk. A byte-identical one is done (deduped, not a duplicate-id error); a divergent-body one is
788
788
  // corrupt → FAIL. The FINAL store = existing ∪ freshly-exploded, deduped by id.
789
789
  const existingById = new Map(existingStore.map((e) => [e.id, e]));
@@ -341,7 +341,7 @@ describe('1.3 --migrate --apply — records + snapshot + retire monoliths + HOT
341
341
  assert.equal(run(['--check', '--today=2026-07-09'], root).code, 0, 'the resumed tree passes --check');
342
342
  });
343
343
 
344
- it('refuses when a pre-existing adr/ record duplicates an ADR that stays in HOT (never two places — codex R4)', () => {
344
+ it('refuses when a pre-existing adr/ record duplicates an ADR that stays in HOT (never two places — review-adr-archive-r04-major-01)', () => {
345
345
  const root = makeRoot();
346
346
  seedLegacy(root, { hot: ['005', '006'], warm: ['003'], cold: ['001'] });
347
347
  mkdirSync(join(root, ADR_DIR_REL), { recursive: true });
@@ -452,7 +452,7 @@ describe('1.4 --check', () => {
452
452
  assert.match(errText, /duplicate ADR id AD-002/);
453
453
  });
454
454
 
455
- it('a decisions.md with NO maxLines cap fails loud (never operates against an unknown budget — codex R4)', () => {
455
+ it('a decisions.md with NO maxLines cap fails loud (never operates against an unknown budget — review-adr-archive-r04-major-02)', () => {
456
456
  const root = makeRoot();
457
457
  mkdirSync(join(root, 'docs', 'ai'), { recursive: true });
458
458
  const noCapFm = '---\ntype: reference\nlastUpdated: 2026-01-01\nscope: permanent\nstaleAfter: never\nowner: none\n---\n';
@@ -514,7 +514,7 @@ describe('1.4 --check', () => {
514
514
  assert.match(errText, /two records for AD-001/);
515
515
  });
516
516
 
517
- it('a NESTED subdirectory in adr/ fails loud (the store is a flat directory — codex R2)', () => {
517
+ it('a NESTED subdirectory in adr/ fails loud (the store is a flat directory — review-adr-archive-r02-major-01)', () => {
518
518
  const root = makeRoot();
519
519
  seedMigrated(root, { hotIds: ['005'], storeIds: ['001'] });
520
520
  mkdirSync(join(root, ADR_DIR_REL, 'nested'), { recursive: true });
@@ -561,7 +561,7 @@ describe('1.4 default rotate — explode the oldest beyond cap + regenerate the
561
561
  assert.equal(regenCalls.length, 0, 'a no-op never regenerates the index');
562
562
  });
563
563
 
564
- it('is crash-resumable: a byte-identical record from a crashed prior rotate is deduped, not a fatal duplicate (codex R2)', () => {
564
+ it('is crash-resumable: a byte-identical record from a crashed prior rotate is deduped, not a fatal duplicate (review-adr-archive-r02-major-02)', () => {
565
565
  const root = makeRoot();
566
566
  mkdirSync(join(root, ADR_DIR_REL), { recursive: true });
567
567
  const blocks = ['005', '006', '007', '008'].map((id) => adrBlock(id));
@@ -579,7 +579,7 @@ describe('1.4 default rotate — explode the oldest beyond cap + regenerate the
579
579
  assert.equal(run(['--check', '--today=2026-07-09'], root).code, 0);
580
580
  });
581
581
 
582
- it('a no-op rotate still REFUSES a corrupt store (partition) — never a silent green no-op (codex R2)', () => {
582
+ it('a no-op rotate still REFUSES a corrupt store (partition) — never a silent green no-op (review-adr-archive-r02-major-03)', () => {
583
583
  const root = makeRoot();
584
584
  seedMigrated(root, { hotIds: ['005'], storeIds: ['001'] }); // under cap
585
585
  const rec = explode(parseDecisionsText(tierText(999, '# T', [adrBlock('010')]), 'x').entries, '2026-07-09')[0];