@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
@@ -60,6 +60,21 @@ function v2FixtureWithProject() {
60
60
  });
61
61
  }
62
62
 
63
+ // ─── v2 fixture for multi-project case (existing active + new slug) ──────────
64
+
65
+ function v2FixtureMultiProject() {
66
+ // Active project exists and is populated; we are about to create a second, distinct project.
67
+ return createFixture({
68
+ 'config.json': JSON.stringify({ current_project: 'test-project' }),
69
+ 'PROJECTS.md': '# Projects\n\n| Project | Status |\n|---------|--------|\n| test-project | Active |\n',
70
+ 'REPOS.md': '# Repos\n\n| Name | Path |\n|------|------|\n',
71
+ 'projects/test-project/STATE.md': '# State',
72
+ 'projects/test-project/ROADMAP.md': '# Roadmap',
73
+ 'projects/test-project/REQUIREMENTS.md': '# Requirements',
74
+ 'projects/test-project/PROJECT.md': '# Project',
75
+ });
76
+ }
77
+
63
78
  // ─── v2 fixture without current_project (for guard tests) ────────────────────
64
79
 
65
80
  function v2FixtureOneProject() {
@@ -67,6 +82,7 @@ function v2FixtureOneProject() {
67
82
  'config.json': JSON.stringify({}),
68
83
  'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
69
84
  'REPOS.md': '# Repos\n\n| Name | Path |\n',
85
+ 'projects/test-project/PROJECT.md': '# Project\n',
70
86
  'projects/test-project/STATE.md': '# State',
71
87
  });
72
88
  }
@@ -84,7 +100,9 @@ function v2FixtureMultipleProjects() {
84
100
  'config.json': JSON.stringify({}),
85
101
  'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
86
102
  'REPOS.md': '# Repos\n\n| Name | Path |\n',
103
+ 'projects/alpha-project/PROJECT.md': '# Project\n',
87
104
  'projects/alpha-project/STATE.md': '# State',
105
+ 'projects/beta-project/PROJECT.md': '# Project\n',
88
106
  'projects/beta-project/STATE.md': '# State',
89
107
  });
90
108
  }
@@ -278,7 +296,8 @@ describe('v1 mode: init todos', () => {
278
296
  });
279
297
 
280
298
  it('returns v1 pending_dir path', () => {
281
- assert.equal(result.pending_dir, 'todos/pending');
299
+ // Flat layout: pending_dir points to todos/ (not todos/pending/)
300
+ assert.equal(result.pending_dir, 'todos');
282
301
  });
283
302
 
