@sabaiway/agent-workflow-kit 2.1.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +94 -1
  2. package/README.md +7 -5
  3. package/SKILL.md +16 -8
  4. package/bin/install.mjs +11 -2
  5. package/bridges/antigravity-cli-bridge/SKILL.md +8 -1
  6. package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +163 -0
  7. package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +76 -0
  8. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +80 -15
  9. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +171 -22
  10. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +59 -41
  11. package/bridges/antigravity-cli-bridge/capability.json +3 -2
  12. package/bridges/codex-cli-bridge/SKILL.md +8 -1
  13. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +67 -22
  14. package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +156 -0
  15. package/bridges/codex-cli-bridge/bin/codex-review.sh +71 -10
  16. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +158 -27
  17. package/bridges/codex-cli-bridge/capability.json +3 -2
  18. package/capability.json +1 -1
  19. package/migrations/3.0.0-hardened-core-loop.md +44 -0
  20. package/migrations/README.md +1 -1
  21. package/package.json +2 -2
  22. package/references/hooks/gate-approve.mjs +1 -1
  23. package/references/modes/bootstrap.md +3 -3
  24. package/references/modes/commit-guard.md +19 -0
  25. package/references/modes/core-evidence.md +15 -0
  26. package/references/modes/coverage-check.md +12 -0
  27. package/references/modes/doc-parity.md +7 -6
  28. package/references/modes/gates.md +7 -6
  29. package/references/modes/grounding.md +1 -2
  30. package/references/modes/hook.md +1 -1
  31. package/references/modes/recommendations.md +1 -0
  32. package/references/modes/review-state.md +3 -3
  33. package/references/modes/upgrade.md +6 -6
  34. package/references/modes/velocity.md +2 -2
  35. package/references/modes/worktrees.md +120 -0
  36. package/references/scripts/archive-decisions.mjs +3 -3
  37. package/references/scripts/archive-decisions.test.mjs +5 -5
  38. package/references/scripts/check-docs-size-cli.test.mjs +41 -0
  39. package/references/scripts/check-docs-size.mjs +46 -29
  40. package/references/scripts/install-git-hooks-repo-exec.test.mjs +82 -0
  41. package/references/scripts/install-git-hooks.mjs +90 -18
  42. package/references/scripts/install-git-hooks.test.mjs +102 -0
  43. package/references/scripts/migrate-gates-branches.test.mjs +157 -0
  44. package/references/scripts/migrate-gates.mjs +395 -0
  45. package/references/scripts/migrate-gates.test.mjs +284 -0
  46. package/references/templates/agent_rules.md +2 -2
  47. package/tools/ack-write.mjs +1 -1
  48. package/tools/atomic-write.mjs +2 -2
  49. package/tools/autonomy-config.mjs +1 -1
  50. package/tools/autonomy-doctor.mjs +2 -2
  51. package/tools/autonomy-write.mjs +1 -1
  52. package/tools/bridge-settings-read.mjs +1 -1
  53. package/tools/bridge-settings.mjs +1 -1
  54. package/tools/changed-surface.mjs +16 -77
  55. package/tools/cheap-agents.mjs +2 -2
  56. package/tools/commands.mjs +26 -10
  57. package/tools/commit-guard.mjs +167 -0
  58. package/tools/core-evidence.mjs +914 -0
  59. package/tools/coverage-check.mjs +260 -0
  60. package/tools/delegation.mjs +1 -1
  61. package/tools/detect-backends.mjs +3 -3
  62. package/tools/doc-parity.mjs +11 -27
  63. package/tools/engine-source.mjs +1 -1
  64. package/tools/family-members.mjs +1 -1
  65. package/tools/family-registry.mjs +1 -1
  66. package/tools/fs-safe.mjs +1 -1
  67. package/tools/gate-hook.mjs +1 -1
  68. package/tools/{seed-gates.mjs → gates-init.mjs} +58 -52
  69. package/tools/grounding.mjs +19 -96
  70. package/tools/hide-footprint.mjs +1 -1
  71. package/tools/inject-methodology.mjs +72 -41
  72. package/tools/known-footprint.mjs +1 -1
  73. package/tools/labels.mjs +1 -1
  74. package/tools/lcov.mjs +6 -10
  75. package/tools/lens-region.mjs +1 -1
  76. package/tools/manifest/validate.mjs +19 -0
  77. package/tools/migrate-adr-store.mjs +5 -5
  78. package/tools/orchestration-config.mjs +1 -1
  79. package/tools/orchestration-write.mjs +1 -1
  80. package/tools/presentation.mjs +1 -1
  81. package/tools/procedures.mjs +3 -3
  82. package/tools/recipes.mjs +62 -11
  83. package/tools/recommendations.mjs +97 -14
  84. package/tools/release-scan.mjs +10 -3
  85. package/tools/renderers.mjs +1 -1
  86. package/tools/review-state.mjs +291 -343
  87. package/tools/run-gates.mjs +216 -92
  88. package/tools/sandbox-masks.mjs +14 -14
  89. package/tools/semver-lite.mjs +1 -1
  90. package/tools/set-autonomy.mjs +1 -1
  91. package/tools/set-recipe.mjs +2 -2
  92. package/tools/setup-backends.mjs +1 -1
  93. package/tools/surface.mjs +1 -1
  94. package/tools/uninstall.mjs +1 -1
  95. package/tools/velocity-profile.mjs +9 -9
  96. package/tools/view-model.mjs +1 -1
  97. package/tools/worktrees.mjs +2292 -0
  98. package/references/modes/fold-completeness.md +0 -30
  99. package/references/modes/review-ledger.md +0 -34
  100. package/references/templates/verification-profile.json +0 -10
  101. package/tools/fold-completeness-run.mjs +0 -1120
  102. package/tools/fold-completeness.mjs +0 -672
  103. package/tools/review-ledger-core.mjs +0 -428
  104. package/tools/review-ledger-write.mjs +0 -647
  105. package/tools/review-ledger.mjs +0 -630
  106. package/tools/sarif.mjs +0 -52
  107. package/tools/verification-profile.mjs +0 -219
