@sabaiway/agent-workflow-kit 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +82 -1
  2. package/README.md +1 -0
  3. package/SKILL.md +7 -3
  4. package/bin/install.mjs +10 -1
  5. package/bridges/antigravity-cli-bridge/SKILL.md +6 -4
  6. package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +20 -7
  7. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +84 -7
  8. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +189 -18
  9. package/bridges/antigravity-cli-bridge/bin/agy.sh +55 -6
  10. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +77 -41
  11. package/bridges/antigravity-cli-bridge/capability.json +4 -2
  12. package/bridges/antigravity-cli-bridge/references/driving-agy.md +5 -0
  13. package/bridges/codex-cli-bridge/SKILL.md +8 -4
  14. package/bridges/codex-cli-bridge/bin/codex-exec.sh +129 -11
  15. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +278 -23
  16. package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +20 -7
  17. package/bridges/codex-cli-bridge/bin/codex-review.sh +76 -13
  18. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +112 -17
  19. package/bridges/codex-cli-bridge/capability.json +19 -4
  20. package/bridges/codex-cli-bridge/references/driving-codex.md +11 -0
  21. package/capability.json +1 -1
  22. package/package.json +1 -1
  23. package/references/modes/bootstrap.md +3 -2
  24. package/references/modes/recommendations.md +1 -0
  25. package/references/modes/upgrade.md +9 -4
  26. package/references/modes/velocity.md +2 -0
  27. package/references/modes/worktrees.md +120 -0
  28. package/references/scripts/archive-decisions.mjs +2 -2
  29. package/references/scripts/archive-decisions.test.mjs +5 -5
  30. package/references/scripts/check-docs-size-cli.test.mjs +41 -0
  31. package/references/scripts/check-docs-size.mjs +46 -29
  32. package/references/shared/command-shapes.md +22 -0
  33. package/references/shared/composition-handoff.md +7 -2
  34. package/references/templates/agent_rules.md +10 -1
  35. package/tools/autonomy-doctor.mjs +1 -1
  36. package/tools/bridge-settings-read.mjs +25 -5
  37. package/tools/changed-surface.mjs +8 -8
  38. package/tools/commands.mjs +9 -0
  39. package/tools/delegation.mjs +2 -2
  40. package/tools/detect-backends.mjs +10 -0
  41. package/tools/grounding.mjs +13 -13
  42. package/tools/inject-methodology.mjs +71 -40
  43. package/tools/lens-region.mjs +113 -36
  44. package/tools/migrate-adr-store.mjs +3 -3
  45. package/tools/procedures.mjs +1 -1
  46. package/tools/recipes.mjs +2 -2
  47. package/tools/recommendations.mjs +34 -7
  48. package/tools/release-scan.mjs +12 -2
  49. package/tools/review-state.mjs +3 -3
  50. package/tools/sandbox-masks.mjs +13 -13
  51. package/tools/set-recipe.mjs +1 -1
  52. package/tools/velocity-profile.mjs +7 -7
  53. 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
  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
  });
@@ -684,6 +732,12 @@ describe('codex-review.sh — --help contract (manifest-pinned)', () => {
684
732
  assert.equal(norm(helpSection(help, 'Receipt:').join(' ')), norm(REVIEW_CONTRACT.receipt));
685
733
  assert.match(REVIEW_CONTRACT.receipt, /sha256 over the canonical uncommitted-state payload/, 'the fingerprint definition lives in the manifest contract');
686
734
  });
735
+
736
+ it('Notes renders the manifest review contract.notes verbatim (AD-061 — a typed contract key that MUST surface)', () => {
737
+ const help = runHelp('--help').stdout;
738
+ assert.ok((REVIEW_CONTRACT.notes ?? []).length >= 2, 'the review contract declares the banner-only-timeout + quote-verbatim notes');
739
+ assert.equal(norm(helpSection(help, 'Notes:').join(' ')), norm(REVIEW_CONTRACT.notes.join(' ')));
740
+ });
687
741
  });
688
742
 
