gsd-codex-cli 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/.codex/prompts/gsd-add-phase.md +44 -0
  2. package/.codex/prompts/gsd-add-todo.md +43 -0
  3. package/.codex/prompts/gsd-audit-milestone.md +43 -0
  4. package/.codex/prompts/gsd-check-todos.md +43 -0
  5. package/.codex/prompts/gsd-complete-milestone.md +43 -0
  6. package/.codex/prompts/gsd-debug.md +46 -0
  7. package/.codex/prompts/gsd-discuss-phase.md +43 -0
  8. package/.codex/prompts/gsd-execute-phase.md +43 -0
  9. package/.codex/prompts/gsd-help.md +43 -0
  10. package/.codex/prompts/gsd-insert-phase.md +43 -0
  11. package/.codex/prompts/gsd-list-phase-assumptions.md +43 -0
  12. package/.codex/prompts/gsd-map-codebase.md +43 -0
  13. package/.codex/prompts/gsd-new-milestone.md +43 -0
  14. package/.codex/prompts/gsd-new-project.md +43 -0
  15. package/.codex/prompts/gsd-pause-work.md +43 -0
  16. package/.codex/prompts/gsd-plan-milestone-gaps.md +43 -0
  17. package/.codex/prompts/gsd-plan-phase.md +43 -0
  18. package/.codex/prompts/gsd-progress.md +43 -0
  19. package/.codex/prompts/gsd-quick.md +43 -0
  20. package/.codex/prompts/gsd-remove-phase.md +43 -0
  21. package/.codex/prompts/gsd-research-phase.md +43 -0
  22. package/.codex/prompts/gsd-resume-work.md +43 -0
  23. package/.codex/prompts/gsd-set-profile.md +43 -0
  24. package/.codex/prompts/gsd-settings.md +43 -0
  25. package/.codex/prompts/gsd-update.md +43 -0
  26. package/.codex/prompts/gsd-verify-work.md +43 -0
  27. package/.codex/skills/get-shit-done-codex/SKILL.md +65 -0
  28. package/.codex/skills/get-shit-done-codex/references/compat.md +32 -0
  29. package/.codex/skills/get-shit-done-codex/references/windows.md +23 -0
  30. package/CHANGELOG.md +1434 -0
  31. package/LICENSE +21 -0
  32. package/README.md +690 -0
  33. package/agents/gsd-codebase-mapper.md +761 -0
  34. package/agents/gsd-debugger.md +1198 -0
  35. package/agents/gsd-executor.md +419 -0
  36. package/agents/gsd-integration-checker.md +423 -0
  37. package/agents/gsd-phase-researcher.md +469 -0
  38. package/agents/gsd-plan-checker.md +622 -0
  39. package/agents/gsd-planner.md +1159 -0
  40. package/agents/gsd-project-researcher.md +618 -0
  41. package/agents/gsd-research-synthesizer.md +236 -0
  42. package/agents/gsd-roadmapper.md +639 -0
  43. package/agents/gsd-verifier.md +541 -0
  44. package/bin/install-codex.js +100 -0
  45. package/bin/install.js +1806 -0
  46. package/commands/gsd/add-phase.md +39 -0
  47. package/commands/gsd/add-todo.md +42 -0
  48. package/commands/gsd/audit-milestone.md +42 -0
  49. package/commands/gsd/check-todos.md +41 -0
  50. package/commands/gsd/cleanup.md +18 -0
  51. package/commands/gsd/complete-milestone.md +136 -0
  52. package/commands/gsd/debug.md +162 -0
  53. package/commands/gsd/discuss-phase.md +87 -0
  54. package/commands/gsd/execute-phase.md +42 -0
  55. package/commands/gsd/health.md +22 -0
  56. package/commands/gsd/help.md +22 -0
  57. package/commands/gsd/insert-phase.md +33 -0
  58. package/commands/gsd/join-discord.md +18 -0
  59. package/commands/gsd/list-phase-assumptions.md +50 -0
  60. package/commands/gsd/map-codebase.md +71 -0
  61. package/commands/gsd/new-milestone.md +51 -0
  62. package/commands/gsd/new-project.md +42 -0
  63. package/commands/gsd/pause-work.md +35 -0
  64. package/commands/gsd/plan-milestone-gaps.md +40 -0
  65. package/commands/gsd/plan-phase.md +44 -0
  66. package/commands/gsd/progress.md +24 -0
  67. package/commands/gsd/quick.md +40 -0
  68. package/commands/gsd/reapply-patches.md +110 -0
  69. package/commands/gsd/remove-phase.md +32 -0
  70. package/commands/gsd/research-phase.md +187 -0
  71. package/commands/gsd/resume-work.md +40 -0
  72. package/commands/gsd/set-profile.md +34 -0
  73. package/commands/gsd/settings.md +36 -0
  74. package/commands/gsd/update.md +37 -0
  75. package/commands/gsd/verify-work.md +39 -0
  76. package/get-shit-done/bin/gsd-tools.cjs +5243 -0
  77. package/get-shit-done/bin/gsd-tools.test.cjs +2273 -0
  78. package/get-shit-done/references/checkpoints.md +775 -0
  79. package/get-shit-done/references/continuation-format.md +249 -0
  80. package/get-shit-done/references/decimal-phase-calculation.md +65 -0
  81. package/get-shit-done/references/git-integration.md +248 -0
  82. package/get-shit-done/references/git-planning-commit.md +38 -0
  83. package/get-shit-done/references/model-profile-resolution.md +34 -0
  84. package/get-shit-done/references/model-profiles.md +92 -0
  85. package/get-shit-done/references/phase-argument-parsing.md +61 -0
  86. package/get-shit-done/references/planning-config.md +196 -0
  87. package/get-shit-done/references/questioning.md +145 -0
  88. package/get-shit-done/references/tdd.md +263 -0
  89. package/get-shit-done/references/ui-brand.md +160 -0
  90. package/get-shit-done/references/verification-patterns.md +612 -0
  91. package/get-shit-done/templates/DEBUG.md +159 -0
  92. package/get-shit-done/templates/UAT.md +247 -0
  93. package/get-shit-done/templates/codebase/architecture.md +255 -0
  94. package/get-shit-done/templates/codebase/concerns.md +310 -0
  95. package/get-shit-done/templates/codebase/conventions.md +307 -0
  96. package/get-shit-done/templates/codebase/integrations.md +280 -0
  97. package/get-shit-done/templates/codebase/stack.md +186 -0
  98. package/get-shit-done/templates/codebase/structure.md +285 -0
  99. package/get-shit-done/templates/codebase/testing.md +480 -0
  100. package/get-shit-done/templates/config.json +36 -0
  101. package/get-shit-done/templates/context.md +283 -0
  102. package/get-shit-done/templates/continue-here.md +78 -0
  103. package/get-shit-done/templates/debug-subagent-prompt.md +91 -0
  104. package/get-shit-done/templates/discovery.md +146 -0
  105. package/get-shit-done/templates/milestone-archive.md +123 -0
  106. package/get-shit-done/templates/milestone.md +115 -0
  107. package/get-shit-done/templates/phase-prompt.md +567 -0
  108. package/get-shit-done/templates/planner-subagent-prompt.md +117 -0
  109. package/get-shit-done/templates/project.md +184 -0
  110. package/get-shit-done/templates/requirements.md +231 -0
  111. package/get-shit-done/templates/research-project/ARCHITECTURE.md +204 -0
  112. package/get-shit-done/templates/research-project/FEATURES.md +147 -0
  113. package/get-shit-done/templates/research-project/PITFALLS.md +200 -0
  114. package/get-shit-done/templates/research-project/STACK.md +120 -0
  115. package/get-shit-done/templates/research-project/SUMMARY.md +170 -0
  116. package/get-shit-done/templates/research.md +552 -0
  117. package/get-shit-done/templates/roadmap.md +202 -0
  118. package/get-shit-done/templates/state.md +176 -0
  119. package/get-shit-done/templates/summary-complex.md +59 -0
  120. package/get-shit-done/templates/summary-minimal.md +41 -0
  121. package/get-shit-done/templates/summary-standard.md +48 -0
  122. package/get-shit-done/templates/summary.md +246 -0
  123. package/get-shit-done/templates/user-setup.md +311 -0
  124. package/get-shit-done/templates/verification-report.md +322 -0
  125. package/get-shit-done/workflows/add-phase.md +111 -0
  126. package/get-shit-done/workflows/add-todo.md +157 -0
  127. package/get-shit-done/workflows/audit-milestone.md +242 -0
  128. package/get-shit-done/workflows/check-todos.md +176 -0
  129. package/get-shit-done/workflows/cleanup.md +152 -0
  130. package/get-shit-done/workflows/complete-milestone.md +674 -0
  131. package/get-shit-done/workflows/diagnose-issues.md +219 -0
  132. package/get-shit-done/workflows/discovery-phase.md +289 -0
  133. package/get-shit-done/workflows/discuss-phase.md +485 -0
  134. package/get-shit-done/workflows/execute-phase.md +408 -0
  135. package/get-shit-done/workflows/execute-plan.md +441 -0
  136. package/get-shit-done/workflows/health.md +156 -0
  137. package/get-shit-done/workflows/help.md +486 -0
  138. package/get-shit-done/workflows/insert-phase.md +129 -0
  139. package/get-shit-done/workflows/list-phase-assumptions.md +178 -0
  140. package/get-shit-done/workflows/map-codebase.md +327 -0
  141. package/get-shit-done/workflows/new-milestone.md +373 -0
  142. package/get-shit-done/workflows/new-project.md +1113 -0
  143. package/get-shit-done/workflows/pause-work.md +122 -0
  144. package/get-shit-done/workflows/plan-milestone-gaps.md +256 -0
  145. package/get-shit-done/workflows/plan-phase.md +448 -0
  146. package/get-shit-done/workflows/progress.md +393 -0
  147. package/get-shit-done/workflows/quick.md +444 -0
  148. package/get-shit-done/workflows/remove-phase.md +154 -0
  149. package/get-shit-done/workflows/research-phase.md +74 -0
  150. package/get-shit-done/workflows/resume-project.md +306 -0
  151. package/get-shit-done/workflows/set-profile.md +80 -0
  152. package/get-shit-done/workflows/settings.md +200 -0
  153. package/get-shit-done/workflows/transition.md +539 -0
  154. package/get-shit-done/workflows/update.md +214 -0
  155. package/get-shit-done/workflows/verify-phase.md +242 -0
  156. package/get-shit-done/workflows/verify-work.md +570 -0
  157. package/hooks/dist/gsd-check-update.js +62 -0
  158. package/hooks/dist/gsd-statusline.js +91 -0
  159. package/package.json +54 -0
  160. package/scripts/build-hooks.js +42 -0