@@ -0,0 +1,284 @@
1
+ // migrate-gates.test.mjs — spec for the consented legacy gates.json migration (strip-the-kit D8).
2
+ // The migration is ATOMIC and COMPLETE: canonical legacy entries removed + the unit-tests cmd
3
+ // extended with the lcov reporters + the coverage-check gate added LAST; customized entries are
4
+ // NEVER auto-touched (loud report + recovery); preview writes NOTHING; apply is tmp+rename.
5
+
6
+ import { describe, it } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, symlinkSync } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { spawnSync } from 'node:child_process';
12
+ import {
13
+ LEGACY_FORMS,
14
+ UNIT_TESTS_COVERAGE_FLAGS,
15
+ RETIRED_STORE_BASENAMES,
16
+ findRetiredStores,
17
+ buildMigrationPlan,
18
+ resultingGates,
19
+ formatPreview,
20
+ main,
21
+ } from './migrate-gates.mjs';
22
+
23
+ const KIT_TOOLS = mkdtempSync(join(tmpdir(), 'migrate-gates-kit-'));
24
+ writeFileSync(join(KIT_TOOLS, 'coverage-check.mjs'), '// the installed checker the migration points at\n');
25
+
26
+ const mkProject = (gates) => {
27
+ const root = mkdtempSync(join(tmpdir(), 'migrate-gates-'));
28
+ mkdirSync(join(root, 'docs', 'ai'), { recursive: true });
29
+ writeFileSync(join(root, 'docs', 'ai', 'gates.json'), `${JSON.stringify({ _README: 'mine', gates }, null, 2)}\n`);
30
+ return root;
31
+ };
32
+ const gatesOf = (root) => JSON.parse(readFileSync(join(root, 'docs', 'ai', 'gates.json'), 'utf8')).gates;
33
+ const quiet = () => {
34
+ const out = [];
35
+ const err = [];
36
+ return { log: (l) => out.push(String(l)), error: (l) => err.push(String(l)), out, err };
37
+ };
38
+
39
+ const LEGACY_LEDGER = { id: 'review-ledger', title: 'L', cmd: 'node "/kit/tools/review-ledger.mjs" --check' };
40
+ const LEGACY_FOLD = { id: 'fold-completeness', title: 'F', cmd: 'node /kit/tools/fold-completeness.mjs --check' };
41
+ const UNIT = { id: 'unit-tests', title: 'U', cmd: 'node --test tools/*.test.mjs' };
42
+ const CUSTOM = { id: 'my-ledger-wrap', title: 'C', cmd: 'node scripts/wrap.mjs && node /kit/tools/review-ledger.mjs --check' };
43
+
44
+ describe('migrate-gates — the pure migration plan', () => {
45
+ it('matches BOTH documented legacy forms (quoted and bare paths) and removes them', () => {
46
+ for (const form of LEGACY_FORMS) assert.ok(form.re instanceof RegExp);
47
+ const { plan } = buildMigrationPlan([LEGACY_LEDGER, LEGACY_FOLD], KIT_TOOLS);
48
+ assert.deepEqual(plan.filter((r) => r.action === 'remove').map((r) => r.entry.id), ['review-ledger', 'fold-completeness']);
49
+ });
50
+
51
+ it('extends the canonical unit-tests cmd with the lcov reporters (flags inserted after `node --test`)', () => {
52
+ const { plan, unitTestsExtended } = buildMigrationPlan([UNIT], KIT_TOOLS);
53
+ assert.ok(unitTestsExtended);
54
+ const extended = plan.find((r) => r.action === 'extend').entry;
55
+ assert.equal(extended.cmd, `node --test ${UNIT_TESTS_COVERAGE_FLAGS} tools/*.test.mjs`);
56
+ });
57
+
58
+ it('an already-extended unit-tests cmd is left alone (idempotent)', () => {
59
+ const done = { id: 'unit-tests', title: 'U', cmd: `node --test ${UNIT_TESTS_COVERAGE_FLAGS} tools/*.test.mjs` };
60
+ const { plan } = buildMigrationPlan([done], KIT_TOOLS);
61
+ assert.equal(plan.find((r) => r.entry.id === 'unit-tests').action, 'keep');
62
+ });
63
+
64
+ it('adds the coverage-check gate LAST with the RESOLVED quoted path; never a second one', () => {
65
+ const { plan } = buildMigrationPlan([UNIT], KIT_TOOLS);
66
+ const result = resultingGates(plan);
67
+ const last = result[result.length - 1];
68
+ assert.equal(last.id, 'coverage-check');
69
+ assert.equal(last.cmd, `node "${join(KIT_TOOLS, 'coverage-check.mjs')}" --check`);
70
+ const again = buildMigrationPlan(result, KIT_TOOLS);
71
+ assert.ok(!again.plan.some((r) => r.action === 'add'), 'a declaration already carrying the checker gains no duplicate');
72
+ });
73
+
74
+ it('a CUSTOMIZED dead-tool reference (compound form) is kept untouched and reported', () => {
75
+ const analysis = buildMigrationPlan([CUSTOM], KIT_TOOLS);
76
+ assert.equal(analysis.plan.find((r) => r.entry.id === 'my-ledger-wrap').action, 'keep');
77
+ assert.deepEqual(analysis.customized.map((g) => g.id), ['my-ledger-wrap']);
78
+ const preview = formatPreview(analysis, 'APPLY');
79
+ assert.match(preview, /CUSTOMIZED \(untouched\): my-ledger-wrap/);
80
+ assert.match(preview, /do NOT install the commit guard/);
81
+ });
82
+ });
83
+
84
+ describe('migrate-gates — the canonical anchor + final-capability validation (round-1 folds)', () => {
85
+ it('a canonical checker NOT in the last position is MOVED last (never left mid-list)', () => {
86
+ const canonical = { id: 'coverage-check', title: 'CC', cmd: `node "${join(KIT_TOOLS, 'coverage-check.mjs')}" --check` };
87
+ const { plan } = buildMigrationPlan([canonical, UNIT], KIT_TOOLS);
88
+ const result = resultingGates(plan);
89
+ assert.equal(result[result.length - 1].id, 'coverage-check', 'the canonical checker ends up LAST');
90
+ assert.ok(plan.some((r) => r.action === 'move' && r.entry.id === 'coverage-check'), 'the reorder is an explicit move action');
91
+ assert.ok(!plan.some((r) => r.action === 'add'), 'no duplicate checker is added');
92
+ });
93
+
94
+ it('a LOOKALIKE checker cmd is CUSTOMIZED (never counted canonical) and the canonical one is still added', () => {
95
+ const lookalike = { id: 'cov', title: 'C', cmd: 'node scripts/coverage-check.mjs --check' };
96
+ const analysis = buildMigrationPlan([lookalike, UNIT], KIT_TOOLS);
97
+ assert.ok(analysis.customized.some((g) => g.id === 'cov'), 'the lookalike is reported customized');
98
+ const result = resultingGates(analysis.plan);
99
+ assert.equal(result[result.length - 1].id, 'coverage-check', 'the REAL canonical checker is added last');
100
+ });
101
+
102
+ it('the result is judged final-capable ONLY with a canonical review-state present; missing → a LOUD warning with the candidate line, never "final-run-capable"', () => {
103
+ const analysis = buildMigrationPlan([UNIT], KIT_TOOLS);
104
+ assert.equal(analysis.finalCapable, false, 'no review-state → not final-capable');
105
+ const preview = formatPreview(analysis, 'APPLY');
106
+ assert.match(preview, /review-state/, 'the warning names the missing core check');
107
+ assert.match(preview, /"id": "review-state"|review-state\.mjs/, 'the paste-ready candidate is carried');
108
+ assert.doesNotMatch(preview, /already final-run-capable/);
109
+ const withRs = buildMigrationPlan(
110
+ [UNIT, { id: 'review-state', title: 'RS', cmd: `node "${join(KIT_TOOLS, 'review-state.mjs')}" --check` }],
111
+ KIT_TOOLS,
112
+ );
113
+ assert.equal(withRs.finalCapable, true);
114
+ });
115
+
116
+ it('a NON-canonical unit-tests cmd (npm test / wrapper) is CUSTOMIZED with the full flag set as the recovery', () => {
117
+ const npmTest = { id: 'unit-tests', title: 'U', cmd: 'npm test' };
118
+ const analysis = buildMigrationPlan([npmTest], KIT_TOOLS);
119
+ assert.equal(analysis.plan.find((r) => r.entry.id === 'unit-tests').action, 'keep');
120
+ assert.ok(analysis.customized.some((g) => g.id === 'unit-tests'), 'a non-canonical suite cmd is customized');
121
+ const preview = formatPreview(analysis, 'APPLY');
122
+ assert.match(preview, /--experimental-test-coverage/, 'the recovery carries the full flag set');
123
+ });
124
+
125
+ it('a PARTIALLY-flagged unit-tests cmd is CUSTOMIZED (a lone coverage flag never reads as configured)', () => {
126
+ const partial = { id: 'unit-tests', title: 'U', cmd: 'node --test --experimental-test-coverage tools/*.test.mjs' };
127
+ const analysis = buildMigrationPlan([partial], KIT_TOOLS);
128
+ assert.equal(analysis.plan.find((r) => r.entry.id === 'unit-tests').action, 'keep');
129
+ assert.ok(analysis.customized.some((g) => g.id === 'unit-tests'), 'the half-wired cmd is customized, never silently left');
130
+ });
131
+
132
+ it('a kitTools path with DQ-unsafe characters is a loud STOP before any write', () => {
133
+ const root = mkProject([UNIT]);
134
+ const evil = mkdtempSync(join(tmpdir(), 'migrate-gates-$evil-'));
135
+ writeFileSync(join(evil, 'coverage-check.mjs'), '// lookalike\n');
136
+ const io = quiet();
137
+ assert.equal(main(['--cwd', root, '--kit-tools', evil, '--apply'], io), 1);
138
+ assert.match(io.err.join('\n'), /double-quot|shell metacharacter/i);
139
+ rmSync(evil, { recursive: true, force: true });
140
+ rmSync(root, { recursive: true, force: true });
141
+ });
142
+
143
+ it('CUSTOMIZED warnings + the guard warning print even when apply has nothing else to do', () => {
144
+ const root = mkProject([CUSTOM]);
145
+ const io = quiet();
146
+ assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], io), 0);
147
+ const text = io.out.join('\n');
148
+ assert.match(text, /CUSTOMIZED \(untouched\): my-ledger-wrap/, 'the customized row survives the no-op path');
149
+ assert.match(text, /do NOT install the commit guard/i, 'the guard warning survives the no-op path');
150
+ rmSync(root, { recursive: true, force: true });
151
+ });
152
+
153
+ it('a SYMLINKED docs (or docs/ai) PARENT is a STOP on preview AND apply (never written through)', () => {
154
+ const outside = mkdtempSync(join(tmpdir(), 'migrate-gates-target-'));
155
+ mkdirSync(join(outside, 'ai'), { recursive: true });
156
+ writeFileSync(join(outside, 'ai', 'gates.json'), `${JSON.stringify({ gates: [UNIT] })}\n`);
157
+ const root = mkdtempSync(join(tmpdir(), 'migrate-gates-symparent-'));
158
+ symlinkSync(outside, join(root, 'docs'));
159
+ for (const argv of [['--cwd', root, '--kit-tools', KIT_TOOLS], ['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply']]) {
160
+ const io = quiet();
161
+ assert.equal(main(argv, io), 1, `must STOP: ${argv.join(' ')}`);
162
+ assert.match(io.err.join('\n'), /symlink/i);
163
+ }
164
+ rmSync(root, { recursive: true, force: true });
165
+ rmSync(outside, { recursive: true, force: true });
166
+ });
167
+ });
168
+
169
+ describe('migrate-gates — preview writes NOTHING; apply is atomic and complete', () => {
170
+ it('the dry-run default leaves gates.json byte-identical and prints the plan + the apply hint', () => {
171
+ const root = mkProject([LEGACY_LEDGER, UNIT]);
172
+ const before = readFileSync(join(root, 'docs', 'ai', 'gates.json'), 'utf8');
173
+ const io = quiet();
174
+ assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS], io), 0);
175
+ assert.equal(readFileSync(join(root, 'docs', 'ai', 'gates.json'), 'utf8'), before, 'dry-run must write nothing');
176
+ const text = io.out.join('\n');
177
+ assert.match(text, /REMOVE review-ledger/);
178
+ assert.match(text, /EXTEND unit-tests/);
179
+ assert.match(text, /ADD coverage-check/);
180
+ assert.match(text, /apply exactly this migration: node "\/[^"]*migrate-gates\.mjs" --kit-tools/, 'the apply hint is a REAL runnable path (never a file: URL)');
181
+ rmSync(root, { recursive: true, force: true });
182
+ });
183
+
184
+ it('--apply lands the full migration: legacy gone, unit-tests extended, coverage-check LAST', () => {
185
+ const root = mkProject([UNIT, LEGACY_LEDGER, LEGACY_FOLD]);
186
+ const io = quiet();
187
+ assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], io), 0);
188
+ const gates = gatesOf(root);
189
+ assert.deepEqual(gates.map((g) => g.id), ['unit-tests', 'coverage-check']);
190
+ assert.match(gates[0].cmd, /--experimental-test-coverage/);
191
+ assert.equal(gates[gates.length - 1].id, 'coverage-check');
192
+ rmSync(root, { recursive: true, force: true });
193
+ });
194
+
195
+ it('an ALREADY-migrated declaration is a stated no-op on apply', () => {
196
+ const root = mkProject([UNIT, LEGACY_LEDGER]);
197
+ assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], quiet()), 0);
198
+ const io = quiet();
199
+ assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], io), 0);
200
+ assert.match(io.out.join('\n'), /nothing to migrate/);
201
+ rmSync(root, { recursive: true, force: true });
202
+ });
203
+
204
+ it('a MISSING gates.json is a stated no-op; a MALFORMED one is a loud STOP (never written over)', () => {
205
+ const empty = mkdtempSync(join(tmpdir(), 'migrate-gates-none-'));
206
+ const io = quiet();
207
+ assert.equal(main(['--cwd', empty, '--kit-tools', KIT_TOOLS], io), 0);
208
+ assert.match(io.out.join('\n'), /nothing to migrate/);
209
+ rmSync(empty, { recursive: true, force: true });
210
+
211
+ const root = mkProject([]);
212
+ writeFileSync(join(root, 'docs', 'ai', 'gates.json'), '{ not json');
213
+ const io2 = quiet();
214
+ assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], io2), 1);
215
+ assert.match(io2.err.join('\n'), /malformed JSON/);
216
+ rmSync(root, { recursive: true, force: true });
217
+ });
218
+
219
+ it('a SYMLINKED gates.json is a STOP on preview AND apply', () => {
220
+ const root = mkdtempSync(join(tmpdir(), 'migrate-gates-link-'));
221
+ mkdirSync(join(root, 'docs', 'ai'), { recursive: true });
222
+ writeFileSync(join(root, 'real.json'), '{"gates":[]}\n');
223
+ symlinkSync(join(root, 'real.json'), join(root, 'docs', 'ai', 'gates.json'));
224
+ for (const argv of [['--cwd', root, '--kit-tools', KIT_TOOLS], ['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply']]) {
225
+ const io = quiet();
226
+ assert.equal(main(argv, io), 1);
227
+ assert.match(io.err.join('\n'), /symlink/);
228
+ }
229
+ rmSync(root, { recursive: true, force: true });
230
+ });
231
+
232
+ it('retired git-dir stores: previewed as CLEAN rows (nothing deleted), unlinked on apply, ENOENT a no-op', () => {
233
+ const root = mkProject([LEGACY_LEDGER]);
234
+ const g = (...a) => spawnSync('git', a, { cwd: root, encoding: 'utf8' });
235
+ g('init', '-q');
236
+ for (const name of RETIRED_STORE_BASENAMES) writeFileSync(join(root, '.git', name), '{"dead":1}\n');
237
+ assert.equal(findRetiredStores(root).length, RETIRED_STORE_BASENAMES.length, 'all three retired basenames are found');
238
+ const io = quiet();
239
+ assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS], io), 0);
240
+ assert.match(io.out.join('\n'), /CLEAN .*agent-workflow-review-ledger\.v5-orphans\.jsonl/, 'the v5-orphans archive is previewed too');
241
+ assert.equal(findRetiredStores(root).length, RETIRED_STORE_BASENAMES.length, 'the dry-run deleted NOTHING');
242
+ const io2 = quiet();
243
+ assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], io2), 0);
244
+ assert.deepEqual(findRetiredStores(root), [], 'apply unlinked every retired store');
245
+ assert.match(io2.out.join('\n'), /cleaned .*agent-workflow-fold-completeness\.jsonl/);
246
+ // A second apply: stores gone, declaration migrated — a stated no-op (ENOENT never errors).
247
+ const io3 = quiet();
248
+ assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], io3), 0);
249
+ assert.match(io3.out.join('\n'), /nothing to migrate/);
250
+ rmSync(root, { recursive: true, force: true });
251
+ });
252
+
253
+ it('stores alone (an already-migrated declaration) still get cleaned on apply', () => {
254
+ const root = mkProject([UNIT]);
255
+ const g = (...a) => spawnSync('git', a, { cwd: root, encoding: 'utf8' });
256
+ g('init', '-q');
257
+ assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], quiet()), 0); // migrate first
258
+ writeFileSync(join(root, '.git', RETIRED_STORE_BASENAMES[0]), '{"dead":1}\n');
259
+ const io = quiet();
260
+ assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], io), 0);
261
+ assert.match(io.out.join('\n'), /cleaned .*agent-workflow-review-ledger\.jsonl/);
262
+ assert.deepEqual(findRetiredStores(root), []);
263
+ rmSync(root, { recursive: true, force: true });
264
+ });
265
+
266
+ it('outside a git tree the store cleanup is a silent no-op (no crash, gates.json still migrates)', () => {
267
+ const root = mkProject([LEGACY_LEDGER, UNIT]);
268
+ const io = quiet();
269
+ assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], io), 0);
270
+ assert.deepEqual(gatesOf(root).map((g2) => g2.id), ['unit-tests', 'coverage-check']);
271
+ rmSync(root, { recursive: true, force: true });
272
+ });
273
+
274
+ it('--kit-tools is REQUIRED and must contain the checker (no runtime guessing)', () => {
275
+ const root = mkProject([UNIT]);
276
+ const io = quiet();
277
+ assert.equal(main(['--cwd', root], io), 2);
278
+ assert.match(io.err.join('\n'), /--kit-tools/);
279
+ const io2 = quiet();
280
+ assert.equal(main(['--cwd', root, '--kit-tools', root], io2), 1);
281
+ assert.match(io2.err.join('\n'), /coverage-check\.mjs/);
282
+ rmSync(root, { recursive: true, force: true });
283
+ });
284
+ });
@@ -76,7 +76,7 @@ Apply these when authoring a plan, reviewing, folding a finding, or editing code
76
76
  - **Per-round emission.** Every review round emits **{round N · finding-origin tally · per-backend verdict}** so the crossover is a computed, visible signal, not a remembered rule.