689
743
  describe('codex-review.sh — source-level reverse guard (parser arms ⟷ manifest)', () => {
@@ -1277,4 +1331,45 @@ describe('codex-review.sh — dispatch-posture labeling (D5)', () => {
1277
1331
  assert.equal(receipts.length, 0);
1278
1332
  assert.match(r.stderr, /control/i, 'named as the control-byte class, not a policy refusal');
1279
1333
  });
1334
+
1335
+ it('the banner appends the RESOLVED hard timeout — banner-only, never in the receipt (AD-061)', () => {
1336
+ const sb = makeSandbox();
1337
+ const r = run(sb, {});
1338
+ const receipts = readReceipts(sb.repo);
1339
+ rmSync(sb.root, { recursive: true, force: true });
1340
+ assert.equal(r.status, 0, r.stderr);
1341
+ assert.match(r.stderr, /^review posture: model=gpt-5\.6-sol effort=xhigh tier=standard timeout=1800s$/m);
1342
+ assert.deepEqual(Object.keys(receipts[0].posture), ['model', 'effort', 'tier'], 'timeout never enters the receipt posture');
1343
+ });
1344
+
1345
+ it('an INVALID effective CODEX_HARD_TIMEOUT (env — the closed aw_settings_valid bypass) warns and the banner prints the default', () => {
1346
+ const sb = makeSandbox();
1347
+ const r = run(sb, { env: { CODEX_HARD_TIMEOUT: 'nonsense' } });
1348
+ rmSync(sb.root, { recursive: true, force: true });
1349
+ assert.equal(r.status, 0, r.stderr);
1350
+ assert.match(r.stderr, /invalid value 'nonsense' for CODEX_HARD_TIMEOUT/, 'the fallback is loud');
1351
+ assert.match(r.stderr, /^review posture: .* timeout=1800s$/m, 'the banner prints the built-in default');
1352
+ });
1353
+
1354
+ it('a CODEX_HARD_TIMEOUT carrying CONTROL BYTES refuses pre-spend (the banner-field screen)', () => {
1355
+ const sb = makeSandbox();
1356
+ const r = run(sb, { env: { CODEX_HARD_TIMEOUT: `1800${String.fromCharCode(1)}` } });
1357
+ const receipts = readReceipts(sb.repo);
1358
+ rmSync(sb.root, { recursive: true, force: true });
1359
+ assert.notEqual(r.status, 0);
1360
+ assert.equal(r.capStdin, '', 'codex is never invoked');
1361
+ assert.equal(receipts.length, 0);
1362
+ assert.match(r.stderr, /control/i);
1363
+ });
1364
+
1365
+ it('a DEL (0x7f) byte in a banner field refuses pre-spend like the C0 range', () => {
1366
+ const sb = makeSandbox();
1367
+ const r = run(sb, { env: { CODEX_MODEL: `gpt-5.6-sol${String.fromCharCode(127)}` } });
1368
+ const receipts = readReceipts(sb.repo);
1369
+ rmSync(sb.root, { recursive: true, force: true });
1370
+ assert.notEqual(r.status, 0);
1371
+ assert.equal(r.capStdin, '', 'codex is never invoked');
1372
+ assert.equal(receipts.length, 0);
1373
+ assert.match(r.stderr, /control/i);
1374
+ });
1280
1375
  });
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "codex-cli-bridge",
5
5
  "kind": "execution-backend",
6
- "version": "3.0.0",
6
+ "version": "3.1.0",
7
7
  "posture": { "model": "gpt-5.6-sol", "effort": "xhigh", "tier": null },
8
8
  "provides": ["execute", "review"],
9
9
  "roles": {
@@ -27,7 +27,11 @@
27
27
  "probeRelaxed": ["--add-dir*", "-C*", "--cd*", "--skip-git-repo-check", "--ignore-rules", "--enable*", "--disable*"]
28
28
  },
