@ktpartners/dgs-platform 2.6.2

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 (256) hide show
  1. package/LICENSE +38 -0
  2. package/README.md +851 -0
  3. package/agents/dgs-codebase-cross-analyzer.md +183 -0
  4. package/agents/dgs-codebase-mapper.md +782 -0
  5. package/agents/dgs-codebase-synthesizer.md +156 -0
  6. package/agents/dgs-debugger.md +1256 -0
  7. package/agents/dgs-executor.md +550 -0
  8. package/agents/dgs-integration-checker.md +481 -0
  9. package/agents/dgs-nyquist-auditor.md +178 -0
  10. package/agents/dgs-phase-researcher.md +563 -0
  11. package/agents/dgs-phase-verifier.md +450 -0
  12. package/agents/dgs-plan-checker.md +708 -0
  13. package/agents/dgs-planner.md +1324 -0
  14. package/agents/dgs-project-researcher.md +631 -0
  15. package/agents/dgs-research-synthesizer.md +249 -0
  16. package/agents/dgs-roadmapper.md +652 -0
  17. package/agents/dgs-verifier.md +607 -0
  18. package/bin/install.js +2073 -0
  19. package/commands/dgs/add-doc.md +45 -0
  20. package/commands/dgs/add-idea.md +38 -0
  21. package/commands/dgs/add-phase.md +43 -0
  22. package/commands/dgs/add-repo.md +54 -0
  23. package/commands/dgs/add-tests.md +41 -0
  24. package/commands/dgs/add-todo.md +47 -0
  25. package/commands/dgs/approve-spec.md +38 -0
  26. package/commands/dgs/audit-milestone.md +36 -0
  27. package/commands/dgs/audit-phase.md +37 -0
  28. package/commands/dgs/cancel-job.md +23 -0
  29. package/commands/dgs/capture-principle.md +143 -0
  30. package/commands/dgs/check-todos.md +45 -0
  31. package/commands/dgs/cleanup.md +18 -0
  32. package/commands/dgs/complete-milestone.md +136 -0
  33. package/commands/dgs/complete-project.md +70 -0
  34. package/commands/dgs/consolidate-ideas.md +50 -0
  35. package/commands/dgs/create-milestone-job.md +37 -0
  36. package/commands/dgs/debug.md +164 -0
  37. package/commands/dgs/develop-idea.md +53 -0
  38. package/commands/dgs/discuss-idea.md +41 -0
  39. package/commands/dgs/discuss-phase.md +83 -0
  40. package/commands/dgs/execute-phase.md +41 -0
  41. package/commands/dgs/fast.md +38 -0
  42. package/commands/dgs/find-related-ideas.md +43 -0
  43. package/commands/dgs/health.md +28 -0
  44. package/commands/dgs/help.md +22 -0
  45. package/commands/dgs/import-spec.md +36 -0
  46. package/commands/dgs/init-product.md +28 -0
  47. package/commands/dgs/insert-phase.md +32 -0
  48. package/commands/dgs/join-discord.md +18 -0
  49. package/commands/dgs/list-docs.md +40 -0
  50. package/commands/dgs/list-ideas.md +42 -0
  51. package/commands/dgs/list-jobs.md +22 -0
  52. package/commands/dgs/list-phase-assumptions.md +46 -0
  53. package/commands/dgs/list-projects.md +57 -0
  54. package/commands/dgs/list-specs.md +40 -0
  55. package/commands/dgs/map-codebase.md +92 -0
  56. package/commands/dgs/new-milestone.md +44 -0
  57. package/commands/dgs/new-project.md +42 -0
  58. package/commands/dgs/node-repair.md +26 -0
  59. package/commands/dgs/overlap-check.md +20 -0
  60. package/commands/dgs/pause-work.md +38 -0
  61. package/commands/dgs/plan-milestone-gaps.md +34 -0
  62. package/commands/dgs/plan-phase.md +44 -0
  63. package/commands/dgs/progress.md +24 -0
  64. package/commands/dgs/quick.md +41 -0
  65. package/commands/dgs/reactivate-project.md +70 -0
  66. package/commands/dgs/reapply-patches.md +110 -0
  67. package/commands/dgs/refine-spec.md +38 -0
  68. package/commands/dgs/reject-idea.md +43 -0
  69. package/commands/dgs/remove-doc.md +44 -0
  70. package/commands/dgs/remove-phase.md +31 -0
  71. package/commands/dgs/remove-repo.md +69 -0
  72. package/commands/dgs/research-idea.md +43 -0
  73. package/commands/dgs/research-phase.md +189 -0
  74. package/commands/dgs/restore-idea.md +45 -0
  75. package/commands/dgs/resume-work.md +40 -0
  76. package/commands/dgs/rollback-job.md +24 -0
  77. package/commands/dgs/run-job.md +35 -0
  78. package/commands/dgs/search.md +40 -0
  79. package/commands/dgs/set-profile.md +34 -0
  80. package/commands/dgs/settings.md +38 -0
  81. package/commands/dgs/switch-project.md +58 -0
  82. package/commands/dgs/undo-consolidation.md +42 -0
  83. package/commands/dgs/update-idea.md +44 -0
  84. package/commands/dgs/update.md +37 -0
  85. package/commands/dgs/validate-phase.md +35 -0
  86. package/commands/dgs/verify-work.md +39 -0
  87. package/commands/dgs/write-spec.md +49 -0
  88. package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-01-SUMMARY.md +84 -0
  89. package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-02-SUMMARY.md +86 -0
  90. package/deliver-great-systems/.planning/phases/10-v1-to-v2-migration-flow/10-01-SUMMARY.md +85 -0
  91. package/deliver-great-systems/bin/dgs-tools.cjs +1444 -0
  92. package/deliver-great-systems/bin/lib/auto-test.cjs +1365 -0
  93. package/deliver-great-systems/bin/lib/commands.cjs +570 -0
  94. package/deliver-great-systems/bin/lib/config.cjs +417 -0
  95. package/deliver-great-systems/bin/lib/conflict-agent.cjs +1063 -0
  96. package/deliver-great-systems/bin/lib/conflict-agent.test.cjs +554 -0
  97. package/deliver-great-systems/bin/lib/context.cjs +929 -0
  98. package/deliver-great-systems/bin/lib/context.test.cjs +693 -0
  99. package/deliver-great-systems/bin/lib/core.cjs +744 -0
  100. package/deliver-great-systems/bin/lib/core.test.cjs +822 -0
  101. package/deliver-great-systems/bin/lib/docs.cjs +919 -0
  102. package/deliver-great-systems/bin/lib/docs.test.cjs +211 -0
  103. package/deliver-great-systems/bin/lib/execution.cjs +705 -0
  104. package/deliver-great-systems/bin/lib/execution.test.cjs +1472 -0
  105. package/deliver-great-systems/bin/lib/frontmatter.cjs +324 -0
  106. package/deliver-great-systems/bin/lib/ideas.cjs +1406 -0
  107. package/deliver-great-systems/bin/lib/ideas.test.cjs +1417 -0
  108. package/deliver-great-systems/bin/lib/identity.cjs +125 -0
  109. package/deliver-great-systems/bin/lib/init.cjs +1114 -0
  110. package/deliver-great-systems/bin/lib/init.test.cjs +1271 -0
  111. package/deliver-great-systems/bin/lib/jobs.cjs +2015 -0
  112. package/deliver-great-systems/bin/lib/jobs.test.cjs +2619 -0
  113. package/deliver-great-systems/bin/lib/merge-conflicts.cjs +654 -0
  114. package/deliver-great-systems/bin/lib/merge-conflicts.test.cjs +370 -0
  115. package/deliver-great-systems/bin/lib/migration.cjs +352 -0
  116. package/deliver-great-systems/bin/lib/migration.test.cjs +582 -0
  117. package/deliver-great-systems/bin/lib/milestone.cjs +243 -0
  118. package/deliver-great-systems/bin/lib/overlap.cjs +437 -0
  119. package/deliver-great-systems/bin/lib/overlap.test.cjs +747 -0
  120. package/deliver-great-systems/bin/lib/path-audit.test.cjs +384 -0
  121. package/deliver-great-systems/bin/lib/paths.cjs +144 -0
  122. package/deliver-great-systems/bin/lib/paths.test.cjs +486 -0
  123. package/deliver-great-systems/bin/lib/phase.cjs +910 -0
  124. package/deliver-great-systems/bin/lib/projects.cjs +691 -0
  125. package/deliver-great-systems/bin/lib/projects.test.cjs +871 -0
  126. package/deliver-great-systems/bin/lib/repos.cjs +1432 -0
  127. package/deliver-great-systems/bin/lib/repos.test.cjs +1882 -0
  128. package/deliver-great-systems/bin/lib/roadmap.cjs +305 -0
  129. package/deliver-great-systems/bin/lib/search.cjs +570 -0
  130. package/deliver-great-systems/bin/lib/specs.cjs +1303 -0
  131. package/deliver-great-systems/bin/lib/state.cjs +893 -0
  132. package/deliver-great-systems/bin/lib/template.cjs +228 -0
  133. package/deliver-great-systems/bin/lib/test-helpers.cjs +291 -0
  134. package/deliver-great-systems/bin/lib/verify.cjs +796 -0
  135. package/deliver-great-systems/references/checkpoints.md +776 -0
  136. package/deliver-great-systems/references/conflict-resolution.md +66 -0
  137. package/deliver-great-systems/references/context-tiers.md +166 -0
  138. package/deliver-great-systems/references/continuation-format.md +249 -0
  139. package/deliver-great-systems/references/decimal-phase-calculation.md +67 -0
  140. package/deliver-great-systems/references/git-integration.md +250 -0
  141. package/deliver-great-systems/references/git-planning-commit.md +40 -0
  142. package/deliver-great-systems/references/model-profile-resolution.md +36 -0
  143. package/deliver-great-systems/references/model-profiles.md +95 -0
  144. package/deliver-great-systems/references/phase-argument-parsing.md +61 -0
  145. package/deliver-great-systems/references/planning-config.md +224 -0
  146. package/deliver-great-systems/references/questioning.md +162 -0
  147. package/deliver-great-systems/references/spec-review-loop.md +177 -0
  148. package/deliver-great-systems/references/tdd.md +265 -0
  149. package/deliver-great-systems/references/ui-brand.md +160 -0
  150. package/deliver-great-systems/references/verification-patterns.md +612 -0
  151. package/deliver-great-systems/templates/DEBUG.md +166 -0
  152. package/deliver-great-systems/templates/UAT.md +251 -0
  153. package/deliver-great-systems/templates/VALIDATION.md +95 -0
  154. package/deliver-great-systems/templates/claude-md.md +74 -0
  155. package/deliver-great-systems/templates/codebase/architecture.md +257 -0
  156. package/deliver-great-systems/templates/codebase/concerns.md +312 -0
  157. package/deliver-great-systems/templates/codebase/conventions.md +309 -0
  158. package/deliver-great-systems/templates/codebase/integrations.md +282 -0
  159. package/deliver-great-systems/templates/codebase/stack.md +188 -0
  160. package/deliver-great-systems/templates/codebase/structure.md +287 -0
  161. package/deliver-great-systems/templates/codebase/testing.md +482 -0
  162. package/deliver-great-systems/templates/config.json +38 -0
  163. package/deliver-great-systems/templates/context.md +354 -0
  164. package/deliver-great-systems/templates/continue-here.md +80 -0
  165. package/deliver-great-systems/templates/debug-subagent-prompt.md +93 -0
  166. package/deliver-great-systems/templates/discovery.md +148 -0
  167. package/deliver-great-systems/templates/milestone-archive.md +125 -0
  168. package/deliver-great-systems/templates/milestone.md +117 -0
  169. package/deliver-great-systems/templates/phase-prompt.md +615 -0
  170. package/deliver-great-systems/templates/planner-subagent-prompt.md +119 -0
  171. package/deliver-great-systems/templates/project.md +186 -0
  172. package/deliver-great-systems/templates/requirements.md +233 -0
  173. package/deliver-great-systems/templates/research-project/ARCHITECTURE.md +206 -0
  174. package/deliver-great-systems/templates/research-project/FEATURES.md +149 -0
  175. package/deliver-great-systems/templates/research-project/PITFALLS.md +202 -0
  176. package/deliver-great-systems/templates/research-project/STACK.md +122 -0
  177. package/deliver-great-systems/templates/research-project/SUMMARY.md +172 -0
  178. package/deliver-great-systems/templates/research.md +554 -0
  179. package/deliver-great-systems/templates/retrospective.md +54 -0
  180. package/deliver-great-systems/templates/roadmap.md +204 -0
  181. package/deliver-great-systems/templates/state.md +178 -0
  182. package/deliver-great-systems/templates/summary-complex.md +59 -0
  183. package/deliver-great-systems/templates/summary-minimal.md +41 -0
  184. package/deliver-great-systems/templates/summary-standard.md +48 -0
  185. package/deliver-great-systems/templates/summary.md +253 -0
  186. package/deliver-great-systems/templates/user-setup.md +313 -0
  187. package/deliver-great-systems/templates/verification-report.md +324 -0
  188. package/deliver-great-systems/workflows/add-doc.md +151 -0
  189. package/deliver-great-systems/workflows/add-idea.md +96 -0
  190. package/deliver-great-systems/workflows/add-phase.md +120 -0
  191. package/deliver-great-systems/workflows/add-tests.md +359 -0
  192. package/deliver-great-systems/workflows/add-todo.md +162 -0
  193. package/deliver-great-systems/workflows/approve-spec.md +194 -0
  194. package/deliver-great-systems/workflows/audit-milestone.md +364 -0
  195. package/deliver-great-systems/workflows/audit-phase.md +462 -0
  196. package/deliver-great-systems/workflows/cancel-job.md +108 -0
  197. package/deliver-great-systems/workflows/check-todos.md +181 -0
  198. package/deliver-great-systems/workflows/cleanup.md +247 -0
  199. package/deliver-great-systems/workflows/codereview.md +526 -0
  200. package/deliver-great-systems/workflows/complete-milestone.md +1298 -0
  201. package/deliver-great-systems/workflows/consolidate-ideas.md +365 -0
  202. package/deliver-great-systems/workflows/create-milestone-job.md +177 -0
  203. package/deliver-great-systems/workflows/develop-idea.md +544 -0
  204. package/deliver-great-systems/workflows/diagnose-issues.md +231 -0
  205. package/deliver-great-systems/workflows/discovery-phase.md +301 -0
  206. package/deliver-great-systems/workflows/discuss-idea.md +263 -0
  207. package/deliver-great-systems/workflows/discuss-phase.md +733 -0
  208. package/deliver-great-systems/workflows/execute-phase.md +571 -0
  209. package/deliver-great-systems/workflows/execute-plan.md +592 -0
  210. package/deliver-great-systems/workflows/find-related-ideas.md +271 -0
  211. package/deliver-great-systems/workflows/health.md +173 -0
  212. package/deliver-great-systems/workflows/help.md +997 -0
  213. package/deliver-great-systems/workflows/import-spec.md +381 -0
  214. package/deliver-great-systems/workflows/init-product.md +767 -0
  215. package/deliver-great-systems/workflows/insert-phase.md +138 -0
  216. package/deliver-great-systems/workflows/list-docs.md +119 -0
  217. package/deliver-great-systems/workflows/list-ideas.md +154 -0
  218. package/deliver-great-systems/workflows/list-jobs.md +89 -0
  219. package/deliver-great-systems/workflows/list-phase-assumptions.md +192 -0
  220. package/deliver-great-systems/workflows/list-specs.md +101 -0
  221. package/deliver-great-systems/workflows/map-codebase.md +621 -0
  222. package/deliver-great-systems/workflows/new-milestone.md +591 -0
  223. package/deliver-great-systems/workflows/new-project.md +1113 -0
  224. package/deliver-great-systems/workflows/node-repair.md +94 -0
  225. package/deliver-great-systems/workflows/overlap-check.md +86 -0
  226. package/deliver-great-systems/workflows/pause-work.md +134 -0
  227. package/deliver-great-systems/workflows/plan-milestone-gaps.md +306 -0
  228. package/deliver-great-systems/workflows/plan-phase.md +698 -0
  229. package/deliver-great-systems/workflows/progress.md +386 -0
  230. package/deliver-great-systems/workflows/quick.md +845 -0
  231. package/deliver-great-systems/workflows/refine-spec.md +275 -0
  232. package/deliver-great-systems/workflows/reject-idea.md +109 -0
  233. package/deliver-great-systems/workflows/remove-doc.md +117 -0
  234. package/deliver-great-systems/workflows/remove-phase.md +163 -0
  235. package/deliver-great-systems/workflows/research-idea.md +325 -0
  236. package/deliver-great-systems/workflows/research-phase.md +81 -0
  237. package/deliver-great-systems/workflows/restore-idea.md +101 -0
  238. package/deliver-great-systems/workflows/resume-project.md +311 -0
  239. package/deliver-great-systems/workflows/rollback-job.md +130 -0
  240. package/deliver-great-systems/workflows/run-job.md +498 -0
  241. package/deliver-great-systems/workflows/search.md +130 -0
  242. package/deliver-great-systems/workflows/set-profile.md +83 -0
  243. package/deliver-great-systems/workflows/settings.md +470 -0
  244. package/deliver-great-systems/workflows/transition.md +563 -0
  245. package/deliver-great-systems/workflows/undo-consolidation.md +155 -0
  246. package/deliver-great-systems/workflows/update-idea.md +157 -0
  247. package/deliver-great-systems/workflows/update.md +242 -0
  248. package/deliver-great-systems/workflows/validate-phase.md +177 -0
  249. package/deliver-great-systems/workflows/verify-phase.md +253 -0
  250. package/deliver-great-systems/workflows/verify-work.md +671 -0
  251. package/deliver-great-systems/workflows/write-spec.md +450 -0
  252. package/hooks/dist/dgs-check-update.js +62 -0
  253. package/hooks/dist/dgs-context-monitor.js +141 -0
  254. package/hooks/dist/dgs-statusline.js +115 -0
  255. package/package.json +60 -0
  256. package/scripts/build-hooks.js +43 -0
