@sabaiway/agent-workflow-memory 2.3.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -1
- package/README.md +1 -1
- package/SKILL.md +13 -18
- package/bin/install.mjs +1 -1
- package/capability.json +1 -1
- package/migrations/3.0.0-hardened-core-loop.md +29 -0
- package/migrations/README.md +1 -1
- package/migrations/legacy-stamp-takeover.md +3 -3
- package/package.json +2 -2
- package/references/scripts/archive-decisions.mjs +3 -3
- package/references/scripts/archive-decisions.test.mjs +5 -5
- package/references/scripts/check-docs-size-cli.test.mjs +41 -0
- package/references/scripts/check-docs-size.mjs +46 -29
- package/references/scripts/install-git-hooks-repo-exec.test.mjs +82 -0
- package/references/scripts/install-git-hooks.mjs +90 -18
- package/references/scripts/install-git-hooks.test.mjs +102 -0
- package/references/scripts/migrate-gates-branches.test.mjs +157 -0
- package/references/scripts/migrate-gates.mjs +395 -0
- package/references/scripts/migrate-gates.test.mjs +284 -0
- package/references/templates/agent_rules.md +2 -2
- package/scripts/stamp-takeover.mjs +3 -3
- package/references/templates/verification-profile.json +0 -10
|
@@ -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
|
+
});
|
|
@@ -86,7 +86,7 @@ Apply these when authoring a plan, reviewing, folding a finding, or editing code
|
|
|
86
86
|
- **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.
|
|
87
87
|
- **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.
|
|
88
88
|
- **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.
|
|
89
|
-
- **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
|
|
89
|
+
- **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).
|
|
90
90
|
|
|
91
91
|
---
|
|
92
92
|
|
|
@@ -103,7 +103,7 @@ Split a complex task across sessions for **focus and review hygiene**, not becau
|
|
|
103
103
|
## 4. User Interaction
|
|
104
104
|
|
|
105
105
|
1. **Don't rush to commit.** Prepare changes, run local quality checks, report progress with test outcomes.
|
|
106
|
-
2. **Get explicit approval** before
|
|
106
|
+
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).
|
|
107
107
|
|
|
108
108
|
---
|
|
109
109
|
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
// The deployment lineage is a SINGLE shared sequence; its current head is LINEAGE_HEAD.
|
|
7
7
|
// Both `.memory-version` and the kit-fallback `.workflow-version` track THAT sequence —
|
|
8
8
|
// never their package versions. So this substrate's package may be 1.0.0 while the stamp
|
|
9
|
-
// it writes is the lineage head (
|
|
9
|
+
// it writes is the lineage head (3.0.0 today).
|
|
10
10
|
//
|
|
11
11
|
// `decideTakeover` is a PURE function (stamp state in → action out) so the state machine is
|
|
12
12
|
// unit-testable per row. `applyTakeover` is the thin fs wrapper; stamp writes are ATOMIC
|
|
13
13
|
// (write temp + rename) so an interrupted write can never corrupt the prior stamp.
|
|
14
14
|
//
|
|
15
15
|
// The Markdown twin `migrations/legacy-stamp-takeover.md` documents the same table as the
|
|
16
|
-
// no-Node manual fallback. Dependency-free, Node >=
|
|
16
|
+
// no-Node manual fallback. Dependency-free, Node >= 22.
|
|
17
17
|
|
|
18
18
|
import { readFile, writeFile, rename, unlink } from 'node:fs/promises';
|
|
19
19
|
import { dirname, basename, join, resolve } from 'node:path';
|
|
@@ -22,7 +22,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
22
22
|
|
|
23
23
|
// The shared agent-workflow deployment-lineage head. Bumped only when a project-migration
|
|
24
24
|
// changes the deployed docs/ai structure — NOT on a packaging-only release.
|
|
25
|
-
export const LINEAGE_HEAD = '
|
|
25
|
+
export const LINEAGE_HEAD = '3.0.0';
|
|
26
26
|
|
|
27
27
|
const SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)$/;
|
|
28
28
|
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_README": "Optional per-project VERIFICATION PROFILE for the fold-completeness gate (the language-independence contract). DELETE this file to reproduce the exact default behaviour (V8 line coverage + node:test TAP on stdout) — an absent profile is fully supported. Present, it GENERALIZES three inputs so a consumer on another language/runner can drive the same gate: (1) coverage.kind is \"v8\" (default) or \"lcov\" — with lcov, set coverage.lcovPath to where your suite leaves an LCOV file; (2) singleTest.argv is the shell-free command template for probing ONE test (placeholders {file} and {pattern} are required; a file-based resultFormat also requires {resultPath}), and singleTest.resultFormat is \"tap-stdout\" (default), \"tap-file\", or \"junit-xml\"; (3) findings.sarifPath (optional) points at a SARIF file for advisory-only findings (never gate-blocking). The suite COMMAND is NOT declared here — it stays your docs/ai/gates.json unit-tests gate (so the fold run and the gate share command-identity). Every DECLARED path (coverage.lcovPath, findings.sarifPath) MUST be gitignored or outside the repo (a symlink is refused): an in-tree, non-ignored file the suite writes would move the review fingerprint. Env knobs still override (AW_FOLD_SUITE_CMD / AW_FOLD_BOUND_CMD / AW_FOLD_RESULTS). Strict JSON — no comments.",
|
|
3
|
-
"schema": 1,
|
|
4
|
-
"coverage": { "kind": "v8" },
|
|
5
|
-
"singleTest": {
|
|
6
|
-
"argv": ["node", "--test", "--test-reporter", "tap", "--test-name-pattern={pattern}", "{file}"],
|
|
7
|
-
"resultFormat": "tap-stdout"
|
|
8
|
-
},
|
|
9
|
-
"findings": {}
|
|
10
|
-
}
|