@ktpartners/dgs-platform 2.9.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/CHANGELOG.md +197 -0
  2. package/README.md +34 -2
  3. package/agents/dgs-executor.md +124 -3
  4. package/agents/dgs-idea-researcher.md +447 -0
  5. package/agents/dgs-plan-checker.md +61 -3
  6. package/agents/dgs-planner.md +51 -8
  7. package/bin/install.js +44 -0
  8. package/commands/dgs/abandon-quick.md +28 -0
  9. package/commands/dgs/add-tests.md +2 -2
  10. package/commands/dgs/audit-milestone.md +4 -3
  11. package/commands/dgs/capture-principle.md +11 -11
  12. package/commands/dgs/cleanup.md +2 -2
  13. package/commands/dgs/complete-milestone.md +11 -11
  14. package/commands/dgs/complete-quick.md +28 -0
  15. package/commands/dgs/create-milestone-job.md +2 -2
  16. package/commands/dgs/debug.md +3 -3
  17. package/commands/dgs/develop-idea.md +1 -1
  18. package/commands/dgs/diff-report.md +124 -0
  19. package/commands/dgs/fast.md +3 -1
  20. package/commands/dgs/health.md +1 -1
  21. package/commands/dgs/map-codebase.md +6 -6
  22. package/commands/dgs/new-milestone.md +5 -5
  23. package/commands/dgs/new-project.md +8 -21
  24. package/commands/dgs/package-scan.md +43 -0
  25. package/commands/dgs/plan-milestone-gaps.md +1 -1
  26. package/commands/dgs/progress.md +3 -3
  27. package/commands/dgs/quick-abandon.md +8 -0
  28. package/commands/dgs/quick-complete.md +8 -0
  29. package/commands/dgs/quick.md +10 -3
  30. package/commands/dgs/research-idea.md +3 -2
  31. package/commands/dgs/research-phase.md +3 -3
  32. package/commands/dgs/switch-project.md +14 -1
  33. package/commands/dgs/write-spec.md +3 -3
  34. package/deliver-great-systems/bin/dgs-tools.cjs +401 -32
  35. package/deliver-great-systems/bin/lib/audit-tolerance.cjs +77 -0
  36. package/deliver-great-systems/bin/lib/audit-tolerance.test.cjs +101 -0
  37. package/deliver-great-systems/bin/lib/commands.cjs +626 -46
  38. package/deliver-great-systems/bin/lib/commands.test.cjs +451 -0
  39. package/deliver-great-systems/bin/lib/commit-verify.test.cjs +236 -0
  40. package/deliver-great-systems/bin/lib/config.cjs +80 -6
  41. package/deliver-great-systems/bin/lib/config.test.cjs +309 -0
  42. package/deliver-great-systems/bin/lib/context.cjs +120 -0
  43. package/deliver-great-systems/bin/lib/core.cjs +35 -14
  44. package/deliver-great-systems/bin/lib/core.test.cjs +79 -1
  45. package/deliver-great-systems/bin/lib/execution.cjs +49 -17
  46. package/deliver-great-systems/bin/lib/fast-routing.cjs +199 -0
  47. package/deliver-great-systems/bin/lib/fast-routing.test.cjs +108 -0
  48. package/deliver-great-systems/bin/lib/final-commit-precondition.test.cjs +87 -0
  49. package/deliver-great-systems/bin/lib/fixtures/package-scan/bundler-audit-gemfile.json +21 -0
  50. package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-expected.md +186 -0
  51. package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-runresult.json +235 -0
  52. package/deliver-great-systems/bin/lib/fixtures/package-scan/govulncheck-import.json +3 -0
  53. package/deliver-great-systems/bin/lib/fixtures/package-scan/npm-audit-v10.json +37 -0
  54. package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-clean.json +3 -0
  55. package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-vulns.json +77 -0
  56. package/deliver-great-systems/bin/lib/fixtures/package-scan/pip-audit-requirements.json +28 -0
  57. package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-lodash.json +30 -0
  58. package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-workspaces.json +55 -0
  59. package/deliver-great-systems/bin/lib/flat-migration.test.cjs +396 -0
  60. package/deliver-great-systems/bin/lib/frontmatter.cjs +1 -1
  61. package/deliver-great-systems/bin/lib/governance.cjs +211 -0
  62. package/deliver-great-systems/bin/lib/governance.test.cjs +339 -0
  63. package/deliver-great-systems/bin/lib/health-untracked-phase.test.cjs +269 -0
  64. package/deliver-great-systems/bin/lib/ideas.cjs +206 -91
  65. package/deliver-great-systems/bin/lib/ideas.test.cjs +244 -1
  66. package/deliver-great-systems/bin/lib/init.cjs +357 -61
  67. package/deliver-great-systems/bin/lib/init.test.cjs +625 -8
  68. package/deliver-great-systems/bin/lib/jobs.cjs +131 -25
  69. package/deliver-great-systems/bin/lib/jobs.test.cjs +193 -74
  70. package/deliver-great-systems/bin/lib/migration.cjs +409 -1
  71. package/deliver-great-systems/bin/lib/migration.test.cjs +158 -1
  72. package/deliver-great-systems/bin/lib/milestone.cjs +154 -31
  73. package/deliver-great-systems/bin/lib/milestone.test.cjs +203 -0
  74. package/deliver-great-systems/bin/lib/package-adapters.cjs +530 -0
  75. package/deliver-great-systems/bin/lib/package-adapters.test.cjs +618 -0
  76. package/deliver-great-systems/bin/lib/package-ecosystems.cjs +350 -0
  77. package/deliver-great-systems/bin/lib/package-ecosystems.test.cjs +348 -0
  78. package/deliver-great-systems/bin/lib/package-runner.cjs +199 -0
  79. package/deliver-great-systems/bin/lib/package-runner.test.cjs +198 -0
  80. package/deliver-great-systems/bin/lib/package-scan-provenance.cjs +56 -0
  81. package/deliver-great-systems/bin/lib/package-scan-provenance.test.cjs +103 -0
  82. package/deliver-great-systems/bin/lib/package-scan-report.cjs +1140 -0
  83. package/deliver-great-systems/bin/lib/package-scan-report.test.cjs +1963 -0
  84. package/deliver-great-systems/bin/lib/package-scan-skill.cjs +96 -0
  85. package/deliver-great-systems/bin/lib/package-scan-skill.test.cjs +136 -0
  86. package/deliver-great-systems/bin/lib/package-scan.cjs +919 -0
  87. package/deliver-great-systems/bin/lib/package-scan.test.cjs +2147 -0
  88. package/deliver-great-systems/bin/lib/phase.cjs +146 -3
  89. package/deliver-great-systems/bin/lib/phase.test.cjs +420 -0
  90. package/deliver-great-systems/bin/lib/plan-number-validity.test.cjs +48 -0
  91. package/deliver-great-systems/bin/lib/projects.cjs +65 -10
  92. package/deliver-great-systems/bin/lib/projects.test.cjs +198 -2
  93. package/deliver-great-systems/bin/lib/quick.cjs +739 -0
  94. package/deliver-great-systems/bin/lib/quick.test.cjs +730 -0
  95. package/deliver-great-systems/bin/lib/repos.cjs +37 -13
  96. package/deliver-great-systems/bin/lib/review.cjs +1821 -0
  97. package/deliver-great-systems/bin/lib/roadmap.cjs +34 -13
  98. package/deliver-great-systems/bin/lib/specs.cjs +3 -81
  99. package/deliver-great-systems/bin/lib/state-transition-gate.test.cjs +160 -0
  100. package/deliver-great-systems/bin/lib/state.cjs +147 -55
  101. package/deliver-great-systems/bin/lib/summary-frontmatter.cjs +54 -0
  102. package/deliver-great-systems/bin/lib/summary-frontmatter.test.cjs +78 -0
  103. package/deliver-great-systems/bin/lib/sweep-scope.test.cjs +263 -0
  104. package/deliver-great-systems/bin/lib/sync.cjs +75 -0
  105. package/deliver-great-systems/bin/lib/verify.cjs +198 -7
  106. package/deliver-great-systems/bin/lib/verify.test.cjs +82 -0
  107. package/deliver-great-systems/bin/lib/wave-0-template-rename.test.cjs +40 -0
  108. package/deliver-great-systems/bin/lib/worktrees.cjs +790 -0
  109. package/deliver-great-systems/bin/lib/worktrees.test.cjs +963 -0
  110. package/deliver-great-systems/references/agent-step-reliability.md +60 -0
  111. package/deliver-great-systems/references/conflict-resolution.md +4 -0
  112. package/deliver-great-systems/references/context-tiers.md +4 -0
  113. package/deliver-great-systems/references/package-scan-config.md +151 -0
  114. package/deliver-great-systems/references/questioning.md +0 -30
  115. package/deliver-great-systems/references/spec-review-loop.md +1 -2
  116. package/deliver-great-systems/references/workflow-conventions.md +29 -0
  117. package/deliver-great-systems/skills/dgs-tests/package-scan.md +44 -0
  118. package/deliver-great-systems/templates/REVIEW.md +35 -0
  119. package/deliver-great-systems/templates/VALIDATION.md +1 -1
  120. package/deliver-great-systems/templates/claude-md.md +27 -0
  121. package/deliver-great-systems/templates/package-scan-report.md +108 -0
  122. package/deliver-great-systems/templates/project.md +6 -170
  123. package/deliver-great-systems/templates/summary.md +3 -1
  124. package/deliver-great-systems/workflows/abandon-quick.md +89 -0
  125. package/deliver-great-systems/workflows/add-idea.md +3 -3
  126. package/deliver-great-systems/workflows/add-phase.md +5 -0
  127. package/deliver-great-systems/workflows/add-tests.md +14 -0
  128. package/deliver-great-systems/workflows/add-todo.md +1 -0
  129. package/deliver-great-systems/workflows/approve-spec.md +25 -4
  130. package/deliver-great-systems/workflows/audit-milestone.md +66 -10
  131. package/deliver-great-systems/workflows/audit-phase.md +15 -5
  132. package/deliver-great-systems/workflows/cancel-job.md +2 -2
  133. package/deliver-great-systems/workflows/check-todos.md +2 -3
  134. package/deliver-great-systems/workflows/codereview.md +103 -9
  135. package/deliver-great-systems/workflows/complete-milestone.md +218 -24
  136. package/deliver-great-systems/workflows/complete-quick.md +106 -0
  137. package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
  138. package/deliver-great-systems/workflows/create-milestone-job.md +4 -4
  139. package/deliver-great-systems/workflows/develop-idea.md +11 -11
  140. package/deliver-great-systems/workflows/diagnose-issues.md +14 -0
  141. package/deliver-great-systems/workflows/discuss-idea.md +1 -1
  142. package/deliver-great-systems/workflows/discuss-phase.md +3 -2
  143. package/deliver-great-systems/workflows/execute-phase.md +209 -33
  144. package/deliver-great-systems/workflows/execute-plan.md +22 -22
  145. package/deliver-great-systems/workflows/help.md +53 -20
  146. package/deliver-great-systems/workflows/import-spec.md +65 -7
  147. package/deliver-great-systems/workflows/init-product.md +45 -167
  148. package/deliver-great-systems/workflows/new-milestone.md +140 -33
  149. package/deliver-great-systems/workflows/new-project.md +60 -331
  150. package/deliver-great-systems/workflows/package-scan.md +59 -0
  151. package/deliver-great-systems/workflows/plan-phase.md +79 -1
  152. package/deliver-great-systems/workflows/progress-all.md +133 -0
  153. package/deliver-great-systems/workflows/quick-abandon.md +89 -0
  154. package/deliver-great-systems/workflows/quick-complete.md +106 -0
  155. package/deliver-great-systems/workflows/quick.md +328 -26
  156. package/deliver-great-systems/workflows/refine-spec.md +1 -1
  157. package/deliver-great-systems/workflows/research-idea.md +77 -139
  158. package/deliver-great-systems/workflows/resume-project.md +2 -2
  159. package/deliver-great-systems/workflows/run-job.md +29 -43
  160. package/deliver-great-systems/workflows/settings.md +13 -77
  161. package/deliver-great-systems/workflows/validate-phase.md +39 -1
  162. package/deliver-great-systems/workflows/verify-work.md +14 -0
  163. package/deliver-great-systems/workflows/write-spec.md +11 -13
  164. package/hooks/dist/dgs-enforce-discipline.js +196 -0
  165. package/package.json +1 -1
  166. package/scripts/build-hooks.js +1 -0
@@ -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
+ });