@@ -0,0 +1,747 @@
1
+ /**
2
+ * Tests for overlap.cjs — Cross-project overlap detection at repo and file level
3
+ */
4
+
5
+ const { describe, it, beforeEach, afterEach } = require('node:test');
6
+ const assert = require('node:assert');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const os = require('os');
10
+
11
+ const { createTempDir, cleanupDir, writeFile } = require('./test-helpers.cjs');
12
+
13
+ // Helper to create a minimal project with STATE.md under .planning/projects/<slug>/
14
+ function createProject(cwd, slug, status = 'Ready to execute') {
15
+ writeFile(cwd, `.planning/projects/${slug}/STATE.md`, `# Project State\n\nPhase: 1\nStatus: ${status}\nProgress: [░░░░░░░░░░] 0%\n`);
16
+ writeFile(cwd, `.planning/projects/${slug}/PROJECT.md`, `# Project: ${slug}\n`);
17
+ fs.mkdirSync(path.join(cwd, '.planning', 'projects', slug, 'phases'), { recursive: true });
18
+ }
19
+
20
+ // Helper to create a plan file with repos and files tags under .planning/projects/<slug>/phases/
21
+ function createPlanFile(cwd, slug, phaseDir, planName, tasks) {
22
+ let content = `---\nphase: ${phaseDir}\nplan: 01\n---\n\n<tasks>\n`;
23
+ for (const task of tasks) {
24
+ content += `\n<task type="auto">\n <name>${task.name}</name>\n <files>${(task.files || []).join(', ')}</files>\n`;
25
+ if (task.repos) {
26
+ content += ` <repos>${task.repos.join(', ')}</repos>\n`;
27
+ }
28
+ content += ` <action>Do something</action>\n <verify>Check it</verify>\n <done>Done</done>\n</task>\n`;
29
+ }
30
+ content += '\n</tasks>\n';
31
+ writeFile(cwd, `.planning/projects/${slug}/phases/${phaseDir}/${planName}`, content);
32
+ }
33
+
34
+ // Helper to create PROJECTS.md
35
+ function createProjectsMd(cwd, projects) {
36
+ let content = '# Projects\n\n## Active\n\n';
37
+ content += '| Project | Status | Repos Touched | Current Phase |\n';
38
+ content += '|---------|--------|---------------|---------------|\n';
39
+ for (const p of projects.filter(p => p.status !== 'completed')) {
40
+ content += `| ${p.name} | ${p.status} | ${p.repos || ''} | ${p.phase || '1'} |\n`;
41
+ }
42
+ content += '\n## Completed\n\n';
43
+ content += '| Project | Completed | Duration |\n';
44
+ content += '|---------|-----------|----------|\n';
45
+ for (const p of projects.filter(p => p.status === 'completed')) {
46
+ content += `| ${p.name} | ${p.completed || ''} | |\n`;
47
+ }
48
+ writeFile(cwd, '.planning/PROJECTS.md', content);
49
+ }
50
+
51
+ const {
52
+ scanProjectPlanFiles,
53
+ buildOverlapMatrix,
54
+ excludeArchivedPhases,
55
+ formatOverlapReport,
56
+ getActiveProjectSlugs,
57
+ classifyOverlapSeverity,
58
+ } = require('./overlap.cjs');
59
+
60
+ // ─── scanProjectPlanFiles ────────────────────────────────────────────────────
61
+
62
+ describe('scanProjectPlanFiles', () => {
63
+ let tmpDir;
64
+ beforeEach(() => { tmpDir = createTempDir(); });
65
+ afterEach(() => { cleanupDir(tmpDir); });
66
+
67
+ it('returns empty array when project has no phases directory', () => {
68
+ createProject(tmpDir, 'project-a');
69
+ // Remove phases dir
70
+ fs.rmSync(path.join(tmpDir, '.planning', 'projects', 'project-a', 'phases'), { recursive: true, force: true });
71
+ const result = scanProjectPlanFiles(tmpDir, 'project-a');
72
+ assert.ok(Array.isArray(result));
73
+ assert.strictEqual(result.length, 0);
74
+ });
75
+
76
+ it('returns repos and files from plan file tags', () => {
77
+ createProject(tmpDir, 'project-a');
78
+ createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
79
+ { name: 'Task 1', files: ['web-app/src/index.js', 'web-app/src/app.js'], repos: ['web-app'] },
80
+ ]);
81
+ const result = scanProjectPlanFiles(tmpDir, 'project-a');
82
+ assert.strictEqual(result.length, 1);
83
+ assert.deepStrictEqual(result[0].repos, ['web-app']);
84
+ assert.deepStrictEqual(result[0].files, ['web-app/src/app.js', 'web-app/src/index.js']);
85
+ });
86
+
87
+ it('handles plans with multiple tasks', () => {
88
+ createProject(tmpDir, 'project-a');
89
+ createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
90
+ { name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
91
+ { name: 'Task 2', files: ['server/src/b.js'], repos: ['server'] },
92
+ ]);
93
+ const result = scanProjectPlanFiles(tmpDir, 'project-a');
94
+ assert.strictEqual(result.length, 1);
95
+ // Should aggregate repos and files across all tasks in the plan
96
+ assert.ok(result[0].repos.includes('web-app'));
97
+ assert.ok(result[0].repos.includes('server'));
98
+ assert.ok(result[0].files.includes('web-app/src/a.js'));
99
+ assert.ok(result[0].files.includes('server/src/b.js'));
100
+ });
101
+
102
+ it('skips non-PLAN.md files', () => {
103
+ createProject(tmpDir, 'project-a');
104
+ createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
105
+ { name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
106
+ ]);
107
+ // Create a SUMMARY.md file (should be skipped)
108
+ writeFile(tmpDir, '.planning/projects/project-a/phases/01-setup/01-01-SUMMARY.md', '<repos>server</repos>\n<files>server/x.js</files>');
109
+ // Create a CONTEXT.md file (should be skipped)
110
+ writeFile(tmpDir, '.planning/projects/project-a/phases/01-setup/01-CONTEXT.md', '<repos>other</repos>');
111
+ const result = scanProjectPlanFiles(tmpDir, 'project-a');
112
+ assert.strictEqual(result.length, 1);
113
+ // Only web-app from the PLAN.md, not server from SUMMARY.md
114
+ assert.deepStrictEqual(result[0].repos, ['web-app']);
115
+ });
116
+
117
+ it('handles multiple phases with multiple plans', () => {
118
+ createProject(tmpDir, 'project-a');
119
+ createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
120
+ { name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
121
+ ]);
122
+ createPlanFile(tmpDir, 'project-a', '02-build', '02-01-PLAN.md', [
123
+ { name: 'Task 1', files: ['server/src/b.js'], repos: ['server'] },
124
+ ]);
125
+ const result = scanProjectPlanFiles(tmpDir, 'project-a');
126
+ assert.strictEqual(result.length, 2);
127
+ });
128
+
129
+ it('handles plan files without repos or files tags', () => {
130
+ createProject(tmpDir, 'project-a');
131
+ writeFile(tmpDir, '.planning/projects/project-a/phases/01-setup/01-01-PLAN.md',
132
+ '---\nphase: 01\n---\n\n<tasks>\n<task type="auto">\n <name>Task 1</name>\n <action>Do</action>\n</task>\n</tasks>\n'
133
+ );
134
+ const result = scanProjectPlanFiles(tmpDir, 'project-a');
135
+ assert.strictEqual(result.length, 1);
136
+ assert.deepStrictEqual(result[0].repos, []);
137
+ assert.deepStrictEqual(result[0].files, []);
138
+ });
139
+ });
140
+
141
+ // ─── buildOverlapMatrix ──────────────────────────────────────────────────────
142
+
143
+ describe('buildOverlapMatrix', () => {
144
+ let tmpDir;
145
+ beforeEach(() => { tmpDir = createTempDir(); });
146
+ afterEach(() => { cleanupDir(tmpDir); });
147
+
148
+ it('returns empty overlap when only one project exists', () => {
149
+ createProject(tmpDir, 'project-a');
150
+ createProjectsMd(tmpDir, [{ name: 'project-a', status: 'Active' }]);
151
+ writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
152
+ createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
153
+ { name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
154
+ ]);
155
+ const result = buildOverlapMatrix(tmpDir);
156
+ assert.ok(result);
157
+ assert.strictEqual(result.overlapping_repos.length, 0);
158
+ });
159
+
160
+ it('detects repo-level overlap when two projects touch the same repo', () => {
161
+ createProject(tmpDir, 'project-a');
162
+ createProject(tmpDir, 'project-b');
163
+ createProjectsMd(tmpDir, [
164
+ { name: 'project-a', status: 'Active' },
165
+ { name: 'project-b', status: 'Active' },
166
+ ]);
167
+ writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
168
+ createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
169
+ { name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
170
+ ]);
171
+ createPlanFile(tmpDir, 'project-b', '01-setup', '01-01-PLAN.md', [
172
+ { name: 'Task 1', files: ['web-app/src/b.js'], repos: ['web-app'] },
173
+ ]);
174
+ const result = buildOverlapMatrix(tmpDir);
175
+ assert.strictEqual(result.overlapping_repos.length, 1);
176
+ assert.strictEqual(result.overlapping_repos[0].repo, 'web-app');
177
+ assert.ok(result.overlapping_repos[0].projects.includes('project-a'));
178
+ assert.ok(result.overlapping_repos[0].projects.includes('project-b'));
179
+ });
180
+
181
+ it('returns file-level detail for overlapping repos', () => {
182
+ createProject(tmpDir, 'project-a');
183
+ createProject(tmpDir, 'project-b');
184
+ createProjectsMd(tmpDir, [
185
+ { name: 'project-a', status: 'Active' },
186
+ { name: 'project-b', status: 'Active' },
187
+ ]);
188
+ writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
189
+ createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
190
+ { name: 'Task 1', files: ['web-app/src/shared.js', 'web-app/src/a.js'], repos: ['web-app'] },
191
+ ]);
192
+ createPlanFile(tmpDir, 'project-b', '01-setup', '01-01-PLAN.md', [
193
+ { name: 'Task 1', files: ['web-app/src/shared.js', 'web-app/src/b.js'], repos: ['web-app'] },
194
+ ]);
195
+ const result = buildOverlapMatrix(tmpDir);
196
+ assert.strictEqual(result.overlapping_repos.length, 1);
197
+ const overlap = result.overlapping_repos[0];
198
+ // file_conflicts should contain shared.js (touched by both)
199
+ assert.ok(overlap.file_conflicts);
200
+ const sharedConflict = overlap.file_conflicts.find(f => f.file === 'web-app/src/shared.js');
201
+ assert.ok(sharedConflict, 'Expected shared.js to be in file_conflicts');
202
+ assert.ok(sharedConflict.projects.includes('project-a'));
203
+ assert.ok(sharedConflict.projects.includes('project-b'));
204
+ });
205
+
206
+ it('skips completed projects', () => {
207
+ createProject(tmpDir, 'project-a');
208
+ createProject(tmpDir, 'project-b', 'completed');
209
+ createProjectsMd(tmpDir, [
210
+ { name: 'project-a', status: 'Active' },
211
+ { name: 'project-b', status: 'completed', completed: '2026-01-01' },
212
+ ]);
213
+ writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
214
+ createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
215
+ { name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
216
+ ]);
217
+ createPlanFile(tmpDir, 'project-b', '01-setup', '01-01-PLAN.md', [
218
+ { name: 'Task 1', files: ['web-app/src/b.js'], repos: ['web-app'] },
219
+ ]);
220
+ const result = buildOverlapMatrix(tmpDir);
221
+ assert.strictEqual(result.overlapping_repos.length, 0);
222
+ });
223
+
224
+ it('ghost project guard: skips projects with missing folders', () => {
225
+ createProject(tmpDir, 'project-a');
226
+ createProjectsMd(tmpDir, [
227
+ { name: 'project-a', status: 'Active' },
228
+ { name: 'ghost-project', status: 'Active' },
229
+ ]);
230
+ writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
231
+ createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
232
+ { name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
233
+ ]);
234
+ // ghost-project has no folder on disk
235
+ const result = buildOverlapMatrix(tmpDir);
236
+ assert.ok(result);
237
+ assert.ok(result.warnings.some(w => w.includes('ghost-project')));
238
+ });
239
+
240
+ it('no overlap when projects touch different repos', () => {
241
+ createProject(tmpDir, 'project-a');
242
+ createProject(tmpDir, 'project-b');
243
+ createProjectsMd(tmpDir, [
244
+ { name: 'project-a', status: 'Active' },
245
+ { name: 'project-b', status: 'Active' },
246
+ ]);
247
+ writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n| server | ./server |\n');
248
+ createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
249
+ { name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
250
+ ]);
251
+ createPlanFile(tmpDir, 'project-b', '01-setup', '01-01-PLAN.md', [
252
+ { name: 'Task 1', files: ['server/src/b.js'], repos: ['server'] },
253
+ ]);
254
+ const result = buildOverlapMatrix(tmpDir);
255
+ assert.strictEqual(result.overlapping_repos.length, 0);
256
+ });
257
+
258
+ it('handles three projects overlapping on same repo', () => {
259
+ createProject(tmpDir, 'project-a');
260
+ createProject(tmpDir, 'project-b');
261
+ createProject(tmpDir, 'project-c');
262
+ createProjectsMd(tmpDir, [
263
+ { name: 'project-a', status: 'Active' },
264
+ { name: 'project-b', status: 'Active' },
265
+ { name: 'project-c', status: 'Active' },
266
+ ]);
267
+ writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| shared | ./shared |\n');
268
+ createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
269
+ { name: 'Task 1', files: ['shared/lib.js'], repos: ['shared'] },
270
+ ]);
271
+ createPlanFile(tmpDir, 'project-b', '01-setup', '01-01-PLAN.md', [
272
+ { name: 'Task 1', files: ['shared/utils.js'], repos: ['shared'] },
273
+ ]);
274
+ createPlanFile(tmpDir, 'project-c', '01-setup', '01-01-PLAN.md', [
275
+ { name: 'Task 1', files: ['shared/core.js'], repos: ['shared'] },
276
+ ]);
277
+ const result = buildOverlapMatrix(tmpDir);
278
+ assert.strictEqual(result.overlapping_repos.length, 1);
279
+ assert.strictEqual(result.overlapping_repos[0].projects.length, 3);
280
+ });
281
+
282
+ it('resolves files to correct repos using resolveFileToRepo (nested prefixes)', () => {
283
+ createProject(tmpDir, 'project-a');
284
+ createProject(tmpDir, 'project-b');
285
+ createProjectsMd(tmpDir, [
286
+ { name: 'project-a', status: 'Active' },
287
+ { name: 'project-b', status: 'Active' },
288
+ ]);
289
+ writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| services-api | ./services/api |\n| services-api-gateway | ./services/api-gateway |\n');
290
+ createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
291
+ { name: 'Task 1', files: ['services/api/shared.ts', 'services/api-gateway/handler.ts'], repos: ['services-api', 'services-api-gateway'] },
292
+ ]);
293
+ createPlanFile(tmpDir, 'project-b', '01-setup', '01-01-PLAN.md', [
294
+ { name: 'Task 1', files: ['services/api/shared.ts'], repos: ['services-api'] },
295
+ ]);
296
+ const result = buildOverlapMatrix(tmpDir);
297
+ // services-api overlaps (both projects touch it)
298
+ const apiOverlap = result.overlapping_repos.find(o => o.repo === 'services-api');
299
+ assert.ok(apiOverlap, 'Expected services-api to overlap');
300
+ assert.ok(apiOverlap.projects.includes('project-a'));
301
+ assert.ok(apiOverlap.projects.includes('project-b'));
302
+ // shared.ts is the file-level conflict under services-api
303
+ const sharedConflict = apiOverlap.file_conflicts.find(f => f.file === 'services/api/shared.ts');
304
+ assert.ok(sharedConflict, 'Expected services/api/shared.ts in file_conflicts');
305
+ // services-api-gateway should NOT overlap (only project-a touches it)
306
+ const gatewayOverlap = result.overlapping_repos.find(o => o.repo === 'services-api-gateway');
307
+ assert.strictEqual(gatewayOverlap, undefined, 'services-api-gateway should not overlap');
308
+ });
309
+
310
+ it('maps repo-relative file paths to repos when <repos> tags are present', () => {
311
+ createProject(tmpDir, 'project-a');
312
+ createProject(tmpDir, 'project-b');
313
+ createProjectsMd(tmpDir, [
314
+ { name: 'project-a', status: 'Active' },
315
+ { name: 'project-b', status: 'Active' },
316
+ ]);
317
+ writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
318
+ // Repo-relative paths: src/index.ts instead of web-app/src/index.ts
319
+ createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
320
+ { name: 'Task 1', files: ['src/index.ts', 'src/app.ts'], repos: ['web-app'] },
321
+ ]);
322
+ createPlanFile(tmpDir, 'project-b', '01-setup', '01-01-PLAN.md', [
323
+ { name: 'Task 1', files: ['src/index.ts', 'src/other.ts'], repos: ['web-app'] },
324
+ ]);
325
+ const result = buildOverlapMatrix(tmpDir);
326
+ assert.strictEqual(result.overlapping_repos.length, 1);
327
+ assert.strictEqual(result.overlapping_repos[0].repo, 'web-app');
328
+ assert.ok(result.overlapping_repos[0].projects.includes('project-a'));
329
+ assert.ok(result.overlapping_repos[0].projects.includes('project-b'));
330
+ // src/index.ts should appear as a file conflict
331
+ const indexConflict = result.overlapping_repos[0].file_conflicts.find(f => f.file === 'src/index.ts');
332
+ assert.ok(indexConflict, 'Expected src/index.ts in file_conflicts');
333
+ assert.ok(indexConflict.projects.includes('project-a'));
334
+ assert.ok(indexConflict.projects.includes('project-b'));
335
+ });
336
+
337
+ it('falls back to resolveFileToRepo for legacy paths without <repos> tags', () => {
338
+ createProject(tmpDir, 'project-a');
339
+ createProject(tmpDir, 'project-b');
340
+ createProjectsMd(tmpDir, [
341
+ { name: 'project-a', status: 'Active' },
342
+ { name: 'project-b', status: 'Active' },
343
+ ]);
344
+ writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
345
+ // Legacy paths without repos tags — files include repo prefix
346
+ createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
347
+ { name: 'Task 1', files: ['web-app/src/shared.js'] },
348
+ ]);
349
+ createPlanFile(tmpDir, 'project-b', '01-setup', '01-01-PLAN.md', [
350
+ { name: 'Task 1', files: ['web-app/src/shared.js'] },
351
+ ]);
352
+ const result = buildOverlapMatrix(tmpDir);
353
+ // Without repos tags, files still get resolved via resolveFileToRepo
354
+ // Both projects touch web-app/src/shared.js
355
+ assert.strictEqual(result.overlapping_repos.length, 1);
356
+ assert.strictEqual(result.overlapping_repos[0].repo, 'web-app');
357
+ });
358
+ });
359
+
360
+ // ─── excludeArchivedPhases ───────────────────────────────────────────────────
361
+
362
+ describe('excludeArchivedPhases', () => {
363
+ let tmpDir;
364
+ beforeEach(() => { tmpDir = createTempDir(); });
365
+ afterEach(() => { cleanupDir(tmpDir); });
366
+
367
+ it('keeps current phase directories', () => {
368
+ const dirs = [
369
+ path.join(tmpDir, '.planning/projects/project-a/phases/01-setup'),
370
+ path.join(tmpDir, '.planning/projects/project-a/phases/02-build'),
371
+ ];
372
+ const result = excludeArchivedPhases(dirs);
373
+ assert.strictEqual(result.length, 2);
374
+ });
375
+
376
+ it('excludes directories in milestones archive path', () => {
377
+ const dirs = [
378
+ path.join(tmpDir, '.planning/projects/project-a/phases/01-setup'),
379
+ path.join(tmpDir, '.planning/milestones/v1.0-phases/01-setup'),
380
+ ];
381
+ const result = excludeArchivedPhases(dirs);
382
+ assert.strictEqual(result.length, 1);
383
+ assert.ok(result[0].includes('project-a'));
384
+ });
385
+
386
+ it('returns empty array for empty input', () => {
387
+ const result = excludeArchivedPhases([]);
388
+ assert.strictEqual(result.length, 0);
389
+ });
390
+ });
391
+
392
+ // ─── formatOverlapReport ─────────────────────────────────────────────────────
393
+
394
+ describe('formatOverlapReport', () => {
395
+ it('returns no-overlap message when matrix is empty', () => {
396
+ const matrix = { overlapping_repos: [], warnings: [] };
397
+ const report = formatOverlapReport(matrix);
398
+ assert.ok(report.includes('No cross-project overlaps'));
399
+ });
400
+
401
+ it('formats repo-level overlap table', () => {
402
+ const matrix = {
403
+ overlapping_repos: [
404
+ { repo: 'web-app', projects: ['project-a', 'project-b'], file_conflicts: [] },
405
+ ],
406
+ warnings: [],
407
+ };
408
+ const report = formatOverlapReport(matrix);
409
+ assert.ok(report.includes('web-app'));
410
+ assert.ok(report.includes('project-a'));
411
+ assert.ok(report.includes('project-b'));
412
+ });
413
+
414
+ it('includes file-level drill-down for HIGH severity overlapping repos', () => {
415
+ const matrix = {
416
+ overlapping_repos: [
417
+ {
418
+ repo: 'web-app',
419
+ projects: ['project-a', 'project-b'],
420
+ file_conflicts: [
421
+ { file: 'web-app/src/shared.js', projects: ['project-a', 'project-b'] },
422
+ ],
423
+ severity: 'HIGH',
424
+ severity_reason: '1 file(s) modified by multiple projects',
425
+ },
426
+ ],
427
+ warnings: [],
428
+ };
429
+ const report = formatOverlapReport(matrix);
430
+ assert.ok(report.includes('shared.js'));
431
+ assert.ok(report.includes('project-a'));
432
+ assert.ok(report.includes('project-b'));
433
+ });
434
+
435
+ it('handles multiple overlapping repos', () => {
436
+ const matrix = {
437
+ overlapping_repos: [
438
+ { repo: 'web-app', projects: ['project-a', 'project-b'], file_conflicts: [] },
439
+ { repo: 'server', projects: ['project-a', 'project-c'], file_conflicts: [] },
440
+ ],
441
+ warnings: [],
442
+ };
443
+ const report = formatOverlapReport(matrix);
444
+ assert.ok(report.includes('web-app'));
445
+ assert.ok(report.includes('server'));
446
+ });
447
+ });
448
+
449
+ // ─── classifyOverlapSeverity ──────────────────────────────────────────────────
450
+
451
+ describe('classifyOverlapSeverity', () => {
452
+ it('returns HIGH when two projects touch the same file', () => {
453
+ const result = classifyOverlapSeverity({
454
+ file_conflicts: [
455
+ { file: 'web-app/src/shared.js', projects: ['project-a', 'project-b'] },
456
+ ],
457
+ project_files: {
458
+ 'project-a': ['web-app/src/shared.js'],
459
+ 'project-b': ['web-app/src/shared.js'],
460
+ },
461
+ });
462
+ assert.strictEqual(result.severity, 'HIGH');
463
+ assert.ok(result.reason.includes('1 file(s)'));
464
+ assert.ok(result.reason.includes('shared.js'));
465
+ });
466
+
467
+ it('returns MEDIUM when projects share a directory but not files', () => {
468
+ const result = classifyOverlapSeverity({
469
+ file_conflicts: [],
470
+ project_files: {
471
+ 'project-a': ['web-app/src/auth/login.js'],
472
+ 'project-b': ['web-app/src/auth/signup.js'],
473
+ },
474
+ });
475
+ assert.strictEqual(result.severity, 'MEDIUM');
476
+ assert.ok(result.reason.includes('1 directory'));
477
+ assert.ok(result.reason.includes('web-app/src/auth'));
478
+ });
479
+
480
+ it('returns LOW when projects touch different directories', () => {
481
+ const result = classifyOverlapSeverity({
482
+ file_conflicts: [],
483
+ project_files: {
484
+ 'project-a': ['web-app/src/auth/login.js'],
485
+ 'project-b': ['web-app/src/billing/invoice.js'],
486
+ },
487
+ });
488
+ assert.strictEqual(result.severity, 'LOW');
489
+ assert.ok(result.reason.includes('different directories'));
490
+ });
491
+
492
+ it('returns HIGH with multiple file conflicts', () => {
493
+ const result = classifyOverlapSeverity({
494
+ file_conflicts: [
495
+ { file: 'web-app/src/a.js', projects: ['project-a', 'project-b'] },
496
+ { file: 'web-app/src/b.js', projects: ['project-a', 'project-b'] },
497
+ ],
498
+ project_files: {
499
+ 'project-a': ['web-app/src/a.js', 'web-app/src/b.js'],
500
+ 'project-b': ['web-app/src/a.js', 'web-app/src/b.js'],
501
+ },
502
+ });
503
+ assert.strictEqual(result.severity, 'HIGH');
504
+ assert.ok(result.reason.includes('2 file(s)'));
505
+ });
506
+
507
+ it('returns LOW when no file_conflicts and no project_files', () => {
508
+ const result = classifyOverlapSeverity({
509
+ file_conflicts: [],
510
+ project_files: {},
511
+ });
512
+ assert.strictEqual(result.severity, 'LOW');
513
+ });
514
+ });
515
+
516
+ // ─── buildOverlapMatrix severity integration ─────────────────────────────────
517
+
518
+ describe('buildOverlapMatrix severity', () => {
519
+ let tmpDir;
520
+ beforeEach(() => { tmpDir = createTempDir(); });
521
+ afterEach(() => { cleanupDir(tmpDir); });
522
+
523
+ it('two projects sharing same file -> severity HIGH', () => {
524
+ createProject(tmpDir, 'project-a');
525
+ createProject(tmpDir, 'project-b');
526
+ createProjectsMd(tmpDir, [
527
+ { name: 'project-a', status: 'Active' },
528
+ { name: 'project-b', status: 'Active' },
529
+ ]);
530
+ writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
531
+ createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
532
+ { name: 'Task 1', files: ['web-app/src/shared.js'], repos: ['web-app'] },
533
+ ]);
534
+ createPlanFile(tmpDir, 'project-b', '01-setup', '01-01-PLAN.md', [
535
+ { name: 'Task 1', files: ['web-app/src/shared.js'], repos: ['web-app'] },
536
+ ]);
537
+ const result = buildOverlapMatrix(tmpDir);
538
+ assert.strictEqual(result.overlapping_repos.length, 1);
539
+ assert.strictEqual(result.overlapping_repos[0].severity, 'HIGH');
540
+ assert.ok(result.overlapping_repos[0].severity_reason.includes('file(s)'));
541
+ });
542
+
543
+ it('two projects in same dir, different files -> severity MEDIUM', () => {
544
+ createProject(tmpDir, 'project-a');
545
+ createProject(tmpDir, 'project-b');
546
+ createProjectsMd(tmpDir, [
547
+ { name: 'project-a', status: 'Active' },
548
+ { name: 'project-b', status: 'Active' },
549
+ ]);
550
+ writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
551
+ createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
552
+ { name: 'Task 1', files: ['web-app/src/auth/login.js'], repos: ['web-app'] },
553
+ ]);
554
+ createPlanFile(tmpDir, 'project-b', '01-setup', '01-01-PLAN.md', [
555
+ { name: 'Task 1', files: ['web-app/src/auth/signup.js'], repos: ['web-app'] },
556
+ ]);
557
+ const result = buildOverlapMatrix(tmpDir);
558
+ assert.strictEqual(result.overlapping_repos.length, 1);
559
+ assert.strictEqual(result.overlapping_repos[0].severity, 'MEDIUM');
560
+ assert.ok(result.overlapping_repos[0].severity_reason.includes('directory'));
561
+ });
562
+
563
+ it('two projects in different dirs of same repo -> severity LOW', () => {
564
+ createProject(tmpDir, 'project-a');
565
+ createProject(tmpDir, 'project-b');
566
+ createProjectsMd(tmpDir, [
567
+ { name: 'project-a', status: 'Active' },
568
+ { name: 'project-b', status: 'Active' },
569
+ ]);
570
+ writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
571
+ createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
572
+ { name: 'Task 1', files: ['web-app/src/auth/login.js'], repos: ['web-app'] },
573
+ ]);
574
+ createPlanFile(tmpDir, 'project-b', '01-setup', '01-01-PLAN.md', [
575
+ { name: 'Task 1', files: ['web-app/src/billing/invoice.js'], repos: ['web-app'] },
576
+ ]);
577
+ const result = buildOverlapMatrix(tmpDir);
578
+ assert.strictEqual(result.overlapping_repos.length, 1);
579
+ assert.strictEqual(result.overlapping_repos[0].severity, 'LOW');
580
+ assert.ok(result.overlapping_repos[0].severity_reason.includes('different directories'));
581
+ });
582
+
583
+ it('existing tests still have severity field present', () => {
584
+ createProject(tmpDir, 'project-a');
585
+ createProject(tmpDir, 'project-b');
586
+ createProjectsMd(tmpDir, [
587
+ { name: 'project-a', status: 'Active' },
588
+ { name: 'project-b', status: 'Active' },
589
+ ]);
590
+ writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
591
+ createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
592
+ { name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
593
+ ]);
594
+ createPlanFile(tmpDir, 'project-b', '01-setup', '01-01-PLAN.md', [
595
+ { name: 'Task 1', files: ['web-app/src/b.js'], repos: ['web-app'] },
596
+ ]);
597
+ const result = buildOverlapMatrix(tmpDir);
598
+ assert.strictEqual(result.overlapping_repos.length, 1);
599
+ assert.ok(result.overlapping_repos[0].severity, 'severity field should be present');
600
+ assert.ok(result.overlapping_repos[0].severity_reason, 'severity_reason field should be present');
601
+ });
602
+
603
+ it('multiple overlaps sorted by severity (HIGH before LOW)', () => {
604
+ createProject(tmpDir, 'project-a');
605
+ createProject(tmpDir, 'project-b');
606
+ createProjectsMd(tmpDir, [
607
+ { name: 'project-a', status: 'Active' },
608
+ { name: 'project-b', status: 'Active' },
609
+ ]);
610
+ writeFile(tmpDir, '.planning/REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| server | ./server |\n| web-app | ./web-app |\n');
611
+ // Use separate plan files per repo so each plan has a single <repos> tag
612
+ // This ensures files are mapped precisely to one repo each
613
+ // project-a: server plan (shared.js) + web-app plan (auth)
614
+ createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
615
+ { name: 'Task 1', files: ['src/shared.js'], repos: ['server'] },
616
+ ]);
617
+ createPlanFile(tmpDir, 'project-a', '02-build', '02-01-PLAN.md', [
618
+ { name: 'Task 1', files: ['src/auth/login.js'], repos: ['web-app'] },
619
+ ]);
620
+ // project-b: server plan (same shared.js -> HIGH) + web-app plan (billing -> LOW)
621
+ createPlanFile(tmpDir, 'project-b', '01-setup', '01-01-PLAN.md', [
622
+ { name: 'Task 1', files: ['src/shared.js'], repos: ['server'] },
623
+ ]);
624
+ createPlanFile(tmpDir, 'project-b', '02-build', '02-01-PLAN.md', [
625
+ { name: 'Task 1', files: ['src/billing/invoice.js'], repos: ['web-app'] },
626
+ ]);
627
+ const result = buildOverlapMatrix(tmpDir);
628
+ assert.strictEqual(result.overlapping_repos.length, 2);
629
+ // HIGH first
630
+ assert.strictEqual(result.overlapping_repos[0].severity, 'HIGH');
631
+ assert.strictEqual(result.overlapping_repos[0].repo, 'server');
632
+ // LOW second
633
+ assert.strictEqual(result.overlapping_repos[1].severity, 'LOW');
634
+ assert.strictEqual(result.overlapping_repos[1].repo, 'web-app');
635
+ });
636
+ });
637
+
638
+ // ─── formatOverlapReport severity ────────────────────────────────────────────
639
+
640
+ describe('formatOverlapReport severity', () => {
641
+ it('severity column appears in table output', () => {
642
+ const matrix = {
643
+ overlapping_repos: [
644
+ { repo: 'web-app', projects: ['project-a', 'project-b'], file_conflicts: [], severity: 'LOW', severity_reason: 'Different dirs' },
645
+ ],
646
+ warnings: [],
647
+ };
648
+ const report = formatOverlapReport(matrix);
649
+ assert.ok(report.includes('| Repo | Severity | Projects |'));
650
+ assert.ok(report.includes('| web-app | LOW | project-a, project-b |'));
651
+ });
652
+
653
+ it('HIGH overlaps listed before LOW overlaps', () => {
654
+ const matrix = {
655
+ overlapping_repos: [
656
+ { repo: 'server', projects: ['project-a', 'project-b'], file_conflicts: [{ file: 'server/shared.js', projects: ['project-a', 'project-b'] }], severity: 'HIGH', severity_reason: '1 file(s)' },
657
+ { repo: 'web-app', projects: ['project-a', 'project-b'], file_conflicts: [], severity: 'LOW', severity_reason: 'Different dirs' },
658
+ ],
659
+ warnings: [],
660
+ };
661
+ const report = formatOverlapReport(matrix);
662
+ const serverIdx = report.indexOf('server');
663
+ const webAppIdx = report.indexOf('web-app');
664
+ assert.ok(serverIdx < webAppIdx, 'HIGH severity (server) should appear before LOW (web-app)');
665
+ });
666
+
667
+ it('file-level drill-down only shown for HIGH severity repos', () => {
668
+ const matrix = {
669
+ overlapping_repos: [
670
+ { repo: 'server', projects: ['project-a', 'project-b'], file_conflicts: [{ file: 'server/shared.js', projects: ['project-a', 'project-b'] }], severity: 'HIGH', severity_reason: '1 file(s)' },
671
+ { repo: 'web-app', projects: ['project-a', 'project-b'], file_conflicts: [], severity: 'LOW', severity_reason: 'Different dirs' },
672
+ ],
673
+ warnings: [],
674
+ };
675
+ const report = formatOverlapReport(matrix);
676
+ assert.ok(report.includes('### File-Level Conflicts'));
677
+ assert.ok(report.includes('server/shared.js'));
678
+ // web-app should NOT have file-level section
679
+ });
680
+
681
+ it('no file-level section when no HIGH severity repos', () => {
682
+ const matrix = {
683
+ overlapping_repos: [
684
+ { repo: 'web-app', projects: ['project-a', 'project-b'], file_conflicts: [], severity: 'LOW', severity_reason: 'Different dirs' },
685
+ ],
686
+ warnings: [],
687
+ };
688
+ const report = formatOverlapReport(matrix);
689
+ assert.ok(!report.includes('### File-Level Conflicts'));
690
+ });
691
+ });
692
+
693
+ // ─── getActiveProjectSlugs ───────────────────────────────────────────────────
694
+
695
+ describe('getActiveProjectSlugs', () => {
696
+ let tmpDir;
697
+ beforeEach(() => { tmpDir = createTempDir(); });
698
+ afterEach(() => { cleanupDir(tmpDir); });
699
+
700
+ it('returns active slugs from PROJECTS.md', () => {
701
+ createProject(tmpDir, 'project-a');
702
+ createProject(tmpDir, 'project-b');
703
+ createProjectsMd(tmpDir, [
704
+ { name: 'project-a', status: 'Active' },
705
+ { name: 'project-b', status: 'Active' },
706
+ ]);
707
+ const result = getActiveProjectSlugs(tmpDir);
708
+ assert.ok(result.includes('project-a'));
709
+ assert.ok(result.includes('project-b'));
710
+ });
711
+
712
+ it('filters out completed projects', () => {
713
+ createProject(tmpDir, 'project-a');
714
+ createProject(tmpDir, 'project-b', 'completed');
715
+ createProjectsMd(tmpDir, [
716
+ { name: 'project-a', status: 'Active' },
717
+ { name: 'project-b', status: 'completed', completed: '2026-01-01' },
718
+ ]);
719
+ const result = getActiveProjectSlugs(tmpDir);
720
+ assert.ok(result.includes('project-a'));
721
+ assert.ok(!result.includes('project-b'));
722
+ });
723
+
724
+ it('falls back to scanning folders when PROJECTS.md missing', () => {
725
+ createProject(tmpDir, 'project-a');
726
+ createProject(tmpDir, 'project-b');
727
+ // No PROJECTS.md — should scan .planning/ subfolders
728
+ const result = getActiveProjectSlugs(tmpDir);
729
+ assert.ok(result.includes('project-a'));
730
+ assert.ok(result.includes('project-b'));
731
+ });
732
+
733
+ it('ghost project guard: skips folders without STATE.md', () => {
734
+ createProject(tmpDir, 'project-a');
735
+ // Create a ghost folder without STATE.md
736
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'projects', 'ghost-project'), { recursive: true });
737
+ const result = getActiveProjectSlugs(tmpDir);
738
+ assert.ok(result.includes('project-a'));
739
+ assert.ok(!result.includes('ghost-project'));
740
+ });
741
+
742
+ it('returns empty array when no projects exist', () => {
743
+ fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
744
+ const result = getActiveProjectSlugs(tmpDir);
745
+ assert.strictEqual(result.length, 0);
746
+ });
747
+ });