77
77
  - **Recipe fidelity.** Council runs every backend the recipe names, **every round**; silently dropping a ready backend for quota/convenience is a forbidden downgrade — an unavailable backend is a LOUD, stated degrade, never a quiet drop.
78
78
  - **ExitPlanMode ≠ execute.** A harness "approved — start coding" prompt authorizes the PLAN only; this methodology overrides it. Continue into execution only as a DELIBERATE transition after the plan + cold-start prompt exist, never an implicit slide.
79
- - **Cost lanes.** Route every step to the **cheapest adequate executor** — L0 deterministic script (the batched gate matrix over `gates.json`, the rotation `--check`s) · L1 cheap subagent (extraction/drafting only; the orchestrator verifies) · L2 subscription bridge · L3 frontier judgment. A step with **no named guardrail does not move down** a lane, and the **red lines never move down** (council review models · real code · ADR/handover/changelog-entry wording · persuasive copy · go/no-go · the approval asks). Own-error repair: salvage recorded state first (L0/L1, batched), never frontier re-derivation. **Prompt economy:** read-only fan-out (research/sweeps/extraction) runs ONLY on restricted-tool vehicles — a full-tool subagent for read-only work is a forbidden lane downgrade (invisible prompt-flood + blast radius), and a subagent is never told to shell out for facts obtainable read-only; the orchestrator's own shell form is ONE plain pipeline per call (a `;`/`&&` chain or env-prefixed invocation never matches a prefix allow rule); a fan-out launcher that gates per call yields to the agent-spawn lane — capability-gated: without restricted-tool vehicles (generic full-tool spawning does not count), read-only research stays in the orchestrator's own context, never a vehicle mandate a host cannot satisfy. Judgment, code, synthesis stay at the frontier lane (a task that genuinely runs/writes keeps a full-tool subagent); honest limit: no deterministic gate classifies a dispatch — canon at the point of use + placed vehicles + the retro loop. **Writer economy:** a stage's repeated WRITER commands batch into ONE invocation the review-ledger triad rides one batched write, other stage writers combine via one launcher per stage; never one writer call at a time (each write is its own prompt).
79
+ - **Cost lanes.** Route every step to the **cheapest adequate executor** — L0 deterministic script (the batched gate matrix over `gates.json`, the rotation `--check`s) · L1 cheap subagent (extraction/drafting only; the orchestrator verifies) · L2 subscription bridge · L3 frontier judgment. A step with **no named guardrail does not move down** a lane, and the **red lines never move down** (council review models · real code · ADR/handover/changelog-entry wording · persuasive copy · go/no-go · the approval asks). Own-error repair: salvage recorded state first (L0/L1, batched), never frontier re-derivation. **Prompt economy:** read-only fan-out (research/sweeps/extraction) runs ONLY on restricted-tool vehicles — a full-tool subagent for read-only work is a forbidden lane downgrade (invisible prompt-flood + blast radius), and a subagent is never told to shell out for facts obtainable read-only; the orchestrator's own shell form is ONE plain pipeline per call (a `;`/`&&` chain or env-prefixed invocation never matches a prefix allow rule); a fan-out launcher that gates per call yields to the agent-spawn lane — capability-gated: without restricted-tool vehicles (generic full-tool spawning does not count), read-only research stays in the orchestrator's own context, never a vehicle mandate a host cannot satisfy. Judgment, code, synthesis stay at the frontier lane (a task that genuinely runs/writes keeps a full-tool subagent); honest limit: no deterministic gate classifies a dispatch — canon at the point of use + placed vehicles + the retro loop. **Writer economy:** a stage's repeated WRITER commands batch evidence declarations ride consecutive plain invocations of ONE allow-listed tool, other stage writers combine via one launcher per stage; never an unbatched writer scatter (each gated write is its own prompt).
80
80
 