284
303
  it('returns v1 completed_dir path', () => {
@@ -518,7 +537,8 @@ describe('v2 mode with project: init todos', () => {
518
537
  });
519
538
 
520
539
  it('returns product-level pending_dir', () => {
521
- assert.equal(result.pending_dir, path.join('todos', 'pending'));
540
+ // Flat layout: pending_dir points to todos/ (not todos/pending/)
541
+ assert.equal(result.pending_dir, 'todos');
522
542
  });
523
543
 
524
544
  it('returns product-level completed_dir', () => {
@@ -1148,7 +1168,7 @@ describe('v2 guard: multiple projects (prompt)', () => {
1148
1168
  describe('branch_name with {project} resolution', () => {
1149
1169
  it('v1 execute-phase branch_name resolves {project} from current_project', () => {
1150
1170
  const fixture = createFixture({
1151
- 'config.json': JSON.stringify({ current_project: 'myapp', git: { branching_strategy: 'phase' } }),
1171
+ 'config.json': JSON.stringify({ current_project: 'myapp' }),
1152
1172
  'STATE.md': '# State',
1153
1173
  'ROADMAP.md': '# Roadmap\n\n## Phases\n\n- [ ] **Phase 3: Auth** - Auth\n',
1154
1174
  'REQUIREMENTS.md': '# Requirements',
@@ -1157,7 +1177,7 @@ describe('branch_name with {project} resolution', () => {
1157
1177
  });
1158
1178
  try {
1159
1179
  const result = runInit(fixture.cwd, 'execute-phase 3');
1160
- assert.equal(result.branch_name, 'dgs/myapp/phase-03-auth');
1180
+ assert.equal(result.branch_name, 'dgs/myapp/v1.0-milestone');
1161
1181
  } finally {
1162
1182
  fixture.cleanup();
1163
1183
  }
@@ -1165,7 +1185,7 @@ describe('branch_name with {project} resolution', () => {
1165
1185
 
1166
1186
  it('v2 execute-phase branch_name resolves {project} from current_project', () => {
1167
1187
  const fixture = createFixture({
1168
- 'config.json': JSON.stringify({ current_project: 'checkout', git: { branching_strategy: 'phase' } }),
1188
+ 'config.json': JSON.stringify({ current_project: 'checkout' }),
1169
1189
  'PROJECTS.md': '# Projects\n\n| Project | Status |\n|---------|--------|\n| checkout | Active |\n',
1170
1190
  'REPOS.md': '# Repos\n\n| Name | Path |\n|------|------|\n',
1171
1191
  'projects/checkout/STATE.md': '# State',
@@ -1176,13 +1196,13 @@ describe('branch_name with {project} resolution', () => {
1176
1196
  });
1177
1197
  try {
1178
1198
  const result = runInit(fixture.cwd, 'execute-phase 1');
1179
- assert.equal(result.branch_name, 'dgs/checkout/phase-01-init');
1199
+ assert.equal(result.branch_name, 'dgs/checkout/v1.0-milestone');
1180
1200
  } finally {
1181
1201
  fixture.cleanup();
1182
1202
  }
1183
1203
  });
1184
1204
 
1185
- it('branch_name with custom template without {project} is unchanged', () => {
1205
+ it('branch_name always uses milestone template regardless of config', () => {
1186
1206
  const fixture = createFixture({
1187
1207
  'config.json': JSON.stringify({
1188
1208
  current_project: 'myapp',
@@ -1196,7 +1216,7 @@ describe('branch_name with {project} resolution', () => {
1196
1216
  });
1197
1217
  try {
1198
1218
  const result = runInit(fixture.cwd, 'execute-phase 3');
1199
- assert.equal(result.branch_name, 'feature/phase-03-auth');
1219
+ assert.equal(result.branch_name, 'dgs/myapp/v1.0-milestone');
1200
1220
  } finally {
1201
1221
  fixture.cleanup();
1202
1222
  }
@@ -1521,3 +1541,600 @@ describe('archived milestone phase rejection', () => {
1521
1541
  assert.ok(result.phase_dir);
1522
1542
  });
1523
1543
  });
1544
+
1545
+ // ─── cmdInitQuick quick_dir resolution ──────────────────────────────────────
1546
+
1547
+ describe('cmdInitQuick quick_dir resolution', () => {
1548
+ describe('product mode (no active milestone)', () => {
1549
+ let fixture;
1550
+
1551
+ beforeEach(() => {
1552
+ fixture = v2FixtureWithProject();
1553
+ });
1554
+
1555
+ afterEach(() => {
1556
+ fixture.cleanup();
1557
+ });
1558
+
1559
+ it('init quick uses product-root quick_dir in product mode (no active milestone)', () => {
1560
+ const result = runInit(fixture.cwd, 'quick "test task"');
1561
+ assert.equal(result.quick_dir, 'quick');
1562
+ });
1563
+
1564
+ it('task_dir follows quick_dir in product mode', () => {
1565
+ const result = runInit(fixture.cwd, 'quick "test task"');
1566
+ assert.ok(result.task_dir, 'task_dir should be set');
1567
+ assert.ok(result.task_dir.startsWith('quick/'), 'task_dir should start with quick/');
1568
+ });
1569
+
1570
+ it('state_path resolves to product-root in product mode', () => {
1571
+ const result = runInit(fixture.cwd, 'quick "test task"');
1572
+ assert.equal(result.state_path, 'STATE.md');
1573
+ });
1574
+
1575
+ it('roadmap_exists remains project-scoped in product mode', () => {
1576
+ const result = runInit(fixture.cwd, 'quick "test task"');
1577
+ assert.equal(result.roadmap_exists, true);
1578
+ });
1579
+
1580
+ it('creates planning-root STATE.md on demand when missing', () => {
1581
+ // Remove the planning-root STATE.md if present (fixture has a project STATE.md but not a product one)
1582
+ const productStatePath = path.join(fixture.cwd, 'STATE.md');
1583
+ if (fs.existsSync(productStatePath)) {
1584
+ fs.unlinkSync(productStatePath);
1585
+ }
1586
+ assert.equal(fs.existsSync(productStatePath), false, 'precondition: product STATE.md absent');
1587
+
1588
+ runInit(fixture.cwd, 'quick "test task"');
1589
+
1590
+ assert.equal(fs.existsSync(productStatePath), true, 'product STATE.md should be auto-created');
1591
+ const contents = fs.readFileSync(productStatePath, 'utf-8');
1592
+ assert.match(contents, /### Blockers\/Concerns/, 'skeleton should contain Blockers/Concerns anchor');
1593
+ });
1594
+ });
1595
+
1596
+ describe('milestone-context mode (active milestone)', () => {
1597
+ let fixture;
1598
+
1599
+ beforeEach(() => {
1600
+ fixture = createFixture({
1601
+ 'config.json': JSON.stringify({ current_project: 'test-project' }),
1602
+ 'config.local.json': JSON.stringify({
1603
+ execution: { active_context: 'v19-git-worktrees' },
1604
+ projects: {
1605
+ 'test-project': {
1606
+ worktrees: {
1607
+ 'v19-git-worktrees': { type: 'milestone' }
1608
+ }
1609
+ }
1610
+ }
1611
+ }),
1612
+ 'PROJECTS.md': '# Projects\n\n| Project | Status |\n|---------|--------|\n| test-project | Active |\n',
1613
+ 'REPOS.md': '# Repos\n\n| Name | Path |\n|------|------|\n',
1614
+ 'projects/test-project/STATE.md': '# State',
1615
+ 'projects/test-project/ROADMAP.md': '# Roadmap\n\n## Phases\n\n- [ ] **Phase 1: Test Phase** - A test\n',
1616
+ 'projects/test-project/REQUIREMENTS.md': '# Requirements',
1617
+ 'projects/test-project/PROJECT.md': '# Project',
1618
+ 'projects/test-project/phases/01-test-phase/01-CONTEXT.md': '# Context',
1619
+ 'projects/test-project/phases/01-test-phase/01-01-PLAN.md': '---\nphase: 01-test-phase\nplan: 01\n---\n# Plan',
1620
+ });
1621
+ });
1622
+
1623
+ afterEach(() => {
1624
+ fixture.cleanup();
1625
+ });
1626
+
1627
+ it('init quick uses project quick_dir when in milestone context', () => {
1628
+ const result = runInit(fixture.cwd, 'quick "test task"');
1629
+ assert.equal(result.quick_dir, 'projects/test-project/quick');
1630
+ });
1631
+
1632
+ it('state_path remains project-scoped in milestone-context mode', () => {
1633
+ const result = runInit(fixture.cwd, 'quick "test task"');
1634
+ assert.equal(result.state_path, 'projects/test-project/STATE.md');
1635
+ });
1636
+ });
1637
+
1638
+ describe('--main flag override', () => {
1639
+ let fixture;
1640
+
1641
+ beforeEach(() => {
1642
+ fixture = createFixture({
1643
+ 'config.json': JSON.stringify({ current_project: 'test-project' }),
1644
+ 'config.local.json': JSON.stringify({
1645
+ execution: { active_context: 'v19-git-worktrees' },
1646
+ projects: {
1647
+ 'test-project': {
1648
+ worktrees: {
1649
+ 'v19-git-worktrees': { type: 'milestone' }
1650
+ }
1651
+ }
1652
+ }
1653
+ }),
1654
+ 'PROJECTS.md': '# Projects\n\n| Project | Status |\n|---------|--------|\n| test-project | Active |\n',
1655
+ 'REPOS.md': '# Repos\n\n| Name | Path |\n|------|------|\n',
1656
+ 'projects/test-project/STATE.md': '# State',
1657
+ 'projects/test-project/ROADMAP.md': '# Roadmap\n\n## Phases\n\n- [ ] **Phase 1: Test Phase** - A test\n',
1658
+ 'projects/test-project/REQUIREMENTS.md': '# Requirements',
1659
+ 'projects/test-project/PROJECT.md': '# Project',
1660
+ 'projects/test-project/phases/01-test-phase/01-CONTEXT.md': '# Context',
1661
+ 'projects/test-project/phases/01-test-phase/01-01-PLAN.md': '---\nphase: 01-test-phase\nplan: 01\n---\n# Plan',
1662
+ });
1663
+ });
1664
+
1665
+ afterEach(() => {
1666
+ fixture.cleanup();
1667
+ });
1668
+
1669
+ it('init quick --main forces product-level quick_dir even with active milestone', () => {
1670
+ const result = runInit(fixture.cwd, 'quick --main "test task"');
1671
+ assert.equal(result.quick_dir, 'quick');
1672
+ });
1673
+
1674
+ it('init quick --main forces product-level state_path even with active milestone', () => {
1675
+ const result = runInit(fixture.cwd, 'quick --main "test task"');
1676
+ assert.equal(result.state_path, 'STATE.md');
1677
+ });
1678
+
1679
+ it('init quick --main strips flag from description', () => {
1680
+ const result = runInit(fixture.cwd, 'quick --main "test task"');
1681
+ assert.equal(result.description, 'test task');
1682
+ });
1683
+ });
1684
+
1685
+ describe('product mode with active quick worktree (parity)', () => {
1686
+ let fixture;
1687
+
1688
+ beforeEach(() => {
1689
+ // Hand-rolled fixture (not v2FixtureWithProject) so we can seed
1690
+ // config.local.json with an active quick worktree whose slug has
1691
+ // already been truncated by worktrees.cjs::_sanitizeSlug to 50 chars.
1692
+ fixture = createFixture({
1693
+ 'config.json': JSON.stringify({ current_project: 'test-project' }),
1694
+ 'PROJECTS.md': '# Projects\n\n| Project | Status |\n|---------|--------|\n| test-project | Active |\n',
1695
+ 'REPOS.md': '# Repos\n\n| Name | Path |\n|------|------|\n',
1696
+ 'projects/test-project/PROJECT.md': '# Project',
1697
+ 'projects/test-project/STATE.md': '# State',
1698
+ 'projects/test-project/ROADMAP.md': '# Roadmap\n\n## Phases\n\n- [ ] **Phase 1: Test Phase** - A test\n',
1699
+ 'projects/test-project/REQUIREMENTS.md': '# Requirements',
1700
+ });
1701
+ // Seed the active quick worktree AFTER createFixture so we can point
1702
+ // entry.repos at fixture.cwd (which is a real on-disk tmp dir).
1703
+ // Slug is the SHORT (50-char) form that worktrees.cjs would produce.
1704
+ const shortSlug = '260428-k7f-this-is-a-very-long-description-that-w';
1705
+ const localConfig = {
1706
+ execution: { active_context: shortSlug },
1707
+ projects: {
1708
+ 'test-project': {
1709
+ worktrees: {
1710
+ [shortSlug]: {
1711
+ type: 'quick',
1712
+ repos: { 'deliver-great-systems': fixture.cwd },
1713
+ },
1714
+ },
1715
+ },
1716
+ },
1717
+ };
1718
+ fs.writeFileSync(path.join(fixture.cwd, 'config.local.json'),
1719
+ JSON.stringify(localConfig), 'utf-8');
1720
+ });
1721
+
1722
+ afterEach(() => {
1723
+ fixture.cleanup();
1724
+ });
1725
+
1726
+ it('task_dir basename matches active worktree slug (parity with getActiveQuick)', () => {
1727
+ // Description deliberately long enough that init.cjs's own 40-char
1728
+ // sanitiser would produce a descSlug differing from the worktree's
1729
+ // 39-char descSlug by exactly one char (pre-fix). Post-fix, init
1730
+ // defers to the worktree slug, so the two align.
1731
+ const result = runInit(fixture.cwd,
1732
+ 'quick "this is a very long description that would otherwise"');
1733
+
1734
+ assert.ok(result.task_dir, 'task_dir should be set');
1735
+ assert.equal(
1736
+ path.basename(result.task_dir),
1737
+ '260428-k7f-this-is-a-very-long-description-that-w',
1738
+ 'task_dir basename must equal active worktree slug (no second sanitiser pass)'
1739
+ );
1740
+ assert.equal(
1741
+ result.slug,
1742
+ 'this-is-a-very-long-description-that-w',
1743
+ 'slug must be derived from worktree slug (39 chars), not recomputed from description (40 chars)'
1744
+ );
1745
+ assert.equal(result.quick_id, '260428-k7f',
1746
+ 'quick_id should still be extracted from worktree slug prefix (regression guard)');
1747
+ });
1748
+ });
1749
+ });
1750
+
1751
+ // ─── init progress-all ───────────────────────────────────────────────────────
1752
+
1753
+ describe('init progress-all: v2 fixture with 1 active project', () => {
1754
+ let fixture;
1755
+ let result;
1756
+
1757
+ beforeEach(() => {
1758
+ fixture = createFixture({
1759
+ 'config.json': JSON.stringify({ current_project: 'alpha' }),
1760
+ 'PROJECTS.md': '# Projects\n\n| Project | Status |\n|---------|--------|\n| alpha | Active |\n',
1761
+ 'REPOS.md': '# Repos\n\n| Name | Path |\n|------|------|\n',
1762
+ 'projects/alpha/STATE.md': '# Project State\n\nPhase: 3\nStatus: In progress\nProgress: [####------] 40%\n',
1763
+ 'projects/alpha/ROADMAP.md': '# Roadmap\n\n- 🚧 **v2.0 Auth** — Phases 1-5 (in progress)\n',
1764
+ 'projects/alpha/PROJECT.md': '# Project',
1765
+ });
1766
+ result = runInit(fixture.cwd, 'progress-all --raw');
1767
+ });
1768
+
1769
+ afterEach(() => {
1770
+ fixture.cleanup();
1771
+ });
1772
+
1773
+ it('returns valid JSON with product and projects keys', () => {
1774
+ assert.ok(result.product, 'result.product must exist');
1775
+ assert.ok(Array.isArray(result.projects), 'result.projects must be an array');
1776
+ });
1777
+
1778
+ it('product has active_count, completed_count, and backlog', () => {
1779
+ assert.equal(typeof result.product.active_count, 'number');
1780
+ assert.equal(typeof result.product.completed_count, 'number');
1781
+ assert.ok(result.product.backlog, 'product.backlog must exist');
1782
+ });
1783
+
1784
+ it('product.backlog has all four int fields (todos, ideas, specs, debug_active)', () => {
1785
+ assert.equal(typeof result.product.backlog.todos, 'number');
1786
+ assert.equal(typeof result.product.backlog.ideas, 'number');
1787
+ assert.equal(typeof result.product.backlog.specs, 'number');
1788
+ assert.equal(typeof result.product.backlog.debug_active, 'number');
1789
+ });
1790
+
1791
+ it('product.backlog counts are all zero on empty fixture', () => {
1792
+ assert.equal(result.product.backlog.todos, 0);
1793
+ assert.equal(result.product.backlog.ideas, 0);
1794
+ assert.equal(result.product.backlog.specs, 0);
1795
+ assert.equal(result.product.backlog.debug_active, 0);
1796
+ });
1797
+
1798
+ it('product.quick_tasks_recent is [] when product-level STATE.md has no Quick Tasks table', () => {
1799
+ assert.deepEqual(result.product.quick_tasks_recent, []);
1800
+ });
1801
+
1802
+ it('product.shipped_milestones_recent is [] when product-level MILESTONES.md is missing', () => {
1803
+ assert.deepEqual(result.product.shipped_milestones_recent, []);
1804
+ });
1805
+
1806
+ it('projects array has 1 active project', () => {
1807
+ assert.equal(result.projects.length, 1);
1808
+ assert.equal(result.projects[0].name, 'alpha');
1809
+ });
1810
+
1811
+ it('project entry has expected fields', () => {
1812
+ const p = result.projects[0];
1813
+ assert.equal(typeof p.name, 'string');
1814
+ assert.equal(typeof p.status, 'string');
1815
+ assert.equal(typeof p.current_phase, 'string');
1816
+ assert.equal(typeof p.progress, 'number');
1817
+ assert.equal(typeof p.repos_touched, 'string');
1818
+ assert.ok('milestone_version' in p);
1819
+ assert.ok('milestone_name' in p);
1820
+ });
1821
+
1822
+ it('project milestone_version is parsed from ROADMAP.md', () => {
1823
+ assert.equal(result.projects[0].milestone_version, 'v2.0');
1824
+ assert.equal(result.projects[0].milestone_name, 'Auth');
1825
+ });
1826
+
1827
+ it('returns top-level auth, dgs_mode, planner_model, executor_model', () => {
1828
+ assert.ok('author' in result);
1829
+ assert.equal(typeof result.dgs_mode, 'string');
1830
+ assert.ok('planner_model' in result);
1831
+ assert.ok('executor_model' in result);
1832
+ });
1833
+ });
1834
+
1835
+ describe('init progress-all: excludes completed projects', () => {
1836
+ let fixture;
1837
+ let result;
1838
+
1839
+ beforeEach(() => {
1840
+ fixture = createFixture({
1841
+ 'config.json': JSON.stringify({}),
1842
+ 'PROJECTS.md': '# Projects\n',
1843
+ 'REPOS.md': '# Repos\n',
1844
+ 'projects/active-x/PROJECT.md': '# Project\n',
1845
+ 'projects/active-x/STATE.md': '# Project State\n\nPhase: 1\nStatus: In progress\nProgress: [#---------] 10%\n',
1846
+ 'projects/done-y/PROJECT.md': '# Project\n',
1847
+ 'projects/done-y/STATE.md': '# Project State\n\nPhase: 5\nStatus: completed\nProgress: [##########] 100%\nCompleted: 2026-01-20\n',
1848
+ });
1849
+ result = runInit(fixture.cwd, 'progress-all --raw');
1850
+ });
1851
+
1852
+ afterEach(() => {
1853
+ fixture.cleanup();
1854
+ });
1855
+
1856
+ it('projects array excludes completed projects', () => {
1857
+ assert.equal(result.projects.length, 1);
1858
+ assert.equal(result.projects[0].name, 'active-x');
1859
+ });
1860
+
1861
+ it('product.active_count and completed_count reflect the split', () => {
1862
+ assert.equal(result.product.active_count, 1);
1863
+ assert.equal(result.product.completed_count, 1);
1864
+ });
1865
+ });
1866
+
1867
+ describe('init progress-all: empty projects dir', () => {
1868
+ let fixture;
1869
+ let result;
1870
+
1871
+ beforeEach(() => {
1872
+ fixture = createFixture({
1873
+ 'config.json': JSON.stringify({}),
1874
+ 'PROJECTS.md': '# Projects\n',
1875
+ 'REPOS.md': '# Repos\n',
1876
+ });
1877
+ result = runInit(fixture.cwd, 'progress-all --raw');
1878
+ });
1879
+
1880
+ afterEach(() => {
1881
+ fixture.cleanup();
1882
+ });
1883
+
1884
+ it('projects is [] and active_count is 0', () => {
1885
+ assert.deepEqual(result.projects, []);
1886
+ assert.equal(result.product.active_count, 0);
1887
+ });
1888
+ });
1889
+
1890
+ describe('init progress-all: parses product-level STATE.md quick tasks', () => {
1891
+ let fixture;
1892
+ let result;
1893
+
1894
+ beforeEach(() => {
1895
+ const productState = `# Project State
1896
+
1897
+ ## Accumulated Context
1898
+
1899
+ ### Quick Tasks Completed
1900
+
1901
+ | # | Description | Date | Commit | Status | Directory |
1902
+ |---|-------------|------|--------|--------|-----------|
1903
+ | 260405-laf | Route product quick tasks | 2026-04-05 | 368074f | Verified | [260405-laf](./quick/260405-laf/) |
1904
+ | 260405-lvk | Strip trailing hyphen | 2026-04-05 | 536382a | | fast |
1905
+ | 260405-mbu | Make fast context-aware | 2026-04-05 | d79ffbc | Verified | [260405-mbu](./quick/260405-mbu/) |
1906
+ `;
1907
+ fixture = createFixture({
1908
+ 'config.json': JSON.stringify({}),
1909
+ 'PROJECTS.md': '# Projects\n',
1910
+ 'REPOS.md': '# Repos\n',
1911
+ 'STATE.md': productState,
1912
+ 'projects/alpha/STATE.md': '# Project State\n\nPhase: 1\nStatus: In progress\n',
1913
+ });
1914
+ result = runInit(fixture.cwd, 'progress-all --raw');
1915
+ });
1916
+
1917
+ afterEach(() => {
1918
+ fixture.cleanup();
1919
+ });
1920
+
1921
+ it('parses quick_tasks_recent with 3 entries', () => {
1922
+ assert.equal(result.product.quick_tasks_recent.length, 3);
1923
+ });
1924
+
1925
+ it('quick task entries have id, description, date, commit fields', () => {
1926
+ const q = result.product.quick_tasks_recent[0];
1927
+ assert.ok(q.id);
1928
+ assert.ok(q.description);
1929
+ assert.ok(q.date);
1930
+ assert.ok(q.commit);
1931
+ });
1932
+ });
1933
+
1934
+ describe('init progress-all: parses product-level MILESTONES.md shipped milestones', () => {
1935
+ let fixture;
1936
+ let result;
1937
+
1938
+ beforeEach(() => {
1939
+ const milestones = `# Milestones
1940
+
1941
+ ## v19.0 Git Worktrees (Shipped: 2026-03-27)
1942
+
1943
+ Some content.
1944
+
1945
+ ## v18.0 Root Layout (Shipped: 2026-03-25)
1946
+
1947
+ Some content.
1948
+
1949
+ ## v1.0 Multi-Project Platform (Shipped: 2026-02-24)
1950
+
1951
+ Some content.
1952
+ `;
1953
+ fixture = createFixture({
1954
+ 'config.json': JSON.stringify({}),
1955
+ 'PROJECTS.md': '# Projects\n',
1956
+ 'REPOS.md': '# Repos\n',
1957
+ 'MILESTONES.md': milestones,
1958
+ 'projects/alpha/STATE.md': '# Project State\n\nPhase: 1\nStatus: In progress\n',
1959
+ });
1960
+ result = runInit(fixture.cwd, 'progress-all --raw');
1961
+ });
1962
+
1963
+ afterEach(() => {
1964
+ fixture.cleanup();
1965
+ });
1966
+
1967
+ it('parses shipped_milestones_recent with 3 entries', () => {
1968
+ assert.equal(result.product.shipped_milestones_recent.length, 3);
1969
+ });
1970
+
1971
+ it('shipped milestone entries have version, name, shipped_date fields', () => {
1972
+ const m = result.product.shipped_milestones_recent[0];
1973
+ assert.equal(m.version, 'v19.0');
1974
+ assert.equal(m.name, 'Git Worktrees');
1975
+ assert.equal(m.shipped_date, '2026-03-27');
1976
+ });
1977
+ });
1978
+
1979
+ describe('init progress-all: counts backlog files', () => {
1980
+ let fixture;
1981
+ let result;
1982
+
1983
+ beforeEach(() => {
1984
+ fixture = createFixture({
1985
+ 'config.json': JSON.stringify({}),
1986
+ 'PROJECTS.md': '# Projects\n',
1987
+ 'REPOS.md': '# Repos\n',
1988
+ 'todos/todo-1.md': '---\nstatus: pending\n---\n# todo 1',
1989
+ 'todos/todo-2.md': '---\nstatus: pending\n---\n# todo 2',
1990
+ 'ideas/idea-1.md': '---\nstatus: pending\n---\n# idea 1',
1991
+ 'specs/spec-1.md': '# spec 1',
1992
+ 'specs/spec-2.md': '# spec 2',
1993
+ 'specs/spec-3.md': '# spec 3',
1994
+ 'debug/active-session.md': '# debug',
1995
+ 'debug/session-resolved.md': '# resolved',
1996
+ 'projects/alpha/STATE.md': '# Project State\n\nPhase: 1\nStatus: In progress\n',
1997
+ });
1998
+ result = runInit(fixture.cwd, 'progress-all --raw');
1999
+ });
2000
+
2001
+ afterEach(() => {
2002
+ fixture.cleanup();
2003
+ });
2004
+
2005
+ it('counts todos in todos/pending/', () => {
2006
+ assert.equal(result.product.backlog.todos, 2);
2007
+ });
2008
+
2009
+ it('counts ideas in ideas/pending/', () => {
2010
+ assert.equal(result.product.backlog.ideas, 1);
2011
+ });
2012
+
2013
+ it('counts specs in specs/ (top-level only)', () => {
2014
+ assert.equal(result.product.backlog.specs, 3);
2015
+ });
2016
+
2017
+ it('counts debug files excluding those with "resolved" in name', () => {
2018
+ assert.equal(result.product.backlog.debug_active, 1);
2019
+ });
2020
+ });
2021
+
2022
+ // ─── v2 mode multi-project: init new-project <slug> ──────────────────────────
2023
+
2024
+ describe('v2 mode multi-project: init new-project <slug>', () => {
2025
+ it('returns project_exists false for a new, non-existing slug', () => {
2026
+ const fixture = v2FixtureMultiProject();
2027
+ try {
2028
+ const result = runInit(fixture.cwd, 'new-project second-project');
2029
+ assert.equal(result.project_exists, false);
2030
+ } finally {
2031
+ fixture.cleanup();
2032
+ }
2033
+ });
2034
+
2035
+ it('returns project_path under the requested slug, not current_project', () => {
2036
+ const fixture = v2FixtureMultiProject();
2037
+ try {
2038
+ const result = runInit(fixture.cwd, 'new-project second-project');
2039
+ assert.equal(result.project_path, path.join('projects', 'second-project', 'PROJECT.md'));
2040
+ } finally {
2041
+ fixture.cleanup();
2042
+ }
2043
+ });
2044
+
2045
+ it('returns state/roadmap/requirements/research paths under the requested slug', () => {
2046
+ const fixture = v2FixtureMultiProject();
2047
+ try {
2048
+ const result = runInit(fixture.cwd, 'new-project second-project');
2049
+ assert.equal(result.state_path, path.join('projects', 'second-project', 'STATE.md'));
2050
+ assert.equal(result.roadmap_path, path.join('projects', 'second-project', 'ROADMAP.md'));
2051
+ assert.equal(result.requirements_path, path.join('projects', 'second-project', 'REQUIREMENTS.md'));
2052
+ assert.equal(result.research_dir, path.join('projects', 'second-project', 'research'));
2053
+ } finally {
2054
+ fixture.cleanup();
2055
+ }
2056
+ });
2057
+
2058
+ it('returns project_exists true when the slug DOES already exist', () => {
2059
+ const fixture = v2FixtureWithProject();
2060
+ try {
2061
+ const result = runInit(fixture.cwd, 'new-project test-project');
2062
+ assert.equal(result.project_exists, true);
2063
+ assert.equal(result.project_path, path.join('projects', 'test-project', 'PROJECT.md'));
2064
+ } finally {
2065
+ fixture.cleanup();
2066
+ }
2067
+ });
2068
+
2069
+ it('normalizes the slug consistently with generateSlugInternal', () => {
2070
+ // Lock in the contract that the resolver and `projects create` use the same normalizer.
2071
+ const { generateSlugInternal } = require('./core.cjs');
2072
+ assert.equal(generateSlugInternal('Second Project'), 'second-project');
2073
+ });
2074
+
2075
+ it('exposes requested_slug on the result object', () => {
2076
+ const fixture = v2FixtureMultiProject();
2077
+ try {
2078
+ const result = runInit(fixture.cwd, 'new-project second-project');
2079
+ assert.equal(result.requested_slug, 'second-project');
2080
+ // current_project should still report the active project.
2081
+ assert.equal(result.current_project, 'test-project');
2082
+ } finally {
2083
+ fixture.cleanup();
2084
+ }
2085
+ });
2086
+
2087
+ // Regression: backwards-compatible no-slug behaviour
2088
+ it('v2 with current_project but no slug arg: behaviour unchanged', () => {
2089
+ const fixture = v2FixtureWithProject();
2090
+ try {
2091
+ const result = runInit(fixture.cwd, 'new-project');
2092
+ assert.equal(result.project_path, path.join('projects', 'test-project', 'PROJECT.md'));
2093
+ assert.equal(result.project_exists, true);
2094
+ } finally {
2095
+ fixture.cleanup();
2096
+ }
2097
+ });
2098
+
2099
+ it('v1 mode with no slug arg: behaviour unchanged', () => {
2100
+ const fixture = v1Fixture();
2101
+ try {
2102
+ const result = runInit(fixture.cwd, 'new-project');
2103
+ assert.equal(result.project_path, 'PROJECT.md');
2104
+ } finally {
2105
+ fixture.cleanup();
2106
+ }
2107
+ });
2108
+ });
2109
+
2110
+ // ─── v2 mode multi-project: end-to-end ───────────────────────────────────────
2111
+
2112
+ describe('v2 mode multi-project: end-to-end', () => {
2113
+ it('init new-project <slug> agrees with projects create on the target directory', () => {
2114
+ const fixture = v2FixtureMultiProject();
2115
+ try {
2116
+ // Step 1: init new-project resolves paths under projects/second-project/
2117
+ const initResult = runInit(fixture.cwd, 'new-project second-project');
2118
+ assert.equal(initResult.project_exists, false);
2119
+ assert.equal(initResult.project_path, path.join('projects', 'second-project', 'PROJECT.md'));
2120
+ assert.equal(initResult.requested_slug, 'second-project');
2121
+ // current_project is still the active project — workflow uses requested_slug or
2122
+ // project_path, not current_project, when creating.
2123
+ assert.equal(initResult.current_project, 'test-project');
2124
+
2125
+ // Step 2: projects create creates the same folder init pointed at.
2126
+ execSync(`node "${CLI}" projects create "Second Project"`, {
2127
+ cwd: fixture.cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
2128
+ });
2129
+ const createdDir = path.join(fixture.cwd, 'projects', 'second-project');
2130
+ assert.ok(fs.existsSync(createdDir), 'projects create should create projects/second-project/');
2131
+ assert.ok(fs.existsSync(path.join(createdDir, 'PROJECT.md')), 'PROJECT.md should exist in created folder');
2132
+
2133
+ // Step 3: re-running init new-project for the same slug now reports it exists.
2134
+ const reinit = runInit(fixture.cwd, 'new-project second-project');
2135
+ assert.equal(reinit.project_exists, true);
2136
+ } finally {
2137
+ fixture.cleanup();
2138
+ }
2139
+ });
2140
+ });