@sabaiway/agent-workflow-kit 4.4.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/migrate-adr-store.md +4 -2
- package/references/modes/recommendations.md +2 -0
- package/references/modes/status.md +1 -1
- package/references/modes/upgrade.md +2 -2
- package/references/scripts/archive-changelog.mjs +300 -192
- package/references/scripts/archive-changelog.test.mjs +341 -0
- package/references/scripts/archive-conservation.test.mjs +466 -0
- package/references/scripts/archive-decisions.mjs +48 -20
- package/references/scripts/archive-decisions.test.mjs +120 -0
- package/references/scripts/archive-issues.mjs +344 -108
- package/references/scripts/archive-issues.test.mjs +762 -32
- package/references/scripts/archiver-structure.test.mjs +39 -0
- package/references/scripts/markdown-blocks.mjs +143 -0
- package/references/scripts/markdown-blocks.test.mjs +310 -0
- package/references/templates/changelog.md +3 -1
- package/references/templates/known_issues.md +13 -5
- package/tools/family-registry.mjs +78 -12
- package/tools/known-footprint.mjs +5 -1
- package/tools/migrate-adr-store.mjs +206 -10
- package/tools/recommendations.mjs +41 -2
- package/tools/renderers.mjs +8 -2
|
@@ -29,7 +29,7 @@ export const stop = (message, fields = {}) =>
|
|
|
29
29
|
// ── registries ────────────────────────────────────────────────────────────────
|
|
30
30
|
|
|
31
31
|
// The kit's OWN footprint — canonical anchored patterns. `/docs/ai/` subsumes the deployment stamp
|
|
32
|
-
// (`.workflow-version`); the
|
|
32
|
+
// (`.workflow-version`); the enforcement scripts are enumerated (no bare `/scripts/` — a host repo
|
|
33
33
|
// may have unrelated scripts). `/.claude/settings.json` is carried HIDDEN-ONLY: in hidden mode the
|
|
34
34
|
// kit's own attribution file is a footprint; in visible mode the kit commits it and never runs this
|
|
35
35
|
// tool. It passes the same tracked→ASK classifier, so a project that already commits it gets an ASK,
|
|
@@ -43,13 +43,17 @@ export const KIT_OWN_PATHS = [
|
|
|
43
43
|
'/scripts/_expect-shim.mjs',
|
|
44
44
|
'/scripts/archive-changelog.mjs',
|
|
45
45
|
'/scripts/archive-changelog.test.mjs',
|
|
46
|
+
'/scripts/archive-conservation.test.mjs',
|
|
46
47
|
'/scripts/archive-decisions.mjs',
|
|
47
48
|
'/scripts/archive-decisions.test.mjs',
|
|
48
49
|
'/scripts/archive-issues.mjs',
|
|
49
50
|
'/scripts/archive-issues.test.mjs',
|
|
51
|
+
'/scripts/archiver-structure.test.mjs',
|
|
50
52
|
'/scripts/check-docs-size.mjs',
|
|
51
53
|
'/scripts/check-docs-size.test.mjs',
|
|
52
54
|
'/scripts/install-git-hooks.mjs',
|
|
55
|
+
'/scripts/markdown-blocks.mjs',
|
|
56
|
+
'/scripts/markdown-blocks.test.mjs',
|
|
53
57
|
'/docs/plans/',
|
|
54
58
|
'/.claude/settings.local.json',
|
|
55
59
|
'/.claude/settings.json',
|
|
@@ -7,8 +7,13 @@
|
|
|
7
7
|
// which migrates in the same step (AD-051, Decision 13).
|
|
8
8
|
//
|
|
9
9
|
// What it does (in order, on --apply):
|
|
10
|
-
// 1. GATE — docs/ai must be deployed
|
|
11
|
-
//
|
|
10
|
+
// 1. GATE — docs/ai must be deployed, and the tree must be on the OLD layout. That is TWO shapes:
|
|
11
|
+
// a decisions-archive monolith on disk, OR no monolith at all but a deployed
|
|
12
|
+
// scripts/archive-decisions.mjs that predates the store (the project simply never
|
|
13
|
+
// rotated). The no-monolith shape runs the crossing WITHOUT an explosion — snapshot,
|
|
14
|
+
// script refresh, then SEED the store — and is re-runnable to completion from any crash
|
|
15
|
+
// point, because a store directory alone never counts as finished. A tree with no ADR
|
|
16
|
+
// substrate at all, or one already finalised, is a stated no-op.
|
|
12
17
|
// 2. SNAPSHOT — write a durable pre-migration snapshot (decisions.md + both monoliths + the
|
|
13
18
|
// pre-refresh consumer scripts/ copies) to the project's git dir (uncommittable), with a
|
|
14
19
|
// stated out-of-tree fallback off git; fail LOUD if neither base is writable (Decision 5).
|
|
@@ -38,8 +43,11 @@ import {
|
|
|
38
43
|
WARM_REL,
|
|
39
44
|
COLD_REL,
|
|
40
45
|
ADR_DIR_REL,
|
|
46
|
+
NAV_REL,
|
|
47
|
+
defaultRegenerateIndex,
|
|
41
48
|
runCli as runArchiveDecisions,
|
|
42
49
|
} from '../references/scripts/archive-decisions.mjs';
|
|
50
|
+
import { surveyAdrLayoutStrict, ADR_LAYOUT_PATHS } from './family-registry.mjs';
|
|
43
51
|
|
|
44
52
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
45
53
|
const KIT_ROOT = resolve(HERE, '..');
|
|
@@ -111,6 +119,26 @@ export const planScriptRefresh = (cwd, deps = {}) => {
|
|
|
111
119
|
return out;
|
|
112
120
|
};
|
|
113
121
|
|
|
122
|
+
// COMPANION seeds: modules the refreshed archivers IMPORT. The refresh above is deliberately
|
|
123
|
+
// directional (never ADDS a basename the consumer lacks), but refreshing an OLD deployment's
|
|
124
|
+
// archivers to this kit's canon without their runtime dependency would leave every refreshed
|
|
125
|
+
// script crashing on a missing `./markdown-blocks.mjs` import until a separate upgrade run — so
|
|
126
|
+
// the dependency rides the SAME apply, atomically, written before its importers.
|
|
127
|
+
const COMPANION_SEEDS = ['markdown-blocks.mjs', 'markdown-blocks.test.mjs'];
|
|
128
|
+
export const planCompanionSeeds = (cwd, refresh, deps = {}) => {
|
|
129
|
+
if (refresh.length === 0) return [];
|
|
130
|
+
const exists = deps.exists ?? existsSync;
|
|
131
|
+
const kitScripts = deps.kitScripts ?? KIT_SCRIPTS;
|
|
132
|
+
const consumerScripts = join(cwd, CONSUMER_SCRIPTS_REL);
|
|
133
|
+
const out = [];
|
|
134
|
+
for (const name of COMPANION_SEEDS) {
|
|
135
|
+
const canon = join(kitScripts, name);
|
|
136
|
+
const dst = join(consumerScripts, name);
|
|
137
|
+
if (exists(canon) && !exists(dst)) out.push({ name, canon, dst });
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
};
|
|
141
|
+
|
|
114
142
|
const gitDirOf = (cwd, spawn) => {
|
|
115
143
|
const r = spawn('git', ['rev-parse', '--absolute-git-dir'], { cwd, encoding: 'utf8' });
|
|
116
144
|
return r && r.status === 0 && r.stdout ? r.stdout.trim() : null;
|
|
@@ -183,14 +211,185 @@ export const writeSnapshot = (cwd, refresh, stamp, deps = {}) => {
|
|
|
183
211
|
};
|
|
184
212
|
|
|
185
213
|
// Overwrite each refresh target with the kit canon, atomically, preserving the canon's exec bit.
|
|
214
|
+
//
|
|
215
|
+
// ORDER MATTERS: the rotation script is what the layout discriminator reads, so it is written LAST.
|
|
216
|
+
// A crash partway through a refresh that had already flipped it would otherwise leave a tree that
|
|
217
|
+
// LOOKS refreshed while other scripts are still the old copies — and a resume, keying on that same
|
|
218
|
+
// script, would skip them forever. Written last, an interrupted refresh always re-plans in full.
|
|
219
|
+
const DISCRIMINATOR_SCRIPT = ADR_LAYOUT_PATHS.rotator.split('/').pop();
|
|
220
|
+
const refreshOrder = (refresh) => [
|
|
221
|
+
...refresh.filter((r) => r.name !== DISCRIMINATOR_SCRIPT),
|
|
222
|
+
...refresh.filter((r) => r.name === DISCRIMINATOR_SCRIPT),
|
|
223
|
+
];
|
|
224
|
+
|
|
186
225
|
const applyScriptRefresh = (cwd, refresh, deps = {}) => {
|
|
187
226
|
const read = deps.read ?? readFileSync;
|
|
188
227
|
const chmod = deps.chmod ?? chmodSync;
|
|
189
228
|
const stat = deps.stat ?? statSync;
|
|
190
|
-
|
|
229
|
+
// Companion modules FIRST (a dependency must land before its importers), refresh order after —
|
|
230
|
+
// the discriminator still last, so an interrupted apply always re-plans in full. Returns the
|
|
231
|
+
// seeded names (computed pre-write; recomputing after would see them present and report none).
|
|
232
|
+
const seeds = planCompanionSeeds(cwd, refresh, deps);
|
|
233
|
+
for (const { canon, dst, name } of [...seeds, ...refreshOrder(refresh)]) {
|
|
191
234
|
writeContainedFileAtomic(cwd, dst, read(canon, 'utf8'), deps, { stop, label: `${CONSUMER_SCRIPTS_REL}/${name}` });
|
|
192
235
|
chmod(dst, stat(canon).mode & 0o777); // the exec bit is the git-tracked axis the mirror guard pins
|
|
193
236
|
}
|
|
237
|
+
return seeds.map((s) => s.name);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// ── the no-monolith crossing ─────────────────────────────────────────────────────
|
|
241
|
+
//
|
|
242
|
+
// A consumer on the RETIRED scheme that never rotated far enough to produce a monolith used to read
|
|
243
|
+
// "a fresh new-scheme tree" here and be sent away. The discriminator is the deployed rotation
|
|
244
|
+
// script's own provenance (family-registry.mjs), never "has decisions.md, lacks adr/".
|
|
245
|
+
//
|
|
246
|
+
// Re-entry is decided by what is FINISHED, never by one existence bit: the store directory existing
|
|
247
|
+
// does not prove the navigator was written or the index regenerated, so a crash there must not turn
|
|
248
|
+
// the next --apply into a no-op. Every write below is individually idempotent, which is why this
|
|
249
|
+
// needs no resume ledger.
|
|
250
|
+
|
|
251
|
+
// The crossing is COMPLETE when the navigator exists, the tree's own gate passes, AND the index the
|
|
252
|
+
// crossing regenerates is fresh. The index is part of the criterion because it is a real output of
|
|
253
|
+
// the crossing that `--check` never looks at: a crash (or a failed regeneration) between the
|
|
254
|
+
// navigator write and the index left a tree that reported "already migrated" on the retry and never
|
|
255
|
+
// repaired the index. An unreachable index generator is NOT treated as fresh — the crossing re-runs
|
|
256
|
+
// and fails loudly again, which is the honest outcome for a broken generator.
|
|
257
|
+
const INDEX_GENERATOR = join(KIT_SCRIPTS, 'check-docs-size.mjs');
|
|
258
|
+
const isIndexFresh = (cwd, deps = {}) => {
|
|
259
|
+
const spawn = deps.spawnSync ?? spawnSync;
|
|
260
|
+
const r = spawn(process.execPath, [INDEX_GENERATOR, '--check-index', `--root=${cwd}`], { encoding: 'utf8' });
|
|
261
|
+
return !r.error && r.status === 0;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// The layout verdict leads, and it is the SAME verdict the status line and the advisor read: an
|
|
265
|
+
// old-scheme rotator beside a finished store still answers `old-unrotated`, so treating that tree as
|
|
266
|
+
// done would leave the signal permanently lit with nothing able to clear it.
|
|
267
|
+
const isFinalised = (cwd, runMigrate, deps = {}) =>
|
|
268
|
+
surveyAdrLayoutStrict(cwd, deps) === 'migrated' &&
|
|
269
|
+
substratePresent(join(cwd, NAV_REL), deps) &&
|
|
270
|
+
runMigrate(['--check'], { root: cwd, log: () => {}, logError: () => {} }) === EXIT_OK &&
|
|
271
|
+
isIndexFresh(cwd, deps);
|
|
272
|
+
|
|
273
|
+
// `existsSync` answers false for EVERY failure, EACCES included — so asking it whether the substrate
|
|
274
|
+
// is there would turn an UNREADABLE tree into a confident "nothing to migrate", exit 0. Absence is
|
|
275
|
+
// ENOENT and nothing else; anything else is surfaced, never swallowed. Same policy the layout survey
|
|
276
|
+
// already enforces, now applied where the tool acts on it.
|
|
277
|
+
const substratePresent = (path, deps = {}) => {
|
|
278
|
+
const stat = deps.statSync ?? statSync;
|
|
279
|
+
try {
|
|
280
|
+
stat(path);
|
|
281
|
+
return true;
|
|
282
|
+
} catch (err) {
|
|
283
|
+
if (err && err.code === 'ENOENT') return false;
|
|
284
|
+
throw stop(`cannot read ${path} (${err && err.message}) — refusing to report on a tree it could not inspect`);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const crossWithoutMonolith = (cwd, args, stamp, { log, error, runMigrate, deps }) => {
|
|
289
|
+
const hasStore = substratePresent(join(cwd, ADR_DIR_REL), deps);
|
|
290
|
+
const hasHot = substratePresent(join(cwd, HOT_REL), deps);
|
|
291
|
+
|
|
292
|
+
if (!hasHot && !hasStore) {
|
|
293
|
+
// NOT "a fresh new-scheme tree" — this tree may well be old-scheme; it simply has no ADR
|
|
294
|
+
// substrate for the crossing to carry across, which is exactly the rotator's own skip.
|
|
295
|
+
log(`[migrate-adr-store] nothing to migrate — no ADR substrate (neither ${HOT_REL} nor ${ADR_DIR_REL}/) and no legacy monolith.`);
|
|
296
|
+
return EXIT_OK;
|
|
297
|
+
}
|
|
298
|
+
if (hasStore && isFinalised(cwd, runMigrate, deps)) {
|
|
299
|
+
log('[migrate-adr-store] already migrated — the one-file-per-ADR store is in place and its gate is green; nothing to do.');
|
|
300
|
+
return EXIT_OK;
|
|
301
|
+
}
|
|
302
|
+
if (!hasStore && surveyAdrLayoutStrict(cwd, deps) === 'none' && !substratePresent(join(cwd, ADR_LAYOUT_PATHS.rotator), deps)) {
|
|
303
|
+
// Nothing to refresh (the refresh is directional — it never ADDS a script) and nothing to
|
|
304
|
+
// maintain a store we might seed. The normal upgrade owns seeding the pair.
|
|
305
|
+
log('[migrate-adr-store] nothing to migrate — no deployed rotation script; run the normal upgrade first (it seeds the ADR enforcement pair).');
|
|
306
|
+
return EXIT_OK;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const refresh = planScriptRefresh(cwd, deps);
|
|
310
|
+
const drifted = refresh.filter((r) => r.differs);
|
|
311
|
+
// The read-only preflight: the SAME parse / half-migrated guard / store-integrity check the seed
|
|
312
|
+
// itself runs, stopping before every write. Without it a dry-run could green-light an apply that
|
|
313
|
+
// writes the store and only then discovers it cannot converge.
|
|
314
|
+
const preflight = (logError) => runMigrate(['--write-navigator', '--dry-run'], { root: cwd, log: () => {}, logError });
|
|
315
|
+
|
|
316
|
+
if (!args.apply) {
|
|
317
|
+
const preview = resolveSnapshotDir(cwd, stamp, deps);
|
|
318
|
+
// Three attempts to SUMMARISE why this tree needs the crossing produced three wrong sentences —
|
|
319
|
+
// each true of the common case and false of a state this arm deliberately supports. So the
|
|
320
|
+
// summary is gone: the preview states the two facts it actually knows, one per line, and the
|
|
321
|
+
// reader draws the conclusion. Nothing here can drift out of step with the tree, because nothing
|
|
322
|
+
// here is an inference. (Which scripts are stale is already reported by the refresh line below —
|
|
323
|
+
// never re-asserted here.)
|
|
324
|
+
const layout = surveyAdrLayoutStrict(cwd, deps);
|
|
325
|
+
const rotatorFact = layout === 'old-unrotated'
|
|
326
|
+
? `predates the one-file-per-ADR store`
|
|
327
|
+
: substratePresent(join(cwd, ADR_LAYOUT_PATHS.rotator), deps)
|
|
328
|
+
? `already names the store`
|
|
329
|
+
: `not deployed`; // and NOT "nothing to refresh" — a sibling script may still need one
|
|
330
|
+
log('[migrate-adr-store] --dry-run — no files will be changed. Planned crossing (no legacy monolith to retire):');
|
|
331
|
+
log(` deployed ${ADR_LAYOUT_PATHS.rotator}: ${rotatorFact}`);
|
|
332
|
+
log(` ${ADR_DIR_REL}/: ${hasStore ? 'present, but the crossing has not been completed' : 'absent'}`);
|
|
333
|
+
log(` snapshot → ${preview.dir ? `${preview.dir} (${preview.viaGitDir ? 'git dir' : 'out-of-tree fallback'})` : 'NONE — no out-of-tree location; run inside a git repo (apply would refuse otherwise)'}`);
|
|
334
|
+
log(` refresh ${refresh.length} enforcement script(s) to this kit's version${drifted.length ? ` (${drifted.length} locally differ: ${drifted.map((r) => r.name).join(', ')})` : ''}`);
|
|
335
|
+
for (const s of planCompanionSeeds(cwd, refresh, deps)) log(` seed companion module ${CONSUMER_SCRIPTS_REL}/${s.name} (imported by the refreshed archivers; absent at the consumer)`);
|
|
336
|
+
log(` then seed the store: create ${ADR_DIR_REL}/, write ${NAV_REL} and regenerate docs/ai/index.md`);
|
|
337
|
+
const code = preflight((m) => error(` ${m}`));
|
|
338
|
+
if (code !== EXIT_OK) {
|
|
339
|
+
throw stop(`the tree cannot be seeded as it stands (exit ${code}) — NOT safe to --apply; fix the reported problem, then re-run.`);
|
|
340
|
+
}
|
|
341
|
+
if (preview.dir === null) {
|
|
342
|
+
throw stop('no out-of-tree snapshot location — --apply would refuse; run inside a git repo (or point the fallback outside the project), then re-run.');
|
|
343
|
+
}
|
|
344
|
+
log(' index regeneration is verified at --apply time (a dry-run cannot observe it without writing).');
|
|
345
|
+
log('Run `/agent-workflow-kit migrate-adr-store` again with --apply to perform it (it never commits).');
|
|
346
|
+
return EXIT_OK;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const pre = preflight(error);
|
|
350
|
+
if (pre !== EXIT_OK) {
|
|
351
|
+
throw stop(`the tree cannot be seeded as it stands (preflight exit ${pre}) — refusing to touch the tree; fix the reported problem, then re-run.`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const snapshot = writeSnapshot(cwd, refresh, stamp, deps);
|
|
355
|
+
// The FULL refresh is re-planned and re-applied on every entry, so an interrupted one always
|
|
356
|
+
// completes; the discriminator script is written last (see refreshOrder).
|
|
357
|
+
const seededNames = applyScriptRefresh(cwd, refresh, deps);
|
|
358
|
+
|
|
359
|
+
// Capture the index-regeneration verdict instead of matching log prose: the rotator logs a failed
|
|
360
|
+
// regeneration and still returns 0, so "the gates are green" would not mean the index is fresh.
|
|
361
|
+
const regen = { ok: true, detail: '' };
|
|
362
|
+
const seed = runMigrate(['--write-navigator'], {
|
|
363
|
+
root: cwd,
|
|
364
|
+
log,
|
|
365
|
+
logError: error,
|
|
366
|
+
regenerateIndex: (root, today, d) => {
|
|
367
|
+
const r = (deps.regenerateIndex ?? defaultRegenerateIndex)(root, today, d);
|
|
368
|
+
regen.ok = r.ok;
|
|
369
|
+
regen.detail = r.detail;
|
|
370
|
+
return r;
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
if (seed !== EXIT_OK) {
|
|
374
|
+
throw stop(`seeding the ADR store failed (exit ${seed}) — the pre-crossing snapshot is at ${snapshot.dir}; resolve the reported problem and re-run (the crossing is idempotent).`);
|
|
375
|
+
}
|
|
376
|
+
if (!regen.ok) {
|
|
377
|
+
throw stop(`the ADR store was seeded but docs/ai/index.md was NOT regenerated (${regen.detail}) — the pre-crossing snapshot is at ${snapshot.dir}; fix the index generator and re-run (the crossing is idempotent).`);
|
|
378
|
+
}
|
|
379
|
+
const verify = runMigrate(['--check'], { root: cwd, log: () => {}, logError: error });
|
|
380
|
+
if (verify !== EXIT_OK) {
|
|
381
|
+
throw stop(`the ADR store was seeded but its own gate does not pass (exit ${verify}) — the pre-crossing snapshot is at ${snapshot.dir}; resolve the reported problem and re-run (the crossing is idempotent).`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// States what this run DID, never what the tree was before it: the same arm completes an
|
|
385
|
+
// interrupted crossing whose scripts were already current, which no "old-scheme" claim covers.
|
|
386
|
+
log('[migrate-adr-store] crossing complete — the one-file-per-ADR store is in place (no legacy monolith was present):');
|
|
387
|
+
log(` snapshot: ${snapshot.dir} (${snapshot.viaGitDir ? 'git dir' : 'out-of-tree fallback'}, ${snapshot.fileCount} file(s))`);
|
|
388
|
+
log(` refreshed ${refresh.length} enforcement script(s) to this kit's version${seededNames.length ? ` + seeded ${seededNames.join(', ')}` : ''}`);
|
|
389
|
+
log(` seeded ${ADR_DIR_REL}/ with ${NAV_REL} and regenerated docs/ai/index.md`);
|
|
390
|
+
log(' next: run the normal upgrade (it re-stamps the deployment lineage to the current head),');
|
|
391
|
+
log(' then review the migrated docs/ai/ tree and the re-stamp together and commit them yourself — this command never commits.');
|
|
392
|
+
return EXIT_OK;
|
|
194
393
|
};
|
|
195
394
|
|
|
196
395
|
export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
@@ -209,11 +408,7 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
|
209
408
|
|
|
210
409
|
const monoliths = monolithsPresent(cwd);
|
|
211
410
|
if (monoliths.length === 0) {
|
|
212
|
-
|
|
213
|
-
log(migrated
|
|
214
|
-
? '[migrate-adr-store] already migrated — the one-file-per-ADR store is in place (no legacy monolith); nothing to do.'
|
|
215
|
-
: '[migrate-adr-store] nothing to migrate — no legacy decisions-archive monolith found (a fresh new-scheme tree).');
|
|
216
|
-
return EXIT_OK;
|
|
411
|
+
return crossWithoutMonolith(cwd, args, stamp, { log, error, runMigrate, deps });
|
|
217
412
|
}
|
|
218
413
|
|
|
219
414
|
const refresh = planScriptRefresh(cwd, deps);
|
|
@@ -225,6 +420,7 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
|
225
420
|
log(` old layout: ${monoliths.join(', ')} (will be exploded into ${ADR_DIR_REL}/ then retired)`);
|
|
226
421
|
log(` snapshot → ${preview.dir ? `${preview.dir} (${preview.viaGitDir ? 'git dir' : 'out-of-tree fallback'})` : 'NONE — no out-of-tree location; run inside a git repo (apply would refuse otherwise)'}`);
|
|
227
422
|
log(` refresh ${refresh.length} enforcement script(s) to this kit's version${drifted.length ? ` (${drifted.length} locally differ: ${drifted.map((r) => r.name).join(', ')})` : ''}`);
|
|
423
|
+
for (const s of planCompanionSeeds(cwd, refresh, deps)) log(` seed companion module ${CONSUMER_SCRIPTS_REL}/${s.name} (imported by the refreshed archivers; absent at the consumer)`);
|
|
228
424
|
log(' then the conservation-checked rotation:');
|
|
229
425
|
// Surface the rotation's own exit code: a failed dry-run must NOT print the
|
|
230
426
|
// "run with --apply" go-ahead nor exit 0 — it would send the user to --apply on an unsafe tree.
|
|
@@ -250,14 +446,14 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
|
250
446
|
}
|
|
251
447
|
|
|
252
448
|
const snapshot = writeSnapshot(cwd, refresh, stamp, deps);
|
|
253
|
-
applyScriptRefresh(cwd, refresh, deps);
|
|
449
|
+
const seededNames = applyScriptRefresh(cwd, refresh, deps);
|
|
254
450
|
const code = runMigrate(['--migrate', '--apply'], { root: cwd, log, logError: error });
|
|
255
451
|
if (code !== EXIT_OK) {
|
|
256
452
|
throw stop(`the rotation failed (exit ${code}) — the pre-migration snapshot is at ${snapshot.dir}; resolve the reported problem and re-run (the migration is idempotent).`);
|
|
257
453
|
}
|
|
258
454
|
log('[migrate-adr-store] migrated the 3-tier ADR cascade → one-file-per-ADR store:');
|
|
259
455
|
log(` snapshot: ${snapshot.dir} (${snapshot.viaGitDir ? 'git dir' : 'out-of-tree fallback'}, ${snapshot.fileCount} file(s))`);
|
|
260
|
-
log(` refreshed ${refresh.length} enforcement script(s) to this kit's version`);
|
|
456
|
+
log(` refreshed ${refresh.length} enforcement script(s) to this kit's version${seededNames.length ? ` + seeded ${seededNames.join(', ')}` : ''}`);
|
|
261
457
|
log(' next: run the normal upgrade (it re-stamps the deployment lineage to the current head),');
|
|
262
458
|
log(' then review the migrated docs/ai/ tree and the re-stamp together and commit them yourself — this command never commits.');
|
|
263
459
|
return EXIT_OK;
|
|
@@ -51,7 +51,7 @@ import { loadAutonomy, isSparseSeedConfig, AUTONOMY_REL } from './autonomy-confi
|
|
|
51
51
|
import { deriveDoctorPlan } from './autonomy-doctor.mjs';
|
|
52
52
|
import { detectBackends, findOnPath } from './detect-backends.mjs';
|
|
53
53
|
import { ACTIVITIES, resolveActivityRecipe } from './recipes.mjs';
|
|
54
|
-
import { surveyFamily, surveyGateHook } from './family-registry.mjs';
|
|
54
|
+
import { surveyFamily, surveyGateHook, surveyAdrLayoutStrict } from './family-registry.mjs';
|
|
55
55
|
import { probeSandboxMasks, needsMasksApply } from './sandbox-masks.mjs';
|
|
56
56
|
import { shellQuoteArg } from './review-state.mjs';
|
|
57
57
|
import { isFinalCapableDeclaration } from './run-gates.mjs';
|
|
@@ -108,6 +108,7 @@ export const SEVERITIES = Object.freeze({
|
|
|
108
108
|
'state-block': SEVERITY_OPTIONAL,
|
|
109
109
|
agents: SEVERITY_OPTIONAL,
|
|
110
110
|
'family-freshness': SEVERITY_ATTENTION,
|
|
111
|
+
'adr-store-migration': SEVERITY_ATTENTION,
|
|
111
112
|
'sandbox-masks': SEVERITY_OPTIONAL,
|
|
112
113
|
'sandbox-lane': SEVERITY_OPTIONAL,
|
|
113
114
|
'worktrees-dir': SEVERITY_OPTIONAL,
|
|
@@ -165,6 +166,7 @@ export const WHATS = Object.freeze({
|
|
|
165
166
|
'state-block': 'nothing checks the closing state block — a turn that ends on «nothing needed from you», or on a promise it never started, passes unseen',
|
|
166
167
|
agents: '{n} read-only subagent(s) not placed (Claude Code) — no shell-free vehicle for that work; the apply PREVIEWS first',
|
|
167
168
|
'family-freshness': '{parts}',
|
|
169
|
+
'adr-store-migration': 'still on the retired 3-tier ADR layout — {shape}',
|
|
168
170
|
'sandbox-masks': '{n} sandbox device mask(s) clutter git status — the managed exclude block is absent or stale',
|
|
169
171
|
'sandbox-masks.stale-real': '{n} sandbox device mask(s) clutter git status — the exclude block is stale; {m} fenced entr(ies) are REAL paths (a fresh apply drops them)',
|
|
170
172
|
'sandbox-lane': 'the wired review wrappers declare a session-sandbox recipe (egress hosts + writable state dirs) not yet acknowledged for this project',
|
|
@@ -218,6 +220,7 @@ export const BENEFITS = Object.freeze({
|
|
|
218
220
|
'state-block': 'no silent stalls — a turn ending on «you are not needed», or on work it never started, warns at once instead of waiting to be spotted',
|
|
219
221
|
agents: 'cost and quiet — mechanical work runs on a cheap model, and no vehicle has a shell, so a read-only fan-out cannot flood you with prompts',
|
|
220
222
|
'family-freshness': 'currency — placed family members carry the latest shipped fixes and features',
|
|
223
|
+
'adr-store-migration': 'durability — every decision becomes its own file with a generated navigator, instead of one hand-rotated pile',
|
|
221
224
|
'sandbox-masks': 'zero clutter — git status shows only your changes (the review domain already ignores the masks by construction)',
|
|
222
225
|
'sandbox-lane': 'discoverability — the manifest-declared observed sandbox recipe for bridge runs surfaces itself instead of waiting to be asked',
|
|
223
226
|
'worktrees-dir': 'parallel features — the host-specific write allowance or terminal fallback is surfaced before provision',
|
|
@@ -254,6 +257,7 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
|
|
|
254
257
|
{ id: 'sandbox-masks', mode: 'sandbox-masks', advisorKey: 'sandbox-masks' },
|
|
255
258
|
{ id: 'worktrees-dir', mode: 'worktrees', advisorKey: 'worktrees-dir' },
|
|
256
259
|
{ id: 'family-freshness', mode: 'upgrade', advisorKey: 'family-freshness' },
|
|
260
|
+
{ id: 'adr-store-migration', mode: 'migrate-adr-store', advisorKey: 'adr-store-migration' },
|
|
257
261
|
{ id: 'review-recipe', mode: 'set-recipe', advisorKey: 'review-recipe' },
|
|
258
262
|
// The execute slot is a DISTINCT opt-in from the review slot, and the same probe reports both —
|
|
259
263
|
// which is why the review-recipe benefit is worded for either slot rather than for review alone.
|
|
@@ -788,7 +792,7 @@ const readReadLaneToggle = (root, deps) => {
|
|
|
788
792
|
// D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
|
|
789
793
|
// at the consent moment; the static contract test asserts EXACT bidirectional coverage
|
|
790
794
|
// (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
|
|
791
|
-
export const RISK_NOTED_KEYS = Object.freeze(['sandbox-lane', 'read-lane', 'worktrees-dir']);
|
|
795
|
+
export const RISK_NOTED_KEYS = Object.freeze(['sandbox-lane', 'read-lane', 'worktrees-dir', 'adr-store-migration']);
|
|
792
796
|
|
|
793
797
|
const probeSandboxLane = ({ root, deps, add, skip }) => {
|
|
794
798
|
try {
|
|
@@ -965,6 +969,40 @@ const probeWorktreesDir = ({ root, deps, add, skip }) => {
|
|
|
965
969
|
}
|
|
966
970
|
};
|
|
967
971
|
|
|
972
|
+
// The ADR-store crossing. Until now this mode declared it had NO advisor capability, on the argument
|
|
973
|
+
// that status and upgrade already report the old layout — but they only reported the MONOLITH shape,
|
|
974
|
+
// so a project whose deployed rotator merely predates the store was told nothing by anything.
|
|
975
|
+
//
|
|
976
|
+
// Honest scope: the advisor is the deterministic section every `upgrade` run ends with, so this is
|
|
977
|
+
// NOT a new door for someone who never runs status or upgrade — it MECHANIZES the upgrade door.
|
|
978
|
+
//
|
|
979
|
+
// It reads the STRICT layout survey deliberately: the lenient one turns every fs failure into
|
|
980
|
+
// "no ADR layout here", which would print «flow optimal» over a layout the probe could not read. A
|
|
981
|
+
// failure must become a STATED SKIP, never an absence.
|
|
982
|
+
// Each shape states a fact about THIS tree that holds whether or not a store directory exists —
|
|
983
|
+
// `old-unrotated` also covers a tree whose store is already there but whose rotation script is not,
|
|
984
|
+
// and saying "the store is not in place" there would be false.
|
|
985
|
+
const ADR_LAYOUT_SHAPES = Object.freeze({
|
|
986
|
+
old: 'a legacy archive file is still on disk and must be exploded into the per-file store',
|
|
987
|
+
'old-unrotated': 'the deployed rotation script predates the store and keeps writing the retired layout',
|
|
988
|
+
});
|
|
989
|
+
export const probeAdrStore = ({ root, deps, add, skip }) => {
|
|
990
|
+
try {
|
|
991
|
+
const shape = ADR_LAYOUT_SHAPES[surveyAdrLayoutStrict(root, deps)];
|
|
992
|
+
if (!shape) return; // migrated, or no ADR substrate at all — nothing to offer
|
|
993
|
+
// HAND-APPLY, not the standard lane: the consent flow executes the apply slot against the
|
|
994
|
+
// confirmation given BEFORE the preview, and this crossing requires informed consent AFTER its
|
|
995
|
+
// dry-run. A runnable one-liner here would auto-run a tree-mutating migration on stale consent.
|
|
996
|
+
add(
|
|
997
|
+
'adr-store-migration',
|
|
998
|
+
fillTemplate(WHATS['adr-store-migration'], { shape }),
|
|
999
|
+
`HAND-APPLY: node ${q(toolPath('migrate-adr-store.mjs'))} --dry-run --cwd ${q(root)} — then re-run with --apply ONLY after showing the plan and getting fresh consent`,
|
|
1000
|
+
);
|
|
1001
|
+
} catch (err) {
|
|
1002
|
+
skip('adr-store-migration', err);
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
|
|
968
1006
|
// ── assembly (frozen presentation order) ─────────────────────────────────────────────────────────
|
|
969
1007
|
const PROBES = Object.freeze([
|
|
970
1008
|
probeVelocityItems,
|
|
@@ -977,6 +1015,7 @@ const PROBES = Object.freeze([
|
|
|
977
1015
|
probeStateBlockHook,
|
|
978
1016
|
probeCheapAgents,
|
|
979
1017
|
probeFamilyFreshness,
|
|
1018
|
+
probeAdrStore,
|
|
980
1019
|
probeMasksItem,
|
|
981
1020
|
probeSandboxLane,
|
|
982
1021
|
probeWorktreesDir,
|
package/tools/renderers.mjs
CHANGED
|
@@ -15,6 +15,10 @@ const READINESS_COL = 14;
|
|
|
15
15
|
const STAMP_COL = 26;
|
|
16
16
|
const SETTINGS_COL = 14;
|
|
17
17
|
|
|
18
|
+
// The ADR-layout tokens that carry an action for the user. Kept as a list, not a chain of equality
|
|
19
|
+
// checks, so a future token joins the render by joining this line.
|
|
20
|
+
const ACTIONABLE_ADR_LAYOUTS = Object.freeze(['old', 'old-unrotated']);
|
|
21
|
+
|
|
18
22
|
const SGR = Object.freeze({ bold: '\x1b[1m', reset: '\x1b[0m' });
|
|
19
23
|
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
20
24
|
export const visibleLength = (s) => s.replace(ANSI_RE, '').length;
|
|
@@ -86,8 +90,10 @@ const renderProject = (vm, { color }) => {
|
|
|
86
90
|
}
|
|
87
91
|
for (const s of p.deployStamps) lines.push(` ${pad(s.display, STAMP_COL)}${s.version ?? '—'}`);
|
|
88
92
|
lines.push(` ${pad('docs/ai present', STAMP_COL)}${p.docsAi ? 'yes' : 'no'}`);
|
|
89
|
-
// Only
|
|
90
|
-
|
|
93
|
+
// Only an ACTIONABLE layout renders a line — a migrated/none store needs no note (AD-051). Both
|
|
94
|
+
// actionable tokens render the SAME line: 'old' (a monolith on disk) and 'old-unrotated' (an
|
|
95
|
+
// old-scheme rotator that never rotated) differ only in the discriminator, never in the remedy.
|
|
96
|
+
if (ACTIONABLE_ADR_LAYOUTS.includes(p.adrLayout)) {
|
|
91
97
|
lines.push(` ${pad('ADR store', STAMP_COL)}old layout — run /agent-workflow-kit migrate-adr-store`);
|
|
92
98
|
}
|
|
93
99
|
if (p.visibility) {
|