81
81
  ---
82
82
 
@@ -93,7 +93,7 @@ Split a complex task across sessions for **focus and review hygiene**, not becau
93
93
  ## 4. User Interaction
94
94
 
95
95
  1. **Don't rush to commit.** Prepare changes, run local quality checks, report progress with test outcomes.
96
- 2. **Get explicit approval** before staging/committing or moving to the next phase.
96
+ 2. **Get explicit approval** before COMMITTING or moving to the next phase — staging is reversible loop-work (the final-run ordering stages first, reviews the staged tree, then asks ONCE at the commit; a separate staging ask is a useless approval).
97
97
 
98
98
  ---
99
99
 
@@ -16,7 +16,7 @@
16
16
  // • atomic — exclusive-create *.tmp + rename (no partial-write state); last-writer-wins;
17
17
  // • never commits.
18
18
  //
19
- // Dependency-free beyond the kit's own atomic-write core + the shared ack constants, Node >= 18. No
19
+ // Dependency-free beyond the kit's own atomic-write core + the shared ack constants, Node >= 22. No
20
20
  // side effects on import (the isDirectRun idiom).
21
21
 
22
22
  import { lstatSync, readFileSync } from 'node:fs';
@@ -3,7 +3,7 @@
3
3
  // by (containment ROOT, absolute target, body, stop identity) so every consumer runs the same guarded