29
29
  "notes": [
30
- "nested-sandbox limit: codex-exec ships its OWN OS sandbox (bwrap workspace-write) and cannot run nested inside a harness sandbox (the FS turns read-only) — route it OUTSIDE the harness sandbox (excludedCommands / a per-run consented bypass) on the OBSERVED bwrap/EPERM failure, never a preemptive blanket"
30
+ "nested-sandbox limit: codex-exec ships its OWN OS sandbox (bwrap workspace-write) and cannot run nested inside a harness sandbox (the FS turns read-only) — route it OUTSIDE the harness sandbox (excludedCommands / a per-run consented bypass) on the OBSERVED bwrap/EPERM failure, never a preemptive blanket",
31
+ "exec posture banner: ONE stderr line before dispatch states the ACTUAL run posture — exec posture: model=… effort=… tier=… sandbox=workspace-write session=fresh|resume:<id> timeout=… — from RESOLVED post-validation values; the resume id is validated pre-spend, and control bytes in any banner field refuse pre-spend",
32
+ "threat model: the sidecar byte and grammar screens detect corrupted input under a trusted parent environment. A hostile parent environment — including exported shell functions or PATH substitution of core/backend commands — is outside the threat model and can substitute the backend itself. Targeted shadow-proof resolution protects banner/dispatch honesty from accidental shadowing; it is not an environment security boundary",
33
+ "the exec posture banner appends a banner-only timeout=<duration|uncapped> field — exactly the duration handed to timeout(1), uncapped when no timeout/gtimeout binary caps the run; INFORMATIONAL only: it is never persisted in a receipt or session sidecar",
34
+ "quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts"
31
35
  ]
32
36
  }
33
37
  },
@@ -43,7 +47,11 @@
43
47
  ],
44
48
  "grounding": "automatic — the wrapper precomputes the full working-tree change set (repo map, status, diffs, untracked contents) and codex auto-merges the root AGENTS.md; no grounding flags",
45
49
  "continue": [],
46
- "receipt": "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides): 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 mode; verdict parsed from the mandated literal verdict line (schema mode: the verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge, factsHash null); probe = whether the run relaxed the quality guards (CODEX_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, effort, tier} (tier null on the standard 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, and a posture value carrying control bytes refuses pre-spend in every mode; a run whose final message carries NO recognized 'Verdict: <ship|revise|rethink>' line — empty or missing 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"
50
+ "receipt": "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides): 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 mode; verdict parsed from the mandated literal verdict line (schema mode: the verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge, factsHash null); probe = whether the run relaxed the quality guards (CODEX_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, effort, tier} (tier null on the standard 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, and a posture value carrying control bytes refuses pre-spend in every mode; a run whose final message carries NO recognized 'Verdict: <ship|revise|rethink>' line — empty or missing 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",
51
+ "notes": [
52
+ "the review posture banner appends a banner-only timeout=<duration|uncapped> field — exactly the duration handed to timeout(1), uncapped when no timeout/gtimeout binary caps the run; INFORMATIONAL only: it never enters the receipt posture or the D5 banner↔receipt parity",
53
+ "quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts"
54
+ ]
47
55
  }
48
56
  }
49
57
  },
@@ -72,7 +80,8 @@
72
80
  "guardrails": [
73
81
  { "value": "runs under codex's OWN OS sandbox (workspace-write)", "enforcement": "enforced", "condition": "it cannot nest inside another sandbox — the FS turns read-only; route it outside on the OBSERVED failure", "source": "capability.json roles.execute.contract.notes" },
74
82
  { "value": "the guarded passthrough blocks model / sandbox / approval overrides", "enforcement": "enforced", "source": "bin/codex-exec.sh" },
75
- { "value": "hard wall-clock cap CODEX_HARD_TIMEOUT (built-in default 3600s)", "enforcement": "enforced", "condition": "only while timeout(1)/gtimeout is on PATH — otherwise the wrapper warns and runs uncapped", "source": "capability.json settings.CODEX_HARD_TIMEOUT" }
83
+ { "value": "hard wall-clock cap CODEX_HARD_TIMEOUT (built-in default 3600s)", "enforcement": "enforced", "condition": "only while timeout(1)/gtimeout is on PATH — otherwise the wrapper warns and runs uncapped", "source": "capability.json settings.CODEX_HARD_TIMEOUT" },
84
+ { "value": "ONE exec posture stderr banner line states the ACTUAL run posture before dispatch (session=fresh|resume:<id>; timeout is banner-only)", "enforcement": "enforced", "source": "bin/codex-exec.sh" }
76
85
  ],
77
86
  "customHooks": ["CODEX_PROBE"]
78
87
  },
@@ -87,6 +96,9 @@
87
96
  "operands": [
88
97
  { "slot": "<plan-file|->", "required": true, "description": "the follow-up instruction file, or - to read it from stdin" }
89
98
  ],
