@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.
- package/CHANGELOG.md +49 -1
- package/README.md +1 -0
- package/SKILL.md +5 -1
- package/bin/install.mjs +10 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +20 -7
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +76 -18
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +59 -41
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +67 -22
- package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +20 -7
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +65 -17
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/recommendations.md +1 -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/tools/autonomy-doctor.mjs +1 -1
- package/tools/changed-surface.mjs +8 -8
- package/tools/commands.mjs +9 -0
- package/tools/grounding.mjs +13 -13
- package/tools/inject-methodology.mjs +71 -40
- 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 +9 -2
- package/tools/review-state.mjs +3 -3
- package/tools/sandbox-masks.mjs +13 -13
- package/tools/set-recipe.mjs +1 -1
- package/tools/velocity-profile.mjs +7 -7
- package/tools/worktrees.mjs +2292 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
1
|
+
import { describe, it, after } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import {
|
|
4
4
|
mkdtempSync, mkdirSync, writeFileSync, chmodSync, rmSync, readFileSync,
|
|
5
|
-
existsSync, readdirSync, symlinkSync,
|
|
5
|
+
existsSync, readdirSync, symlinkSync, cpSync,
|
|
6
6
|
} from 'node:fs';
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
8
|
import { join, dirname, resolve } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { spawnSync, execFile } from 'node:child_process';
|
|
11
11
|
|
|
12
12
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const WRAPPER = join(HERE, 'codex-exec.sh');
|
|
@@ -55,13 +55,16 @@ const FAKE_CODEX = [
|
|
|
55
55
|
'',
|
|
56
56
|
].join('\n');
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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}:${
|
|
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}:${
|
|
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 =
|
|
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}:${
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
50
|
-
|
|
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
|
|
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}:${
|
|
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
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
const
|
|
61
|
-
|
|
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
|
-
|
|
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 =
|
|
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}:${
|
|
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 =
|
|
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 =
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-kit",
|
|
3
|
-
"version": "3.
|
|
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
|
|
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
|
|
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 —
|
|
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 —
|
|
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 —
|
|
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 (
|
|
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 (
|
|
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];
|