4
4
  // flow with zero drift:
5
5
  // • writeDocsAiFileAtomic — a file under a project's docs/ai/ (deployment-gated to cwd):
6
- // orchestration-write.mjs → docs/ai/orchestration.json; seed-gates.mjs → docs/ai/gates.json.
6
+ // orchestration-write.mjs → docs/ai/orchestration.json; gates-init.mjs → docs/ai/gates.json.
7
7
  // • writeHostConfigFileAtomic — a file under a host config dir OUTSIDE any project tree
8
8
  // (bridges 2.3.0, D6): ${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf. The
9
9
  // host dir is CREATED if absent (a host config SHOULD materialize), unlike the docs/ai gate
@@ -19,7 +19,7 @@
19
19
  // - tmp cleaned up on any failure after its creation.
20
20
  // - LAST-WRITER-WINS: local, single-user; no cross-process lock (documented, not silently assumed).
21
21
  //
22
- // Dependency-free, Node >= 18. Every fs primitive is injectable (deps.*) so the guards are
22
+ // Dependency-free, Node >= 22. Every fs primitive is injectable (deps.*) so the guards are
23
23
  // unit-testable. NEVER imported by a read-only module (procedures.mjs — pinned by an import guard).
24
24
 
25
25
  import { lstatSync, writeFileSync, renameSync, rmSync, mkdirSync } from 'node:fs';
