@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,871 @@
1
+ /**
2
+ * Tests for projects.cjs — Project lifecycle: subfolder creation, state reading,
3
+ * PROJECTS.md regeneration, repo tag scanning, project completion
4
+ */
5
+
6
+ const { describe, it, beforeEach, afterEach } = require('node:test');
7
+ const assert = require('node:assert');
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ const { createTempDir, cleanupDir, createFixture } = require('./test-helpers.cjs');
13
+ const { getProjectRoot } = require('./core.cjs');
14
+
15
+ // Helper: create .planning/projects/ directory structure
16
+ function setupPlanning(cwd) {
17
+ fs.mkdirSync(path.join(cwd, '.planning', 'projects'), { recursive: true });
18
+ }
19
+
20
+ // Helper: create a project subfolder with STATE.md manually under .planning/projects/<slug>/
21
+ function createProjectManually(cwd, slug, stateContent) {
22
+ const projDir = path.join(cwd, '.planning', 'projects', slug);
23
+ fs.mkdirSync(projDir, { recursive: true });
24
+ if (stateContent) {
25
+ fs.writeFileSync(path.join(projDir, 'STATE.md'), stateContent);
26
+ }
27
+ }
28
+
29
+ // Helper: create plan file with <repos> tags under .planning/projects/<slug>/phases/
30
+ function createPlanFile(cwd, slug, phaseDir, planName, content) {
31
+ const dir = path.join(cwd, '.planning', 'projects', slug, 'phases', phaseDir);
32
+ fs.mkdirSync(dir, { recursive: true });
33
+ fs.writeFileSync(path.join(dir, planName), content);
34
+ }
35
+
36
+ const {
37
+ createProjectSubfolder,
38
+ readProjectState,
39
+ scanProjectReposTags,
40
+ regenerateProjectsMd,
41
+ completeProject,
42
+ reactivateProject,
43
+ parseProjectsMd,
44
+ checkSlugPrefixCollision,
45
+ } = require('./projects.cjs');
46
+
47
+ // ─── createProjectSubfolder ─────────────────────────────────────────────────
48
+
49
+ describe('createProjectSubfolder', () => {
50
+ let tmpDir;
51
+
52
+ beforeEach(() => {
53
+ tmpDir = createTempDir();
54
+ setupPlanning(tmpDir);
55
+ });
56
+
57
+ afterEach(() => cleanupDir(tmpDir));
58
+
59
+ it('creates project directory at .planning/projects/<slug>/', () => {
60
+ const result = createProjectSubfolder(tmpDir, 'auth-overhaul', 'Auth Overhaul');
61
+ assert.strictEqual(result.created, true);
62
+ assert.ok(fs.existsSync(path.join(tmpDir, '.planning', 'projects', 'auth-overhaul')));
63
+ });
64
+
65
+ it('creates all required subdirectories', () => {
66
+ createProjectSubfolder(tmpDir, 'auth-overhaul', 'Auth Overhaul');
67
+ const expectedDirs = ['phases', 'research', 'todos', 'quick', 'debug'];
68
+ for (const dir of expectedDirs) {
69
+ assert.ok(
70
+ fs.existsSync(path.join(tmpDir, '.planning', 'projects', 'auth-overhaul', dir)),
71
+ `Missing directory: ${dir}`
72
+ );
73
+ }
74
+ });
75
+
76
+ it('creates all required files', () => {
77
+ createProjectSubfolder(tmpDir, 'auth-overhaul', 'Auth Overhaul');
78
+ const expectedFiles = ['PROJECT.md', 'REQUIREMENTS.md', 'ROADMAP.md', 'STATE.md'];
79
+ for (const file of expectedFiles) {
80
+ assert.ok(
81
+ fs.existsSync(path.join(tmpDir, '.planning', 'projects', 'auth-overhaul', file)),
82
+ `Missing file: ${file}`
83
+ );
84
+ }
85
+ });
86
+
87
+ it('PROJECT.md contains project name in header', () => {
88
+ createProjectSubfolder(tmpDir, 'auth-overhaul', 'Auth Overhaul');
89
+ const content = fs.readFileSync(
90
+ path.join(tmpDir, '.planning', 'projects', 'auth-overhaul', 'PROJECT.md'),
91
+ 'utf-8'
92
+ );
93
+ assert.ok(content.includes('# Project: Auth Overhaul'));
94
+ });
95
+
96
+ it('STATE.md contains initial "Not started" status', () => {
97
+ createProjectSubfolder(tmpDir, 'auth-overhaul', 'Auth Overhaul');
98
+ const content = fs.readFileSync(
99
+ path.join(tmpDir, '.planning', 'projects', 'auth-overhaul', 'STATE.md'),
100
+ 'utf-8'
101
+ );
102
+ assert.ok(content.includes('Not started'));
103
+ assert.ok(content.includes('Status: New'));
104
+ });
105
+
106
+ it('returns the project directory path', () => {
107
+ const result = createProjectSubfolder(tmpDir, 'auth-overhaul', 'Auth Overhaul');
108
+ assert.ok(result.path.endsWith('auth-overhaul'));
109
+ });
110
+
111
+ it('does NOT overwrite if directory already exists', () => {
112
+ createProjectSubfolder(tmpDir, 'auth-overhaul', 'Auth Overhaul');
113
+ const result = createProjectSubfolder(tmpDir, 'auth-overhaul', 'Auth Overhaul');
114
+ assert.strictEqual(result.created, false);
115
+ assert.strictEqual(result.reason, 'already_exists');
116
+ });
117
+
118
+ it('handles slug that is already clean', () => {
119
+ const result = createProjectSubfolder(tmpDir, 'my-cool-project', 'My Cool Project');
120
+ assert.strictEqual(result.created, true);
121
+ assert.ok(fs.existsSync(path.join(tmpDir, '.planning', 'projects', 'my-cool-project')));
122
+ });
123
+ });
124
+
125
+ // ─── readProjectState ───────────────────────────────────────────────────────
126
+
127
+ describe('readProjectState', () => {
128
+ let tmpDir;
129
+
130
+ beforeEach(() => {
131
+ tmpDir = createTempDir();
132
+ setupPlanning(tmpDir);
133
+ });
134
+
135
+ afterEach(() => cleanupDir(tmpDir));
136
+
137
+ it('extracts phase from "Phase: 3 of 7 (Name)" format', () => {
138
+ createProjectManually(tmpDir, 'proj', '# Project State\n\nPhase: 3 of 7 (Project Lifecycle)\nStatus: In progress\n');
139
+ const state = readProjectState(tmpDir, 'proj');
140
+ assert.ok(state.phase.includes('3 of 7'));
141
+ });
142
+
143
+ it('extracts status from "Status: Ready to plan"', () => {
144
+ createProjectManually(tmpDir, 'proj', '# Project State\n\nPhase: 1\nStatus: Ready to plan\n');
145
+ const state = readProjectState(tmpDir, 'proj');
146
+ assert.strictEqual(state.status, 'Ready to plan');
147
+ });
148
+
149
+ it('extracts progress percentage from progress bar', () => {
150
+ createProjectManually(
151
+ tmpDir,
152
+ 'proj',
153
+ '# Project State\n\nPhase: 2\nStatus: Active\nProgress: [████░░░░░░] 40%\n'
154
+ );
155
+ const state = readProjectState(tmpDir, 'proj');
156
+ assert.strictEqual(state.progress, 40);
157
+ });
158
+
159
+ it('returns null for nonexistent project', () => {
160
+ const state = readProjectState(tmpDir, 'nonexistent');
161
+ assert.strictEqual(state, null);
162
+ });
163
+
164
+ it('returns defaults for STATE.md with missing fields', () => {
165
+ createProjectManually(tmpDir, 'proj', '# Project State\n\nSome random content.\n');
166
+ const state = readProjectState(tmpDir, 'proj');
167
+ assert.strictEqual(state.phase, 'Unknown');
168
+ assert.strictEqual(state.status, 'Unknown');
169
+ assert.strictEqual(state.progress, 0);
170
+ });
171
+
172
+ it('handles "Status: completed" in STATE.md', () => {
173
+ createProjectManually(tmpDir, 'proj', '# Project State\n\nStatus: completed\nCompleted: 2026-02-20\n');
174
+ const state = readProjectState(tmpDir, 'proj');
175
+ assert.strictEqual(state.status, 'completed');
176
+ });
177
+
178
+ it('extracts completed_date from "Completed: YYYY-MM-DD"', () => {
179
+ createProjectManually(tmpDir, 'proj', '# Project State\n\nStatus: completed\nCompleted: 2026-02-20\n');
180
+ const state = readProjectState(tmpDir, 'proj');
181
+ assert.strictEqual(state.completed_date, '2026-02-20');
182
+ });
183
+
184
+ it('handles STATE.md with only a header', () => {
185
+ createProjectManually(tmpDir, 'proj', '# Project State\n');
186
+ const state = readProjectState(tmpDir, 'proj');
187
+ assert.ok(state !== null);
188
+ assert.strictEqual(state.phase, 'Unknown');
189
+ });
190
+ });
191
+
192
+ // ─── scanProjectReposTags ───────────────────────────────────────────────────
193
+
194
+ describe('scanProjectReposTags', () => {
195
+ let tmpDir;
196
+
197
+ beforeEach(() => {
198
+ tmpDir = createTempDir();
199
+ setupPlanning(tmpDir);
200
+ });
201
+
202
+ afterEach(() => cleanupDir(tmpDir));
203
+
204
+ it('returns empty array when no phases directory exists', () => {
205
+ createProjectManually(tmpDir, 'proj', '# State\n');
206
+ const repos = scanProjectReposTags(tmpDir, 'proj');
207
+ assert.deepStrictEqual(repos, []);
208
+ });
209
+
210
+ it('returns empty array when no plan files exist', () => {
211
+ createProjectManually(tmpDir, 'proj', '# State\n');
212
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'phases', '01-setup'), {
213
+ recursive: true,
214
+ });
215
+ const repos = scanProjectReposTags(tmpDir, 'proj');
216
+ assert.deepStrictEqual(repos, []);
217
+ });
218
+
219
+ it('extracts repo names from <repos> tags', () => {
220
+ createProjectManually(tmpDir, 'proj', '# State\n');
221
+ createPlanFile(tmpDir, 'proj', '01-setup', '01-01-PLAN.md', '<repos>web-app, server</repos>\n');
222
+ const repos = scanProjectReposTags(tmpDir, 'proj');
223
+ assert.deepStrictEqual(repos, ['server', 'web-app']);
224
+ });
225
+
226
+ it('deduplicates across multiple plan files', () => {
227
+ createProjectManually(tmpDir, 'proj', '# State\n');
228
+ createPlanFile(tmpDir, 'proj', '01-setup', '01-01-PLAN.md', '<repos>web-app, server</repos>\n');
229
+ createPlanFile(tmpDir, 'proj', '01-setup', '01-02-PLAN.md', '<repos>server, shared-lib</repos>\n');
230
+ const repos = scanProjectReposTags(tmpDir, 'proj');
231
+ assert.deepStrictEqual(repos, ['server', 'shared-lib', 'web-app']);
232
+ });
233
+
234
+ it('sorts results alphabetically', () => {
235
+ createProjectManually(tmpDir, 'proj', '# State\n');
236
+ createPlanFile(tmpDir, 'proj', '01-setup', '01-01-PLAN.md', '<repos>zebra, alpha, middle</repos>\n');
237
+ const repos = scanProjectReposTags(tmpDir, 'proj');
238
+ assert.deepStrictEqual(repos, ['alpha', 'middle', 'zebra']);
239
+ });
240
+
241
+ it('returns empty array for plans with no <repos> tags', () => {
242
+ createProjectManually(tmpDir, 'proj', '# State\n');
243
+ createPlanFile(tmpDir, 'proj', '01-setup', '01-01-PLAN.md', 'No repos tags here.\n');
244
+ const repos = scanProjectReposTags(tmpDir, 'proj');
245
+ assert.deepStrictEqual(repos, []);
246
+ });
247
+ });
248
+
249
+ // ─── regenerateProjectsMd ───────────────────────────────────────────────────
250
+
251
+ describe('regenerateProjectsMd', () => {
252
+ let tmpDir;
253
+
254
+ beforeEach(() => {
255
+ tmpDir = createTempDir();
256
+ setupPlanning(tmpDir);
257
+ });
258
+
259
+ afterEach(() => cleanupDir(tmpDir));
260
+
261
+ it('writes Active table with correct columns', () => {
262
+ createProjectManually(tmpDir, 'proj-a', '# Project State\n\nPhase: 1\nStatus: Active\nProgress: [██░░░░░░░░] 20%\n');
263
+ regenerateProjectsMd(tmpDir);
264
+ const content = fs.readFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), 'utf-8');
265
+ assert.ok(content.includes('| Project | Status | Repos Touched | Current Phase |'));
266
+ });
267
+
268
+ it('writes Completed table with correct columns', () => {
269
+ createProjectManually(tmpDir, 'proj-a', '# Project State\n\nStatus: Active\n');
270
+ regenerateProjectsMd(tmpDir);
271
+ const content = fs.readFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), 'utf-8');
272
+ assert.ok(content.includes('| Project | Completed | Duration |'));
273
+ });
274
+
275
+ it('active projects appear in Active table', () => {
276
+ createProjectManually(tmpDir, 'proj-a', '# Project State\n\nPhase: 2\nStatus: Active\n');
277
+ regenerateProjectsMd(tmpDir);
278
+ const content = fs.readFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), 'utf-8');
279
+ // Find active section and check for project row
280
+ const activeSection = content.split('## Completed')[0];
281
+ assert.ok(activeSection.includes('proj-a'));
282
+ });
283
+
284
+ it('completed projects appear in Completed table', () => {
285
+ createProjectManually(tmpDir, 'proj-done', '# Project State\n\nStatus: completed\nCompleted: 2026-02-20\n');
286
+ regenerateProjectsMd(tmpDir);
287
+ const content = fs.readFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), 'utf-8');
288
+ const completedSection = content.split('## Completed')[1];
289
+ assert.ok(completedSection.includes('proj-done'));
290
+ });
291
+
292
+ it('ghost projects emit warning and are omitted', () => {
293
+ // Create directory but NO STATE.md
294
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'projects', 'ghost-project'), { recursive: true });
295
+ // getProjectFolders requires STATE.md, so ghost won't even be found
296
+ // But let's also test a project whose STATE.md is unreadable
297
+ createProjectManually(tmpDir, 'real-project', '# State\n\nStatus: Active\n');
298
+ const result = regenerateProjectsMd(tmpDir);
299
+ assert.ok(Array.isArray(result.warnings));
300
+ // ghost-project won't appear in projects because getProjectFolders filters on STATE.md
301
+ assert.ok(!result.projects.some(p => p.name === 'ghost-project'));
302
+ });
303
+
304
+ it('returns warnings array', () => {
305
+ createProjectManually(tmpDir, 'proj-a', '# State\n\nStatus: Active\n');
306
+ const result = regenerateProjectsMd(tmpDir);
307
+ assert.ok(Array.isArray(result.warnings));
308
+ });
309
+
310
+ it('empty project list produces tables with headers but no data rows', () => {
311
+ regenerateProjectsMd(tmpDir);
312
+ const content = fs.readFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), 'utf-8');
313
+ assert.ok(content.includes('# Projects'));
314
+ assert.ok(content.includes('## Active'));
315
+ assert.ok(content.includes('## Completed'));
316
+ });
317
+
318
+ it('starts with # Projects header (v2 marker)', () => {
319
+ regenerateProjectsMd(tmpDir);
320
+ const content = fs.readFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), 'utf-8');
321
+ assert.ok(content.startsWith('# Projects'));
322
+ });
323
+ });
324
+
325
+ // ─── completeProject ────────────────────────────────────────────────────────
326
+
327
+ describe('completeProject', () => {
328
+ let tmpDir;
329
+
330
+ beforeEach(() => {
331
+ tmpDir = createTempDir();
332
+ setupPlanning(tmpDir);
333
+ });
334
+
335
+ afterEach(() => cleanupDir(tmpDir));
336
+
337
+ it('adds "Status: completed" to STATE.md', () => {
338
+ createProjectManually(tmpDir, 'proj', '# Project State\n\nPhase: 3\nStatus: Active\n');
339
+ completeProject(tmpDir, 'proj');
340
+ const content = fs.readFileSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'STATE.md'), 'utf-8');
341
+ assert.ok(content.includes('Status: completed'));
342
+ });
343
+
344
+ it('adds "Completed: YYYY-MM-DD" to STATE.md', () => {
345
+ createProjectManually(tmpDir, 'proj', '# Project State\n\nStatus: Active\n');
346
+ const result = completeProject(tmpDir, 'proj');
347
+ const content = fs.readFileSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'STATE.md'), 'utf-8');
348
+ assert.ok(content.includes('Completed:'));
349
+ assert.ok(/Completed: \d{4}-\d{2}-\d{2}/.test(content));
350
+ });
351
+
352
+ it('returns success result with completion date', () => {
353
+ createProjectManually(tmpDir, 'proj', '# Project State\n\nStatus: Active\n');
354
+ const result = completeProject(tmpDir, 'proj');
355
+ assert.strictEqual(result.completed, true);
356
+ assert.ok(result.date);
357
+ assert.ok(/^\d{4}-\d{2}-\d{2}$/.test(result.date));
358
+ });
359
+
360
+ it('returns error if project directory does not exist', () => {
361
+ const result = completeProject(tmpDir, 'nonexistent');
362
+ assert.strictEqual(result.completed, false);
363
+ assert.strictEqual(result.error, 'not_found');
364
+ });
365
+
366
+ it('returns error if STATE.md does not exist', () => {
367
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'projects', 'no-state'), { recursive: true });
368
+ const result = completeProject(tmpDir, 'no-state');
369
+ assert.strictEqual(result.completed, false);
370
+ assert.strictEqual(result.error, 'no_state_md');
371
+ });
372
+
373
+ it('returns error if project is already completed', () => {
374
+ createProjectManually(tmpDir, 'proj', '# Project State\n\nStatus: completed\nCompleted: 2026-01-15\n');
375
+ const result = completeProject(tmpDir, 'proj');
376
+ assert.strictEqual(result.completed, false);
377
+ assert.strictEqual(result.error, 'already_completed');
378
+ });
379
+ });
380
+
381
+ // ─── reactivateProject ───────────────────────────────────────────────────────
382
+
383
+ describe('reactivateProject', () => {
384
+ let tmpDir;
385
+
386
+ beforeEach(() => {
387
+ tmpDir = createTempDir();
388
+ setupPlanning(tmpDir);
389
+ });
390
+
391
+ afterEach(() => cleanupDir(tmpDir));
392
+
393
+ it('changes Status from completed back to In progress', () => {
394
+ createProjectManually(tmpDir, 'proj', '# Project State\n\nStatus: completed\nCompleted: 2026-01-15\n');
395
+ reactivateProject(tmpDir, 'proj');
396
+ const content = fs.readFileSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'STATE.md'), 'utf-8');
397
+ assert.ok(content.includes('Status: In progress'));
398
+ });
399
+
400
+ it('removes Completed date line', () => {
401
+ createProjectManually(tmpDir, 'proj', '# Project State\n\nStatus: completed\nCompleted: 2026-01-15\n');
402
+ reactivateProject(tmpDir, 'proj');
403
+ const content = fs.readFileSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'STATE.md'), 'utf-8');
404
+ assert.ok(!content.includes('Completed:'));
405
+ });
406
+
407
+ it('returns success result', () => {
408
+ createProjectManually(tmpDir, 'proj', '# Project State\n\nStatus: completed\nCompleted: 2026-01-15\n');
409
+ const result = reactivateProject(tmpDir, 'proj');
410
+ assert.strictEqual(result.reactivated, true);
411
+ });
412
+
413
+ it('returns error if project directory does not exist', () => {
414
+ const result = reactivateProject(tmpDir, 'nonexistent');
415
+ assert.strictEqual(result.reactivated, false);
416
+ assert.strictEqual(result.error, 'not_found');
417
+ });
418
+
419
+ it('returns error if STATE.md does not exist', () => {
420
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'projects', 'no-state'), { recursive: true });
421
+ const result = reactivateProject(tmpDir, 'no-state');
422
+ assert.strictEqual(result.reactivated, false);
423
+ assert.strictEqual(result.error, 'no_state_md');
424
+ });
425
+
426
+ it('returns error if project is not completed', () => {
427
+ createProjectManually(tmpDir, 'proj', '# Project State\n\nStatus: Active\n');
428
+ const result = reactivateProject(tmpDir, 'proj');
429
+ assert.strictEqual(result.reactivated, false);
430
+ assert.strictEqual(result.error, 'not_completed');
431
+ });
432
+
433
+ it('preserves existing STATE.md content beyond status fields', () => {
434
+ const original = '# Project State\n\n## Current Position\n\nPhase: 5 of 7\nStatus: completed\nCompleted: 2026-01-15\n\n## Decisions\n\n- Some important decision\n';
435
+ createProjectManually(tmpDir, 'proj', original);
436
+ reactivateProject(tmpDir, 'proj');
437
+ const content = fs.readFileSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'STATE.md'), 'utf-8');
438
+ assert.ok(content.includes('# Project State'));
439
+ assert.ok(content.includes('## Decisions'));
440
+ assert.ok(content.includes('Some important decision'));
441
+ assert.ok(content.includes('Status: In progress'));
442
+ });
443
+
444
+ it('does not modify STATE.md when project is not completed', () => {
445
+ const original = '# Project State\n\nStatus: Active\n';
446
+ createProjectManually(tmpDir, 'proj', original);
447
+ reactivateProject(tmpDir, 'proj');
448
+ const content = fs.readFileSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'STATE.md'), 'utf-8');
449
+ assert.strictEqual(content, original);
450
+ });
451
+
452
+ it('handles case-insensitive Status: Completed', () => {
453
+ createProjectManually(tmpDir, 'proj', '# Project State\n\nStatus: Completed\nCompleted: 2026-01-15\n');
454
+ const result = reactivateProject(tmpDir, 'proj');
455
+ assert.strictEqual(result.reactivated, true);
456
+ const content = fs.readFileSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'STATE.md'), 'utf-8');
457
+ assert.ok(content.includes('Status: In progress'));
458
+ });
459
+ });
460
+
461
+ // ─── parseProjectsMd ────────────────────────────────────────────────────────
462
+
463
+ describe('parseProjectsMd', () => {
464
+ let tmpDir;
465
+
466
+ beforeEach(() => {
467
+ tmpDir = createTempDir();
468
+ setupPlanning(tmpDir);
469
+ });
470
+
471
+ afterEach(() => cleanupDir(tmpDir));
472
+
473
+ it('parses Active table rows into structured objects', () => {
474
+ const content = `# Projects
475
+
476
+ ## Active
477
+
478
+ | Project | Status | Repos Touched | Current Phase |
479
+ |---------|--------|---------------|---------------|
480
+ | auth-overhaul | Active | web-app, server | Phase 2 |
481
+
482
+ ## Completed
483
+
484
+ | Project | Completed | Duration |
485
+ |---------|-----------|----------|
486
+ `;
487
+ fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), content);
488
+ const result = parseProjectsMd(tmpDir);
489
+ assert.strictEqual(result.active.length, 1);
490
+ assert.strictEqual(result.active[0].name, 'auth-overhaul');
491
+ assert.strictEqual(result.active[0].status, 'Active');
492
+ assert.strictEqual(result.active[0].repos_touched, 'web-app, server');
493
+ assert.strictEqual(result.active[0].current_phase, 'Phase 2');
494
+ });
495
+
496
+ it('parses Completed table rows into structured objects', () => {
497
+ const content = `# Projects
498
+
499
+ ## Active
500
+
501
+ | Project | Status | Repos Touched | Current Phase |
502
+ |---------|--------|---------------|---------------|
503
+
504
+ ## Completed
505
+
506
+ | Project | Completed | Duration |
507
+ |---------|-----------|----------|
508
+ | old-project | 2026-01-15 | 30 days |
509
+ `;
510
+ fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), content);
511
+ const result = parseProjectsMd(tmpDir);
512
+ assert.strictEqual(result.completed.length, 1);
513
+ assert.strictEqual(result.completed[0].name, 'old-project');
514
+ assert.strictEqual(result.completed[0].completed, '2026-01-15');
515
+ assert.strictEqual(result.completed[0].duration, '30 days');
516
+ });
517
+
518
+ it('returns null if file does not exist', () => {
519
+ const result = parseProjectsMd(tmpDir);
520
+ assert.strictEqual(result, null);
521
+ });
522
+
523
+ it('returns null if file does not start with # Projects', () => {
524
+ fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), '# Not Projects\n');
525
+ const result = parseProjectsMd(tmpDir);
526
+ assert.strictEqual(result, null);
527
+ });
528
+
529
+ it('handles empty tables (headers only)', () => {
530
+ const content = `# Projects
531
+
532
+ ## Active
533
+
534
+ | Project | Status | Repos Touched | Current Phase |
535
+ |---------|--------|---------------|---------------|
536
+
537
+ ## Completed
538
+
539
+ | Project | Completed | Duration |
540
+ |---------|-----------|----------|
541
+ `;
542
+ fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), content);
543
+ const result = parseProjectsMd(tmpDir);
544
+ assert.strictEqual(result.active.length, 0);
545
+ assert.strictEqual(result.completed.length, 0);
546
+ });
547
+ });
548
+
549
+ // ─── Edge Cases and Integration ─────────────────────────────────────────────
550
+
551
+ describe('createProjectSubfolder edge cases', () => {
552
+ let tmpDir;
553
+
554
+ beforeEach(() => {
555
+ tmpDir = createTempDir();
556
+ setupPlanning(tmpDir);
557
+ });
558
+
559
+ afterEach(() => cleanupDir(tmpDir));
560
+
561
+ it('includes initial_repos in PROJECT.md when provided', () => {
562
+ createProjectSubfolder(tmpDir, 'my-proj', 'My Project', {
563
+ initial_repos: 'web-app, server, shared-lib',
564
+ });
565
+ const content = fs.readFileSync(
566
+ path.join(tmpDir, '.planning', 'projects', 'my-proj', 'PROJECT.md'),
567
+ 'utf-8'
568
+ );
569
+ assert.ok(content.includes('## Initial Repos'));
570
+ assert.ok(content.includes('web-app, server, shared-lib'));
571
+ });
572
+
573
+ it('STATE.md has 0% progress bar initially', () => {
574
+ createProjectSubfolder(tmpDir, 'my-proj', 'My Project');
575
+ const content = fs.readFileSync(
576
+ path.join(tmpDir, '.planning', 'projects', 'my-proj', 'STATE.md'),
577
+ 'utf-8'
578
+ );
579
+ assert.ok(content.includes('0%'));
580
+ });
581
+ });
582
+
583
+ describe('readProjectState edge cases', () => {
584
+ let tmpDir;
585
+
586
+ beforeEach(() => {
587
+ tmpDir = createTempDir();
588
+ setupPlanning(tmpDir);
589
+ });
590
+
591
+ afterEach(() => cleanupDir(tmpDir));
592
+
593
+ it('extracts multi-part Phase field', () => {
594
+ createProjectManually(
595
+ tmpDir,
596
+ 'proj',
597
+ '# State\n\nPhase: 3 of 7 (Project Lifecycle)\nStatus: In progress\n'
598
+ );
599
+ const state = readProjectState(tmpDir, 'proj');
600
+ assert.strictEqual(state.phase, '3 of 7 (Project Lifecycle)');
601
+ });
602
+
603
+ it('returns progress 0 when no progress bar present', () => {
604
+ createProjectManually(tmpDir, 'proj', '# State\n\nPhase: 1\nStatus: Active\n');
605
+ const state = readProjectState(tmpDir, 'proj');
606
+ assert.strictEqual(state.progress, 0);
607
+ });
608
+
609
+ it('returns null completed_date when not present', () => {
610
+ createProjectManually(tmpDir, 'proj', '# State\n\nStatus: Active\n');
611
+ const state = readProjectState(tmpDir, 'proj');
612
+ assert.strictEqual(state.completed_date, null);
613
+ });
614
+ });
615
+
616
+ describe('scanProjectReposTags edge cases', () => {
617
+ let tmpDir;
618
+
619
+ beforeEach(() => {
620
+ tmpDir = createTempDir();
621
+ setupPlanning(tmpDir);
622
+ });
623
+
624
+ afterEach(() => cleanupDir(tmpDir));
625
+
626
+ it('handles multiple <repos> tags in one plan file', () => {
627
+ createProjectManually(tmpDir, 'proj', '# State\n');
628
+ createPlanFile(
629
+ tmpDir,
630
+ 'proj',
631
+ '01-setup',
632
+ '01-01-PLAN.md',
633
+ '<repos>web-app</repos>\n\nSome content\n\n<repos>server, shared-lib</repos>\n'
634
+ );
635
+ const repos = scanProjectReposTags(tmpDir, 'proj');
636
+ assert.deepStrictEqual(repos, ['server', 'shared-lib', 'web-app']);
637
+ });
638
+
639
+ it('handles whitespace in repo names', () => {
640
+ createProjectManually(tmpDir, 'proj', '# State\n');
641
+ createPlanFile(
642
+ tmpDir,
643
+ 'proj',
644
+ '01-setup',
645
+ '01-01-PLAN.md',
646
+ '<repos> web-app , server </repos>\n'
647
+ );
648
+ const repos = scanProjectReposTags(tmpDir, 'proj');
649
+ assert.deepStrictEqual(repos, ['server', 'web-app']);
650
+ });
651
+ });
652
+
653
+ describe('regenerateProjectsMd edge cases', () => {
654
+ let tmpDir;
655
+
656
+ beforeEach(() => {
657
+ tmpDir = createTempDir();
658
+ setupPlanning(tmpDir);
659
+ });
660
+
661
+ afterEach(() => cleanupDir(tmpDir));
662
+
663
+ it('handles mix of active and completed projects', () => {
664
+ createProjectManually(tmpDir, 'active-proj', '# State\n\nPhase: 2\nStatus: In progress\n');
665
+ createProjectManually(tmpDir, 'done-proj', '# State\n\nStatus: completed\nCompleted: 2026-01-15\n');
666
+ const result = regenerateProjectsMd(tmpDir);
667
+ assert.strictEqual(result.projects.length, 2);
668
+ const content = fs.readFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), 'utf-8');
669
+ const activeSection = content.split('## Completed')[0];
670
+ const completedSection = content.split('## Completed')[1];
671
+ assert.ok(activeSection.includes('active-proj'));
672
+ assert.ok(completedSection.includes('done-proj'));
673
+ });
674
+
675
+ it('handles project with no phases directory', () => {
676
+ createProjectManually(tmpDir, 'proj', '# State\n\nStatus: Active\n');
677
+ // No phases dir created
678
+ const result = regenerateProjectsMd(tmpDir);
679
+ assert.strictEqual(result.projects.length, 1);
680
+ assert.strictEqual(result.projects[0].repos_touched, '');
681
+ });
682
+
683
+ it('includes repos touched in Active table', () => {
684
+ createProjectManually(tmpDir, 'proj', '# State\n\nStatus: Active\nPhase: 1\n');
685
+ createPlanFile(tmpDir, 'proj', '01-setup', '01-01-PLAN.md', '<repos>web-app, server</repos>\n');
686
+ regenerateProjectsMd(tmpDir);
687
+ const content = fs.readFileSync(path.join(tmpDir, '.planning', 'PROJECTS.md'), 'utf-8');
688
+ assert.ok(content.includes('server, web-app'));
689
+ });
690
+ });
691
+
692
+ describe('completeProject edge cases', () => {
693
+ let tmpDir;
694
+
695
+ beforeEach(() => {
696
+ tmpDir = createTempDir();
697
+ setupPlanning(tmpDir);
698
+ });
699
+
700
+ afterEach(() => cleanupDir(tmpDir));
701
+
702
+ it('preserves existing STATE.md content', () => {
703
+ const original = '# Project State\n\n## Current Position\n\nPhase: 5 of 7\nStatus: In progress\nProgress: [████████░░] 80%\n\n## Decisions\n\n- Some important decision\n';
704
+ createProjectManually(tmpDir, 'proj', original);
705
+ completeProject(tmpDir, 'proj');
706
+ const content = fs.readFileSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'STATE.md'), 'utf-8');
707
+ assert.ok(content.includes('# Project State'));
708
+ assert.ok(content.includes('## Decisions'));
709
+ assert.ok(content.includes('Some important decision'));
710
+ assert.ok(content.includes('Status: completed'));
711
+ });
712
+
713
+ it('overwrites existing Status line instead of adding duplicate', () => {
714
+ createProjectManually(tmpDir, 'proj', '# State\n\nStatus: Active\n');
715
+ completeProject(tmpDir, 'proj');
716
+ const content = fs.readFileSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'STATE.md'), 'utf-8');
717
+ const statusCount = (content.match(/Status:/g) || []).length;
718
+ assert.strictEqual(statusCount, 1);
719
+ });
720
+
721
+ it('does not modify STATE.md when project is already completed', () => {
722
+ const original = '# Project State\n\nStatus: completed\nCompleted: 2026-01-15\n';
723
+ createProjectManually(tmpDir, 'proj', original);
724
+ completeProject(tmpDir, 'proj');
725
+ const content = fs.readFileSync(path.join(tmpDir, '.planning', 'projects', 'proj', 'STATE.md'), 'utf-8');
726
+ assert.strictEqual(content, original);
727
+ });
728
+
729
+ it('detects already completed status case-insensitively', () => {
730
+ createProjectManually(tmpDir, 'proj', '# Project State\n\nStatus: Completed\n');
731
+ const result = completeProject(tmpDir, 'proj');
732
+ assert.strictEqual(result.completed, false);
733
+ assert.strictEqual(result.error, 'already_completed');
734
+ });
735
+ });
736
+
737
+ describe('roundtrip integration', () => {
738
+ let tmpDir;
739
+
740
+ beforeEach(() => {
741
+ tmpDir = createTempDir();
742
+ setupPlanning(tmpDir);
743
+ });
744
+
745
+ afterEach(() => cleanupDir(tmpDir));
746
+
747
+ it('create -> read -> regenerate -> parse produces consistent data', () => {
748
+ // Create
749
+ createProjectSubfolder(tmpDir, 'test-project', 'Test Project');
750
+
751
+ // Read
752
+ const state = readProjectState(tmpDir, 'test-project');
753
+ assert.ok(state);
754
+ assert.strictEqual(state.status, 'New');
755
+ assert.strictEqual(state.progress, 0);
756
+
757
+ // Regenerate
758
+ const regenResult = regenerateProjectsMd(tmpDir);
759
+ assert.strictEqual(regenResult.projects.length, 1);
760
+ assert.strictEqual(regenResult.projects[0].name, 'test-project');
761
+
762
+ // Parse
763
+ const parsed = parseProjectsMd(tmpDir);
764
+ assert.ok(parsed);
765
+ assert.strictEqual(parsed.active.length, 1);
766
+ assert.strictEqual(parsed.active[0].name, 'test-project');
767
+ assert.strictEqual(parsed.active[0].status, 'New');
768
+ });
769
+
770
+ it('create -> complete -> regenerate -> parse shows completed project', () => {
771
+ createProjectSubfolder(tmpDir, 'done-proj', 'Done Project');
772
+ completeProject(tmpDir, 'done-proj');
773
+ regenerateProjectsMd(tmpDir);
774
+ const parsed = parseProjectsMd(tmpDir);
775
+ assert.strictEqual(parsed.active.length, 0);
776
+ assert.strictEqual(parsed.completed.length, 1);
777
+ assert.strictEqual(parsed.completed[0].name, 'done-proj');
778
+ });
779
+ });
780
+
781
+ // ─── checkSlugPrefixCollision ─────────────────────────────────────────────────
782
+
783
+ describe('checkSlugPrefixCollision', () => {
784
+ it('detects existing slug as prefix of new slug', () => {
785
+ const result = checkSlugPrefixCollision('api-v2-hotfix', ['api-v2', 'dashboard']);
786
+ assert.strictEqual(result.collision, true);
787
+ assert.strictEqual(result.collidingSlug, 'api-v2');
788
+ });
789
+
790
+ it('detects new slug as prefix of existing slug', () => {
791
+ const result = checkSlugPrefixCollision('api-v2', ['api-v2-hotfix', 'dashboard']);
792
+ assert.strictEqual(result.collision, true);
793
+ assert.strictEqual(result.collidingSlug, 'api-v2-hotfix');
794
+ });
795
+
796
+ it('no collision when slugs are unrelated', () => {
797
+ const result = checkSlugPrefixCollision('billing', ['api-v2', 'dashboard']);
798
+ assert.strictEqual(result.collision, false);
799
+ assert.strictEqual(result.collidingSlug, undefined);
800
+ });
801
+
802
+ it('detects short prefix collision', () => {
803
+ const result = checkSlugPrefixCollision('api', ['api-v2']);
804
+ assert.strictEqual(result.collision, true);
805
+ assert.strictEqual(result.collidingSlug, 'api-v2');
806
+ });
807
+
808
+ it('no collision with empty existing slugs', () => {
809
+ const result = checkSlugPrefixCollision('new-project', []);
810
+ assert.strictEqual(result.collision, false);
811
+ });
812
+
813
+ it('exact match is not flagged (handled by createProjectSubfolder)', () => {
814
+ const result = checkSlugPrefixCollision('api-v2', ['api-v2', 'dashboard']);
815
+ assert.strictEqual(result.collision, false);
816
+ });
817
+ });
818
+
819
+ // ─── cmdProjectsSwitch Guards ─────────────────────────────────────────────────
820
+
821
+ describe('cmdProjectsSwitch guards', () => {
822
+ let tmpDir;
823
+
824
+ beforeEach(() => {
825
+ tmpDir = createTempDir();
826
+ setupPlanning(tmpDir);
827
+ });
828
+
829
+ afterEach(() => cleanupDir(tmpDir));
830
+
831
+ it('readProjectState detects completed status for cmdProjectsSwitch guard', () => {
832
+ createProjectManually(tmpDir, 'completed-proj', '# Project State\n\nStatus: completed\nCompleted: 2026-02-20\n');
833
+ const state = readProjectState(tmpDir, 'completed-proj');
834
+ assert.ok(state);
835
+ assert.ok(state.status.toLowerCase().includes('completed'));
836
+ });
837
+
838
+ it('readProjectState detects active status does not trigger guard', () => {
839
+ createProjectManually(tmpDir, 'active-proj', '# Project State\n\nStatus: Active\n');
840
+ const state = readProjectState(tmpDir, 'active-proj');
841
+ assert.ok(state);
842
+ assert.ok(!state.status.toLowerCase().includes('completed'));
843
+ });
844
+
845
+ it('completeProject sets status that triggers the switch guard', () => {
846
+ createProjectManually(tmpDir, 'proj', '# Project State\n\nStatus: Active\n');
847
+ completeProject(tmpDir, 'proj');
848
+ const state = readProjectState(tmpDir, 'proj');
849
+ assert.ok(state);
850
+ assert.ok(state.status.toLowerCase().includes('completed'));
851
+ });
852
+
853
+ it('getProjectRoot throws PROJECT_COMPLETED for completed project (integration)', () => {
854
+ // Set up a v2 fixture with completed project as current_project
855
+ const fixture = createFixture({
856
+ '.planning/config.json': JSON.stringify({ current_project: 'finished-proj' }),
857
+ '.planning/PROJECTS.md': '# Projects\n\n## Active\n\n| Project | Status | Repos Touched | Current Phase |\n|---------|--------|---------------|---------------|\n\n## Completed\n\n| Project | Completed | Duration |\n|---------|-----------|----------|\n| finished-proj | 2026-02-20 | |\n',
858
+ '.planning/REPOS.md': '# Repos\n\n| Name | Path |\n',
859
+ '.planning/projects/finished-proj/STATE.md': '# Project State\n\nStatus: completed\nCompleted: 2026-02-20\n',
860
+ });
861
+
862
+ try {
863
+ assert.throws(
864
+ () => getProjectRoot(fixture.cwd),
865
+ (err) => err.message.includes('PROJECT_COMPLETED')
866
+ );
867
+ } finally {
868
+ fixture.cleanup();
869
+ }
870
+ });
871
+ });