99
+ "guardrails": [
100
+ { "value": "ONE exec posture stderr banner line states the ACTUAL run posture before dispatch (session=resume:<id> only AFTER the sidecar id is resolved and validated; timeout is banner-only)", "enforcement": "enforced", "source": "bin/codex-exec.sh" }
101
+ ],
90
102
  "customHooks": ["CODEX_PROBE"]
91
103
  },
92
104
  {
@@ -100,6 +112,9 @@
100
112
  { "slot": "<session-id>", "required": true, "description": "the session id the original run printed on stderr" },
101
113
  { "slot": "<plan-file|->", "required": true, "description": "the follow-up instruction file, or - to read it from stdin" }
102
114
  ],
115
+ "guardrails": [
116
+ { "value": "ONE exec posture stderr banner line states the ACTUAL run posture before dispatch (session=resume:<id> only AFTER the explicit id is validated against the session-id grammar; timeout is banner-only)", "enforcement": "enforced", "source": "bin/codex-exec.sh" }
117
+ ],
103
118
  "customHooks": ["CODEX_PROBE"]
104
119
  },
105
120
  {
@@ -27,6 +27,17 @@ model or effort for a real run — the wrapper will stop you. Quota is metered i
27
27
  never from a downgrade. The only opt-out is a throwaway, effort-independent probe: `CODEX_PROBE=1`
28
28
  (loud) — never use its output as real work.
29
29
 
30
+ ## Posture banners — quote them verbatim
31
+
32
+ Every dispatch states its ACTUAL posture on ONE stderr line: `codex-exec` emits
33
+ `exec posture: model=… effort=… tier=… sandbox=workspace-write session=fresh|resume:<id> timeout=…`
34
+ (fresh and resume alike, only after the resume id is resolved and validated), `codex-review` emits
35
+ `review posture: model=… effort=… tier=… timeout=…`. When you label a dispatch for a user or a
36
+ record, **quote the posture banner verbatim** — the banner is the machine-stated posture; a prose
37
+ re-type drifts. The `timeout=` field is **banner-only** (exactly the duration handed to
38
+ `timeout(1)`, or `uncapped` when no capping binary is on PATH) — informational, never part of a
39
+ receipt or the banner↔receipt parity.
40
+
30
41
  ## Exec vs review
31
42
 
32
43
  Use **`codex-exec`** when there is a concrete plan or focused instruction to implement, the project
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.2.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.2.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",
@@ -1,6 +1,6 @@
1
1
  ### Mode: bootstrap
2
2
 
3
- Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKILL_DIR}/references/shared/composition-handoff.md · ${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md
3
+ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKILL_DIR}/references/shared/composition-handoff.md · ${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md · ${CLAUDE_SKILL_DIR}/references/shared/command-shapes.md
4
4
 
5
5
  > Bundled sources below (templates, scripts) live in **this skill's own directory** — `${CLAUDE_SKILL_DIR}/` in Claude Code, or the folder containing this `SKILL.md` in Codex / other agents. Use that as the copy/read source; the working directory is the **target project**, not the skill.
6
6
 
@@ -9,7 +9,8 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
9
9
  1. **Recon (read-only).** Before writing anything:
10
10
  - `package.json` / `pyproject.toml` / `go.mod` / `Cargo.toml` → stack, package manager, scripts.
11
11
  - `ls -la` root → `README`, existing `AGENTS.md`/`CLAUDE.md`, CI configs, linter/formatter configs.
12
- - `git log --oneline -30` + `git status` → recent activity, branch, uncommitted changes.
12
+ - `git log --oneline -30` → recent activity.
13
+ - `git status` → branch, uncommitted changes.
13
14
  - `src/` (or equivalent) 2–3 levels deep → modules, routes/pages, components, services, types.
14
15
  - Tests (framework, location, E2E?) and linter rules.
15
16
  - Record: stack, package manager, daily commands (`dev`/`test`/`lint`/`type-check`), routes/pages, architecture layers.
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  ### Mode: upgrade
2
2
 
3
- Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKILL_DIR}/references/shared/composition-handoff.md · ${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md
3
+ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKILL_DIR}/references/shared/composition-handoff.md · ${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md · ${CLAUDE_SKILL_DIR}/references/shared/command-shapes.md
4
4
 
5
5
  1. Read `docs/ai/.workflow-version` (the project's stamped lineage). If missing, treat as a pre-versioned deployment and offer to re-bootstrap conservatively.
6
6
  2. **Never-downgrade gate — FIRST, before any write.** Compare the stamp to the **deployment-lineage head** (`3.0.0` — NOT this kit's package version). If the stamp is **greater than the head** or unparseable → **STOP and report**; do not touch a newer / unknown deployment at all (not even the methodology slot). This STOP is one of the few places the number is actionable (*Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`): show the user **the `docs/ai` structure version** their deployment carries versus the one this kit expects, plus the plain one-line two-axes note — naming it the structure version, **never** "lineage head".
@@ -48,7 +48,12 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
48
48
  (refresh it with `npx @sabaiway/agent-workflow-engine@latest init`, then re-run) · **over the
49
49
  file's line cap — refused** (trim the file, re-run). The section is found by its heading — no
50
50
  markers; a renamed heading is preserved + noted. A fully absent/invalid engine is the same hard
51
- STOP as (c). Exit 0 covers every soft outcome; only the STOP is non-zero.
51
+ STOP as (c). Exit 0 covers every soft outcome; only the STOP is non-zero. The same invocation
52
+ also reconciles the **Communication (user-facing messages)** section from the kit's own bundled
53
+ template canon (no engine involved) — relay its line too: *refreshed* / *already current* /
54
+ *custom edit preserved + note* / *section absent — noted (never an insert)* / *over the line
55
+ cap — refused*; an unreadable bundled template canon is its own loud STOP naming the kit
56
+ reinstall command.
52
57
 
53
58
  **Bridge settings reconcile — stamp-independent, same gate, BEFORE the equal-head short-circuit.**
54
59
  Run `node ${CLAUDE_SKILL_DIR}/tools/bridge-settings.mjs --reconcile` and **paste its outcome line
@@ -56,7 +61,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
56
61
  **NEVER writes** it (the file lives outside every kit tree — D2), so an unknown/retired key is
57
62
  flagged + preserved, never edited. Runs on **every** upgrade; exit 0 covers every outcome.
58
63
  4. **Equal-head exit — a real successful-exit report, not a bare stop.** If the stamp **equals** the head, the lineage is up to date — but step 3 (the stamp-independent reconciles) ran first and may have changed things, so this is a proper exit report, not a no-op:
59
- - **Report step 3's outcome in plain language** — for **each** pointer (workflow-methodology, orchestration-recipes and autonomy-policy) whether it was *added*, was *already present* (nothing changed), or was *skipped* (the soft-skip from step 3, with its reason — over the line limit / engine too old / the autonomy pointer's anchor absent); whether the `docs/ai/orchestration.json` config was *seeded* (created from the template), had its onboarding note *refreshed*, was *already current*, or carried a *customized note that was preserved* (a user edit is never clobbered); whether the `docs/ai/gates.json` gate declaration was *seeded* or was *already present* (preserved byte-for-byte); whether the `docs/ai/autonomy.json` declaration was *seeded* (the sparse defaults-equivalent note) or was *already present* (preserved byte-for-byte); whether the enforcement-script ensure *added* the `archive-decisions` pair to `scripts/`, found it *already present*, or found an *old ADR layout — migration instructed*; the **placed-bridge refresh** outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / `skipped-readonly` / *could not refresh* + recovery); the **agent-rules lens** outcome (*refreshed* / *already current* / *custom edit preserved + note* / *file absent* / *engine too old* / *over the line cap*); the **bridge-settings reconcile** outcome (paste the tool's line verbatim); and, for a hidden deployment, whether the hidden-mode footprint was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility / a leftover machine-wide block). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
64
+ - **Report step 3's outcome in plain language** — for **each** pointer (workflow-methodology, orchestration-recipes and autonomy-policy) whether it was *added*, was *already present* (nothing changed), or was *skipped* (the soft-skip from step 3, with its reason — over the line limit / engine too old / the autonomy pointer's anchor absent); whether the `docs/ai/orchestration.json` config was *seeded* (created from the template), had its onboarding note *refreshed*, was *already current*, or carried a *customized note that was preserved* (a user edit is never clobbered); whether the `docs/ai/gates.json` gate declaration was *seeded* or was *already present* (preserved byte-for-byte); whether the `docs/ai/autonomy.json` declaration was *seeded* (the sparse defaults-equivalent note) or was *already present* (preserved byte-for-byte); whether the enforcement-script ensure *added* the `archive-decisions` pair to `scripts/`, found it *already present*, or found an *old ADR layout — migration instructed*; the **placed-bridge refresh** outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / `skipped-readonly` / *could not refresh* + recovery); the **agent-rules lens** outcome (*refreshed* / *already current* / *custom edit preserved + note* / *file absent* / *engine too old* / *over the line cap*) and the **Communication-section** outcome (its own set: refreshed / already current / custom preserved + note / section absent — noted / over the cap — refused); the **bridge-settings reconcile** outcome (paste the tool's line verbatim); and, for a hidden deployment, whether the hidden-mode footprint was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility / a leftover machine-wide block). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
60
65
  - **Never surface the structure number on this exit.** Whatever step 3 did, do **not** recite the `docs/ai` structure version, the internal versioning vocabulary, or the two-axes note here — the number is inert on an equal-head exit; it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md` (shown at the never-downgrade STOP, the explicit status view, or on an explicit ask). Frame the success itself per the final bullet: if step 3 changed anything, say **what changed** in plain human terms; only a pure zero-diff no-op is *settings already current — no update needed*.
61
66
  - **Render the mandatory Recommendations section — on this exit too, BEFORE the footer.** Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language: every fact, count and item from the tool, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; show the raw tool block on request. The section is present-even-when-empty (with everything optimal the body is exactly `no recommendations — flow optimal.`) and VERDICT-FIRST — the composed verdict line renders from the frozen templates `{K} item(s) need attention` / `nothing is broken` / `{N} optional recommendation(s), apply any you want` / `optimality NOT attested — {M} probe check(s) skipped`. Then OFFER the consent-gated applies: the user picks items in plain language; surface each picked item's posture note, get the explicit confirm, then run EXACTLY the rendered one-liners (a HAND-APPLY item is never run by you) — the full lane in `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`. Pinned order on this exit: Recommendations block → optional applies → report footer → the commit ask (the advisor/apply lane never lands after the commit ask).
62
67
  - **Live host/session facts are tool-composed only.** Any claim this report makes about the current host or session state — prompts fired, sandbox scope, whether a bypass was needed, network reachability, approval counts — must trace to **live tool output** from **this session** (the lines you just composed, or a probe you ran this run); a memory/handover snapshot is **context, never report facts**, and a claim with no live signal is **omitted or explicitly marked unverified** — never asserted from recollection. Full clause: *Live host/session facts* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`.
@@ -65,4 +70,4 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
65
70
  5. Show the relevant `${CLAUDE_SKILL_DIR}/CHANGELOG.md` diff (entries newer than the project's stamp).
66
71
  6. **Collect the migration answers FIRST, then apply.** If `AGENTS.md` is missing BOTH the *Communication language* and *Attribution* blocks — i.e. both blocks are missing (a pre-1.1.0 deployment) — ask the two questions as ONE structured multi-question prompt; record each answer individually, write nothing until ALL are answered, and carry the answers into the migrations below: a migration whose answer was already collected never re-asks (its own "Ask the user" step is the standalone fallback); a single missing block keeps its single ask (step 7). Then apply `${CLAUDE_SKILL_DIR}/migrations/<version>-<slug>.md` in **semver order**, only those newer than the project's stamp. Migrations are **idempotent** — safe to re-run.
67
72
  7. Reconcile drift: add any kernel files/scripts the project is missing; never clobber project-authored content (their `decisions.md`, `known_issues.md`, page specs stay). Any user question a migration raises follows the same rule as bootstrap — **structured multiple-choice where supported** (`AskUserQuestion` in Claude Code), otherwise prose. If `AGENTS.md` has no *Communication language* block (pre-1.1.0 deployment), **ask the user their conversational language** and insert the block — see `migrations/1.1.0-communication-language.md`. If it has no *Attribution* block (pre-1.2.0 deployment), **ask whether the agent may attribute work to itself / AI** and insert the block (defaulting to `off`) — see `migrations/1.2.0-agent-attribution.md`. (An answer already collected by the step-6 batched prompt is carried in — never re-asked here.)
68
- 8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`3.0.0`, not the package version — mechanics unchanged: the atomic write to the stamp file). In the report, **describe what the upgrade changed in plain human terms** — which parts of their `docs/ai` are now different (the migrations that ran), plus the step-3 **placed-bridge refresh** lines (pasted verbatim), the step-3 **agent-rules lens** outcome (same outcome set as step 4), the step-3 **bridge-settings reconcile** outcome, and the step-3 **autonomy-declaration ensure** outcome (*seeded* / *already present, preserved*) — rather than reciting a version number; **omit the raw structure number**, and do **not** print the two-axes note here (it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`, on demand only). Then **render the mandatory Recommendations section**: run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language (every fact, count and item, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; raw tool block on request; present-even-when-empty: `no recommendations — flow optimal.`), then OFFER the consent-gated applies (per picked item: posture note → explicit confirm → run EXACTLY the rendered one-liner; a HAND-APPLY item is never run by you — `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`). **Every current host/session claim in this report is tool-composed only** — prompts fired, sandbox scope, whether a bypass was needed, network reachability and approval counts must trace to **live tool output** from **this session**, a memory/handover snapshot is **context, never report facts**, and an unbacked claim is **omitted or explicitly marked unverified** (full clause: *Live host/session facts* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`). Then **print the report footer** in the canonical order (version block → one-line backend-status line → welcome mat — the shared contracts in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`; rendered from the helpers, same host-can't-run skip-with-reason; the welcome mat closes on one caveat-aware next step). Then **ask before committing** — the pinned order on this exit is: Recommendations block → optional applies → report footer → the commit ask.
73
+ 8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`3.0.0`, not the package version — mechanics unchanged: the atomic write to the stamp file). In the report, **describe what the upgrade changed in plain human terms** — which parts of their `docs/ai` are now different (the migrations that ran), plus the step-3 **placed-bridge refresh** lines (pasted verbatim), the step-3 **agent-rules lens** + **Communication-section** outcomes (same outcome sets as step 4), the step-3 **bridge-settings reconcile** outcome, and the step-3 **autonomy-declaration ensure** outcome (*seeded* / *already present, preserved*) — rather than reciting a version number; **omit the raw structure number**, and do **not** print the two-axes note here (it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`, on demand only). Then **render the mandatory Recommendations section**: run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language (every fact, count and item, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; raw tool block on request; present-even-when-empty: `no recommendations — flow optimal.`), then OFFER the consent-gated applies (per picked item: posture note → explicit confirm → run EXACTLY the rendered one-liner; a HAND-APPLY item is never run by you — `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`). **Every current host/session claim in this report is tool-composed only** — prompts fired, sandbox scope, whether a bypass was needed, network reachability and approval counts must trace to **live tool output** from **this session**, a memory/handover snapshot is **context, never report facts**, and an unbacked claim is **omitted or explicitly marked unverified** (full clause: *Live host/session facts* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`). Then **print the report footer** in the canonical order (version block → one-line backend-status line → welcome mat — the shared contracts in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`; rendered from the helpers, same host-can't-run skip-with-reason; the welcome mat closes on one caveat-aware next step). Then **ask before committing** — the pinned order on this exit is: Recommendations block → optional applies → report footer → the commit ask.
@@ -1,5 +1,7 @@
1
1
  ### Mode: velocity
2
2
 
3
+ Requires: ${CLAUDE_SKILL_DIR}/references/shared/command-shapes.md
4
+
3
5
  The opt-in onboarding **velocity profile** — it seeds a fixed, audited **read-only** Claude Code allowlist into `.claude/settings.json` so an agent stops idling on approval prompts for routine read-only commands while the maintainer is away. It is the family's **first programmatic `.claude/settings.json` writer** (attribution stayed an agent-driven prose merge). **In-agent, opt-in, writes only `.claude/settings.json`**, on one hard rule: **it never allowlists `commit`/`push`/`publish`** — so a direct commit/push/publish still ASKs; the only caveat is the trust-posture residual (below) — its closure is **shipped, opt-in: `${CLAUDE_SKILL_DIR}/references/modes/hook.md`**.
4
6
 
5
7
  **Version-status routing (like the other writer modes):** read `docs/ai/.workflow-version` first — not-deployed → bootstrap; stamp < `3.0.0` → `upgrade`; stamp > head / unparseable → STOP. The tool enforces this in code too (`--apply` STOPs unless the stamp is the lineage head).
@@ -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];