@@ -16,7 +16,7 @@
16
16
  //
17
17
  // This module performs NO filesystem WRITES — only reads (loadAutonomy). The single fs-writer lives in
18
18
  // autonomy-write.mjs, which no read-only module imports, so the read surface stays fs-write-free.
19
- // Pure-where-possible (fs injectable), dependency-free, Node >= 18. No side effects on import.
19
+ // Pure-where-possible (fs injectable), dependency-free, Node >= 22. No side effects on import.
20
20
 
21
21
  import { readFileSync, lstatSync } from 'node:fs';
22
22
  import { join } from 'node:path';
@@ -35,7 +35,7 @@
35
35
  // a PATH shadow, never fixes it; (iii) the harness permission prompt shows the --apply tuple, not
36
36
  // the absolute-path command — bridged by the apply-time re-print of the exact resolved command.
37
37
  //
38
- // Dependency-free, Node >= 18. No side effects on import (the isDirectRun idiom).
38
+ // Dependency-free, Node >= 22. No side effects on import (the isDirectRun idiom).
39
39
 
40
40
  import { spawnSync } from 'node:child_process';
41
41
  import { closeSync, lstatSync, openSync } from 'node:fs';
@@ -254,7 +254,7 @@ const renderDiagnosis = ({ plan, probeResult, log }) => {
254
254
  }
255
255
  };
256
256
 
257
- // The untrusted degrade every lane runs FIRST (codex R1): while any sandbox binary resolves only
257
+ // The untrusted degrade every lane runs FIRST: while any sandbox binary resolves only
258
258
  // outside the trusted dirs, no offer, no consent invitation, and no privileged path may proceed —
259
259
  // an install cannot make such a host verify-ready.
