@sabaiway/agent-workflow-kit 4.3.0 → 4.5.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 +82 -0
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/hooks/gate-approve.mjs +58 -7
- 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/modes/velocity.md +3 -2
- package/references/scripts/archive-decisions.mjs +14 -3
- package/references/scripts/archive-decisions.test.mjs +27 -0
- package/references/shared/command-shapes.md +25 -24
- package/tools/family-registry.mjs +78 -12
- package/tools/migrate-adr-store.mjs +177 -8
- package/tools/path-inventory.mjs +516 -0
- package/tools/recommendations.mjs +41 -2
- package/tools/renderers.mjs +8 -2
- package/tools/repo-search.mjs +217 -29
- package/tools/velocity-profile.mjs +6 -2
|
@@ -342,23 +342,89 @@ const hasHiddenFence = (projectDir, deps = {}) => {
|
|
|
342
342
|
// The retired 3-tier ADR monoliths (AD-051): their presence is the old-layout signal a consumer must
|
|
343
343
|
// migrate away from via the opt-in `migrate-adr-store` mode. Stable relative paths (a status probe
|
|
344
344
|
// never imports the rotator).
|
|
345
|
-
|
|
346
|
-
|
|
345
|
+
// The ADR-layout probe's paths. LOCAL literals, not an import: this module backs the read-only
|
|
346
|
+
// `status` view and importing the rotator would drag child_process + crypto into a status read. A
|
|
347
|
+
// drift guard in family-registry.test.mjs pins each literal to the rotator's exported constant —
|
|
348
|
+
// the same baked-frozen-copy shape the gate hook uses for its velocity constants.
|
|
349
|
+
export const ADR_LAYOUT_PATHS = Object.freeze({
|
|
350
|
+
hot: 'docs/ai/decisions.md',
|
|
351
|
+
monoliths: Object.freeze(['docs/ai/history/decisions-archive.md', 'docs/ai/history/decisions-archive-early.md']),
|
|
352
|
+
store: 'docs/ai/adr',
|
|
353
|
+
// The DEPLOYED rotator is the discriminator: the new-scheme file names the store path, the
|
|
354
|
+
// pre-migration one does not. Never "has decisions.md, lacks adr/" — that shape false-positives a
|
|
355
|
+
// tree whose NEW rotator already reds its own gate, and every No-Node project.
|
|
356
|
+
rotator: 'scripts/archive-decisions.mjs',
|
|
357
|
+
storeMarker: 'docs/ai/adr',
|
|
358
|
+
});
|
|
359
|
+
const ENOENT = 'ENOENT';
|
|
347
360
|
|
|
348
361
|
// The ADR-store layout axis: 'old' (a retired decisions-archive monolith is still on disk — needs
|
|
349
362
|
// the opt-in migration), 'migrated' (the one-file-per-ADR adr/ store is in place), or 'none' (no ADR
|
|
350
363
|
// substrate at all). Keys on the monolith presence, NOT on the stamp/head (Decision 6/13).
|
|
351
|
-
|
|
352
|
-
|
|
364
|
+
// The STRICT core. Absence is ENOENT and nothing else: existsSync collapses EVERY failure to false
|
|
365
|
+
// (EACCES included), so a strict policy layered on it would be vacuous in production and observable
|
|
366
|
+
// only through injected deps — and ENOTDIR means a file where a directory belongs, i.e. corruption,
|
|
367
|
+
// not "not there". Everything else propagates, so the advisor can degrade to a STATED SKIP instead
|
|
368
|
+
// of printing "flow optimal" over a layout it could not read.
|
|
369
|
+
export const PATH_DIR = 'dir';
|
|
370
|
+
export const PATH_FILE = 'file';
|
|
371
|
+
export const PATH_OTHER = 'other';
|
|
372
|
+
const probeStat = (path) => {
|
|
373
|
+
const st = statSync(path);
|
|
374
|
+
return st.isDirectory() ? PATH_DIR : st.isFile() ? PATH_FILE : PATH_OTHER;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
export const surveyAdrLayoutStrict = (dir, deps = {}) => {
|
|
378
|
+
const stat = deps.statPath ?? probeStat;
|
|
379
|
+
const read = deps.readFile ?? readFileSync;
|
|
380
|
+
const absent = (err) => err && err.code === ENOENT;
|
|
381
|
+
// The TYPE is part of the question: a regular file named `docs/ai/adr` is not a store, and a
|
|
382
|
+
// directory named `archive-decisions.mjs` is not a rotator. Answering `migrated` off a name alone
|
|
383
|
+
// is the same fail-open this survey exists to avoid.
|
|
384
|
+
const typeAt = (rel) => {
|
|
353
385
|
try {
|
|
354
|
-
return
|
|
355
|
-
} catch {
|
|
356
|
-
return
|
|
386
|
+
return stat(join(dir, rel));
|
|
387
|
+
} catch (err) {
|
|
388
|
+
if (absent(err)) return null;
|
|
389
|
+
throw err;
|
|
357
390
|
}
|
|
358
391
|
};
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
return '
|
|
392
|
+
const isA = (rel, kind) => typeAt(rel) === kind;
|
|
393
|
+
|
|
394
|
+
if (ADR_LAYOUT_PATHS.monoliths.some((rel) => isA(rel, PATH_FILE))) return 'old';
|
|
395
|
+
|
|
396
|
+
// Rotator provenance is resolved BEFORE any verdict, because it also disambiguates a tree that
|
|
397
|
+
// HAS a store: an old-scheme rotator beside a store is not finished — `upgrade` preserves that
|
|
398
|
+
// script and it will write a monolith again the next time it rotates.
|
|
399
|
+
const rotatorScheme = (() => {
|
|
400
|
+
if (!isA(ADR_LAYOUT_PATHS.rotator, PATH_FILE)) return null;
|
|
401
|
+
try {
|
|
402
|
+
return String(read(join(dir, ADR_LAYOUT_PATHS.rotator), 'utf8')).includes(ADR_LAYOUT_PATHS.storeMarker) ? 'new' : 'old';
|
|
403
|
+
} catch (err) {
|
|
404
|
+
if (absent(err)) return null;
|
|
405
|
+
throw err;
|
|
406
|
+
}
|
|
407
|
+
})();
|
|
408
|
+
|
|
409
|
+
if (isA(ADR_LAYOUT_PATHS.store, PATH_DIR)) return rotatorScheme === 'old' ? 'old-unrotated' : 'migrated';
|
|
410
|
+
// No substrate at all: the crossing cannot seed a store with nothing to put in it, so the
|
|
411
|
+
// detector must not ask for one (this is also what the rotator's own no-substrate skip does).
|
|
412
|
+
if (!isA(ADR_LAYOUT_PATHS.hot, PATH_FILE)) return 'none';
|
|
413
|
+
// No deployed rotator: no evidence of scheme, and nothing actionable — upgrade's seed-if-missing
|
|
414
|
+
// owns that path, and on a No-Node project a Node tool would be a permanent unactionable nag.
|
|
415
|
+
// A NEW-scheme rotator with no store is cohort A: its own --check already reds on every commit
|
|
416
|
+
// naming --write-navigator, so it is never nagged twice.
|
|
417
|
+
return rotatorScheme === 'old' ? 'old-unrotated' : 'none';
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// The LENIENT wrapper — the read-only status view must never crash, so any failure reads `none`.
|
|
421
|
+
// One implementation, two stated policies; the advisor uses the strict core deliberately.
|
|
422
|
+
const surveyAdrLayout = (dir, deps) => {
|
|
423
|
+
try {
|
|
424
|
+
return surveyAdrLayoutStrict(dir, deps);
|
|
425
|
+
} catch {
|
|
426
|
+
return 'none';
|
|
427
|
+
}
|
|
362
428
|
};
|
|
363
429
|
|
|
364
430
|
// surveyProject → the deploy axis for a target project dir: the per-member deployment stamps, whether
|
|
@@ -378,7 +444,7 @@ export const surveyProject = (projectDir, deps = {}) => {
|
|
|
378
444
|
}
|
|
379
445
|
})();
|
|
380
446
|
const deployed = stamps.some((s) => s.version != null) || docsAiPresent;
|
|
381
|
-
return { dir, deployed, docsAiPresent, adrLayout: surveyAdrLayout(dir,
|
|
447
|
+
return { dir, deployed, docsAiPresent, adrLayout: surveyAdrLayout(dir, deps), hiddenFence: hasHiddenFence(dir, deps), stamps };
|
|
382
448
|
};
|
|
383
449
|
|
|
384
450
|
// ── report ───────────────────────────────────────────────────────────────────────
|
|
@@ -615,7 +681,7 @@ export const buildEnvelope = (family, project = null, extras = {}) => {
|
|
|
615
681
|
dir: project.dir,
|
|
616
682
|
deployed: project.deployed,
|
|
617
683
|
docsAi: project.docsAiPresent,
|
|
618
|
-
adrLayout: project.adrLayout, // 'old' | 'migrated' | 'none' — a user-safe token, never a raw path
|
|
684
|
+
adrLayout: project.adrLayout, // 'old' | 'old-unrotated' | 'migrated' | 'none' — a user-safe token, never a raw path
|
|
619
685
|
// member + display + version only — never the internal stamp FILENAME (s.file).
|
|
620
686
|
deployStamps: project.stamps.map((s) => ({ member: s.name, display: displayOf(s.name), version: s.version ?? null })),
|
|
621
687
|
};
|
|
@@ -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, '..');
|
|
@@ -183,16 +191,181 @@ export const writeSnapshot = (cwd, refresh, stamp, deps = {}) => {
|
|
|
183
191
|
};
|
|
184
192
|
|
|
185
193
|
// Overwrite each refresh target with the kit canon, atomically, preserving the canon's exec bit.
|
|
194
|
+
//
|
|
195
|
+
// ORDER MATTERS: the rotation script is what the layout discriminator reads, so it is written LAST.
|
|
196
|
+
// A crash partway through a refresh that had already flipped it would otherwise leave a tree that
|
|
197
|
+
// LOOKS refreshed while other scripts are still the old copies — and a resume, keying on that same
|
|
198
|
+
// script, would skip them forever. Written last, an interrupted refresh always re-plans in full.
|
|
199
|
+
const DISCRIMINATOR_SCRIPT = ADR_LAYOUT_PATHS.rotator.split('/').pop();
|
|
200
|
+
const refreshOrder = (refresh) => [
|
|
201
|
+
...refresh.filter((r) => r.name !== DISCRIMINATOR_SCRIPT),
|
|
202
|
+
...refresh.filter((r) => r.name === DISCRIMINATOR_SCRIPT),
|
|
203
|
+
];
|
|
204
|
+
|
|
186
205
|
const applyScriptRefresh = (cwd, refresh, deps = {}) => {
|
|
187
206
|
const read = deps.read ?? readFileSync;
|
|
188
207
|
const chmod = deps.chmod ?? chmodSync;
|
|
189
208
|
const stat = deps.stat ?? statSync;
|
|
190
|
-
for (const { canon, dst, name } of refresh) {
|
|
209
|
+
for (const { canon, dst, name } of refreshOrder(refresh)) {
|
|
191
210
|
writeContainedFileAtomic(cwd, dst, read(canon, 'utf8'), deps, { stop, label: `${CONSUMER_SCRIPTS_REL}/${name}` });
|
|
192
211
|
chmod(dst, stat(canon).mode & 0o777); // the exec bit is the git-tracked axis the mirror guard pins
|
|
193
212
|
}
|
|
194
213
|
};
|
|
195
214
|
|
|
215
|
+
// ── the no-monolith crossing ─────────────────────────────────────────────────────
|
|
216
|
+
//
|
|
217
|
+
// A consumer on the RETIRED scheme that never rotated far enough to produce a monolith used to read
|
|
218
|
+
// "a fresh new-scheme tree" here and be sent away. The discriminator is the deployed rotation
|
|
219
|
+
// script's own provenance (family-registry.mjs), never "has decisions.md, lacks adr/".
|
|
220
|
+
//
|
|
221
|
+
// Re-entry is decided by what is FINISHED, never by one existence bit: the store directory existing
|
|
222
|
+
// does not prove the navigator was written or the index regenerated, so a crash there must not turn
|
|
223
|
+
// the next --apply into a no-op. Every write below is individually idempotent, which is why this
|
|
224
|
+
// needs no resume ledger.
|
|
225
|
+
|
|
226
|
+
// The crossing is COMPLETE when the navigator exists, the tree's own gate passes, AND the index the
|
|
227
|
+
// crossing regenerates is fresh. The index is part of the criterion because it is a real output of
|
|
228
|
+
// the crossing that `--check` never looks at: a crash (or a failed regeneration) between the
|
|
229
|
+
// navigator write and the index left a tree that reported "already migrated" on the retry and never
|
|
230
|
+
// repaired the index. An unreachable index generator is NOT treated as fresh — the crossing re-runs
|
|
231
|
+
// and fails loudly again, which is the honest outcome for a broken generator.
|
|
232
|
+
const INDEX_GENERATOR = join(KIT_SCRIPTS, 'check-docs-size.mjs');
|
|
233
|
+
const isIndexFresh = (cwd, deps = {}) => {
|
|
234
|
+
const spawn = deps.spawnSync ?? spawnSync;
|
|
235
|
+
const r = spawn(process.execPath, [INDEX_GENERATOR, '--check-index', `--root=${cwd}`], { encoding: 'utf8' });
|
|
236
|
+
return !r.error && r.status === 0;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// The layout verdict leads, and it is the SAME verdict the status line and the advisor read: an
|
|
240
|
+
// old-scheme rotator beside a finished store still answers `old-unrotated`, so treating that tree as
|
|
241
|
+
// done would leave the signal permanently lit with nothing able to clear it.
|
|
242
|
+
const isFinalised = (cwd, runMigrate, deps = {}) =>
|
|
243
|
+
surveyAdrLayoutStrict(cwd, deps) === 'migrated' &&
|
|
244
|
+
substratePresent(join(cwd, NAV_REL), deps) &&
|
|
245
|
+
runMigrate(['--check'], { root: cwd, log: () => {}, logError: () => {} }) === EXIT_OK &&
|
|
246
|
+
isIndexFresh(cwd, deps);
|
|
247
|
+
|
|
248
|
+
// `existsSync` answers false for EVERY failure, EACCES included — so asking it whether the substrate
|
|
249
|
+
// is there would turn an UNREADABLE tree into a confident "nothing to migrate", exit 0. Absence is
|
|
250
|
+
// ENOENT and nothing else; anything else is surfaced, never swallowed. Same policy the layout survey
|
|
251
|
+
// already enforces, now applied where the tool acts on it.
|
|
252
|
+
const substratePresent = (path, deps = {}) => {
|
|
253
|
+
const stat = deps.statSync ?? statSync;
|
|
254
|
+
try {
|
|
255
|
+
stat(path);
|
|
256
|
+
return true;
|
|
257
|
+
} catch (err) {
|
|
258
|
+
if (err && err.code === 'ENOENT') return false;
|
|
259
|
+
throw stop(`cannot read ${path} (${err && err.message}) — refusing to report on a tree it could not inspect`);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const crossWithoutMonolith = (cwd, args, stamp, { log, error, runMigrate, deps }) => {
|
|
264
|
+
const hasStore = substratePresent(join(cwd, ADR_DIR_REL), deps);
|
|
265
|
+
const hasHot = substratePresent(join(cwd, HOT_REL), deps);
|
|
266
|
+
|
|
267
|
+
if (!hasHot && !hasStore) {
|
|
268
|
+
// NOT "a fresh new-scheme tree" — this tree may well be old-scheme; it simply has no ADR
|
|
269
|
+
// substrate for the crossing to carry across, which is exactly the rotator's own skip.
|
|
270
|
+
log(`[migrate-adr-store] nothing to migrate — no ADR substrate (neither ${HOT_REL} nor ${ADR_DIR_REL}/) and no legacy monolith.`);
|
|
271
|
+
return EXIT_OK;
|
|
272
|
+
}
|
|
273
|
+
if (hasStore && isFinalised(cwd, runMigrate, deps)) {
|
|
274
|
+
log('[migrate-adr-store] already migrated — the one-file-per-ADR store is in place and its gate is green; nothing to do.');
|
|
275
|
+
return EXIT_OK;
|
|
276
|
+
}
|
|
277
|
+
if (!hasStore && surveyAdrLayoutStrict(cwd, deps) === 'none' && !substratePresent(join(cwd, ADR_LAYOUT_PATHS.rotator), deps)) {
|
|
278
|
+
// Nothing to refresh (the refresh is directional — it never ADDS a script) and nothing to
|
|
279
|
+
// maintain a store we might seed. The normal upgrade owns seeding the pair.
|
|
280
|
+
log('[migrate-adr-store] nothing to migrate — no deployed rotation script; run the normal upgrade first (it seeds the ADR enforcement pair).');
|
|
281
|
+
return EXIT_OK;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const refresh = planScriptRefresh(cwd, deps);
|
|
285
|
+
const drifted = refresh.filter((r) => r.differs);
|
|
286
|
+
// The read-only preflight: the SAME parse / half-migrated guard / store-integrity check the seed
|
|
287
|
+
// itself runs, stopping before every write. Without it a dry-run could green-light an apply that
|
|
288
|
+
// writes the store and only then discovers it cannot converge.
|
|
289
|
+
const preflight = (logError) => runMigrate(['--write-navigator', '--dry-run'], { root: cwd, log: () => {}, logError });
|
|
290
|
+
|
|
291
|
+
if (!args.apply) {
|
|
292
|
+
const preview = resolveSnapshotDir(cwd, stamp, deps);
|
|
293
|
+
// Three attempts to SUMMARISE why this tree needs the crossing produced three wrong sentences —
|
|
294
|
+
// each true of the common case and false of a state this arm deliberately supports. So the
|
|
295
|
+
// summary is gone: the preview states the two facts it actually knows, one per line, and the
|
|
296
|
+
// reader draws the conclusion. Nothing here can drift out of step with the tree, because nothing
|
|
297
|
+
// here is an inference. (Which scripts are stale is already reported by the refresh line below —
|
|
298
|
+
// never re-asserted here.)
|
|
299
|
+
const layout = surveyAdrLayoutStrict(cwd, deps);
|
|
300
|
+
const rotatorFact = layout === 'old-unrotated'
|
|
301
|
+
? `predates the one-file-per-ADR store`
|
|
302
|
+
: substratePresent(join(cwd, ADR_LAYOUT_PATHS.rotator), deps)
|
|
303
|
+
? `already names the store`
|
|
304
|
+
: `not deployed`; // and NOT "nothing to refresh" — a sibling script may still need one
|
|
305
|
+
log('[migrate-adr-store] --dry-run — no files will be changed. Planned crossing (no legacy monolith to retire):');
|
|
306
|
+
log(` deployed ${ADR_LAYOUT_PATHS.rotator}: ${rotatorFact}`);
|
|
307
|
+
log(` ${ADR_DIR_REL}/: ${hasStore ? 'present, but the crossing has not been completed' : 'absent'}`);
|
|
308
|
+
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)'}`);
|
|
309
|
+
log(` refresh ${refresh.length} enforcement script(s) to this kit's version${drifted.length ? ` (${drifted.length} locally differ: ${drifted.map((r) => r.name).join(', ')})` : ''}`);
|
|
310
|
+
log(` then seed the store: create ${ADR_DIR_REL}/, write ${NAV_REL} and regenerate docs/ai/index.md`);
|
|
311
|
+
const code = preflight((m) => error(` ${m}`));
|
|
312
|
+
if (code !== EXIT_OK) {
|
|
313
|
+
throw stop(`the tree cannot be seeded as it stands (exit ${code}) — NOT safe to --apply; fix the reported problem, then re-run.`);
|
|
314
|
+
}
|
|
315
|
+
if (preview.dir === null) {
|
|
316
|
+
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.');
|
|
317
|
+
}
|
|
318
|
+
log(' index regeneration is verified at --apply time (a dry-run cannot observe it without writing).');
|
|
319
|
+
log('Run `/agent-workflow-kit migrate-adr-store` again with --apply to perform it (it never commits).');
|
|
320
|
+
return EXIT_OK;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const pre = preflight(error);
|
|
324
|
+
if (pre !== EXIT_OK) {
|
|
325
|
+
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.`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const snapshot = writeSnapshot(cwd, refresh, stamp, deps);
|
|
329
|
+
// The FULL refresh is re-planned and re-applied on every entry, so an interrupted one always
|
|
330
|
+
// completes; the discriminator script is written last (see refreshOrder).
|
|
331
|
+
applyScriptRefresh(cwd, refresh, deps);
|
|
332
|
+
|
|
333
|
+
// Capture the index-regeneration verdict instead of matching log prose: the rotator logs a failed
|
|
334
|
+
// regeneration and still returns 0, so "the gates are green" would not mean the index is fresh.
|
|
335
|
+
const regen = { ok: true, detail: '' };
|
|
336
|
+
const seed = runMigrate(['--write-navigator'], {
|
|
337
|
+
root: cwd,
|
|
338
|
+
log,
|
|
339
|
+
logError: error,
|
|
340
|
+
regenerateIndex: (root, today, d) => {
|
|
341
|
+
const r = (deps.regenerateIndex ?? defaultRegenerateIndex)(root, today, d);
|
|
342
|
+
regen.ok = r.ok;
|
|
343
|
+
regen.detail = r.detail;
|
|
344
|
+
return r;
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
if (seed !== EXIT_OK) {
|
|
348
|
+
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).`);
|
|
349
|
+
}
|
|
350
|
+
if (!regen.ok) {
|
|
351
|
+
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).`);
|
|
352
|
+
}
|
|
353
|
+
const verify = runMigrate(['--check'], { root: cwd, log: () => {}, logError: error });
|
|
354
|
+
if (verify !== EXIT_OK) {
|
|
355
|
+
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).`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// States what this run DID, never what the tree was before it: the same arm completes an
|
|
359
|
+
// interrupted crossing whose scripts were already current, which no "old-scheme" claim covers.
|
|
360
|
+
log('[migrate-adr-store] crossing complete — the one-file-per-ADR store is in place (no legacy monolith was present):');
|
|
361
|
+
log(` snapshot: ${snapshot.dir} (${snapshot.viaGitDir ? 'git dir' : 'out-of-tree fallback'}, ${snapshot.fileCount} file(s))`);
|
|
362
|
+
log(` refreshed ${refresh.length} enforcement script(s) to this kit's version`);
|
|
363
|
+
log(` seeded ${ADR_DIR_REL}/ with ${NAV_REL} and regenerated docs/ai/index.md`);
|
|
364
|
+
log(' next: run the normal upgrade (it re-stamps the deployment lineage to the current head),');
|
|
365
|
+
log(' then review the migrated docs/ai/ tree and the re-stamp together and commit them yourself — this command never commits.');
|
|
366
|
+
return EXIT_OK;
|
|
367
|
+
};
|
|
368
|
+
|
|
196
369
|
export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
197
370
|
const log = deps.log ?? console.log;
|
|
198
371
|
const error = deps.error ?? console.error;
|
|
@@ -209,11 +382,7 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
|
209
382
|
|
|
210
383
|
const monoliths = monolithsPresent(cwd);
|
|
211
384
|
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;
|
|
385
|
+
return crossWithoutMonolith(cwd, args, stamp, { log, error, runMigrate, deps });
|
|
217
386
|
}
|
|
218
387
|
|
|
219
388
|
const refresh = planScriptRefresh(cwd, deps);
|