@ktpartners/dgs-platform 2.8.0 → 3.0.4
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 +96 -0
- package/README.md +41 -13
- package/agents/dgs-plan-checker.md +29 -3
- package/agents/dgs-planner.md +10 -0
- package/commands/dgs/abandon-quick.md +28 -0
- package/commands/dgs/add-tests.md +2 -2
- package/commands/dgs/audit-milestone.md +2 -2
- package/commands/dgs/capture-principle.md +11 -11
- package/commands/dgs/cleanup.md +2 -2
- package/commands/dgs/complete-milestone.md +11 -11
- package/commands/dgs/complete-quick.md +28 -0
- package/commands/dgs/create-milestone-job.md +2 -2
- package/commands/dgs/debug.md +3 -3
- package/commands/dgs/develop-idea.md +1 -1
- package/commands/dgs/fast.md +3 -1
- package/commands/dgs/health.md +1 -1
- package/commands/dgs/map-codebase.md +6 -6
- package/commands/dgs/new-milestone.md +5 -5
- package/commands/dgs/new-project.md +6 -6
- package/commands/dgs/plan-milestone-gaps.md +1 -1
- package/commands/dgs/progress.md +3 -3
- package/commands/dgs/quick-abandon.md +8 -0
- package/commands/dgs/quick-complete.md +8 -0
- package/commands/dgs/quick.md +10 -3
- package/commands/dgs/research-idea.md +2 -2
- package/commands/dgs/research-phase.md +3 -3
- package/commands/dgs/switch-project.md +1 -1
- package/commands/dgs/write-spec.md +3 -3
- package/deliver-great-systems/bin/dgs-tools.cjs +284 -30
- package/deliver-great-systems/bin/lib/commands.cjs +316 -31
- package/deliver-great-systems/bin/lib/commands.test.cjs +336 -0
- package/deliver-great-systems/bin/lib/config.cjs +39 -6
- package/deliver-great-systems/bin/lib/context.cjs +120 -0
- package/deliver-great-systems/bin/lib/core.cjs +28 -11
- package/deliver-great-systems/bin/lib/execution.cjs +49 -17
- package/deliver-great-systems/bin/lib/flat-migration.test.cjs +396 -0
- package/deliver-great-systems/bin/lib/ideas.cjs +206 -91
- package/deliver-great-systems/bin/lib/ideas.test.cjs +244 -1
- package/deliver-great-systems/bin/lib/init.cjs +306 -39
- package/deliver-great-systems/bin/lib/init.test.cjs +416 -6
- package/deliver-great-systems/bin/lib/jobs.cjs +124 -21
- package/deliver-great-systems/bin/lib/jobs.test.cjs +193 -74
- package/deliver-great-systems/bin/lib/migration.cjs +409 -1
- package/deliver-great-systems/bin/lib/migration.test.cjs +158 -1
- package/deliver-great-systems/bin/lib/milestone.cjs +54 -29
- package/deliver-great-systems/bin/lib/phase.cjs +128 -2
- package/deliver-great-systems/bin/lib/phase.test.cjs +420 -0
- package/deliver-great-systems/bin/lib/projects.cjs +28 -8
- package/deliver-great-systems/bin/lib/projects.test.cjs +86 -0
- package/deliver-great-systems/bin/lib/quick.cjs +584 -0
- package/deliver-great-systems/bin/lib/quick.test.cjs +596 -0
- package/deliver-great-systems/bin/lib/repos.cjs +25 -1
- package/deliver-great-systems/bin/lib/roadmap.cjs +34 -13
- package/deliver-great-systems/bin/lib/specs.cjs +3 -81
- package/deliver-great-systems/bin/lib/state-transition-gate.test.cjs +160 -0
- package/deliver-great-systems/bin/lib/state.cjs +142 -54
- package/deliver-great-systems/bin/lib/sync.cjs +75 -0
- package/deliver-great-systems/bin/lib/verify.cjs +80 -1
- package/deliver-great-systems/bin/lib/worktrees.cjs +764 -0
- package/deliver-great-systems/bin/lib/worktrees.test.cjs +887 -0
- package/deliver-great-systems/templates/claude-md.md +16 -0
- package/deliver-great-systems/workflows/abandon-quick.md +89 -0
- package/deliver-great-systems/workflows/add-idea.md +3 -3
- package/deliver-great-systems/workflows/add-tests.md +14 -0
- package/deliver-great-systems/workflows/add-todo.md +1 -0
- package/deliver-great-systems/workflows/approve-spec.md +25 -4
- package/deliver-great-systems/workflows/audit-phase.md +15 -5
- package/deliver-great-systems/workflows/cancel-job.md +1 -1
- package/deliver-great-systems/workflows/check-todos.md +2 -3
- package/deliver-great-systems/workflows/complete-milestone.md +197 -22
- package/deliver-great-systems/workflows/complete-quick.md +68 -0
- package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
- package/deliver-great-systems/workflows/create-milestone-job.md +4 -4
- package/deliver-great-systems/workflows/develop-idea.md +11 -11
- package/deliver-great-systems/workflows/diagnose-issues.md +14 -0
- package/deliver-great-systems/workflows/discuss-idea.md +1 -1
- package/deliver-great-systems/workflows/execute-phase.md +121 -32
- package/deliver-great-systems/workflows/execute-plan.md +12 -21
- package/deliver-great-systems/workflows/help.md +33 -29
- package/deliver-great-systems/workflows/init-product.md +2 -18
- package/deliver-great-systems/workflows/new-milestone.md +40 -24
- package/deliver-great-systems/workflows/new-project.md +22 -680
- package/deliver-great-systems/workflows/progress-all.md +133 -0
- package/deliver-great-systems/workflows/quick-abandon.md +89 -0
- package/deliver-great-systems/workflows/quick-complete.md +68 -0
- package/deliver-great-systems/workflows/quick.md +152 -23
- package/deliver-great-systems/workflows/refine-spec.md +1 -1
- package/deliver-great-systems/workflows/research-idea.md +8 -8
- package/deliver-great-systems/workflows/resume-project.md +2 -2
- package/deliver-great-systems/workflows/run-job.md +8 -8
- package/deliver-great-systems/workflows/validate-phase.md +39 -1
- package/deliver-great-systems/workflows/verify-work.md +14 -0
- package/deliver-great-systems/workflows/write-spec.md +2 -2
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const { execGit, safeReadFile, error } = require('./core.cjs');
|
|
15
|
+
const { extractFrontmatter, spliceFrontmatter } = require('./frontmatter.cjs');
|
|
15
16
|
|
|
16
17
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
17
18
|
|
|
@@ -286,6 +287,413 @@ function migrateDotPlanningToRoot(cwd, options) {
|
|
|
286
287
|
};
|
|
287
288
|
}
|
|
288
289
|
|
|
290
|
+
// ─── Flat Status Migration ────────────���──────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Migrates ideas, todos, jobs from state subdirectories to flat directories
|
|
294
|
+
* with frontmatter status fields. Also flattens research docs in docs/ideas/.
|
|
295
|
+
*
|
|
296
|
+
* @param {string} [cwd] - Working directory (defaults to process.cwd())
|
|
297
|
+
* @param {Object} [options] - Migration options
|
|
298
|
+
* @param {boolean} [options.apply=false] - If true, execute migration; otherwise dry-run
|
|
299
|
+
* @returns {{
|
|
300
|
+
* migrated: boolean,
|
|
301
|
+
* dryRun: boolean,
|
|
302
|
+
* actions: Array<{type: string, subsystem?: string, from: string, to: string, state?: string, mappedStatus?: string, filename?: string}>,
|
|
303
|
+
* filesMoved: number,
|
|
304
|
+
* statusFieldsAdded: number,
|
|
305
|
+
* pathsNormalized: number,
|
|
306
|
+
* commitHash: string|null,
|
|
307
|
+
* }}
|
|
308
|
+
*/
|
|
309
|
+
function migrateFlatStatus(cwd, options) {
|
|
310
|
+
const opts = options || {};
|
|
311
|
+
const apply = opts.apply || false;
|
|
312
|
+
|
|
313
|
+
// Step 1 — Resolve paths and check idempotency
|
|
314
|
+
const resolved = path.resolve(cwd || process.cwd());
|
|
315
|
+
const localPath = path.join(resolved, 'config.local.json');
|
|
316
|
+
let localConfig = {};
|
|
317
|
+
try {
|
|
318
|
+
if (fs.existsSync(localPath)) {
|
|
319
|
+
localConfig = JSON.parse(fs.readFileSync(localPath, 'utf-8'));
|
|
320
|
+
}
|
|
321
|
+
} catch { /* ignore */ }
|
|
322
|
+
if (localConfig.flat_status_migration_done) {
|
|
323
|
+
return { migrated: false, dryRun: !apply, actions: [], filesMoved: 0, statusFieldsAdded: 0, pathsNormalized: 0, commitHash: null };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Step 2 — Define subsystem configs
|
|
327
|
+
const SUBSYSTEMS = [
|
|
328
|
+
{
|
|
329
|
+
name: 'ideas',
|
|
330
|
+
dir: 'ideas',
|
|
331
|
+
states: ['pending', 'done', 'rejected', 'consolidated'],
|
|
332
|
+
statusMap: { pending: 'pending', done: 'done', rejected: 'rejected', consolidated: 'consolidated' },
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: 'todos',
|
|
336
|
+
dir: 'todos',
|
|
337
|
+
states: ['pending', 'completed', 'done', 'resolved'],
|
|
338
|
+
statusMap: { pending: 'pending', completed: 'done', done: 'done', resolved: 'done' },
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: 'jobs',
|
|
342
|
+
dir: 'jobs',
|
|
343
|
+
states: ['pending', 'in-progress', 'completed', 'failed'],
|
|
344
|
+
statusMap: { pending: 'pending', 'in-progress': 'in-progress', completed: 'completed', failed: 'failed' },
|
|
345
|
+
},
|
|
346
|
+
];
|
|
347
|
+
|
|
348
|
+
const researchStates = ['pending', 'done', 'rejected', 'consolidated'];
|
|
349
|
+
|
|
350
|
+
// Step 3 — Detect legacy layout
|
|
351
|
+
let hasLegacyFiles = false;
|
|
352
|
+
for (const sub of SUBSYSTEMS) {
|
|
353
|
+
for (const state of sub.states) {
|
|
354
|
+
const stateDir = path.join(resolved, sub.dir, state);
|
|
355
|
+
if (fs.existsSync(stateDir) && fs.statSync(stateDir).isDirectory()) {
|
|
356
|
+
const files = fs.readdirSync(stateDir).filter(f => f.endsWith('.md'));
|
|
357
|
+
if (files.length > 0) { hasLegacyFiles = true; break; }
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (hasLegacyFiles) break;
|
|
361
|
+
}
|
|
362
|
+
// Also check research docs
|
|
363
|
+
if (!hasLegacyFiles) {
|
|
364
|
+
for (const state of researchStates) {
|
|
365
|
+
const resDir = path.join(resolved, 'docs', 'ideas', state);
|
|
366
|
+
if (fs.existsSync(resDir) && fs.statSync(resDir).isDirectory()) {
|
|
367
|
+
const files = fs.readdirSync(resDir).filter(f => f.endsWith('.md'));
|
|
368
|
+
if (files.length > 0) { hasLegacyFiles = true; break; }
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (!hasLegacyFiles) {
|
|
373
|
+
localConfig.flat_status_migration_done = true;
|
|
374
|
+
fs.writeFileSync(localPath, JSON.stringify(localConfig, null, 2) + '\n');
|
|
375
|
+
return { migrated: false, dryRun: !apply, actions: [], filesMoved: 0, statusFieldsAdded: 0, pathsNormalized: 0, commitHash: null };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Step 4 — Clean working tree check (apply mode only)
|
|
379
|
+
const inGit = isGitRepo(resolved);
|
|
380
|
+
if (apply && inGit && !isCleanWorkingTree(resolved)) {
|
|
381
|
+
error('Flat status migration requires a clean working tree. Commit or stash your changes first.');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Step 5 — Count all .md files before migration (for validation)
|
|
385
|
+
let beforeTotal = 0;
|
|
386
|
+
for (const sub of SUBSYSTEMS) {
|
|
387
|
+
const flatDir = path.join(resolved, sub.dir);
|
|
388
|
+
if (fs.existsSync(flatDir)) {
|
|
389
|
+
beforeTotal += fs.readdirSync(flatDir).filter(f => f.endsWith('.md') && !fs.statSync(path.join(flatDir, f)).isDirectory()).length;
|
|
390
|
+
}
|
|
391
|
+
for (const state of sub.states) {
|
|
392
|
+
const stateDir = path.join(resolved, sub.dir, state);
|
|
393
|
+
if (fs.existsSync(stateDir) && fs.statSync(stateDir).isDirectory()) {
|
|
394
|
+
beforeTotal += fs.readdirSync(stateDir).filter(f => f.endsWith('.md')).length;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const researchBaseDir = path.join(resolved, 'docs', 'ideas');
|
|
399
|
+
if (fs.existsSync(researchBaseDir)) {
|
|
400
|
+
beforeTotal += fs.readdirSync(researchBaseDir).filter(f => f.endsWith('.md') && !fs.statSync(path.join(researchBaseDir, f)).isDirectory()).length;
|
|
401
|
+
}
|
|
402
|
+
for (const state of researchStates) {
|
|
403
|
+
const resDir = path.join(resolved, 'docs', 'ideas', state);
|
|
404
|
+
if (fs.existsSync(resDir) && fs.statSync(resDir).isDirectory()) {
|
|
405
|
+
beforeTotal += fs.readdirSync(resDir).filter(f => f.endsWith('.md')).length;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Step 6 — Collect actions for each subsystem
|
|
410
|
+
const actions = [];
|
|
411
|
+
for (const sub of SUBSYSTEMS) {
|
|
412
|
+
for (const state of sub.states) {
|
|
413
|
+
const stateDir = path.join(resolved, sub.dir, state);
|
|
414
|
+
if (!fs.existsSync(stateDir) || !fs.statSync(stateDir).isDirectory()) continue;
|
|
415
|
+
const files = fs.readdirSync(stateDir).filter(f => f.endsWith('.md'));
|
|
416
|
+
for (const filename of files) {
|
|
417
|
+
const from = path.join(sub.dir, state, filename);
|
|
418
|
+
const to = path.join(sub.dir, filename);
|
|
419
|
+
const destAbs = path.join(resolved, to);
|
|
420
|
+
if (fs.existsSync(destAbs)) continue; // already flat, skip
|
|
421
|
+
actions.push({ type: 'move', subsystem: sub.name, from, to, state, mappedStatus: sub.statusMap[state], filename });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// Research docs
|
|
426
|
+
for (const state of researchStates) {
|
|
427
|
+
const resDir = path.join(resolved, 'docs', 'ideas', state);
|
|
428
|
+
if (!fs.existsSync(resDir) || !fs.statSync(resDir).isDirectory()) continue;
|
|
429
|
+
const files = fs.readdirSync(resDir).filter(f => f.endsWith('.md'));
|
|
430
|
+
for (const filename of files) {
|
|
431
|
+
const from = path.join('docs', 'ideas', state, filename);
|
|
432
|
+
const to = path.join('docs', 'ideas', filename);
|
|
433
|
+
const destAbs = path.join(resolved, to);
|
|
434
|
+
if (fs.existsSync(destAbs)) continue; // already flat, skip
|
|
435
|
+
actions.push({ type: 'move-research', from, to, state, filename });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Step 7 — Filename collision check
|
|
440
|
+
const destPaths = new Set();
|
|
441
|
+
for (const action of actions) {
|
|
442
|
+
if (destPaths.has(action.to)) {
|
|
443
|
+
error('Flat status migration aborted: filename collision at ' + action.to + '. Two files from different state directories have the same name.');
|
|
444
|
+
}
|
|
445
|
+
destPaths.add(action.to);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Step 8 — Dry-run return
|
|
449
|
+
if (!apply) {
|
|
450
|
+
return {
|
|
451
|
+
migrated: false,
|
|
452
|
+
dryRun: true,
|
|
453
|
+
actions,
|
|
454
|
+
filesMoved: actions.filter(a => a.type === 'move' || a.type === 'move-research').length,
|
|
455
|
+
statusFieldsAdded: actions.filter(a => a.type === 'move').length,
|
|
456
|
+
pathsNormalized: 0,
|
|
457
|
+
commitHash: null,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Step 9 — Execute migration (apply mode)
|
|
462
|
+
let pathsNormalized = 0;
|
|
463
|
+
|
|
464
|
+
for (const action of actions) {
|
|
465
|
+
const srcAbs = path.join(resolved, action.from);
|
|
466
|
+
const destAbs = path.join(resolved, action.to);
|
|
467
|
+
|
|
468
|
+
if (action.type === 'move') {
|
|
469
|
+
// 1. Git mv to preserve history
|
|
470
|
+
fs.mkdirSync(path.dirname(destAbs), { recursive: true });
|
|
471
|
+
if (inGit) {
|
|
472
|
+
execGit(resolved, ['mv', action.from, action.to]);
|
|
473
|
+
} else {
|
|
474
|
+
fs.renameSync(srcAbs, destAbs);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// 2. Add/update status in frontmatter
|
|
478
|
+
let content = fs.readFileSync(destAbs, 'utf-8');
|
|
479
|
+
const fm = extractFrontmatter(content);
|
|
480
|
+
if (fm && typeof fm === 'object' && Object.keys(fm).length > 0) {
|
|
481
|
+
fm.status = action.mappedStatus;
|
|
482
|
+
content = spliceFrontmatter(content, fm);
|
|
483
|
+
} else {
|
|
484
|
+
// No frontmatter — prepend
|
|
485
|
+
content = '---\nstatus: ' + action.mappedStatus + '\n---\n' + content;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 3. For jobs: update bold-key Status header
|
|
489
|
+
if (action.subsystem === 'jobs') {
|
|
490
|
+
content = content.replace(/\*\*Status:\*\*\s*\S+/, '**Status:** ' + action.mappedStatus);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// 4. Normalize consolidated_into and consolidated_from paths in ideas
|
|
494
|
+
if (action.subsystem === 'ideas') {
|
|
495
|
+
const before = content;
|
|
496
|
+
content = content.replace(/ideas\/(pending|done|rejected|consolidated)\//g, 'ideas/');
|
|
497
|
+
if (content !== before) pathsNormalized++;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// 5. Normalize research doc path references in ideas
|
|
501
|
+
if (action.subsystem === 'ideas') {
|
|
502
|
+
content = content.replace(/docs\/ideas\/(pending|done|rejected|consolidated)\//g, 'docs/ideas/');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
fs.writeFileSync(destAbs, content);
|
|
506
|
+
} else if (action.type === 'move-research') {
|
|
507
|
+
// Research docs: just move, no frontmatter changes
|
|
508
|
+
fs.mkdirSync(path.dirname(destAbs), { recursive: true });
|
|
509
|
+
if (inGit) {
|
|
510
|
+
execGit(resolved, ['mv', action.from, action.to]);
|
|
511
|
+
} else {
|
|
512
|
+
fs.renameSync(srcAbs, destAbs);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Step 10 — Normalize paths in idea files already in flat dir
|
|
518
|
+
const ideasDir = path.join(resolved, 'ideas');
|
|
519
|
+
if (fs.existsSync(ideasDir)) {
|
|
520
|
+
const flatIdeas = fs.readdirSync(ideasDir).filter(f => f.endsWith('.md'));
|
|
521
|
+
for (const f of flatIdeas) {
|
|
522
|
+
const fPath = path.join(ideasDir, f);
|
|
523
|
+
if (!fs.statSync(fPath).isFile()) continue;
|
|
524
|
+
let content = fs.readFileSync(fPath, 'utf-8');
|
|
525
|
+
const before = content;
|
|
526
|
+
content = content.replace(/ideas\/(pending|done|rejected|consolidated)\//g, 'ideas/');
|
|
527
|
+
content = content.replace(/docs\/ideas\/(pending|done|rejected|consolidated)\//g, 'docs/ideas/');
|
|
528
|
+
if (content !== before) {
|
|
529
|
+
fs.writeFileSync(fPath, content);
|
|
530
|
+
pathsNormalized++;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Step 11 — Remove empty state subdirectories
|
|
536
|
+
for (const sub of SUBSYSTEMS) {
|
|
537
|
+
for (const state of sub.states) {
|
|
538
|
+
const stateDir = path.join(resolved, sub.dir, state);
|
|
539
|
+
if (fs.existsSync(stateDir)) {
|
|
540
|
+
try {
|
|
541
|
+
const remaining = fs.readdirSync(stateDir);
|
|
542
|
+
if (remaining.length === 0) {
|
|
543
|
+
fs.rmdirSync(stateDir);
|
|
544
|
+
}
|
|
545
|
+
} catch { /* ignore — directory may have non-.md files */ }
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
for (const state of researchStates) {
|
|
550
|
+
const resDir = path.join(resolved, 'docs', 'ideas', state);
|
|
551
|
+
if (fs.existsSync(resDir)) {
|
|
552
|
+
try {
|
|
553
|
+
const remaining = fs.readdirSync(resDir);
|
|
554
|
+
if (remaining.length === 0) {
|
|
555
|
+
fs.rmdirSync(resDir);
|
|
556
|
+
}
|
|
557
|
+
} catch { /* ignore */ }
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Step 12 — File count validation (post-move)
|
|
562
|
+
let afterTotal = 0;
|
|
563
|
+
for (const sub of SUBSYSTEMS) {
|
|
564
|
+
const flatDir = path.join(resolved, sub.dir);
|
|
565
|
+
if (fs.existsSync(flatDir)) {
|
|
566
|
+
afterTotal += fs.readdirSync(flatDir).filter(f => f.endsWith('.md') && !fs.statSync(path.join(flatDir, f)).isDirectory()).length;
|
|
567
|
+
}
|
|
568
|
+
for (const state of sub.states) {
|
|
569
|
+
const stateDir = path.join(resolved, sub.dir, state);
|
|
570
|
+
if (fs.existsSync(stateDir) && fs.statSync(stateDir).isDirectory()) {
|
|
571
|
+
afterTotal += fs.readdirSync(stateDir).filter(f => f.endsWith('.md')).length;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (fs.existsSync(researchBaseDir)) {
|
|
576
|
+
afterTotal += fs.readdirSync(researchBaseDir).filter(f => f.endsWith('.md') && !fs.statSync(path.join(researchBaseDir, f)).isDirectory()).length;
|
|
577
|
+
}
|
|
578
|
+
for (const state of researchStates) {
|
|
579
|
+
const resDir = path.join(resolved, 'docs', 'ideas', state);
|
|
580
|
+
if (fs.existsSync(resDir) && fs.statSync(resDir).isDirectory()) {
|
|
581
|
+
afterTotal += fs.readdirSync(resDir).filter(f => f.endsWith('.md')).length;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (beforeTotal !== afterTotal) {
|
|
585
|
+
// Rollback
|
|
586
|
+
if (inGit) {
|
|
587
|
+
execGit(resolved, ['checkout', 'HEAD', '--', '.']);
|
|
588
|
+
}
|
|
589
|
+
error('Flat status migration aborted: file count mismatch. Before: ' + beforeTotal + ', After: ' + afterTotal + '. Changes rolled back.');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Step 13 — Atomic git commit
|
|
593
|
+
let commitHash = null;
|
|
594
|
+
if (inGit) {
|
|
595
|
+
execGit(resolved, ['add', '-A']);
|
|
596
|
+
const commitResult = execGit(resolved, ['commit', '-m', 'chore: migrate to flat status directories']);
|
|
597
|
+
if (commitResult.exitCode !== 0) {
|
|
598
|
+
execGit(resolved, ['checkout', 'HEAD', '--', '.']);
|
|
599
|
+
error('Flat status migration commit failed. Changes rolled back. Error: ' + commitResult.stderr);
|
|
600
|
+
}
|
|
601
|
+
const hashResult = execGit(resolved, ['rev-parse', '--short', 'HEAD']);
|
|
602
|
+
commitHash = hashResult.stdout;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Step 14 — Set migration flag
|
|
606
|
+
localConfig.flat_status_migration_done = true;
|
|
607
|
+
fs.writeFileSync(localPath, JSON.stringify(localConfig, null, 2) + '\n');
|
|
608
|
+
if (inGit) {
|
|
609
|
+
execGit(resolved, ['add', localPath]);
|
|
610
|
+
execGit(resolved, ['commit', '--amend', '--no-edit']);
|
|
611
|
+
const hashResult = execGit(resolved, ['rev-parse', '--short', 'HEAD']);
|
|
612
|
+
commitHash = hashResult.stdout;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Step 15 — Return result
|
|
616
|
+
return {
|
|
617
|
+
migrated: true,
|
|
618
|
+
dryRun: false,
|
|
619
|
+
actions,
|
|
620
|
+
filesMoved: actions.filter(a => a.type === 'move' || a.type === 'move-research').length,
|
|
621
|
+
statusFieldsAdded: actions.filter(a => a.type === 'move').length,
|
|
622
|
+
pathsNormalized,
|
|
623
|
+
commitHash,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// ─── Branching Config Migration ────────��─────────────────────────────────────
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Remove legacy branching config keys. Runs lazily — called once per CLI invocation.
|
|
631
|
+
* No-op if already migrated or no legacy keys present.
|
|
632
|
+
*
|
|
633
|
+
* Removes: branching_strategy, phase_branch_template, milestone_branch_template,
|
|
634
|
+
* git.worktree_setup_command. Sets branching_migration_done flag in config.local.json
|
|
635
|
+
* so the check only runs once.
|
|
636
|
+
*
|
|
637
|
+
* @param {string} cwd - Planning root directory
|
|
638
|
+
*/
|
|
639
|
+
function migrateBranchingConfig(cwd) {
|
|
640
|
+
const resolved = path.resolve(cwd || process.cwd());
|
|
641
|
+
const localPath = path.join(resolved, 'config.local.json');
|
|
642
|
+
const sharedPath = path.join(resolved, 'config.json');
|
|
643
|
+
|
|
644
|
+
// 1. Check migration_done flag in config.local.json
|
|
645
|
+
let localConfig = {};
|
|
646
|
+
try {
|
|
647
|
+
if (fs.existsSync(localPath)) {
|
|
648
|
+
localConfig = JSON.parse(fs.readFileSync(localPath, 'utf-8'));
|
|
649
|
+
}
|
|
650
|
+
} catch { /* ignore */ }
|
|
651
|
+
|
|
652
|
+
if (localConfig.branching_migration_done) return; // already migrated
|
|
653
|
+
|
|
654
|
+
// 2. Read config.json
|
|
655
|
+
let config = {};
|
|
656
|
+
try {
|
|
657
|
+
if (fs.existsSync(sharedPath)) {
|
|
658
|
+
config = JSON.parse(fs.readFileSync(sharedPath, 'utf-8'));
|
|
659
|
+
}
|
|
660
|
+
} catch { /* ignore */ }
|
|
661
|
+
|
|
662
|
+
// 3. Check for legacy keys
|
|
663
|
+
const hasStrategy = 'branching_strategy' in config;
|
|
664
|
+
const hasPhaseTemplate = 'phase_branch_template' in config;
|
|
665
|
+
const hasMilestoneTemplate = 'milestone_branch_template' in config;
|
|
666
|
+
const hasWorktreeSetup = config.git && 'worktree_setup_command' in config.git;
|
|
667
|
+
|
|
668
|
+
if (!hasStrategy && !hasPhaseTemplate && !hasMilestoneTemplate && !hasWorktreeSetup) {
|
|
669
|
+
// No legacy keys — set flag and return silently
|
|
670
|
+
localConfig.branching_migration_done = true;
|
|
671
|
+
fs.writeFileSync(localPath, JSON.stringify(localConfig, null, 2) + '\n');
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// 4. Remove legacy keys
|
|
676
|
+
delete config.branching_strategy;
|
|
677
|
+
delete config.phase_branch_template;
|
|
678
|
+
delete config.milestone_branch_template;
|
|
679
|
+
if (config.git && 'worktree_setup_command' in config.git) {
|
|
680
|
+
delete config.git.worktree_setup_command;
|
|
681
|
+
if (Object.keys(config.git).length === 0) {
|
|
682
|
+
delete config.git;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Write updated config.json
|
|
687
|
+
fs.writeFileSync(sharedPath, JSON.stringify(config, null, 2) + '\n');
|
|
688
|
+
|
|
689
|
+
// 5. Info message
|
|
690
|
+
process.stderr.write('[DGS] Migrated: removed branching_strategy config (worktrees are now the only model)\n');
|
|
691
|
+
|
|
692
|
+
// 6. Set migration flag
|
|
693
|
+
localConfig.branching_migration_done = true;
|
|
694
|
+
fs.writeFileSync(localPath, JSON.stringify(localConfig, null, 2) + '\n');
|
|
695
|
+
}
|
|
696
|
+
|
|
289
697
|
// ─── V1 Rejection ────────────────────────────────────────────────────────────
|
|
290
698
|
|
|
291
699
|
/**
|
|
@@ -324,4 +732,4 @@ function rejectV1Install(cwd) {
|
|
|
324
732
|
|
|
325
733
|
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
326
734
|
|
|
327
|
-
module.exports = { migrateDotPlanningToRoot, rejectV1Install };
|
|
735
|
+
module.exports = { migrateDotPlanningToRoot, rejectV1Install, migrateBranchingConfig, migrateFlatStatus };
|
|
@@ -13,7 +13,7 @@ const { execSync } = require('child_process');
|
|
|
13
13
|
|
|
14
14
|
const { createTempDir, cleanupDir, writeFile, initGitRepo } = require('./test-helpers.cjs');
|
|
15
15
|
const { resetPaths } = require('./paths.cjs');
|
|
16
|
-
const { migrateDotPlanningToRoot } = require('./migration.cjs');
|
|
16
|
+
const { migrateDotPlanningToRoot, migrateBranchingConfig } = require('./migration.cjs');
|
|
17
17
|
|
|
18
18
|
const libDir = path.resolve(__dirname);
|
|
19
19
|
|
|
@@ -523,3 +523,160 @@ describe('CLI: auto-trigger migration', () => {
|
|
|
523
523
|
'stderr should NOT contain migration message, got: ' + result.stderr);
|
|
524
524
|
});
|
|
525
525
|
});
|
|
526
|
+
|
|
527
|
+
// ─── Suite 13: Branching config migration ─────────────────────────────────────
|
|
528
|
+
|
|
529
|
+
describe('migrateBranchingConfig', () => {
|
|
530
|
+
let dir;
|
|
531
|
+
|
|
532
|
+
beforeEach(() => {
|
|
533
|
+
dir = createTempDir('dgs-branchmig-test-');
|
|
534
|
+
dir = fs.realpathSync(dir);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
afterEach(() => {
|
|
538
|
+
cleanupDir(dir);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('clean config — no branching keys — migration is no-op', () => {
|
|
542
|
+
writeFile(dir, 'config.json', JSON.stringify({
|
|
543
|
+
model_profile: 'balanced',
|
|
544
|
+
base_branch: 'main',
|
|
545
|
+
}, null, 2));
|
|
546
|
+
|
|
547
|
+
migrateBranchingConfig(dir);
|
|
548
|
+
|
|
549
|
+
const config = JSON.parse(fs.readFileSync(path.join(dir, 'config.json'), 'utf-8'));
|
|
550
|
+
assert.strictEqual(config.model_profile, 'balanced', 'config.json should be unchanged');
|
|
551
|
+
assert.strictEqual(config.base_branch, 'main', 'base_branch should be preserved');
|
|
552
|
+
|
|
553
|
+
const local = JSON.parse(fs.readFileSync(path.join(dir, 'config.local.json'), 'utf-8'));
|
|
554
|
+
assert.strictEqual(local.branching_migration_done, true, 'migration_done flag should be set');
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('config with branching_strategy none — keys removed, message shown', () => {
|
|
558
|
+
writeFile(dir, 'config.json', JSON.stringify({
|
|
559
|
+
model_profile: 'balanced',
|
|
560
|
+
branching_strategy: 'none',
|
|
561
|
+
phase_branch_template: 'dgs/{project}/phase-{phase}-{slug}',
|
|
562
|
+
base_branch: 'main',
|
|
563
|
+
}, null, 2));
|
|
564
|
+
|
|
565
|
+
// Capture stderr
|
|
566
|
+
const origWrite = process.stderr.write;
|
|
567
|
+
let stderrOutput = '';
|
|
568
|
+
process.stderr.write = (chunk) => { stderrOutput += chunk; return true; };
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
migrateBranchingConfig(dir);
|
|
572
|
+
} finally {
|
|
573
|
+
process.stderr.write = origWrite;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const config = JSON.parse(fs.readFileSync(path.join(dir, 'config.json'), 'utf-8'));
|
|
577
|
+
assert.ok(!('branching_strategy' in config), 'branching_strategy should be removed');
|
|
578
|
+
assert.ok(!('phase_branch_template' in config), 'phase_branch_template should be removed');
|
|
579
|
+
assert.strictEqual(config.model_profile, 'balanced', 'Other keys preserved');
|
|
580
|
+
assert.strictEqual(config.base_branch, 'main', 'base_branch preserved');
|
|
581
|
+
|
|
582
|
+
const local = JSON.parse(fs.readFileSync(path.join(dir, 'config.local.json'), 'utf-8'));
|
|
583
|
+
assert.strictEqual(local.branching_migration_done, true, 'migration_done flag should be set');
|
|
584
|
+
|
|
585
|
+
assert.ok(stderrOutput.includes('Migrated'), 'Should show migration info message');
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('config with branching_strategy milestone — all keys removed', () => {
|
|
589
|
+
writeFile(dir, 'config.json', JSON.stringify({
|
|
590
|
+
model_profile: 'balanced',
|
|
591
|
+
branching_strategy: 'milestone',
|
|
592
|
+
phase_branch_template: 'dgs/{project}/phase-{phase}-{slug}',
|
|
593
|
+
milestone_branch_template: 'dgs/{project}/{milestone}-{slug}',
|
|
594
|
+
base_branch: 'main',
|
|
595
|
+
}, null, 2));
|
|
596
|
+
|
|
597
|
+
// Suppress stderr
|
|
598
|
+
const origWrite = process.stderr.write;
|
|
599
|
+
process.stderr.write = () => true;
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
migrateBranchingConfig(dir);
|
|
603
|
+
} finally {
|
|
604
|
+
process.stderr.write = origWrite;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const config = JSON.parse(fs.readFileSync(path.join(dir, 'config.json'), 'utf-8'));
|
|
608
|
+
assert.ok(!('branching_strategy' in config), 'branching_strategy should be removed');
|
|
609
|
+
assert.ok(!('phase_branch_template' in config), 'phase_branch_template should be removed');
|
|
610
|
+
assert.ok(!('milestone_branch_template' in config), 'milestone_branch_template should be removed');
|
|
611
|
+
assert.strictEqual(config.model_profile, 'balanced', 'Other keys preserved');
|
|
612
|
+
assert.strictEqual(config.base_branch, 'main', 'base_branch preserved');
|
|
613
|
+
|
|
614
|
+
const local = JSON.parse(fs.readFileSync(path.join(dir, 'config.local.json'), 'utf-8'));
|
|
615
|
+
assert.strictEqual(local.branching_migration_done, true, 'migration_done flag should be set');
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('config with git.worktree_setup_command — key removed, empty git object removed', () => {
|
|
619
|
+
writeFile(dir, 'config.json', JSON.stringify({
|
|
620
|
+
model_profile: 'balanced',
|
|
621
|
+
git: { worktree_setup_command: 'npm install' },
|
|
622
|
+
}, null, 2));
|
|
623
|
+
|
|
624
|
+
// Suppress stderr
|
|
625
|
+
const origWrite = process.stderr.write;
|
|
626
|
+
process.stderr.write = () => true;
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
migrateBranchingConfig(dir);
|
|
630
|
+
} finally {
|
|
631
|
+
process.stderr.write = origWrite;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const config = JSON.parse(fs.readFileSync(path.join(dir, 'config.json'), 'utf-8'));
|
|
635
|
+
assert.ok(!('git' in config), 'git key should be removed when empty');
|
|
636
|
+
assert.strictEqual(config.model_profile, 'balanced', 'Other keys preserved');
|
|
637
|
+
|
|
638
|
+
const local = JSON.parse(fs.readFileSync(path.join(dir, 'config.local.json'), 'utf-8'));
|
|
639
|
+
assert.strictEqual(local.branching_migration_done, true, 'migration_done flag should be set');
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('already migrated — immediate no-op', () => {
|
|
643
|
+
writeFile(dir, 'config.json', JSON.stringify({
|
|
644
|
+
branching_strategy: 'none',
|
|
645
|
+
model_profile: 'balanced',
|
|
646
|
+
}, null, 2));
|
|
647
|
+
writeFile(dir, 'config.local.json', JSON.stringify({
|
|
648
|
+
branching_migration_done: true,
|
|
649
|
+
}, null, 2));
|
|
650
|
+
|
|
651
|
+
migrateBranchingConfig(dir);
|
|
652
|
+
|
|
653
|
+
// config.json should NOT be modified (branching_strategy still there)
|
|
654
|
+
const config = JSON.parse(fs.readFileSync(path.join(dir, 'config.json'), 'utf-8'));
|
|
655
|
+
assert.strictEqual(config.branching_strategy, 'none', 'branching_strategy should still be present (migration skipped)');
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it('git object preserved when other keys exist besides worktree_setup_command', () => {
|
|
659
|
+
writeFile(dir, 'config.json', JSON.stringify({
|
|
660
|
+
model_profile: 'balanced',
|
|
661
|
+
git: {
|
|
662
|
+
worktree_setup_command: 'npm install',
|
|
663
|
+
sync_push: 'auto',
|
|
664
|
+
},
|
|
665
|
+
}, null, 2));
|
|
666
|
+
|
|
667
|
+
// Suppress stderr
|
|
668
|
+
const origWrite = process.stderr.write;
|
|
669
|
+
process.stderr.write = () => true;
|
|
670
|
+
|
|
671
|
+
try {
|
|
672
|
+
migrateBranchingConfig(dir);
|
|
673
|
+
} finally {
|
|
674
|
+
process.stderr.write = origWrite;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const config = JSON.parse(fs.readFileSync(path.join(dir, 'config.json'), 'utf-8'));
|
|
678
|
+
assert.ok(!('worktree_setup_command' in config.git), 'worktree_setup_command should be removed');
|
|
679
|
+
assert.strictEqual(config.git.sync_push, 'auto', 'Other git keys preserved');
|
|
680
|
+
assert.ok('git' in config, 'git object should still exist');
|
|
681
|
+
});
|
|
682
|
+
});
|