260
260
  const refuseUntrusted = ({ plan, log }) => {
@@ -10,7 +10,7 @@
10
10
  // core (tools/atomic-write.mjs, AD-042) and is consumed here with this module's own STOP identity, so
11
11
  // the public API + error contract are its own (this file's tests pin them).
12
12
  //
13
- // Dependency-free, Node >= 18. Every fs primitive is injectable (deps.*) so the guards are unit-testable.
13
+ // Dependency-free, Node >= 22. Every fs primitive is injectable (deps.*) so the guards are unit-testable.
14
14
 
15
15
  import { AUTONOMY_REL, serializeAutonomy } from './autonomy-config.mjs';
16
16
  import { writeDocsAiFileAtomic } from './atomic-write.mjs';
@@ -6,7 +6,7 @@
6
6
  // lines, the manifest-as-source registry (the union of the bundled bridges' `settings` blocks), and
7
7
  // the effective value of each knob (env > file > built-in default). It NEVER writes.
8
8
  //
9
- // Dependency-free, Node >= 18. No side effects on import.
9
+ // Dependency-free, Node >= 22. No side effects on import.
10
10
 
11
11
  import { readFileSync, statSync } from 'node:fs';
12
12
  import { homedir } from 'node:os';
@@ -21,7 +21,7 @@
21
21
  // bundle (cannot determine the allowlist). main(argv, ctx) → { code, stdout, stderr }; getenv / home /
22
22
  // bundleRoot / fs are injectable for host-independent tests.
23
23
  //
24
- // Dependency-free, Node >= 18. No side effects on import (the isDirectRun idiom).
24
+ // Dependency-free, Node >= 22. No side effects on import (the isDirectRun idiom).
25
25
 
26
26
  import { pathToFileURL } from 'node:url';
27
27
  import { settingValueValid } from './manifest/validate.mjs';
@@ -1,20 +1,14 @@
1
1
  #!/usr/bin/env node
2
- // changed-surface.mjs — the NEUTRAL shared read-only core (BUGFREE-2 / AD-048, D4 + D8). One home
3
- // for the changed-surface computation that BOTH the fold-completeness runner (coverage domain) and
4
- // the review-ledger writer (the D4 diff-size cap) consume one computation, so the cap and the
5
- // coverage gate can never drift (the probeVerdict single-home precedent, AD-047). The D8 telemetry
6
- // fold-ledger read path lives beside it: fold-completeness.mjs already imports review-ledger.mjs,
7
- // so the review-ledger telemetry must reach the fold ledger WITHOUT importing back the other
8
- // direction — it imports THIS module instead.
2
+ // changed-surface.mjs — the NEUTRAL shared read-only core: ONE home for the changed-surface
3
+ // computation the coverage checker consumes (the closed classification + new-side line numbering),
4
+ // plus the shared fail-closed knob parser and the strict N/N probe-verdict algebra the
5
+ // core-evidence red-proof observer reads.
9
6
  //
10
7
  // Import-graph invariant (pinned by import-split tests): this module imports NOTHING from the
11
- // family — node built-ins only. Everyone may import it; it imports no one:
12
- // review-ledger.mjs → changed-surface.mjs ← fold-completeness.mjs ← fold-completeness-run.mjs
13
- // review-ledger-write.mjs → changed-surface.mjs (the writer NEVER imports the runner — the
14
- // sole-tree-toucher boundary, codex R2)
8
+ // family — node built-ins only. Everyone may import it; it imports no one.
15
9
  //
16
10
  // Read-only: never writes, never commits. It DOES spawn read-only `git` queries (diff/ls-files/
17
- // rev-parse). Dependency-free, Node >= 18. No side effects on import.
11
+ // rev-parse). Dependency-free. No side effects on import.
18
12
 
19
13
  import { readFileSync, lstatSync } from 'node:fs';
20
14
  import { join } from 'node:path';
@@ -43,7 +37,7 @@ export const classifyChangedPath = (rel) => {
43
37
 
44
38
  // Strip the quotes and decode escapes BYTE-wise (octal escapes are UTF-8 bytes) — an unparsed
45
39
  // quoted path compares unequal to its classifier/testId form and would silently escape the
46
- // coverage/cap surface (agy R5, AD-047).
40
+ // coverage/cap surface (AD-047).
47
41
  const CQUOTE_SIMPLE = { n: 10, t: 9, r: 13, f: 12, v: 11, b: 8, a: 7, '"': 34, '\\': 92 };
48
42
  export const unquoteDiffPath = (p) => {
49
43
  if (!(p.length >= 2 && p.startsWith('"') && p.endsWith('"'))) return p;
@@ -53,7 +47,7 @@ export const unquoteDiffPath = (p) => {
53
47
  const c = inner[i];
54
48
  if (c !== '\\') {
55
49
  // Consume a full CODE POINT — 16-bit-unit iteration would split a surrogate pair (an
56
- // unescaped non-BMP char, reachable under core.quotepath=false) into replacement bytes (agy R6).
50
+ // unescaped non-BMP char, reachable under core.quotepath=false) into replacement bytes.
57
51
  const ch = String.fromCodePoint(inner.codePointAt(i));
58
52
  for (const b of Buffer.from(ch, 'utf8')) bytes.push(b);
59
53
  i += ch.length - 1;
@@ -95,7 +89,7 @@ export const parseUnifiedDiff = (diffText) => {
95
89
  }
96
90
  if (inHeader && line.startsWith('--- ')) continue;
97
91
  if (inHeader && line.startsWith('+++ ')) {
98
- const p = unquoteDiffPath(line.slice(4).replace(/[\t\r]+$/, '')); // TAB/CR are git artifacts, never filename bytes (agy R6)
92
+ const p = unquoteDiffPath(line.slice(4).replace(/[\t\r]+$/, '')); // TAB/CR are git artifacts, never filename bytes
99
93
  current = p === '/dev/null' ? null : p.startsWith('b/') ? p.slice(2) : p;
100
94
  inHeader = false;
101
95
  continue;
@@ -120,7 +114,7 @@ export const parseUnifiedDiff = (diffText) => {
120
114
  // ── the changed surface (git-driven; the ONE computation both consumers read) ─────────────────────
121
115
 
122
116
  // The one diff-invocation shape every surface pass uses. The a/ b/ prefixes are pinned EXPLICITLY
123
- // (agy R6): a user's global diff.noprefix=true would otherwise drop them and the parsers would eat
117
+ // A user's global diff.noprefix=true would otherwise drop them and the parsers would eat
124
118
  // a real directory named "a" — user git config must never bend the parse.
125
119
  export const DIFF_FLAGS = ['--unified=0', '--no-color', '--no-ext-diff', '--no-renames', '--src-prefix=a/', '--dst-prefix=b/'];
126
120
 
@@ -129,11 +123,6 @@ const gitStdout = (args, cwd) => {
129
123
  const r = runGit(args, cwd);
130
124
  return r.error || r.status > 1 ? null : r.stdout;
131
125
  };
132
- const gitLine = (args, cwd) => {
133
- const r = runGit(args, cwd);
134
- if (r.error || r.status !== 0) return null;
135
- return r.stdout.replace(/\r?\n$/, '');
136
- };
137
126
  // Both leaf guards are exported for the unit tests of exactly their fail-closed edges (the
138
127
  // hashFileBytes precedent, AD-047) — internal consumers stay this module's own passes.
139
128
  export const readFileSafe = (path) => {
@@ -145,7 +134,7 @@ export const readFileSafe = (path) => {
145
134
  };
146
135
  // A changed LEAF is read only if it is a REGULAR file. lstat (no-follow): a symlinked or non-regular
147
136
  // leaf must NEVER be read/followed — following it could read outside the work tree or HANG on a
148
- // FIFO/device (codex R2, AD-046). A non-regular leaf fails closed (routed to `unsupported`).
137
+ // FIFO/device (AD-046). A non-regular leaf fails closed (routed to `unsupported`).
149
138
  export const isRegularLeaf = (abs) => {
150
139
  try {
151
140
  return lstatSync(abs).isFile();
@@ -161,13 +150,13 @@ export const isRegularLeaf = (abs) => {
161
150
  // pure deletions cost nothing, subtractive folds stay free); an untracked file is wholly new, so all
162
151
  // its lines are "changed". `unsupportedLines` is the D4 cap's view of the SAME pass: unsupported
163
152
  // SOURCE files carry their new-side lines too (excluding them would gift a large-TS-fold bypass,
164
- // codex R2 / BUGFREE-2 D4); an unreadable/non-regular leaf counts 0 lines but stays LISTED (the
153
+ // BUGFREE-2 D4); an unreadable/non-regular leaf counts 0 lines but stays LISTED (the
165
154
  // coverage gate still fails closed on it). excluded-test and out-of-domain never carry lines.
166
155
  export const computeChangedSurface = (root) => {
167
156
  // Unborn branch (no HEAD yet): the plain diff alone misses files STAGED for the initial commit —
168
157
  // they sit in the index, so they are neither worktree-vs-index changes nor untracked. Merge the
169
158
  // --cached diff (index vs the empty tree) with the plain one; parseUnifiedDiff unions per-file
170
- // lines across concatenated diffs (codex R1).
159
+ // lines across concatenated diffs.
171
160
  const trackedDiff = gitStdout(['diff', 'HEAD', ...DIFF_FLAGS], root)
172
161
  ?? `${gitStdout(['diff', '--cached', ...DIFF_FLAGS], root) ?? ''}\n${gitStdout(['diff', ...DIFF_FLAGS], root) ?? ''}`;
173
162
  const trackedLines = parseUnifiedDiff(trackedDiff);
@@ -215,22 +204,10 @@ export const computeChangedSurface = (root) => {
215
204
  return { assessable, unsupported: [...unsupportedLines.keys()].sort(), outOfDomain: outOfDomain.sort(), unsupportedLines };
216
205
  };
217
206
 
218
- // countCapLines(surface) → the D4 counted magnitude: new-side changed lines of assessable AND
219
- // unsupported SOURCE files (the two counted classes, pinned by named tests); excluded-test and
220
- // out-of-domain never count; pure deletions already cost nothing (new-side lines only). Stated
221
- // consequence, INTENTIONAL (agy R1): under --no-renames a rename of a large file costs its full
222
- // length — the single-home invariant outweighs a rename special case.
223
- export const countCapLines = ({ assessable, unsupportedLines }) => {
224
- let counted = 0;
225
- for (const lines of assessable.values()) counted += lines.length;
226
- for (const lines of unsupportedLines.values()) counted += lines.length;
227
- return counted;
228
- };
229
-
230
207
  // ── the shared fail-closed positive-integer knob parser (AD-047 precedent) ───────────────────────
231
208
 
232
209
  // Zero / negative / fractional / non-numeric values are refusals by name — the parseInt(...)||default
233
- // idiom would silently accept bad truthy values (codex R2, AD-046). Unset → the default; set → a
210
+ // idiom would silently accept bad truthy values (AD-046). Unset → the default; set → a
234
211
  // positive integer, exactly. `makeError` lets each caller throw its OWN typed STOP.
235
212
  export const parsePositiveIntKnob = (env, name, fallback, makeError = (m) => new Error(m)) => {
236
213
  const raw = env[name];
@@ -241,8 +218,8 @@ export const parsePositiveIntKnob = (env, name, fallback, makeError = (m) => new
241
218
  return Number.parseInt(raw, 10);
242
219
  };
243
220
 
244
- // ── the D4 probe-verdict algebra (AD-047; the SINGLE home — moved here from fold-completeness.mjs,
245
- // which re-exports it, so the runner, the checker, AND the review-ledger telemetry read ONE algebra)
221
+ // ── the probe-verdict algebra (AD-047; the SINGLE home — the core-evidence red-proof observer
222
+ // consumes it) ────────────────────────────────────────────────────────────────────────────────────
246
223
 
247
224
  // probeVerdict(entry) → 'green' | 'red' | 'quarantine' | 'unresolvable'. RED/GREEN are strict N/N
248
225
  // verdicts; any timeout, mixed outcome, or partial resolution is QUARANTINE — it never converts and
@@ -254,41 +231,3 @@ export const probeVerdict = (t) => {
254
231
  if (t.timeouts > 0 || unresolved > 0 || (t.greens > 0 && t.reds > 0)) return 'quarantine';
255
232
  return t.greens === t.runs ? 'green' : 'red';
256
233
  };
257
-
258
- // ── the fold-result ledger locator + a tolerant row reader (the D8 telemetry read path) ──────────
259
-
260
- export const RESULTS_BASENAME = 'agent-workflow-fold-completeness.jsonl';
261
-
262
- // The result-ledger path: AW_FOLD_RESULTS overrides (mirrors AW_REVIEW_LEDGER); else <git dir>/basename.
263
- export const resolveResultsPath = (cwd, env = process.env) => {
264
- if (env.AW_FOLD_RESULTS) return env.AW_FOLD_RESULTS;
265
- const gitDir = gitLine(['rev-parse', '--absolute-git-dir'], cwd);
266
- return gitDir == null ? null : join(gitDir, RESULTS_BASENAME);
267
- };
268
-
269
- // readJsonlRows(path) → { rows, badLines, readError? } — the TOLERANT parse the telemetry counts
270
- // ride on (counts only, no judgment — D8): a row is any parsed JSON object; a bad line is counted,
271
- // never dropped silently. Schema validation stays with each ledger's own reader — telemetry must
272
- // not fork a second validator (drift) nor import one across the cycle boundary.
273
- export const readJsonlRows = (path, readFile = readFileSync) => {
274
- let raw;
275
- try {
276
- raw = readFile(path, 'utf8');
277
- } catch (err) {
278
- if (err && err.code === 'ENOENT') return { rows: [], badLines: 0 };
279
- return { rows: [], badLines: 0, readError: (err && err.code) || (err && err.message) || 'read failed' };
280
- }
281
- const rows = [];
282
- let badLines = 0;
283
- for (const line of raw.split('\n')) {
284
- if (line.trim() === '') continue;
285
- try {
286
- const parsed = JSON.parse(line);
287
- if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) rows.push(parsed);
288
- else badLines += 1;
289
- } catch {
290
- badLines += 1;
291
- }
292
- }
293
- return { rows, badLines };
294
- };
@@ -23,7 +23,7 @@
23
23
  //
24
24
  // Exit codes: 0 done / dry-run (incl. preserved customizations — a user's file is a legitimate
25
25
  // state, not an error); 1 precondition STOP (stamp, symlink, missing bundle); 2 usage.
26
- // Dependency-free, Node >= 18. No side effects on import.
26
+ // Dependency-free, Node >= 22. No side effects on import.
27
27
 
28
28
  import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
29
29
  import { join, resolve, dirname } from 'node:path';
@@ -34,7 +34,7 @@ const HERE = dirname(fileURLToPath(import.meta.url));
34
34
  export const AGENTS_DIR = '.claude/agents';
35
35
  export const CLAUDE_DIR = '.claude';
36
36
  export const WORKFLOW_STAMP = 'docs/ai/.workflow-version';
37
- export const EXPECTED_WORKFLOW_VERSION = '2.0.0';
37
+ export const EXPECTED_WORKFLOW_VERSION = '3.0.0';
38
38
  export const BUNDLED_AGENTS_DIR = resolve(HERE, '..', 'references', 'agents');
39
39
 
40
40
  const EXIT_OK = 0;