@@ -0,0 +1,2273 @@
1
+ /**
2
+ * GSD Tools Tests
3
+ */
4
+
5
+ const { test, describe, beforeEach, afterEach } = require('node:test');
6
+ const assert = require('node:assert');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { execSync } = require('child_process');
10
+
11
+ const TOOLS_PATH = path.join(__dirname, 'gsd-tools.cjs');
12
+
13
+ // Helper to run gsd-tools command
14
+ function runGsdTools(args, cwd = process.cwd()) {
15
+ try {
16
+ const result = execSync(`node "${TOOLS_PATH}" ${args}`, {
17
+ cwd,
18
+ encoding: 'utf-8',
19
+ stdio: ['pipe', 'pipe', 'pipe'],
20
+ });
21
+ return { success: true, output: result.trim() };
22
+ } catch (err) {
23
+ return {
24
+ success: false,
25
+ output: err.stdout?.toString().trim() || '',
26
+ error: err.stderr?.toString().trim() || err.message,
27
+ };
28
+ }
29
+ }
30
+
31
+ // Create temp directory structure
32
+ function createTempProject() {
33
+ const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'gsd-test-'));
34
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true });
35
+ return tmpDir;
36
+ }
37
+
38
+ function cleanup(tmpDir) {
39
+ fs.rmSync(tmpDir, { recursive: true, force: true });
40
+ }
41
+
42
+ describe('history-digest command', () => {
43
+ let tmpDir;
44
+
45
+ beforeEach(() => {
46
+ tmpDir = createTempProject();
47
+ });
48
+
49
+ afterEach(() => {
50
+ cleanup(tmpDir);
51
+ });
52
+
53
+ test('empty phases directory returns valid schema', () => {
54
+ const result = runGsdTools('history-digest', tmpDir);
55
+ assert.ok(result.success, `Command failed: ${result.error}`);
56
+
57
+ const digest = JSON.parse(result.output);
58
+
59
+ assert.deepStrictEqual(digest.phases, {}, 'phases should be empty object');
60
+ assert.deepStrictEqual(digest.decisions, [], 'decisions should be empty array');
61
+ assert.deepStrictEqual(digest.tech_stack, [], 'tech_stack should be empty array');
62
+ });
63
+
64
+ test('nested frontmatter fields extracted correctly', () => {
65
+ // Create phase directory with SUMMARY containing nested frontmatter
66
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
67
+ fs.mkdirSync(phaseDir, { recursive: true });
68
+
69
+ const summaryContent = `---
70
+ phase: "01"
71
+ name: "Foundation Setup"
72
+ dependency-graph:
73
+ provides:
74
+ - "Database schema"
75
+ - "Auth system"
76
+ affects:
77
+ - "API layer"
78
+ tech-stack:
79
+ added:
80
+ - "prisma"
81
+ - "jose"
82
+ patterns-established:
83
+ - "Repository pattern"
84
+ - "JWT auth flow"
85
+ key-decisions:
86
+ - "Use Prisma over Drizzle"
87
+ - "JWT in httpOnly cookies"
88
+ ---
89
+
90
+ # Summary content here
91
+ `;
92
+
93
+ fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), summaryContent);
94
+
95
+ const result = runGsdTools('history-digest', tmpDir);
96
+ assert.ok(result.success, `Command failed: ${result.error}`);
97
+
98
+ const digest = JSON.parse(result.output);
99
+
100
+ // Check nested dependency-graph.provides
101
+ assert.ok(digest.phases['01'], 'Phase 01 should exist');
102
+ assert.deepStrictEqual(
103
+ digest.phases['01'].provides.sort(),
104
+ ['Auth system', 'Database schema'],
105
+ 'provides should contain nested values'
106
+ );
107
+
108
+ // Check nested dependency-graph.affects
109
+ assert.deepStrictEqual(
110
+ digest.phases['01'].affects,
111
+ ['API layer'],
112
+ 'affects should contain nested values'
113
+ );
114
+
115
+ // Check nested tech-stack.added
116
+ assert.deepStrictEqual(
117
+ digest.tech_stack.sort(),
118
+ ['jose', 'prisma'],
119
+ 'tech_stack should contain nested values'
120
+ );
121
+
122
+ // Check patterns-established (flat array)
123
+ assert.deepStrictEqual(
124
+ digest.phases['01'].patterns.sort(),
125
+ ['JWT auth flow', 'Repository pattern'],
126
+ 'patterns should be extracted'
127
+ );
128
+
129
+ // Check key-decisions
130
+ assert.strictEqual(digest.decisions.length, 2, 'Should have 2 decisions');
131
+ assert.ok(
132
+ digest.decisions.some(d => d.decision === 'Use Prisma over Drizzle'),
133
+ 'Should contain first decision'
134
+ );
135
+ });
136
+
137
+ test('multiple phases merged into single digest', () => {
138
+ // Create phase 01
139
+ const phase01Dir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
140
+ fs.mkdirSync(phase01Dir, { recursive: true });
141
+ fs.writeFileSync(
142
+ path.join(phase01Dir, '01-01-SUMMARY.md'),
143
+ `---
144
+ phase: "01"
145
+ name: "Foundation"
146
+ provides:
147
+ - "Database"
148
+ patterns-established:
149
+ - "Pattern A"
150
+ key-decisions:
151
+ - "Decision 1"
152
+ ---
153
+ `
154
+ );
155
+
156
+ // Create phase 02
157
+ const phase02Dir = path.join(tmpDir, '.planning', 'phases', '02-api');
158
+ fs.mkdirSync(phase02Dir, { recursive: true });
159
+ fs.writeFileSync(
160
+ path.join(phase02Dir, '02-01-SUMMARY.md'),
161
+ `---
162
+ phase: "02"
163
+ name: "API"
164
+ provides:
165
+ - "REST endpoints"
166
+ patterns-established:
167
+ - "Pattern B"
168
+ key-decisions:
169
+ - "Decision 2"
170
+ tech-stack:
171
+ added:
172
+ - "zod"
173
+ ---
174
+ `
175
+ );
176
+
177
+ const result = runGsdTools('history-digest', tmpDir);
178
+ assert.ok(result.success, `Command failed: ${result.error}`);
179
+
180
+ const digest = JSON.parse(result.output);
181
+
182
+ // Both phases present
183
+ assert.ok(digest.phases['01'], 'Phase 01 should exist');
184
+ assert.ok(digest.phases['02'], 'Phase 02 should exist');
185
+
186
+ // Decisions merged
187
+ assert.strictEqual(digest.decisions.length, 2, 'Should have 2 decisions total');
188
+
189
+ // Tech stack merged
190
+ assert.deepStrictEqual(digest.tech_stack, ['zod'], 'tech_stack should have zod');
191
+ });
192
+
193
+ test('malformed SUMMARY.md skipped gracefully', () => {
194
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-test');
195
+ fs.mkdirSync(phaseDir, { recursive: true });
196
+
197
+ // Valid summary
198
+ fs.writeFileSync(
199
+ path.join(phaseDir, '01-01-SUMMARY.md'),
200
+ `---
201
+ phase: "01"
202
+ provides:
203
+ - "Valid feature"
204
+ ---
205
+ `
206
+ );
207
+
208
+ // Malformed summary (no frontmatter)
209
+ fs.writeFileSync(
210
+ path.join(phaseDir, '01-02-SUMMARY.md'),
211
+ `# Just a heading
212
+ No frontmatter here
213
+ `
214
+ );
215
+
216
+ // Another malformed summary (broken YAML)
217
+ fs.writeFileSync(
218
+ path.join(phaseDir, '01-03-SUMMARY.md'),
219
+ `---
220
+ broken: [unclosed
221
+ ---
222
+ `
223
+ );
224
+
225
+ const result = runGsdTools('history-digest', tmpDir);
226
+ assert.ok(result.success, `Command should succeed despite malformed files: ${result.error}`);
227
+
228
+ const digest = JSON.parse(result.output);
229
+ assert.ok(digest.phases['01'], 'Phase 01 should exist');
230
+ assert.ok(
231
+ digest.phases['01'].provides.includes('Valid feature'),
232
+ 'Valid feature should be extracted'
233
+ );
234
+ });
235
+
236
+ test('flat provides field still works (backward compatibility)', () => {
237
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-test');
238
+ fs.mkdirSync(phaseDir, { recursive: true });
239
+
240
+ fs.writeFileSync(
241
+ path.join(phaseDir, '01-01-SUMMARY.md'),
242
+ `---
243
+ phase: "01"
244
+ provides:
245
+ - "Direct provides"
246
+ ---
247
+ `
248
+ );
249
+
250
+ const result = runGsdTools('history-digest', tmpDir);
251
+ assert.ok(result.success, `Command failed: ${result.error}`);
252
+
253
+ const digest = JSON.parse(result.output);
254
+ assert.deepStrictEqual(
255
+ digest.phases['01'].provides,
256
+ ['Direct provides'],
257
+ 'Direct provides should work'
258
+ );
259
+ });
260
+
261
+ test('inline array syntax supported', () => {
262
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-test');
263
+ fs.mkdirSync(phaseDir, { recursive: true });
264
+
265
+ fs.writeFileSync(
266
+ path.join(phaseDir, '01-01-SUMMARY.md'),
267
+ `---
268
+ phase: "01"
269
+ provides: [Feature A, Feature B]
270
+ patterns-established: ["Pattern X", "Pattern Y"]
271
+ ---
272
+ `
273
+ );
274
+
275
+ const result = runGsdTools('history-digest', tmpDir);
276
+ assert.ok(result.success, `Command failed: ${result.error}`);
277
+
278
+ const digest = JSON.parse(result.output);
279
+ assert.deepStrictEqual(
280
+ digest.phases['01'].provides.sort(),
281
+ ['Feature A', 'Feature B'],
282
+ 'Inline array should work'
283
+ );
284
+ assert.deepStrictEqual(
285
+ digest.phases['01'].patterns.sort(),
286
+ ['Pattern X', 'Pattern Y'],
287
+ 'Inline quoted array should work'
288
+ );
289
+ });
290
+ });
291
+
292
+ // ─────────────────────────────────────────────────────────────────────────────
293
+ // phases list command
294
+ // ─────────────────────────────────────────────────────────────────────────────
295
+
296
+ describe('phases list command', () => {
297
+ let tmpDir;
298
+
299
+ beforeEach(() => {
300
+ tmpDir = createTempProject();
301
+ });
302
+
303
+ afterEach(() => {
304
+ cleanup(tmpDir);
305
+ });
306
+
307
+ test('empty phases directory returns empty array', () => {
308
+ const result = runGsdTools('phases list', tmpDir);
309
+ assert.ok(result.success, `Command failed: ${result.error}`);
310
+
311
+ const output = JSON.parse(result.output);
312
+ assert.deepStrictEqual(output.directories, [], 'directories should be empty');
313
+ assert.strictEqual(output.count, 0, 'count should be 0');
314
+ });
315
+
316
+ test('lists phase directories sorted numerically', () => {
317
+ // Create out-of-order directories
318
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '10-final'), { recursive: true });
319
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-api'), { recursive: true });
320
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true });
321
+
322
+ const result = runGsdTools('phases list', tmpDir);
323
+ assert.ok(result.success, `Command failed: ${result.error}`);
324
+
325
+ const output = JSON.parse(result.output);
326
+ assert.strictEqual(output.count, 3, 'should have 3 directories');
327
+ assert.deepStrictEqual(
328
+ output.directories,
329
+ ['01-foundation', '02-api', '10-final'],
330
+ 'should be sorted numerically'
331
+ );
332
+ });
333
+
334
+ test('handles decimal phases in sort order', () => {
335
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-api'), { recursive: true });
336
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02.1-hotfix'), { recursive: true });
337
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02.2-patch'), { recursive: true });
338
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-ui'), { recursive: true });
339
+
340
+ const result = runGsdTools('phases list', tmpDir);
341
+ assert.ok(result.success, `Command failed: ${result.error}`);
342
+
343
+ const output = JSON.parse(result.output);
344
+ assert.deepStrictEqual(
345
+ output.directories,
346
+ ['02-api', '02.1-hotfix', '02.2-patch', '03-ui'],
347
+ 'decimal phases should sort correctly between whole numbers'
348
+ );
349
+ });
350
+
351
+ test('--type plans lists only PLAN.md files', () => {
352
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-test');
353
+ fs.mkdirSync(phaseDir, { recursive: true });
354
+ fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan 1');
355
+ fs.writeFileSync(path.join(phaseDir, '01-02-PLAN.md'), '# Plan 2');
356
+ fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Summary');
357
+ fs.writeFileSync(path.join(phaseDir, 'RESEARCH.md'), '# Research');
358
+
359
+ const result = runGsdTools('phases list --type plans', tmpDir);
360
+ assert.ok(result.success, `Command failed: ${result.error}`);
361
+
362
+ const output = JSON.parse(result.output);
363
+ assert.deepStrictEqual(
364
+ output.files.sort(),
365
+ ['01-01-PLAN.md', '01-02-PLAN.md'],
366
+ 'should list only PLAN files'
367
+ );
368
+ });
369
+
370
+ test('--type summaries lists only SUMMARY.md files', () => {
371
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-test');
372
+ fs.mkdirSync(phaseDir, { recursive: true });
373
+ fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan');
374
+ fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Summary 1');
375
+ fs.writeFileSync(path.join(phaseDir, '01-02-SUMMARY.md'), '# Summary 2');
376
+
377
+ const result = runGsdTools('phases list --type summaries', tmpDir);
378
+ assert.ok(result.success, `Command failed: ${result.error}`);
379
+
380
+ const output = JSON.parse(result.output);
381
+ assert.deepStrictEqual(
382
+ output.files.sort(),
383
+ ['01-01-SUMMARY.md', '01-02-SUMMARY.md'],
384
+ 'should list only SUMMARY files'
385
+ );
386
+ });
387
+
388
+ test('--phase filters to specific phase directory', () => {
389
+ const phase01 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
390
+ const phase02 = path.join(tmpDir, '.planning', 'phases', '02-api');
391
+ fs.mkdirSync(phase01, { recursive: true });
392
+ fs.mkdirSync(phase02, { recursive: true });
393
+ fs.writeFileSync(path.join(phase01, '01-01-PLAN.md'), '# Plan');
394
+ fs.writeFileSync(path.join(phase02, '02-01-PLAN.md'), '# Plan');
395
+
396
+ const result = runGsdTools('phases list --type plans --phase 01', tmpDir);
397
+ assert.ok(result.success, `Command failed: ${result.error}`);
398
+
399
+ const output = JSON.parse(result.output);
400
+ assert.deepStrictEqual(output.files, ['01-01-PLAN.md'], 'should only list phase 01 plans');
401
+ assert.strictEqual(output.phase_dir, 'foundation', 'should report phase name without number prefix');
402
+ });
403
+ });
404
+
405
+ // ─────────────────────────────────────────────────────────────────────────────
406
+ // roadmap get-phase command
407
+ // ─────────────────────────────────────────────────────────────────────────────
408
+
409
+ describe('roadmap get-phase command', () => {
410
+ let tmpDir;
411
+
412
+ beforeEach(() => {
413
+ tmpDir = createTempProject();
414
+ });
415
+
416
+ afterEach(() => {
417
+ cleanup(tmpDir);
418
+ });
419
+
420
+ test('extracts phase section from ROADMAP.md', () => {
421
+ fs.writeFileSync(
422
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
423
+ `# Roadmap v1.0
424
+
425
+ ## Phases
426
+
427
+ ### Phase 1: Foundation
428
+ **Goal:** Set up project infrastructure
429
+ **Plans:** 2 plans
430
+
431
+ Some description here.
432
+
433
+ ### Phase 2: API
434
+ **Goal:** Build REST API
435
+ **Plans:** 3 plans
436
+ `
437
+ );
438
+
439
+ const result = runGsdTools('roadmap get-phase 1', tmpDir);
440
+ assert.ok(result.success, `Command failed: ${result.error}`);
441
+
442
+ const output = JSON.parse(result.output);
443
+ assert.strictEqual(output.found, true, 'phase should be found');
444
+ assert.strictEqual(output.phase_number, '1', 'phase number correct');
445
+ assert.strictEqual(output.phase_name, 'Foundation', 'phase name extracted');
446
+ assert.strictEqual(output.goal, 'Set up project infrastructure', 'goal extracted');
447
+ });
448
+
449
+ test('returns not found for missing phase', () => {
450
+ fs.writeFileSync(
451
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
452
+ `# Roadmap v1.0
453
+
454
+ ### Phase 1: Foundation
455
+ **Goal:** Set up project
456
+ `
457
+ );
458
+
459
+ const result = runGsdTools('roadmap get-phase 5', tmpDir);
460
+ assert.ok(result.success, `Command failed: ${result.error}`);
461
+
462
+ const output = JSON.parse(result.output);
463
+ assert.strictEqual(output.found, false, 'phase should not be found');
464
+ });
465
+
466
+ test('handles decimal phase numbers', () => {
467
+ fs.writeFileSync(
468
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
469
+ `# Roadmap
470
+
471
+ ### Phase 2: Main
472
+ **Goal:** Main work
473
+
474
+ ### Phase 2.1: Hotfix
475
+ **Goal:** Emergency fix
476
+ `
477
+ );
478
+
479
+ const result = runGsdTools('roadmap get-phase 2.1', tmpDir);
480
+ assert.ok(result.success, `Command failed: ${result.error}`);
481
+
482
+ const output = JSON.parse(result.output);
483
+ assert.strictEqual(output.found, true, 'decimal phase should be found');
484
+ assert.strictEqual(output.phase_name, 'Hotfix', 'phase name correct');
485
+ assert.strictEqual(output.goal, 'Emergency fix', 'goal extracted');
486
+ });
487
+
488
+ test('extracts full section content', () => {
489
+ fs.writeFileSync(
490
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
491
+ `# Roadmap
492
+
493
+ ### Phase 1: Setup
494
+ **Goal:** Initialize everything
495
+
496
+ This phase covers:
497
+ - Database setup
498
+ - Auth configuration
499
+ - CI/CD pipeline
500
+
501
+ ### Phase 2: Build
502
+ **Goal:** Build features
503
+ `
504
+ );
505
+
506
+ const result = runGsdTools('roadmap get-phase 1', tmpDir);
507
+ assert.ok(result.success, `Command failed: ${result.error}`);
508
+
509
+ const output = JSON.parse(result.output);
510
+ assert.ok(output.section.includes('Database setup'), 'section includes description');
511
+ assert.ok(output.section.includes('CI/CD pipeline'), 'section includes all bullets');
512
+ assert.ok(!output.section.includes('Phase 2'), 'section does not include next phase');
513
+ });
514
+
515
+ test('handles missing ROADMAP.md gracefully', () => {
516
+ const result = runGsdTools('roadmap get-phase 1', tmpDir);
517
+ assert.ok(result.success, `Command failed: ${result.error}`);
518
+
519
+ const output = JSON.parse(result.output);
520
+ assert.strictEqual(output.found, false, 'should return not found');
521
+ assert.strictEqual(output.error, 'ROADMAP.md not found', 'should explain why');
522
+ });
523
+
524
+ test('accepts ## phase headers (two hashes)', () => {
525
+ fs.writeFileSync(
526
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
527
+ `# Roadmap v1.0
528
+
529
+ ## Phase 1: Foundation
530
+ **Goal:** Set up project infrastructure
531
+ **Plans:** 2 plans
532
+
533
+ ## Phase 2: API
534
+ **Goal:** Build REST API
535
+ `
536
+ );
537
+
538
+ const result = runGsdTools('roadmap get-phase 1', tmpDir);
539
+ assert.ok(result.success, `Command failed: ${result.error}`);
540
+
541
+ const output = JSON.parse(result.output);
542
+ assert.strictEqual(output.found, true, 'phase with ## header should be found');
543
+ assert.strictEqual(output.phase_name, 'Foundation', 'phase name extracted');
544
+ assert.strictEqual(output.goal, 'Set up project infrastructure', 'goal extracted');
545
+ });
546
+
547
+ test('detects malformed ROADMAP with summary list but no detail sections', () => {
548
+ fs.writeFileSync(
549
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
550
+ `# Roadmap v1.0
551
+
552
+ ## Phases
553
+
554
+ - [ ] **Phase 1: Foundation** - Set up project
555
+ - [ ] **Phase 2: API** - Build REST API
556
+ `
557
+ );
558
+
559
+ const result = runGsdTools('roadmap get-phase 1', tmpDir);
560
+ assert.ok(result.success, `Command failed: ${result.error}`);
561
+
562
+ const output = JSON.parse(result.output);
563
+ assert.strictEqual(output.found, false, 'phase should not be found');
564
+ assert.strictEqual(output.error, 'malformed_roadmap', 'should identify malformed roadmap');
565
+ assert.ok(output.message.includes('missing'), 'should explain the issue');
566
+ });
567
+ });
568
+
569
+ // ─────────────────────────────────────────────────────────────────────────────
570
+ // phase next-decimal command
571
+ // ─────────────────────────────────────────────────────────────────────────────
572
+
573
+ describe('phase next-decimal command', () => {
574
+ let tmpDir;
575
+
576
+ beforeEach(() => {
577
+ tmpDir = createTempProject();
578
+ });
579
+
580
+ afterEach(() => {
581
+ cleanup(tmpDir);
582
+ });
583
+
584
+ test('returns X.1 when no decimal phases exist', () => {
585
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06-feature'), { recursive: true });
586
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '07-next'), { recursive: true });
587
+
588
+ const result = runGsdTools('phase next-decimal 06', tmpDir);
589
+ assert.ok(result.success, `Command failed: ${result.error}`);
590
+
591
+ const output = JSON.parse(result.output);
592
+ assert.strictEqual(output.next, '06.1', 'should return 06.1');
593
+ assert.deepStrictEqual(output.existing, [], 'no existing decimals');
594
+ });
595
+
596
+ test('increments from existing decimal phases', () => {
597
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06-feature'), { recursive: true });
598
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.1-hotfix'), { recursive: true });
599
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.2-patch'), { recursive: true });
600
+
601
+ const result = runGsdTools('phase next-decimal 06', tmpDir);
602
+ assert.ok(result.success, `Command failed: ${result.error}`);
603
+
604
+ const output = JSON.parse(result.output);
605
+ assert.strictEqual(output.next, '06.3', 'should return 06.3');
606
+ assert.deepStrictEqual(output.existing, ['06.1', '06.2'], 'lists existing decimals');
607
+ });
608
+
609
+ test('handles gaps in decimal sequence', () => {
610
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06-feature'), { recursive: true });
611
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.1-first'), { recursive: true });
612
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.3-third'), { recursive: true });
613
+
614
+ const result = runGsdTools('phase next-decimal 06', tmpDir);
615
+ assert.ok(result.success, `Command failed: ${result.error}`);
616
+
617
+ const output = JSON.parse(result.output);
618
+ // Should take next after highest, not fill gap
619
+ assert.strictEqual(output.next, '06.4', 'should return 06.4, not fill gap at 06.2');
620
+ });
621
+
622
+ test('handles single-digit phase input', () => {
623
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06-feature'), { recursive: true });
624
+
625
+ const result = runGsdTools('phase next-decimal 6', tmpDir);
626
+ assert.ok(result.success, `Command failed: ${result.error}`);
627
+
628
+ const output = JSON.parse(result.output);
629
+ assert.strictEqual(output.next, '06.1', 'should normalize to 06.1');
630
+ assert.strictEqual(output.base_phase, '06', 'base phase should be padded');
631
+ });
632
+
633
+ test('returns error if base phase does not exist', () => {
634
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-start'), { recursive: true });
635
+
636
+ const result = runGsdTools('phase next-decimal 06', tmpDir);
637
+ assert.ok(result.success, `Command should succeed: ${result.error}`);
638
+
639
+ const output = JSON.parse(result.output);
640
+ assert.strictEqual(output.found, false, 'base phase not found');
641
+ assert.strictEqual(output.next, '06.1', 'should still suggest 06.1');
642
+ });
643
+ });
644
+
645
+ // ─────────────────────────────────────────────────────────────────────────────
646
+ // phase-plan-index command
647
+ // ─────────────────────────────────────────────────────────────────────────────
648
+
649
+ describe('phase-plan-index command', () => {
650
+ let tmpDir;
651
+
652
+ beforeEach(() => {
653
+ tmpDir = createTempProject();
654
+ });
655
+
656
+ afterEach(() => {
657
+ cleanup(tmpDir);
658
+ });
659
+
660
+ test('empty phase directory returns empty plans array', () => {
661
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true });
662
+
663
+ const result = runGsdTools('phase-plan-index 03', tmpDir);
664
+ assert.ok(result.success, `Command failed: ${result.error}`);
665
+
666
+ const output = JSON.parse(result.output);
667
+ assert.strictEqual(output.phase, '03', 'phase number correct');
668
+ assert.deepStrictEqual(output.plans, [], 'plans should be empty');
669
+ assert.deepStrictEqual(output.waves, {}, 'waves should be empty');
670
+ assert.deepStrictEqual(output.incomplete, [], 'incomplete should be empty');
671
+ assert.strictEqual(output.has_checkpoints, false, 'no checkpoints');
672
+ });
673
+
674
+ test('extracts single plan with frontmatter', () => {
675
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
676
+ fs.mkdirSync(phaseDir, { recursive: true });
677
+
678
+ fs.writeFileSync(
679
+ path.join(phaseDir, '03-01-PLAN.md'),
680
+ `---
681
+ wave: 1
682
+ autonomous: true
683
+ objective: Set up database schema
684
+ files-modified: [prisma/schema.prisma, src/lib/db.ts]
685
+ ---
686
+
687
+ ## Task 1: Create schema
688
+ ## Task 2: Generate client
689
+ `
690
+ );
691
+
692
+ const result = runGsdTools('phase-plan-index 03', tmpDir);
693
+ assert.ok(result.success, `Command failed: ${result.error}`);
694
+
695
+ const output = JSON.parse(result.output);
696
+ assert.strictEqual(output.plans.length, 1, 'should have 1 plan');
697
+ assert.strictEqual(output.plans[0].id, '03-01', 'plan id correct');
698
+ assert.strictEqual(output.plans[0].wave, 1, 'wave extracted');
699
+ assert.strictEqual(output.plans[0].autonomous, true, 'autonomous extracted');
700
+ assert.strictEqual(output.plans[0].objective, 'Set up database schema', 'objective extracted');
701
+ assert.deepStrictEqual(output.plans[0].files_modified, ['prisma/schema.prisma', 'src/lib/db.ts'], 'files extracted');
702
+ assert.strictEqual(output.plans[0].task_count, 2, 'task count correct');
703
+ assert.strictEqual(output.plans[0].has_summary, false, 'no summary yet');
704
+ });
705
+
706
+ test('groups multiple plans by wave', () => {
707
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
708
+ fs.mkdirSync(phaseDir, { recursive: true });
709
+
710
+ fs.writeFileSync(
711
+ path.join(phaseDir, '03-01-PLAN.md'),
712
+ `---
713
+ wave: 1
714
+ autonomous: true
715
+ objective: Database setup
716
+ ---
717
+
718
+ ## Task 1: Schema
719
+ `
720
+ );
721
+
722
+ fs.writeFileSync(
723
+ path.join(phaseDir, '03-02-PLAN.md'),
724
+ `---
725
+ wave: 1
726
+ autonomous: true
727
+ objective: Auth setup
728
+ ---
729
+
730
+ ## Task 1: JWT
731
+ `
732
+ );
733
+
734
+ fs.writeFileSync(
735
+ path.join(phaseDir, '03-03-PLAN.md'),
736
+ `---
737
+ wave: 2
738
+ autonomous: false
739
+ objective: API routes
740
+ ---
741
+
742
+ ## Task 1: Routes
743
+ `
744
+ );
745
+
746
+ const result = runGsdTools('phase-plan-index 03', tmpDir);
747
+ assert.ok(result.success, `Command failed: ${result.error}`);
748
+
749
+ const output = JSON.parse(result.output);
750
+ assert.strictEqual(output.plans.length, 3, 'should have 3 plans');
751
+ assert.deepStrictEqual(output.waves['1'], ['03-01', '03-02'], 'wave 1 has 2 plans');
752
+ assert.deepStrictEqual(output.waves['2'], ['03-03'], 'wave 2 has 1 plan');
753
+ });
754
+
755
+ test('detects incomplete plans (no matching summary)', () => {
756
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
757
+ fs.mkdirSync(phaseDir, { recursive: true });
758
+
759
+ // Plan with summary
760
+ fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), `---\nwave: 1\n---\n## Task 1`);
761
+ fs.writeFileSync(path.join(phaseDir, '03-01-SUMMARY.md'), `# Summary`);
762
+
763
+ // Plan without summary
764
+ fs.writeFileSync(path.join(phaseDir, '03-02-PLAN.md'), `---\nwave: 2\n---\n## Task 1`);
765
+
766
+ const result = runGsdTools('phase-plan-index 03', tmpDir);
767
+ assert.ok(result.success, `Command failed: ${result.error}`);
768
+
769
+ const output = JSON.parse(result.output);
770
+ assert.strictEqual(output.plans[0].has_summary, true, 'first plan has summary');
771
+ assert.strictEqual(output.plans[1].has_summary, false, 'second plan has no summary');
772
+ assert.deepStrictEqual(output.incomplete, ['03-02'], 'incomplete list correct');
773
+ });
774
+
775
+ test('detects checkpoints (autonomous: false)', () => {
776
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
777
+ fs.mkdirSync(phaseDir, { recursive: true });
778
+
779
+ fs.writeFileSync(
780
+ path.join(phaseDir, '03-01-PLAN.md'),
781
+ `---
782
+ wave: 1
783
+ autonomous: false
784
+ objective: Manual review needed
785
+ ---
786
+
787
+ ## Task 1: Review
788
+ `
789
+ );
790
+
791
+ const result = runGsdTools('phase-plan-index 03', tmpDir);
792
+ assert.ok(result.success, `Command failed: ${result.error}`);
793
+
794
+ const output = JSON.parse(result.output);
795
+ assert.strictEqual(output.has_checkpoints, true, 'should detect checkpoint');
796
+ assert.strictEqual(output.plans[0].autonomous, false, 'plan marked non-autonomous');
797
+ });
798
+
799
+ test('phase not found returns error', () => {
800
+ const result = runGsdTools('phase-plan-index 99', tmpDir);
801
+ assert.ok(result.success, `Command should succeed: ${result.error}`);
802
+
803
+ const output = JSON.parse(result.output);
804
+ assert.strictEqual(output.error, 'Phase not found', 'should report phase not found');
805
+ });
806
+ });
807
+
808
+ // ─────────────────────────────────────────────────────────────────────────────
809
+ // state-snapshot command
810
+ // ─────────────────────────────────────────────────────────────────────────────
811
+
812
+ describe('state-snapshot command', () => {
813
+ let tmpDir;
814
+
815
+ beforeEach(() => {
816
+ tmpDir = createTempProject();
817
+ });
818
+
819
+ afterEach(() => {
820
+ cleanup(tmpDir);
821
+ });
822
+
823
+ test('missing STATE.md returns error', () => {
824
+ const result = runGsdTools('state-snapshot', tmpDir);
825
+ assert.ok(result.success, `Command should succeed: ${result.error}`);
826
+
827
+ const output = JSON.parse(result.output);
828
+ assert.strictEqual(output.error, 'STATE.md not found', 'should report missing file');
829
+ });
830
+
831
+ test('extracts basic fields from STATE.md', () => {
832
+ fs.writeFileSync(
833
+ path.join(tmpDir, '.planning', 'STATE.md'),
834
+ `# Project State
835
+
836
+ **Current Phase:** 03
837
+ **Current Phase Name:** API Layer
838
+ **Total Phases:** 6
839
+ **Current Plan:** 03-02
840
+ **Total Plans in Phase:** 3
841
+ **Status:** In progress
842
+ **Progress:** 45%
843
+ **Last Activity:** 2024-01-15
844
+ **Last Activity Description:** Completed 03-01-PLAN.md
845
+ `
846
+ );
847
+
848
+ const result = runGsdTools('state-snapshot', tmpDir);
849
+ assert.ok(result.success, `Command failed: ${result.error}`);
850
+
851
+ const output = JSON.parse(result.output);
852
+ assert.strictEqual(output.current_phase, '03', 'current phase extracted');
853
+ assert.strictEqual(output.current_phase_name, 'API Layer', 'phase name extracted');
854
+ assert.strictEqual(output.total_phases, 6, 'total phases extracted');
855
+ assert.strictEqual(output.current_plan, '03-02', 'current plan extracted');
856
+ assert.strictEqual(output.total_plans_in_phase, 3, 'total plans extracted');
857
+ assert.strictEqual(output.status, 'In progress', 'status extracted');
858
+ assert.strictEqual(output.progress_percent, 45, 'progress extracted');
859
+ assert.strictEqual(output.last_activity, '2024-01-15', 'last activity date extracted');
860
+ });
861
+
862
+ test('extracts decisions table', () => {
863
+ fs.writeFileSync(
864
+ path.join(tmpDir, '.planning', 'STATE.md'),
865
+ `# Project State
866
+
867
+ **Current Phase:** 01
868
+
869
+ ## Decisions Made
870
+
871
+ | Phase | Decision | Rationale |
872
+ |-------|----------|-----------|
873
+ | 01 | Use Prisma | Better DX than raw SQL |
874
+ | 02 | JWT auth | Stateless authentication |
875
+ `
876
+ );
877
+
878
+ const result = runGsdTools('state-snapshot', tmpDir);
879
+ assert.ok(result.success, `Command failed: ${result.error}`);
880
+
881
+ const output = JSON.parse(result.output);
882
+ assert.strictEqual(output.decisions.length, 2, 'should have 2 decisions');
883
+ assert.strictEqual(output.decisions[0].phase, '01', 'first decision phase');
884
+ assert.strictEqual(output.decisions[0].summary, 'Use Prisma', 'first decision summary');
885
+ assert.strictEqual(output.decisions[0].rationale, 'Better DX than raw SQL', 'first decision rationale');
886
+ });
887
+
888
+ test('extracts blockers list', () => {
889
+ fs.writeFileSync(
890
+ path.join(tmpDir, '.planning', 'STATE.md'),
891
+ `# Project State
892
+
893
+ **Current Phase:** 03
894
+
895
+ ## Blockers
896
+
897
+ - Waiting for API credentials
898
+ - Need design review for dashboard
899
+ `
900
+ );
901
+
902
+ const result = runGsdTools('state-snapshot', tmpDir);
903
+ assert.ok(result.success, `Command failed: ${result.error}`);
904
+
905
+ const output = JSON.parse(result.output);
906
+ assert.deepStrictEqual(output.blockers, [
907
+ 'Waiting for API credentials',
908
+ 'Need design review for dashboard',
909
+ ], 'blockers extracted');
910
+ });
911
+
912
+ test('extracts session continuity info', () => {
913
+ fs.writeFileSync(
914
+ path.join(tmpDir, '.planning', 'STATE.md'),
915
+ `# Project State
916
+
917
+ **Current Phase:** 03
918
+
919
+ ## Session
920
+
921
+ **Last Date:** 2024-01-15
922
+ **Stopped At:** Phase 3, Plan 2, Task 1
923
+ **Resume File:** .planning/phases/03-api/03-02-PLAN.md
924
+ `
925
+ );
926
+
927
+ const result = runGsdTools('state-snapshot', tmpDir);
928
+ assert.ok(result.success, `Command failed: ${result.error}`);
929
+
930
+ const output = JSON.parse(result.output);
931
+ assert.strictEqual(output.session.last_date, '2024-01-15', 'session date extracted');
932
+ assert.strictEqual(output.session.stopped_at, 'Phase 3, Plan 2, Task 1', 'stopped at extracted');
933
+ assert.strictEqual(output.session.resume_file, '.planning/phases/03-api/03-02-PLAN.md', 'resume file extracted');
934
+ });
935
+
936
+ test('handles paused_at field', () => {
937
+ fs.writeFileSync(
938
+ path.join(tmpDir, '.planning', 'STATE.md'),
939
+ `# Project State
940
+
941
+ **Current Phase:** 03
942
+ **Paused At:** Phase 3, Plan 1, Task 2 - mid-implementation
943
+ `
944
+ );
945
+
946
+ const result = runGsdTools('state-snapshot', tmpDir);
947
+ assert.ok(result.success, `Command failed: ${result.error}`);
948
+
949
+ const output = JSON.parse(result.output);
950
+ assert.strictEqual(output.paused_at, 'Phase 3, Plan 1, Task 2 - mid-implementation', 'paused_at extracted');
951
+ });
952
+ });
953
+
954
+ // ─────────────────────────────────────────────────────────────────────────────
955
+ // summary-extract command
956
+ // ─────────────────────────────────────────────────────────────────────────────
957
+
958
+ describe('summary-extract command', () => {
959
+ let tmpDir;
960
+
961
+ beforeEach(() => {
962
+ tmpDir = createTempProject();
963
+ });
964
+
965
+ afterEach(() => {
966
+ cleanup(tmpDir);
967
+ });
968
+
969
+ test('missing file returns error', () => {
970
+ const result = runGsdTools('summary-extract .planning/phases/01-test/01-01-SUMMARY.md', tmpDir);
971
+ assert.ok(result.success, `Command should succeed: ${result.error}`);
972
+
973
+ const output = JSON.parse(result.output);
974
+ assert.strictEqual(output.error, 'File not found', 'should report missing file');
975
+ });
976
+
977
+ test('extracts all fields from SUMMARY.md', () => {
978
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
979
+ fs.mkdirSync(phaseDir, { recursive: true });
980
+
981
+ fs.writeFileSync(
982
+ path.join(phaseDir, '01-01-SUMMARY.md'),
983
+ `---
984
+ one-liner: Set up Prisma with User and Project models
985
+ key-files:
986
+ - prisma/schema.prisma
987
+ - src/lib/db.ts
988
+ tech-stack:
989
+ added:
990
+ - prisma
991
+ - zod
992
+ patterns-established:
993
+ - Repository pattern
994
+ - Dependency injection
995
+ key-decisions:
996
+ - Use Prisma over Drizzle: Better DX and ecosystem
997
+ - Single database: Start simple, shard later
998
+ ---
999
+
1000
+ # Summary
1001
+
1002
+ Full summary content here.
1003
+ `
1004
+ );
1005
+
1006
+ const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md', tmpDir);
1007
+ assert.ok(result.success, `Command failed: ${result.error}`);
1008
+
1009
+ const output = JSON.parse(result.output);
1010
+ assert.strictEqual(output.path, '.planning/phases/01-foundation/01-01-SUMMARY.md', 'path correct');
1011
+ assert.strictEqual(output.one_liner, 'Set up Prisma with User and Project models', 'one-liner extracted');
1012
+ assert.deepStrictEqual(output.key_files, ['prisma/schema.prisma', 'src/lib/db.ts'], 'key files extracted');
1013
+ assert.deepStrictEqual(output.tech_added, ['prisma', 'zod'], 'tech added extracted');
1014
+ assert.deepStrictEqual(output.patterns, ['Repository pattern', 'Dependency injection'], 'patterns extracted');
1015
+ assert.strictEqual(output.decisions.length, 2, 'decisions extracted');
1016
+ });
1017
+
1018
+ test('selective extraction with --fields', () => {
1019
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
1020
+ fs.mkdirSync(phaseDir, { recursive: true });
1021
+
1022
+ fs.writeFileSync(
1023
+ path.join(phaseDir, '01-01-SUMMARY.md'),
1024
+ `---
1025
+ one-liner: Set up database
1026
+ key-files:
1027
+ - prisma/schema.prisma
1028
+ tech-stack:
1029
+ added:
1030
+ - prisma
1031
+ patterns-established:
1032
+ - Repository pattern
1033
+ key-decisions:
1034
+ - Use Prisma: Better DX
1035
+ ---
1036
+ `
1037
+ );
1038
+
1039
+ const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md --fields one_liner,key_files', tmpDir);
1040
+ assert.ok(result.success, `Command failed: ${result.error}`);
1041
+
1042
+ const output = JSON.parse(result.output);
1043
+ assert.strictEqual(output.one_liner, 'Set up database', 'one_liner included');
1044
+ assert.deepStrictEqual(output.key_files, ['prisma/schema.prisma'], 'key_files included');
1045
+ assert.strictEqual(output.tech_added, undefined, 'tech_added excluded');
1046
+ assert.strictEqual(output.patterns, undefined, 'patterns excluded');
1047
+ assert.strictEqual(output.decisions, undefined, 'decisions excluded');
1048
+ });
1049
+
1050
+ test('handles missing frontmatter fields gracefully', () => {
1051
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
1052
+ fs.mkdirSync(phaseDir, { recursive: true });
1053
+
1054
+ fs.writeFileSync(
1055
+ path.join(phaseDir, '01-01-SUMMARY.md'),
1056
+ `---
1057
+ one-liner: Minimal summary
1058
+ ---
1059
+
1060
+ # Summary
1061
+ `
1062
+ );
1063
+
1064
+ const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md', tmpDir);
1065
+ assert.ok(result.success, `Command failed: ${result.error}`);
1066
+
1067
+ const output = JSON.parse(result.output);
1068
+ assert.strictEqual(output.one_liner, 'Minimal summary', 'one-liner extracted');
1069
+ assert.deepStrictEqual(output.key_files, [], 'key_files defaults to empty');
1070
+ assert.deepStrictEqual(output.tech_added, [], 'tech_added defaults to empty');
1071
+ assert.deepStrictEqual(output.patterns, [], 'patterns defaults to empty');
1072
+ assert.deepStrictEqual(output.decisions, [], 'decisions defaults to empty');
1073
+ });
1074
+
1075
+ test('parses key-decisions with rationale', () => {
1076
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
1077
+ fs.mkdirSync(phaseDir, { recursive: true });
1078
+
1079
+ fs.writeFileSync(
1080
+ path.join(phaseDir, '01-01-SUMMARY.md'),
1081
+ `---
1082
+ key-decisions:
1083
+ - Use Prisma: Better DX than alternatives
1084
+ - JWT tokens: Stateless auth for scalability
1085
+ ---
1086
+ `
1087
+ );
1088
+
1089
+ const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md', tmpDir);
1090
+ assert.ok(result.success, `Command failed: ${result.error}`);
1091
+
1092
+ const output = JSON.parse(result.output);
1093
+ assert.strictEqual(output.decisions[0].summary, 'Use Prisma', 'decision summary parsed');
1094
+ assert.strictEqual(output.decisions[0].rationale, 'Better DX than alternatives', 'decision rationale parsed');
1095
+ assert.strictEqual(output.decisions[1].summary, 'JWT tokens', 'second decision summary');
1096
+ assert.strictEqual(output.decisions[1].rationale, 'Stateless auth for scalability', 'second decision rationale');
1097
+ });
1098
+ });
1099
+
1100
+ // ─────────────────────────────────────────────────────────────────────────────
1101
+ // init --include flag tests
1102
+ // ─────────────────────────────────────────────────────────────────────────────
1103
+
1104
+ describe('init commands with --include flag', () => {
1105
+ let tmpDir;
1106
+
1107
+ beforeEach(() => {
1108
+ tmpDir = createTempProject();
1109
+ });
1110
+
1111
+ afterEach(() => {
1112
+ cleanup(tmpDir);
1113
+ });
1114
+
1115
+ test('init execute-phase includes state and config content', () => {
1116
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
1117
+ fs.mkdirSync(phaseDir, { recursive: true });
1118
+ fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
1119
+ fs.writeFileSync(
1120
+ path.join(tmpDir, '.planning', 'STATE.md'),
1121
+ '# State\n\n**Current Phase:** 03\n**Status:** In progress'
1122
+ );
1123
+ fs.writeFileSync(
1124
+ path.join(tmpDir, '.planning', 'config.json'),
1125
+ JSON.stringify({ model_profile: 'balanced' })
1126
+ );
1127
+
1128
+ const result = runGsdTools('init execute-phase 03 --include state,config', tmpDir);
1129
+ assert.ok(result.success, `Command failed: ${result.error}`);
1130
+
1131
+ const output = JSON.parse(result.output);
1132
+ assert.ok(output.state_content, 'state_content should be included');
1133
+ assert.ok(output.state_content.includes('Current Phase'), 'state content correct');
1134
+ assert.ok(output.config_content, 'config_content should be included');
1135
+ assert.ok(output.config_content.includes('model_profile'), 'config content correct');
1136
+ });
1137
+
1138
+ test('init execute-phase without --include omits content', () => {
1139
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
1140
+ fs.mkdirSync(phaseDir, { recursive: true });
1141
+ fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
1142
+ fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State');
1143
+
1144
+ const result = runGsdTools('init execute-phase 03', tmpDir);
1145
+ assert.ok(result.success, `Command failed: ${result.error}`);
1146
+
1147
+ const output = JSON.parse(result.output);
1148
+ assert.strictEqual(output.state_content, undefined, 'state_content should be omitted');
1149
+ assert.strictEqual(output.config_content, undefined, 'config_content should be omitted');
1150
+ });
1151
+
1152
+ test('init plan-phase includes multiple file contents', () => {
1153
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
1154
+ fs.mkdirSync(phaseDir, { recursive: true });
1155
+ fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# Project State');
1156
+ fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap v1.0');
1157
+ fs.writeFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), '# Requirements');
1158
+ fs.writeFileSync(path.join(phaseDir, '03-CONTEXT.md'), '# Phase Context');
1159
+ fs.writeFileSync(path.join(phaseDir, '03-RESEARCH.md'), '# Research Findings');
1160
+
1161
+ const result = runGsdTools('init plan-phase 03 --include state,roadmap,requirements,context,research', tmpDir);
1162
+ assert.ok(result.success, `Command failed: ${result.error}`);
1163
+
1164
+ const output = JSON.parse(result.output);
1165
+ assert.ok(output.state_content, 'state_content included');
1166
+ assert.ok(output.state_content.includes('Project State'), 'state content correct');
1167
+ assert.ok(output.roadmap_content, 'roadmap_content included');
1168
+ assert.ok(output.roadmap_content.includes('Roadmap v1.0'), 'roadmap content correct');
1169
+ assert.ok(output.requirements_content, 'requirements_content included');
1170
+ assert.ok(output.context_content, 'context_content included');
1171
+ assert.ok(output.research_content, 'research_content included');
1172
+ });
1173
+
1174
+ test('init plan-phase includes verification and uat content', () => {
1175
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
1176
+ fs.mkdirSync(phaseDir, { recursive: true });
1177
+ fs.writeFileSync(path.join(phaseDir, '03-VERIFICATION.md'), '# Verification Results');
1178
+ fs.writeFileSync(path.join(phaseDir, '03-UAT.md'), '# UAT Findings');
1179
+
1180
+ const result = runGsdTools('init plan-phase 03 --include verification,uat', tmpDir);
1181
+ assert.ok(result.success, `Command failed: ${result.error}`);
1182
+
1183
+ const output = JSON.parse(result.output);
1184
+ assert.ok(output.verification_content, 'verification_content included');
1185
+ assert.ok(output.verification_content.includes('Verification Results'), 'verification content correct');
1186
+ assert.ok(output.uat_content, 'uat_content included');
1187
+ assert.ok(output.uat_content.includes('UAT Findings'), 'uat content correct');
1188
+ });
1189
+
1190
+ test('init progress includes state, roadmap, project, config', () => {
1191
+ fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State');
1192
+ fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap');
1193
+ fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project');
1194
+ fs.writeFileSync(
1195
+ path.join(tmpDir, '.planning', 'config.json'),
1196
+ JSON.stringify({ model_profile: 'quality' })
1197
+ );
1198
+
1199
+ const result = runGsdTools('init progress --include state,roadmap,project,config', tmpDir);
1200
+ assert.ok(result.success, `Command failed: ${result.error}`);
1201
+
1202
+ const output = JSON.parse(result.output);
1203
+ assert.ok(output.state_content, 'state_content included');
1204
+ assert.ok(output.roadmap_content, 'roadmap_content included');
1205
+ assert.ok(output.project_content, 'project_content included');
1206
+ assert.ok(output.config_content, 'config_content included');
1207
+ });
1208
+
1209
+ test('missing files return null in content fields', () => {
1210
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
1211
+ fs.mkdirSync(phaseDir, { recursive: true });
1212
+ fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
1213
+
1214
+ const result = runGsdTools('init execute-phase 03 --include state,config', tmpDir);
1215
+ assert.ok(result.success, `Command failed: ${result.error}`);
1216
+
1217
+ const output = JSON.parse(result.output);
1218
+ assert.strictEqual(output.state_content, null, 'missing state returns null');
1219
+ assert.strictEqual(output.config_content, null, 'missing config returns null');
1220
+ });
1221
+
1222
+ test('partial includes work correctly', () => {
1223
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
1224
+ fs.mkdirSync(phaseDir, { recursive: true });
1225
+ fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
1226
+ fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State');
1227
+ fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap');
1228
+
1229
+ // Only request state, not roadmap
1230
+ const result = runGsdTools('init execute-phase 03 --include state', tmpDir);
1231
+ assert.ok(result.success, `Command failed: ${result.error}`);
1232
+
1233
+ const output = JSON.parse(result.output);
1234
+ assert.ok(output.state_content, 'state_content included');
1235
+ assert.strictEqual(output.roadmap_content, undefined, 'roadmap_content not requested, should be undefined');
1236
+ });
1237
+ });
1238
+
1239
+ // ─────────────────────────────────────────────────────────────────────────────
1240
+ // roadmap analyze command
1241
+ // ─────────────────────────────────────────────────────────────────────────────
1242
+
1243
+ describe('roadmap analyze command', () => {
1244
+ let tmpDir;
1245
+
1246
+ beforeEach(() => {
1247
+ tmpDir = createTempProject();
1248
+ });
1249
+
1250
+ afterEach(() => {
1251
+ cleanup(tmpDir);
1252
+ });
1253
+
1254
+ test('missing ROADMAP.md returns error', () => {
1255
+ const result = runGsdTools('roadmap analyze', tmpDir);
1256
+ assert.ok(result.success, `Command should succeed: ${result.error}`);
1257
+
1258
+ const output = JSON.parse(result.output);
1259
+ assert.strictEqual(output.error, 'ROADMAP.md not found');
1260
+ });
1261
+
1262
+ test('parses phases with goals and disk status', () => {
1263
+ fs.writeFileSync(
1264
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
1265
+ `# Roadmap v1.0
1266
+
1267
+ ### Phase 1: Foundation
1268
+ **Goal:** Set up infrastructure
1269
+
1270
+ ### Phase 2: Authentication
1271
+ **Goal:** Add user auth
1272
+
1273
+ ### Phase 3: Features
1274
+ **Goal:** Build core features
1275
+ `
1276
+ );
1277
+
1278
+ // Create phase dirs with varying completion
1279
+ const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
1280
+ fs.mkdirSync(p1, { recursive: true });
1281
+ fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
1282
+ fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
1283
+
1284
+ const p2 = path.join(tmpDir, '.planning', 'phases', '02-authentication');
1285
+ fs.mkdirSync(p2, { recursive: true });
1286
+ fs.writeFileSync(path.join(p2, '02-01-PLAN.md'), '# Plan');
1287
+
1288
+ const result = runGsdTools('roadmap analyze', tmpDir);
1289
+ assert.ok(result.success, `Command failed: ${result.error}`);
1290
+
1291
+ const output = JSON.parse(result.output);
1292
+ assert.strictEqual(output.phase_count, 3, 'should find 3 phases');
1293
+ assert.strictEqual(output.phases[0].disk_status, 'complete', 'phase 1 complete');
1294
+ assert.strictEqual(output.phases[1].disk_status, 'planned', 'phase 2 planned');
1295
+ assert.strictEqual(output.phases[2].disk_status, 'no_directory', 'phase 3 no directory');
1296
+ assert.strictEqual(output.completed_phases, 1, '1 phase complete');
1297
+ assert.strictEqual(output.total_plans, 2, '2 total plans');
1298
+ assert.strictEqual(output.total_summaries, 1, '1 total summary');
1299
+ assert.strictEqual(output.progress_percent, 50, '50% complete');
1300
+ assert.strictEqual(output.current_phase, '2', 'current phase is 2');
1301
+ });
1302
+
1303
+ test('extracts goals and dependencies', () => {
1304
+ fs.writeFileSync(
1305
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
1306
+ `# Roadmap
1307
+
1308
+ ### Phase 1: Setup
1309
+ **Goal:** Initialize project
1310
+ **Depends on:** Nothing
1311
+
1312
+ ### Phase 2: Build
1313
+ **Goal:** Build features
1314
+ **Depends on:** Phase 1
1315
+ `
1316
+ );
1317
+
1318
+ const result = runGsdTools('roadmap analyze', tmpDir);
1319
+ assert.ok(result.success, `Command failed: ${result.error}`);
1320
+
1321
+ const output = JSON.parse(result.output);
1322
+ assert.strictEqual(output.phases[0].goal, 'Initialize project');
1323
+ assert.strictEqual(output.phases[0].depends_on, 'Nothing');
1324
+ assert.strictEqual(output.phases[1].goal, 'Build features');
1325
+ assert.strictEqual(output.phases[1].depends_on, 'Phase 1');
1326
+ });
1327
+ });
1328
+
1329
+ // ─────────────────────────────────────────────────────────────────────────────
1330
+ // phase add command
1331
+ // ─────────────────────────────────────────────────────────────────────────────
1332
+
1333
+ describe('phase add command', () => {
1334
+ let tmpDir;
1335
+
1336
+ beforeEach(() => {
1337
+ tmpDir = createTempProject();
1338
+ });
1339
+
1340
+ afterEach(() => {
1341
+ cleanup(tmpDir);
1342
+ });
1343
+
1344
+ test('adds phase after highest existing', () => {
1345
+ fs.writeFileSync(
1346
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
1347
+ `# Roadmap v1.0
1348
+
1349
+ ### Phase 1: Foundation
1350
+ **Goal:** Setup
1351
+
1352
+ ### Phase 2: API
1353
+ **Goal:** Build API
1354
+
1355
+ ---
1356
+ `
1357
+ );
1358
+
1359
+ const result = runGsdTools('phase add User Dashboard', tmpDir);
1360
+ assert.ok(result.success, `Command failed: ${result.error}`);
1361
+
1362
+ const output = JSON.parse(result.output);
1363
+ assert.strictEqual(output.phase_number, 3, 'should be phase 3');
1364
+ assert.strictEqual(output.slug, 'user-dashboard');
1365
+
1366
+ // Verify directory created
1367
+ assert.ok(
1368
+ fs.existsSync(path.join(tmpDir, '.planning', 'phases', '03-user-dashboard')),
1369
+ 'directory should be created'
1370
+ );
1371
+
1372
+ // Verify ROADMAP updated
1373
+ const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
1374
+ assert.ok(roadmap.includes('### Phase 3: User Dashboard'), 'roadmap should include new phase');
1375
+ assert.ok(roadmap.includes('**Depends on:** Phase 2'), 'should depend on previous');
1376
+ });
1377
+
1378
+ test('handles empty roadmap', () => {
1379
+ fs.writeFileSync(
1380
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
1381
+ `# Roadmap v1.0\n`
1382
+ );
1383
+
1384
+ const result = runGsdTools('phase add Initial Setup', tmpDir);
1385
+ assert.ok(result.success, `Command failed: ${result.error}`);
1386
+
1387
+ const output = JSON.parse(result.output);
1388
+ assert.strictEqual(output.phase_number, 1, 'should be phase 1');
1389
+ });
1390
+ });
1391
+
1392
+ // ─────────────────────────────────────────────────────────────────────────────
1393
+ // phase insert command
1394
+ // ─────────────────────────────────────────────────────────────────────────────
1395
+
1396
+ describe('phase insert command', () => {
1397
+ let tmpDir;
1398
+
1399
+ beforeEach(() => {
1400
+ tmpDir = createTempProject();
1401
+ });
1402
+
1403
+ afterEach(() => {
1404
+ cleanup(tmpDir);
1405
+ });
1406
+
1407
+ test('inserts decimal phase after target', () => {
1408
+ fs.writeFileSync(
1409
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
1410
+ `# Roadmap
1411
+
1412
+ ### Phase 1: Foundation
1413
+ **Goal:** Setup
1414
+
1415
+ ### Phase 2: API
1416
+ **Goal:** Build API
1417
+ `
1418
+ );
1419
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true });
1420
+
1421
+ const result = runGsdTools('phase insert 1 Fix Critical Bug', tmpDir);
1422
+ assert.ok(result.success, `Command failed: ${result.error}`);
1423
+
1424
+ const output = JSON.parse(result.output);
1425
+ assert.strictEqual(output.phase_number, '01.1', 'should be 01.1');
1426
+ assert.strictEqual(output.after_phase, '1');
1427
+
1428
+ // Verify directory
1429
+ assert.ok(
1430
+ fs.existsSync(path.join(tmpDir, '.planning', 'phases', '01.1-fix-critical-bug')),
1431
+ 'decimal phase directory should be created'
1432
+ );
1433
+
1434
+ // Verify ROADMAP
1435
+ const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
1436
+ assert.ok(roadmap.includes('Phase 01.1: Fix Critical Bug (INSERTED)'), 'roadmap should include inserted phase');
1437
+ });
1438
+
1439
+ test('increments decimal when siblings exist', () => {
1440
+ fs.writeFileSync(
1441
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
1442
+ `# Roadmap
1443
+
1444
+ ### Phase 1: Foundation
1445
+ **Goal:** Setup
1446
+
1447
+ ### Phase 2: API
1448
+ **Goal:** Build API
1449
+ `
1450
+ );
1451
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true });
1452
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01.1-hotfix'), { recursive: true });
1453
+
1454
+ const result = runGsdTools('phase insert 1 Another Fix', tmpDir);
1455
+ assert.ok(result.success, `Command failed: ${result.error}`);
1456
+
1457
+ const output = JSON.parse(result.output);
1458
+ assert.strictEqual(output.phase_number, '01.2', 'should be 01.2');
1459
+ });
1460
+
1461
+ test('rejects missing phase', () => {
1462
+ fs.writeFileSync(
1463
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
1464
+ `# Roadmap\n### Phase 1: Test\n**Goal:** Test\n`
1465
+ );
1466
+
1467
+ const result = runGsdTools('phase insert 99 Fix Something', tmpDir);
1468
+ assert.ok(!result.success, 'should fail for missing phase');
1469
+ assert.ok(result.error.includes('not found'), 'error mentions not found');
1470
+ });
1471
+
1472
+ test('handles padding mismatch between input and roadmap', () => {
1473
+ fs.writeFileSync(
1474
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
1475
+ `# Roadmap
1476
+
1477
+ ## Phase 09.05: Existing Decimal Phase
1478
+ **Goal:** Test padding
1479
+
1480
+ ## Phase 09.1: Next Phase
1481
+ **Goal:** Test
1482
+ `
1483
+ );
1484
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '09.05-existing'), { recursive: true });
1485
+
1486
+ // Pass unpadded "9.05" but roadmap has "09.05"
1487
+ const result = runGsdTools('phase insert 9.05 Padding Test', tmpDir);
1488
+ assert.ok(result.success, `Command failed: ${result.error}`);
1489
+
1490
+ const output = JSON.parse(result.output);
1491
+ assert.strictEqual(output.after_phase, '9.05');
1492
+
1493
+ const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
1494
+ assert.ok(roadmap.includes('(INSERTED)'), 'roadmap should include inserted phase');
1495
+ });
1496
+
1497
+ test('handles #### heading depth from multi-milestone roadmaps', () => {
1498
+ fs.writeFileSync(
1499
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
1500
+ `# Roadmap
1501
+
1502
+ ### v1.1 Milestone
1503
+
1504
+ #### Phase 5: Feature Work
1505
+ **Goal:** Build features
1506
+
1507
+ #### Phase 6: Polish
1508
+ **Goal:** Polish
1509
+ `
1510
+ );
1511
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '05-feature-work'), { recursive: true });
1512
+
1513
+ const result = runGsdTools('phase insert 5 Hotfix', tmpDir);
1514
+ assert.ok(result.success, `Command failed: ${result.error}`);
1515
+
1516
+ const output = JSON.parse(result.output);
1517
+ assert.strictEqual(output.phase_number, '05.1');
1518
+
1519
+ const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
1520
+ assert.ok(roadmap.includes('Phase 05.1: Hotfix (INSERTED)'), 'roadmap should include inserted phase');
1521
+ });
1522
+ });
1523
+
1524
+ // ─────────────────────────────────────────────────────────────────────────────
1525
+ // phase remove command
1526
+ // ─────────────────────────────────────────────────────────────────────────────
1527
+
1528
+ describe('phase remove command', () => {
1529
+ let tmpDir;
1530
+
1531
+ beforeEach(() => {
1532
+ tmpDir = createTempProject();
1533
+ });
1534
+
1535
+ afterEach(() => {
1536
+ cleanup(tmpDir);
1537
+ });
1538
+
1539
+ test('removes phase directory and renumbers subsequent', () => {
1540
+ // Setup 3 phases
1541
+ fs.writeFileSync(
1542
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
1543
+ `# Roadmap
1544
+
1545
+ ### Phase 1: Foundation
1546
+ **Goal:** Setup
1547
+ **Depends on:** Nothing
1548
+
1549
+ ### Phase 2: Auth
1550
+ **Goal:** Authentication
1551
+ **Depends on:** Phase 1
1552
+
1553
+ ### Phase 3: Features
1554
+ **Goal:** Core features
1555
+ **Depends on:** Phase 2
1556
+ `
1557
+ );
1558
+
1559
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true });
1560
+ const p2 = path.join(tmpDir, '.planning', 'phases', '02-auth');
1561
+ fs.mkdirSync(p2, { recursive: true });
1562
+ fs.writeFileSync(path.join(p2, '02-01-PLAN.md'), '# Plan');
1563
+ const p3 = path.join(tmpDir, '.planning', 'phases', '03-features');
1564
+ fs.mkdirSync(p3, { recursive: true });
1565
+ fs.writeFileSync(path.join(p3, '03-01-PLAN.md'), '# Plan');
1566
+ fs.writeFileSync(path.join(p3, '03-02-PLAN.md'), '# Plan 2');
1567
+
1568
+ // Remove phase 2
1569
+ const result = runGsdTools('phase remove 2', tmpDir);
1570
+ assert.ok(result.success, `Command failed: ${result.error}`);
1571
+
1572
+ const output = JSON.parse(result.output);
1573
+ assert.strictEqual(output.removed, '2');
1574
+ assert.strictEqual(output.directory_deleted, '02-auth');
1575
+
1576
+ // Phase 3 should be renumbered to 02
1577
+ assert.ok(
1578
+ fs.existsSync(path.join(tmpDir, '.planning', 'phases', '02-features')),
1579
+ 'phase 3 should be renumbered to 02-features'
1580
+ );
1581
+ assert.ok(
1582
+ !fs.existsSync(path.join(tmpDir, '.planning', 'phases', '03-features')),
1583
+ 'old 03-features should not exist'
1584
+ );
1585
+
1586
+ // Files inside should be renamed
1587
+ assert.ok(
1588
+ fs.existsSync(path.join(tmpDir, '.planning', 'phases', '02-features', '02-01-PLAN.md')),
1589
+ 'plan file should be renumbered to 02-01'
1590
+ );
1591
+ assert.ok(
1592
+ fs.existsSync(path.join(tmpDir, '.planning', 'phases', '02-features', '02-02-PLAN.md')),
1593
+ 'plan 2 should be renumbered to 02-02'
1594
+ );
1595
+
1596
+ // ROADMAP should be updated
1597
+ const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
1598
+ assert.ok(!roadmap.includes('Phase 2: Auth'), 'removed phase should not be in roadmap');
1599
+ assert.ok(roadmap.includes('Phase 2: Features'), 'phase 3 should be renumbered to 2');
1600
+ });
1601
+
1602
+ test('rejects removal of phase with summaries unless --force', () => {
1603
+ const p1 = path.join(tmpDir, '.planning', 'phases', '01-test');
1604
+ fs.mkdirSync(p1, { recursive: true });
1605
+ fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
1606
+ fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
1607
+ fs.writeFileSync(
1608
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
1609
+ `# Roadmap\n### Phase 1: Test\n**Goal:** Test\n`
1610
+ );
1611
+
1612
+ // Should fail without --force
1613
+ const result = runGsdTools('phase remove 1', tmpDir);
1614
+ assert.ok(!result.success, 'should fail without --force');
1615
+ assert.ok(result.error.includes('executed plan'), 'error mentions executed plans');
1616
+
1617
+ // Should succeed with --force
1618
+ const forceResult = runGsdTools('phase remove 1 --force', tmpDir);
1619
+ assert.ok(forceResult.success, `Force remove failed: ${forceResult.error}`);
1620
+ });
1621
+
1622
+ test('removes decimal phase and renumbers siblings', () => {
1623
+ fs.writeFileSync(
1624
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
1625
+ `# Roadmap\n### Phase 6: Main\n**Goal:** Main\n### Phase 6.1: Fix A\n**Goal:** Fix A\n### Phase 6.2: Fix B\n**Goal:** Fix B\n### Phase 6.3: Fix C\n**Goal:** Fix C\n`
1626
+ );
1627
+
1628
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06-main'), { recursive: true });
1629
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.1-fix-a'), { recursive: true });
1630
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.2-fix-b'), { recursive: true });
1631
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.3-fix-c'), { recursive: true });
1632
+
1633
+ const result = runGsdTools('phase remove 6.2', tmpDir);
1634
+ assert.ok(result.success, `Command failed: ${result.error}`);
1635
+
1636
+ // 06.3 should become 06.2
1637
+ assert.ok(
1638
+ fs.existsSync(path.join(tmpDir, '.planning', 'phases', '06.2-fix-c')),
1639
+ '06.3 should be renumbered to 06.2'
1640
+ );
1641
+ assert.ok(
1642
+ !fs.existsSync(path.join(tmpDir, '.planning', 'phases', '06.3-fix-c')),
1643
+ 'old 06.3 should not exist'
1644
+ );
1645
+ });
1646
+
1647
+ test('updates STATE.md phase count', () => {
1648
+ fs.writeFileSync(
1649
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
1650
+ `# Roadmap\n### Phase 1: A\n**Goal:** A\n### Phase 2: B\n**Goal:** B\n`
1651
+ );
1652
+ fs.writeFileSync(
1653
+ path.join(tmpDir, '.planning', 'STATE.md'),
1654
+ `# State\n\n**Current Phase:** 1\n**Total Phases:** 2\n`
1655
+ );
1656
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
1657
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-b'), { recursive: true });
1658
+
1659
+ runGsdTools('phase remove 2', tmpDir);
1660
+
1661
+ const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
1662
+ assert.ok(state.includes('**Total Phases:** 1'), 'total phases should be decremented');
1663
+ });
1664
+ });
1665
+
1666
+ // ─────────────────────────────────────────────────────────────────────────────
1667
+ // phase complete command
1668
+ // ─────────────────────────────────────────────────────────────────────────────
1669
+
1670
+ describe('phase complete command', () => {
1671
+ let tmpDir;
1672
+
1673
+ beforeEach(() => {
1674
+ tmpDir = createTempProject();
1675
+ });
1676
+
1677
+ afterEach(() => {
1678
+ cleanup(tmpDir);
1679
+ });
1680
+
1681
+ test('marks phase complete and transitions to next', () => {
1682
+ fs.writeFileSync(
1683
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
1684
+ `# Roadmap
1685
+
1686
+ - [ ] Phase 1: Foundation
1687
+ - [ ] Phase 2: API
1688
+
1689
+ ### Phase 1: Foundation
1690
+ **Goal:** Setup
1691
+ **Plans:** 1 plans
1692
+
1693
+ ### Phase 2: API
1694
+ **Goal:** Build API
1695
+ `
1696
+ );
1697
+ fs.writeFileSync(
1698
+ path.join(tmpDir, '.planning', 'STATE.md'),
1699
+ `# State\n\n**Current Phase:** 01\n**Current Phase Name:** Foundation\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working on phase 1\n`
1700
+ );
1701
+
1702
+ const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
1703
+ fs.mkdirSync(p1, { recursive: true });
1704
+ fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
1705
+ fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
1706
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-api'), { recursive: true });
1707
+
1708
+ const result = runGsdTools('phase complete 1', tmpDir);
1709
+ assert.ok(result.success, `Command failed: ${result.error}`);
1710
+
1711
+ const output = JSON.parse(result.output);
1712
+ assert.strictEqual(output.completed_phase, '1');
1713
+ assert.strictEqual(output.plans_executed, '1/1');
1714
+ assert.strictEqual(output.next_phase, '02');
1715
+ assert.strictEqual(output.is_last_phase, false);
1716
+
1717
+ // Verify STATE.md updated
1718
+ const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
1719
+ assert.ok(state.includes('**Current Phase:** 02'), 'should advance to phase 02');
1720
+ assert.ok(state.includes('**Status:** Ready to plan'), 'status should be ready to plan');
1721
+ assert.ok(state.includes('**Current Plan:** Not started'), 'plan should be reset');
1722
+
1723
+ // Verify ROADMAP checkbox
1724
+ const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
1725
+ assert.ok(roadmap.includes('[x]'), 'phase should be checked off');
1726
+ assert.ok(roadmap.includes('completed'), 'completion date should be added');
1727
+ });
1728
+
1729
+ test('detects last phase in milestone', () => {
1730
+ fs.writeFileSync(
1731
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
1732
+ `# Roadmap\n### Phase 1: Only Phase\n**Goal:** Everything\n`
1733
+ );
1734
+ fs.writeFileSync(
1735
+ path.join(tmpDir, '.planning', 'STATE.md'),
1736
+ `# State\n\n**Current Phase:** 01\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
1737
+ );
1738
+
1739
+ const p1 = path.join(tmpDir, '.planning', 'phases', '01-only-phase');
1740
+ fs.mkdirSync(p1, { recursive: true });
1741
+ fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
1742
+ fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
1743
+
1744
+ const result = runGsdTools('phase complete 1', tmpDir);
1745
+ assert.ok(result.success, `Command failed: ${result.error}`);
1746
+
1747
+ const output = JSON.parse(result.output);
1748
+ assert.strictEqual(output.is_last_phase, true, 'should detect last phase');
1749
+ assert.strictEqual(output.next_phase, null, 'no next phase');
1750
+
1751
+ const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
1752
+ assert.ok(state.includes('Milestone complete'), 'status should be milestone complete');
1753
+ });
1754
+
1755
+ test('updates REQUIREMENTS.md traceability when phase completes', () => {
1756
+ fs.writeFileSync(
1757
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
1758
+ `# Roadmap
1759
+
1760
+ - [ ] Phase 1: Auth
1761
+
1762
+ ### Phase 1: Auth
1763
+ **Goal:** User authentication
1764
+ **Requirements:** AUTH-01, AUTH-02
1765
+ **Plans:** 1 plans
1766
+
1767
+ ### Phase 2: API
1768
+ **Goal:** Build API
1769
+ **Requirements:** API-01
1770
+ `
1771
+ );
1772
+ fs.writeFileSync(
1773
+ path.join(tmpDir, '.planning', 'REQUIREMENTS.md'),
1774
+ `# Requirements
1775
+
1776
+ ## v1 Requirements
1777
+
1778
+ ### Authentication
1779
+
1780
+ - [ ] **AUTH-01**: User can sign up with email
1781
+ - [ ] **AUTH-02**: User can log in
1782
+ - [ ] **AUTH-03**: User can reset password
1783
+
1784
+ ### API
1785
+
1786
+ - [ ] **API-01**: REST endpoints
1787
+
1788
+ ## Traceability
1789
+
1790
+ | Requirement | Phase | Status |
1791
+ |-------------|-------|--------|
1792
+ | AUTH-01 | Phase 1 | Pending |
1793
+ | AUTH-02 | Phase 1 | Pending |
1794
+ | AUTH-03 | Phase 2 | Pending |
1795
+ | API-01 | Phase 2 | Pending |
1796
+ `
1797
+ );
1798
+ fs.writeFileSync(
1799
+ path.join(tmpDir, '.planning', 'STATE.md'),
1800
+ `# State\n\n**Current Phase:** 01\n**Current Phase Name:** Auth\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
1801
+ );
1802
+
1803
+ const p1 = path.join(tmpDir, '.planning', 'phases', '01-auth');
1804
+ fs.mkdirSync(p1, { recursive: true });
1805
+ fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
1806
+ fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
1807
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-api'), { recursive: true });
1808
+
1809
+ const result = runGsdTools('phase complete 1', tmpDir);
1810
+ assert.ok(result.success, `Command failed: ${result.error}`);
1811
+
1812
+ const req = fs.readFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), 'utf-8');
1813
+
1814
+ // Checkboxes updated for phase 1 requirements
1815
+ assert.ok(req.includes('- [x] **AUTH-01**'), 'AUTH-01 checkbox should be checked');
1816
+ assert.ok(req.includes('- [x] **AUTH-02**'), 'AUTH-02 checkbox should be checked');
1817
+ // Other requirements unchanged
1818
+ assert.ok(req.includes('- [ ] **AUTH-03**'), 'AUTH-03 should remain unchecked');
1819
+ assert.ok(req.includes('- [ ] **API-01**'), 'API-01 should remain unchecked');
1820
+
1821
+ // Traceability table updated
1822
+ assert.ok(req.includes('| AUTH-01 | Phase 1 | Complete |'), 'AUTH-01 status should be Complete');
1823
+ assert.ok(req.includes('| AUTH-02 | Phase 1 | Complete |'), 'AUTH-02 status should be Complete');
1824
+ assert.ok(req.includes('| AUTH-03 | Phase 2 | Pending |'), 'AUTH-03 should remain Pending');
1825
+ assert.ok(req.includes('| API-01 | Phase 2 | Pending |'), 'API-01 should remain Pending');
1826
+ });
1827
+
1828
+ test('handles phase with no requirements mapping', () => {
1829
+ fs.writeFileSync(
1830
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
1831
+ `# Roadmap
1832
+
1833
+ - [ ] Phase 1: Setup
1834
+
1835
+ ### Phase 1: Setup
1836
+ **Goal:** Project setup (no requirements)
1837
+ **Plans:** 1 plans
1838
+ `
1839
+ );
1840
+ fs.writeFileSync(
1841
+ path.join(tmpDir, '.planning', 'REQUIREMENTS.md'),
1842
+ `# Requirements
1843
+
1844
+ ## v1 Requirements
1845
+
1846
+ - [ ] **REQ-01**: Some requirement
1847
+
1848
+ ## Traceability
1849
+
1850
+ | Requirement | Phase | Status |
1851
+ |-------------|-------|--------|
1852
+ | REQ-01 | Phase 2 | Pending |
1853
+ `
1854
+ );
1855
+ fs.writeFileSync(
1856
+ path.join(tmpDir, '.planning', 'STATE.md'),
1857
+ `# State\n\n**Current Phase:** 01\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
1858
+ );
1859
+
1860
+ const p1 = path.join(tmpDir, '.planning', 'phases', '01-setup');
1861
+ fs.mkdirSync(p1, { recursive: true });
1862
+ fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
1863
+ fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
1864
+
1865
+ const result = runGsdTools('phase complete 1', tmpDir);
1866
+ assert.ok(result.success, `Command failed: ${result.error}`);
1867
+
1868
+ // REQUIREMENTS.md should be unchanged
1869
+ const req = fs.readFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), 'utf-8');
1870
+ assert.ok(req.includes('- [ ] **REQ-01**'), 'REQ-01 should remain unchecked');
1871
+ assert.ok(req.includes('| REQ-01 | Phase 2 | Pending |'), 'REQ-01 should remain Pending');
1872
+ });
1873
+
1874
+ test('handles missing REQUIREMENTS.md gracefully', () => {
1875
+ fs.writeFileSync(
1876
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
1877
+ `# Roadmap
1878
+
1879
+ - [ ] Phase 1: Foundation
1880
+ **Requirements:** REQ-01
1881
+
1882
+ ### Phase 1: Foundation
1883
+ **Goal:** Setup
1884
+ `
1885
+ );
1886
+ fs.writeFileSync(
1887
+ path.join(tmpDir, '.planning', 'STATE.md'),
1888
+ `# State\n\n**Current Phase:** 01\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
1889
+ );
1890
+
1891
+ const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
1892
+ fs.mkdirSync(p1, { recursive: true });
1893
+ fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
1894
+ fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
1895
+
1896
+ const result = runGsdTools('phase complete 1', tmpDir);
1897
+ assert.ok(result.success, `Command should succeed even without REQUIREMENTS.md: ${result.error}`);
1898
+ });
1899
+ });
1900
+
1901
+ // ─────────────────────────────────────────────────────────────────────────────
1902
+ // milestone complete command
1903
+ // ─────────────────────────────────────────────────────────────────────────────
1904
+
1905
+ describe('milestone complete command', () => {
1906
+ let tmpDir;
1907
+
1908
+ beforeEach(() => {
1909
+ tmpDir = createTempProject();
1910
+ });
1911
+
1912
+ afterEach(() => {
1913
+ cleanup(tmpDir);
1914
+ });
1915
+
1916
+ test('archives roadmap, requirements, creates MILESTONES.md', () => {
1917
+ fs.writeFileSync(
1918
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
1919
+ `# Roadmap v1.0 MVP\n\n### Phase 1: Foundation\n**Goal:** Setup\n`
1920
+ );
1921
+ fs.writeFileSync(
1922
+ path.join(tmpDir, '.planning', 'REQUIREMENTS.md'),
1923
+ `# Requirements\n\n- [ ] User auth\n- [ ] Dashboard\n`
1924
+ );
1925
+ fs.writeFileSync(
1926
+ path.join(tmpDir, '.planning', 'STATE.md'),
1927
+ `# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
1928
+ );
1929
+
1930
+ const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
1931
+ fs.mkdirSync(p1, { recursive: true });
1932
+ fs.writeFileSync(
1933
+ path.join(p1, '01-01-SUMMARY.md'),
1934
+ `---\none-liner: Set up project infrastructure\n---\n# Summary\n`
1935
+ );
1936
+
1937
+ const result = runGsdTools('milestone complete v1.0 --name MVP Foundation', tmpDir);
1938
+ assert.ok(result.success, `Command failed: ${result.error}`);
1939
+
1940
+ const output = JSON.parse(result.output);
1941
+ assert.strictEqual(output.version, 'v1.0');
1942
+ assert.strictEqual(output.phases, 1);
1943
+ assert.ok(output.archived.roadmap, 'roadmap should be archived');
1944
+ assert.ok(output.archived.requirements, 'requirements should be archived');
1945
+
1946
+ // Verify archive files exist
1947
+ assert.ok(
1948
+ fs.existsSync(path.join(tmpDir, '.planning', 'milestones', 'v1.0-ROADMAP.md')),
1949
+ 'archived roadmap should exist'
1950
+ );
1951
+ assert.ok(
1952
+ fs.existsSync(path.join(tmpDir, '.planning', 'milestones', 'v1.0-REQUIREMENTS.md')),
1953
+ 'archived requirements should exist'
1954
+ );
1955
+
1956
+ // Verify MILESTONES.md created
1957
+ assert.ok(
1958
+ fs.existsSync(path.join(tmpDir, '.planning', 'MILESTONES.md')),
1959
+ 'MILESTONES.md should be created'
1960
+ );
1961
+ const milestones = fs.readFileSync(path.join(tmpDir, '.planning', 'MILESTONES.md'), 'utf-8');
1962
+ assert.ok(milestones.includes('v1.0 MVP Foundation'), 'milestone entry should contain name');
1963
+ assert.ok(milestones.includes('Set up project infrastructure'), 'accomplishments should be listed');
1964
+ });
1965
+
1966
+ test('appends to existing MILESTONES.md', () => {
1967
+ fs.writeFileSync(
1968
+ path.join(tmpDir, '.planning', 'MILESTONES.md'),
1969
+ `# Milestones\n\n## v0.9 Alpha (Shipped: 2025-01-01)\n\n---\n\n`
1970
+ );
1971
+ fs.writeFileSync(
1972
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
1973
+ `# Roadmap v1.0\n`
1974
+ );
1975
+ fs.writeFileSync(
1976
+ path.join(tmpDir, '.planning', 'STATE.md'),
1977
+ `# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
1978
+ );
1979
+
1980
+ const result = runGsdTools('milestone complete v1.0 --name Beta', tmpDir);
1981
+ assert.ok(result.success, `Command failed: ${result.error}`);
1982
+
1983
+ const milestones = fs.readFileSync(path.join(tmpDir, '.planning', 'MILESTONES.md'), 'utf-8');
1984
+ assert.ok(milestones.includes('v0.9 Alpha'), 'existing entry should be preserved');
1985
+ assert.ok(milestones.includes('v1.0 Beta'), 'new entry should be appended');
1986
+ });
1987
+ });
1988
+
1989
+ // ─────────────────────────────────────────────────────────────────────────────
1990
+ // validate consistency command
1991
+ // ─────────────────────────────────────────────────────────────────────────────
1992
+
1993
+ describe('validate consistency command', () => {
1994
+ let tmpDir;
1995
+
1996
+ beforeEach(() => {
1997
+ tmpDir = createTempProject();
1998
+ });
1999
+
2000
+ afterEach(() => {
2001
+ cleanup(tmpDir);
2002
+ });
2003
+
2004
+ test('passes for consistent project', () => {
2005
+ fs.writeFileSync(
2006
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
2007
+ `# Roadmap\n### Phase 1: A\n### Phase 2: B\n### Phase 3: C\n`
2008
+ );
2009
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
2010
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-b'), { recursive: true });
2011
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-c'), { recursive: true });
2012
+
2013
+ const result = runGsdTools('validate consistency', tmpDir);
2014
+ assert.ok(result.success, `Command failed: ${result.error}`);
2015
+
2016
+ const output = JSON.parse(result.output);
2017
+ assert.strictEqual(output.passed, true, 'should pass');
2018
+ assert.strictEqual(output.warning_count, 0, 'no warnings');
2019
+ });
2020
+
2021
+ test('warns about phase on disk but not in roadmap', () => {
2022
+ fs.writeFileSync(
2023
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
2024
+ `# Roadmap\n### Phase 1: A\n`
2025
+ );
2026
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
2027
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-orphan'), { recursive: true });
2028
+
2029
+ const result = runGsdTools('validate consistency', tmpDir);
2030
+ assert.ok(result.success, `Command failed: ${result.error}`);
2031
+
2032
+ const output = JSON.parse(result.output);
2033
+ assert.ok(output.warning_count > 0, 'should have warnings');
2034
+ assert.ok(
2035
+ output.warnings.some(w => w.includes('disk but not in ROADMAP')),
2036
+ 'should warn about orphan directory'
2037
+ );
2038
+ });
2039
+
2040
+ test('warns about gaps in phase numbering', () => {
2041
+ fs.writeFileSync(
2042
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
2043
+ `# Roadmap\n### Phase 1: A\n### Phase 3: C\n`
2044
+ );
2045
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
2046
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-c'), { recursive: true });
2047
+
2048
+ const result = runGsdTools('validate consistency', tmpDir);
2049
+ assert.ok(result.success, `Command failed: ${result.error}`);
2050
+
2051
+ const output = JSON.parse(result.output);
2052
+ assert.ok(
2053
+ output.warnings.some(w => w.includes('Gap in phase numbering')),
2054
+ 'should warn about gap'
2055
+ );
2056
+ });
2057
+ });
2058
+
2059
+ // ─────────────────────────────────────────────────────────────────────────────
2060
+ // progress command
2061
+ // ─────────────────────────────────────────────────────────────────────────────
2062
+
2063
+ describe('progress command', () => {
2064
+ let tmpDir;
2065
+
2066
+ beforeEach(() => {
2067
+ tmpDir = createTempProject();
2068
+ });
2069
+
2070
+ afterEach(() => {
2071
+ cleanup(tmpDir);
2072
+ });
2073
+
2074
+ test('renders JSON progress', () => {
2075
+ fs.writeFileSync(
2076
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
2077
+ `# Roadmap v1.0 MVP\n`
2078
+ );
2079
+ const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
2080
+ fs.mkdirSync(p1, { recursive: true });
2081
+ fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
2082
+ fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Done');
2083
+ fs.writeFileSync(path.join(p1, '01-02-PLAN.md'), '# Plan 2');
2084
+
2085
+ const result = runGsdTools('progress json', tmpDir);
2086
+ assert.ok(result.success, `Command failed: ${result.error}`);
2087
+
2088
+ const output = JSON.parse(result.output);
2089
+ assert.strictEqual(output.total_plans, 2, '2 total plans');
2090
+ assert.strictEqual(output.total_summaries, 1, '1 summary');
2091
+ assert.strictEqual(output.percent, 50, '50%');
2092
+ assert.strictEqual(output.phases.length, 1, '1 phase');
2093
+ assert.strictEqual(output.phases[0].status, 'In Progress', 'phase in progress');
2094
+ });
2095
+
2096
+ test('renders bar format', () => {
2097
+ fs.writeFileSync(
2098
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
2099
+ `# Roadmap v1.0\n`
2100
+ );
2101
+ const p1 = path.join(tmpDir, '.planning', 'phases', '01-test');
2102
+ fs.mkdirSync(p1, { recursive: true });
2103
+ fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
2104
+ fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Done');
2105
+
2106
+ const result = runGsdTools('progress bar --raw', tmpDir);
2107
+ assert.ok(result.success, `Command failed: ${result.error}`);
2108
+ assert.ok(result.output.includes('1/1'), 'should include count');
2109
+ assert.ok(result.output.includes('100%'), 'should include 100%');
2110
+ });
2111
+
2112
+ test('renders table format', () => {
2113
+ fs.writeFileSync(
2114
+ path.join(tmpDir, '.planning', 'ROADMAP.md'),
2115
+ `# Roadmap v1.0 MVP\n`
2116
+ );
2117
+ const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
2118
+ fs.mkdirSync(p1, { recursive: true });
2119
+ fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
2120
+
2121
+ const result = runGsdTools('progress table --raw', tmpDir);
2122
+ assert.ok(result.success, `Command failed: ${result.error}`);
2123
+ assert.ok(result.output.includes('Phase'), 'should have table header');
2124
+ assert.ok(result.output.includes('foundation'), 'should include phase name');
2125
+ });
2126
+ });
2127
+
2128
+ // ─────────────────────────────────────────────────────────────────────────────
2129
+ // todo complete command
2130
+ // ─────────────────────────────────────────────────────────────────────────────
2131
+
2132
+ describe('todo complete command', () => {
2133
+ let tmpDir;
2134
+
2135
+ beforeEach(() => {
2136
+ tmpDir = createTempProject();
2137
+ });
2138
+
2139
+ afterEach(() => {
2140
+ cleanup(tmpDir);
2141
+ });
2142
+
2143
+ test('moves todo from pending to completed', () => {
2144
+ const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
2145
+ fs.mkdirSync(pendingDir, { recursive: true });
2146
+ fs.writeFileSync(
2147
+ path.join(pendingDir, 'add-dark-mode.md'),
2148
+ `title: Add dark mode\narea: ui\ncreated: 2025-01-01\n`
2149
+ );
2150
+
2151
+ const result = runGsdTools('todo complete add-dark-mode.md', tmpDir);
2152
+ assert.ok(result.success, `Command failed: ${result.error}`);
2153
+
2154
+ const output = JSON.parse(result.output);
2155
+ assert.strictEqual(output.completed, true);
2156
+
2157
+ // Verify moved
2158
+ assert.ok(
2159
+ !fs.existsSync(path.join(tmpDir, '.planning', 'todos', 'pending', 'add-dark-mode.md')),
2160
+ 'should be removed from pending'
2161
+ );
2162
+ assert.ok(
2163
+ fs.existsSync(path.join(tmpDir, '.planning', 'todos', 'completed', 'add-dark-mode.md')),
2164
+ 'should be in completed'
2165
+ );
2166
+
2167
+ // Verify completion timestamp added
2168
+ const content = fs.readFileSync(
2169
+ path.join(tmpDir, '.planning', 'todos', 'completed', 'add-dark-mode.md'),
2170
+ 'utf-8'
2171
+ );
2172
+ assert.ok(content.startsWith('completed:'), 'should have completed timestamp');
2173
+ });
2174
+
2175
+ test('fails for nonexistent todo', () => {
2176
+ const result = runGsdTools('todo complete nonexistent.md', tmpDir);
2177
+ assert.ok(!result.success, 'should fail');
2178
+ assert.ok(result.error.includes('not found'), 'error mentions not found');
2179
+ });
2180
+ });
2181
+
2182
+ // ─────────────────────────────────────────────────────────────────────────────
2183
+ // scaffold command
2184
+ // ─────────────────────────────────────────────────────────────────────────────
2185
+
2186
+ describe('scaffold command', () => {
2187
+ let tmpDir;
2188
+
2189
+ beforeEach(() => {
2190
+ tmpDir = createTempProject();
2191
+ });
2192
+
2193
+ afterEach(() => {
2194
+ cleanup(tmpDir);
2195
+ });
2196
+
2197
+ test('scaffolds context file', () => {
2198
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true });
2199
+
2200
+ const result = runGsdTools('scaffold context --phase 3', tmpDir);
2201
+ assert.ok(result.success, `Command failed: ${result.error}`);
2202
+
2203
+ const output = JSON.parse(result.output);
2204
+ assert.strictEqual(output.created, true);
2205
+
2206
+ // Verify file content
2207
+ const content = fs.readFileSync(
2208
+ path.join(tmpDir, '.planning', 'phases', '03-api', '03-CONTEXT.md'),
2209
+ 'utf-8'
2210
+ );
2211
+ assert.ok(content.includes('Phase 3'), 'should reference phase number');
2212
+ assert.ok(content.includes('Decisions'), 'should have decisions section');
2213
+ assert.ok(content.includes('Discretion Areas'), 'should have discretion section');
2214
+ });
2215
+
2216
+ test('scaffolds UAT file', () => {
2217
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true });
2218
+
2219
+ const result = runGsdTools('scaffold uat --phase 3', tmpDir);
2220
+ assert.ok(result.success, `Command failed: ${result.error}`);
2221
+
2222
+ const output = JSON.parse(result.output);
2223
+ assert.strictEqual(output.created, true);
2224
+
2225
+ const content = fs.readFileSync(
2226
+ path.join(tmpDir, '.planning', 'phases', '03-api', '03-UAT.md'),
2227
+ 'utf-8'
2228
+ );
2229
+ assert.ok(content.includes('User Acceptance Testing'), 'should have UAT heading');
2230
+ assert.ok(content.includes('Test Results'), 'should have test results section');
2231
+ });
2232
+
2233
+ test('scaffolds verification file', () => {
2234
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true });
2235
+
2236
+ const result = runGsdTools('scaffold verification --phase 3', tmpDir);
2237
+ assert.ok(result.success, `Command failed: ${result.error}`);
2238
+
2239
+ const output = JSON.parse(result.output);
2240
+ assert.strictEqual(output.created, true);
2241
+
2242
+ const content = fs.readFileSync(
2243
+ path.join(tmpDir, '.planning', 'phases', '03-api', '03-VERIFICATION.md'),
2244
+ 'utf-8'
2245
+ );
2246
+ assert.ok(content.includes('Goal-Backward Verification'), 'should have verification heading');
2247
+ });
2248
+
2249
+ test('scaffolds phase directory', () => {
2250
+ const result = runGsdTools('scaffold phase-dir --phase 5 --name User Dashboard', tmpDir);
2251
+ assert.ok(result.success, `Command failed: ${result.error}`);
2252
+
2253
+ const output = JSON.parse(result.output);
2254
+ assert.strictEqual(output.created, true);
2255
+ assert.ok(
2256
+ fs.existsSync(path.join(tmpDir, '.planning', 'phases', '05-user-dashboard')),
2257
+ 'directory should be created'
2258
+ );
2259
+ });
2260
+
2261
+ test('does not overwrite existing files', () => {
2262
+ const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
2263
+ fs.mkdirSync(phaseDir, { recursive: true });
2264
+ fs.writeFileSync(path.join(phaseDir, '03-CONTEXT.md'), '# Existing content');
2265
+
2266
+ const result = runGsdTools('scaffold context --phase 3', tmpDir);
2267
+ assert.ok(result.success, `Command failed: ${result.error}`);
2268
+
2269
+ const output = JSON.parse(result.output);
2270
+ assert.strictEqual(output.created, false, 'should not overwrite');
2271
+ assert.strictEqual(output.reason, 'already_exists');
2272
+ });
2273
+ });