gsd-opencode 1.20.4 → 1.22.1

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 (110) hide show
  1. package/agents/gsd-codebase-mapper.md +10 -1
  2. package/agents/gsd-debugger.md +67 -10
  3. package/agents/gsd-executor.md +37 -16
  4. package/agents/gsd-integration-checker.md +3 -0
  5. package/agents/gsd-nyquist-auditor.md +179 -0
  6. package/agents/gsd-phase-researcher.md +29 -34
  7. package/agents/gsd-plan-checker.md +43 -78
  8. package/agents/gsd-planner.md +140 -24
  9. package/agents/gsd-project-researcher.md +12 -1
  10. package/agents/gsd-research-synthesizer.md +14 -3
  11. package/agents/gsd-roadmapper.md +26 -15
  12. package/agents/gsd-verifier.md +30 -6
  13. package/bin/dm/lib/constants.js +6 -1
  14. package/bin/dm/src/services/file-ops.js +14 -1
  15. package/commands/gsd/gsd-add-phase.md +6 -6
  16. package/commands/gsd/gsd-add-tests.md +41 -0
  17. package/commands/gsd/gsd-add-todo.md +7 -7
  18. package/commands/gsd/gsd-audit-milestone.md +9 -9
  19. package/commands/gsd/gsd-check-profile.md +3 -3
  20. package/commands/gsd/gsd-check-todos.md +7 -7
  21. package/commands/gsd/gsd-cleanup.md +2 -2
  22. package/commands/gsd/gsd-complete-milestone.md +6 -6
  23. package/commands/gsd/gsd-debug.md +11 -7
  24. package/commands/gsd/gsd-discuss-phase.md +26 -19
  25. package/commands/gsd/gsd-execute-phase.md +13 -13
  26. package/commands/gsd/gsd-health.md +7 -7
  27. package/commands/gsd/gsd-help.md +2 -2
  28. package/commands/gsd/gsd-insert-phase.md +6 -6
  29. package/commands/gsd/gsd-join-discord.md +1 -1
  30. package/commands/gsd/gsd-list-phase-assumptions.md +6 -6
  31. package/commands/gsd/gsd-map-codebase.md +8 -8
  32. package/commands/gsd/gsd-new-milestone.md +12 -12
  33. package/commands/gsd/gsd-new-project.md +12 -12
  34. package/commands/gsd/gsd-pause-work.md +6 -6
  35. package/commands/gsd/gsd-plan-milestone-gaps.md +9 -9
  36. package/commands/gsd/gsd-plan-phase.md +14 -13
  37. package/commands/gsd/gsd-progress.md +8 -8
  38. package/commands/gsd/gsd-quick.md +17 -13
  39. package/commands/gsd/gsd-reapply-patches.md +20 -9
  40. package/commands/gsd/gsd-remove-phase.md +7 -7
  41. package/commands/gsd/gsd-research-phase.md +12 -11
  42. package/commands/gsd/gsd-resume-work.md +8 -8
  43. package/commands/gsd/gsd-set-profile.md +6 -6
  44. package/commands/gsd/gsd-settings.md +7 -7
  45. package/commands/gsd/gsd-update.md +5 -5
  46. package/commands/gsd/gsd-validate-phase.md +35 -0
  47. package/commands/gsd/gsd-verify-work.md +11 -11
  48. package/get-shit-done/bin/gsd-tools.cjs +45 -6
  49. package/get-shit-done/bin/lib/commands.cjs +11 -19
  50. package/get-shit-done/bin/lib/config.cjs +8 -1
  51. package/get-shit-done/bin/lib/core.cjs +131 -16
  52. package/get-shit-done/bin/lib/init.cjs +28 -12
  53. package/get-shit-done/bin/lib/milestone.cjs +34 -8
  54. package/get-shit-done/bin/lib/phase.cjs +74 -50
  55. package/get-shit-done/bin/lib/roadmap.cjs +7 -7
  56. package/get-shit-done/bin/lib/state.cjs +294 -63
  57. package/get-shit-done/bin/lib/template.cjs +3 -3
  58. package/get-shit-done/bin/lib/verify.cjs +56 -8
  59. package/get-shit-done/references/checkpoints.md +1 -1
  60. package/get-shit-done/references/decimal-phase-calculation.md +6 -6
  61. package/get-shit-done/references/git-integration.md +3 -3
  62. package/get-shit-done/references/git-planning-commit.md +2 -2
  63. package/get-shit-done/references/model-profile-resolution.md +1 -1
  64. package/get-shit-done/references/model-profiles.md +1 -0
  65. package/get-shit-done/references/phase-argument-parsing.md +4 -4
  66. package/get-shit-done/references/planning-config.md +10 -6
  67. package/get-shit-done/references/questioning.md +17 -0
  68. package/get-shit-done/references/verification-patterns.md +1 -1
  69. package/get-shit-done/templates/DEBUG.md +7 -2
  70. package/get-shit-done/templates/VALIDATION.md +18 -46
  71. package/get-shit-done/templates/codebase/structure.md +3 -3
  72. package/get-shit-done/templates/config.json +2 -2
  73. package/get-shit-done/templates/context.md +14 -0
  74. package/get-shit-done/templates/phase-prompt.md +10 -10
  75. package/get-shit-done/templates/retrospective.md +54 -0
  76. package/get-shit-done/templates/roadmap.md +1 -1
  77. package/get-shit-done/workflows/add-phase.md +3 -2
  78. package/get-shit-done/workflows/add-tests.md +351 -0
  79. package/get-shit-done/workflows/add-todo.md +4 -3
  80. package/get-shit-done/workflows/audit-milestone.md +40 -5
  81. package/get-shit-done/workflows/check-todos.md +3 -2
  82. package/get-shit-done/workflows/cleanup.md +1 -1
  83. package/get-shit-done/workflows/complete-milestone.md +69 -5
  84. package/get-shit-done/workflows/diagnose-issues.md +2 -2
  85. package/get-shit-done/workflows/discovery-phase.md +6 -6
  86. package/get-shit-done/workflows/discuss-phase.md +194 -58
  87. package/get-shit-done/workflows/execute-phase.md +29 -23
  88. package/get-shit-done/workflows/execute-plan.md +22 -18
  89. package/get-shit-done/workflows/health.md +5 -2
  90. package/get-shit-done/workflows/help.md +4 -1
  91. package/get-shit-done/workflows/insert-phase.md +3 -2
  92. package/get-shit-done/workflows/map-codebase.md +3 -2
  93. package/get-shit-done/workflows/new-milestone.md +12 -10
  94. package/get-shit-done/workflows/new-project.md +44 -49
  95. package/get-shit-done/workflows/pause-work.md +2 -2
  96. package/get-shit-done/workflows/plan-milestone-gaps.md +3 -3
  97. package/get-shit-done/workflows/plan-phase.md +155 -73
  98. package/get-shit-done/workflows/progress.md +8 -7
  99. package/get-shit-done/workflows/quick.md +158 -10
  100. package/get-shit-done/workflows/remove-phase.md +5 -4
  101. package/get-shit-done/workflows/research-phase.md +5 -4
  102. package/get-shit-done/workflows/resume-project.md +3 -2
  103. package/get-shit-done/workflows/set-profile.md +3 -2
  104. package/get-shit-done/workflows/settings.md +6 -6
  105. package/get-shit-done/workflows/transition.md +5 -5
  106. package/get-shit-done/workflows/update.md +45 -19
  107. package/get-shit-done/workflows/validate-phase.md +167 -0
  108. package/get-shit-done/workflows/verify-phase.md +10 -9
  109. package/get-shit-done/workflows/verify-work.md +18 -4
  110. package/package.json +1 -1
@@ -5,7 +5,7 @@
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { execSync } = require('child_process');
8
- const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, normalizePhaseName, output, error } = require('./core.cjs');
8
+ const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, normalizePhaseName, toPosixPath, output, error } = require('./core.cjs');
9
9
 
10
10
  function cmdInitExecutePhase(cwd, phase, raw) {
11
11
  if (!phase) {
@@ -16,6 +16,13 @@ function cmdInitExecutePhase(cwd, phase, raw) {
16
16
  const phaseInfo = findPhaseInternal(cwd, phase);
17
17
  const milestone = getMilestoneInfo(cwd);
18
18
 
19
+ const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
20
+ const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
21
+ const reqExtracted = reqMatch
22
+ ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ')
23
+ : null;
24
+ const phase_req_ids = (reqExtracted && reqExtracted !== 'TBD') ? reqExtracted : null;
25
+
19
26
  const result = {
20
27
  // Models
21
28
  executor_model: resolveModelInternal(cwd, 'gsd-executor'),
@@ -35,6 +42,7 @@ function cmdInitExecutePhase(cwd, phase, raw) {
35
42
  phase_number: phaseInfo?.phase_number || null,
36
43
  phase_name: phaseInfo?.phase_name || null,
37
44
  phase_slug: phaseInfo?.phase_slug || null,
45
+ phase_req_ids,
38
46
 
39
47
  // Plan inventory
40
48
  plans: phaseInfo?.plans || [],
@@ -80,6 +88,13 @@ function cmdInitPlanPhase(cwd, phase, raw) {
80
88
  const config = loadConfig(cwd);
81
89
  const phaseInfo = findPhaseInternal(cwd, phase);
82
90
 
91
+ const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
92
+ const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
93
+ const reqExtracted = reqMatch
94
+ ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ')
95
+ : null;
96
+ const phase_req_ids = (reqExtracted && reqExtracted !== 'TBD') ? reqExtracted : null;
97
+
83
98
  const result = {
84
99
  // Models
85
100
  researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
@@ -99,6 +114,7 @@ function cmdInitPlanPhase(cwd, phase, raw) {
99
114
  phase_name: phaseInfo?.phase_name || null,
100
115
  phase_slug: phaseInfo?.phase_slug || null,
101
116
  padded_phase: phaseInfo?.phase_number?.padStart(2, '0') || null,
117
+ phase_req_ids,
102
118
 
103
119
  // Existing artifacts
104
120
  has_research: phaseInfo?.has_research || false,
@@ -123,19 +139,19 @@ function cmdInitPlanPhase(cwd, phase, raw) {
123
139
  const files = fs.readdirSync(phaseDirFull);
124
140
  const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
125
141
  if (contextFile) {
126
- result.context_path = path.join(phaseInfo.directory, contextFile);
142
+ result.context_path = toPosixPath(path.join(phaseInfo.directory, contextFile));
127
143
  }
128
144
  const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
129
145
  if (researchFile) {
130
- result.research_path = path.join(phaseInfo.directory, researchFile);
146
+ result.research_path = toPosixPath(path.join(phaseInfo.directory, researchFile));
131
147
  }
132
148
  const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
133
149
  if (verificationFile) {
134
- result.verification_path = path.join(phaseInfo.directory, verificationFile);
150
+ result.verification_path = toPosixPath(path.join(phaseInfo.directory, verificationFile));
135
151
  }
136
152
  const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md');
137
153
  if (uatFile) {
138
- result.uat_path = path.join(phaseInfo.directory, uatFile);
154
+ result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
139
155
  }
140
156
  } catch {}
141
157
  }
@@ -406,19 +422,19 @@ function cmdInitPhaseOp(cwd, phase, raw) {
406
422
  const files = fs.readdirSync(phaseDirFull);
407
423
  const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
408
424
  if (contextFile) {
409
- result.context_path = path.join(phaseInfo.directory, contextFile);
425
+ result.context_path = toPosixPath(path.join(phaseInfo.directory, contextFile));
410
426
  }
411
427
  const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
412
428
  if (researchFile) {
413
- result.research_path = path.join(phaseInfo.directory, researchFile);
429
+ result.research_path = toPosixPath(path.join(phaseInfo.directory, researchFile));
414
430
  }
415
431
  const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
416
432
  if (verificationFile) {
417
- result.verification_path = path.join(phaseInfo.directory, verificationFile);
433
+ result.verification_path = toPosixPath(path.join(phaseInfo.directory, verificationFile));
418
434
  }
419
435
  const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md');
420
436
  if (uatFile) {
421
- result.uat_path = path.join(phaseInfo.directory, uatFile);
437
+ result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
422
438
  }
423
439
  } catch {}
424
440
  }
@@ -453,7 +469,7 @@ function cmdInitTodos(cwd, area, raw) {
453
469
  created: createdMatch ? createdMatch[1].trim() : 'unknown',
454
470
  title: titleMatch ? titleMatch[1].trim() : 'Untitled',
455
471
  area: todoArea,
456
- path: path.join('.planning', 'todos', 'pending', file),
472
+ path: '.planning/todos/pending/' + file,
457
473
  });
458
474
  } catch {}
459
475
  }
@@ -595,7 +611,7 @@ function cmdInitProgress(cwd, raw) {
595
611
  const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
596
612
 
597
613
  for (const dir of dirs) {
598
- const match = dir.match(/^(\d+(?:\.\d+)?)-?(.*)/);
614
+ const match = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
599
615
  const phaseNumber = match ? match[1] : dir;
600
616
  const phaseName = match && match[2] ? match[2] : null;
601
617
 
@@ -613,7 +629,7 @@ function cmdInitProgress(cwd, raw) {
613
629
  const phaseInfo = {
614
630
  number: phaseNumber,
615
631
  name: phaseName,
616
- directory: path.join('.planning', 'phases', dir),
632
+ directory: '.planning/phases/' + dir,
617
633
  status,
618
634
  plan_count: plans.length,
619
635
  summary_count: summaries.length,
@@ -4,8 +4,9 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { output, error } = require('./core.cjs');
7
+ const { escapeRegex, getMilestonePhaseFilter, output, error } = require('./core.cjs');
8
8
  const { extractFrontmatter } = require('./frontmatter.cjs');
9
+ const { writeStateMd } = require('./state.cjs');
9
10
 
10
11
  function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
11
12
  if (!reqIdsRaw || reqIdsRaw.length === 0) {
@@ -36,20 +37,21 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
36
37
 
37
38
  for (const reqId of reqIds) {
38
39
  let found = false;
40
+ const reqEscaped = escapeRegex(reqId);
39
41
 
40
42
  // Update checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID**
41
- const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqId}\\*\\*)`, 'gi');
43
+ const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi');
42
44
  if (checkboxPattern.test(reqContent)) {
43
45
  reqContent = reqContent.replace(checkboxPattern, '$1x$2');
44
46
  found = true;
45
47
  }
46
48
 
47
49
  // Update traceability table: | REQ-ID | Phase N | Pending | → | REQ-ID | Phase N | Complete |
48
- const tablePattern = new RegExp(`(\\|\\s*${reqId}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi');
50
+ const tablePattern = new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi');
49
51
  if (tablePattern.test(reqContent)) {
50
52
  // Re-read since test() advances lastIndex for global regex
51
53
  reqContent = reqContent.replace(
52
- new RegExp(`(\\|\\s*${reqId}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'),
54
+ new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'),
53
55
  '$1 Complete $2'
54
56
  );
55
57
  found = true;
@@ -91,7 +93,12 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
91
93
  // Ensure archive directory exists
92
94
  fs.mkdirSync(archiveDir, { recursive: true });
93
95
 
94
- // Gather stats from phases
96
+ // Scope stats and accomplishments to only the phases belonging to the
97
+ // current milestone's ROADMAP. Uses the shared filter from core.cjs
98
+ // (same logic used by cmdPhasesList and other callers).
99
+ const isDirInMilestone = getMilestonePhaseFilter(cwd);
100
+
101
+ // Gather stats from phases (scoped to current milestone only)
95
102
  let phaseCount = 0;
96
103
  let totalPlans = 0;
97
104
  let totalTasks = 0;
@@ -102,6 +109,8 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
102
109
  const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
103
110
 
104
111
  for (const dir of dirs) {
112
+ if (!isDirInMilestone(dir)) continue;
113
+
105
114
  phaseCount++;
106
115
  const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
107
116
  const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
@@ -149,7 +158,21 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
149
158
 
150
159
  if (fs.existsSync(milestonesPath)) {
151
160
  const existing = fs.readFileSync(milestonesPath, 'utf-8');
152
- fs.writeFileSync(milestonesPath, existing + '\n' + milestoneEntry, 'utf-8');
161
+ if (!existing.trim()) {
162
+ // Empty file — treat like new
163
+ fs.writeFileSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`, 'utf-8');
164
+ } else {
165
+ // Insert after the header line(s) for reverse chronological order (newest first)
166
+ const headerMatch = existing.match(/^(#{1,3}\s+[^\n]*\n\n?)/);
167
+ if (headerMatch) {
168
+ const header = headerMatch[1];
169
+ const rest = existing.slice(header.length);
170
+ fs.writeFileSync(milestonesPath, header + milestoneEntry + rest, 'utf-8');
171
+ } else {
172
+ // No recognizable header — prepend the entry
173
+ fs.writeFileSync(milestonesPath, milestoneEntry + existing, 'utf-8');
174
+ }
175
+ }
153
176
  } else {
154
177
  fs.writeFileSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`, 'utf-8');
155
178
  }
@@ -169,7 +192,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
169
192
  /(\*\*Last Activity Description:\*\*\s*).*/,
170
193
  `$1${version} milestone completed and archived`
171
194
  );
172
- fs.writeFileSync(statePath, stateContent, 'utf-8');
195
+ writeStateMd(statePath, stateContent, cwd);
173
196
  }
174
197
 
175
198
  // Archive phase directories if requested
@@ -181,10 +204,13 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
181
204
 
182
205
  const phaseEntries = fs.readdirSync(phasesDir, { withFileTypes: true });
183
206
  const phaseDirNames = phaseEntries.filter(e => e.isDirectory()).map(e => e.name);
207
+ let archivedCount = 0;
184
208
  for (const dir of phaseDirNames) {
209
+ if (!isDirInMilestone(dir)) continue;
185
210
  fs.renameSync(path.join(phasesDir, dir), path.join(phaseArchiveDir, dir));
211
+ archivedCount++;
186
212
  }
187
- phasesArchived = phaseDirNames.length > 0;
213
+ phasesArchived = archivedCount > 0;
188
214
  } catch {}
189
215
  }
190
216
 
@@ -4,8 +4,9 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { normalizePhaseName, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, output, error } = require('./core.cjs');
7
+ const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, toPosixPath, output, error } = require('./core.cjs');
8
8
  const { extractFrontmatter } = require('./frontmatter.cjs');
9
+ const { writeStateMd } = require('./state.cjs');
9
10
 
10
11
  function cmdPhasesList(cwd, options, raw) {
11
12
  const phasesDir = path.join(cwd, '.planning', 'phases');
@@ -34,12 +35,8 @@ function cmdPhasesList(cwd, options, raw) {
34
35
  }
35
36
  }
36
37
 
37
- // Sort numerically (handles decimals: 01, 02, 02.1, 02.2, 03)
38
- dirs.sort((a, b) => {
39
- const aNum = parseFloat(a.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0');
40
- const bNum = parseFloat(b.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0');
41
- return aNum - bNum;
42
- });
38
+ // Sort numerically (handles integers, decimals, letter-suffix, hybrids)
39
+ dirs.sort((a, b) => comparePhaseNum(a, b));
43
40
 
44
41
  // If filtering by phase number
45
42
  if (phase) {
@@ -74,7 +71,7 @@ function cmdPhasesList(cwd, options, raw) {
74
71
  const result = {
75
72
  files,
76
73
  count: files.length,
77
- phase_dir: phase ? dirs[0].replace(/^\d+(?:\.\d+)?-?/, '') : null,
74
+ phase_dir: phase ? dirs[0].replace(/^\d+(?:\.\d+)*-?/, '') : null,
78
75
  };
79
76
  output(result, raw, files.join('\n'));
80
77
  return;
@@ -125,11 +122,7 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
125
122
  }
126
123
 
127
124
  // Sort numerically
128
- existingDecimals.sort((a, b) => {
129
- const aNum = parseFloat(a);
130
- const bNum = parseFloat(b);
131
- return aNum - bNum;
132
- });
125
+ existingDecimals.sort((a, b) => comparePhaseNum(a, b));
133
126
 
134
127
  // Calculate next decimal
135
128
  let nextDecimal;
@@ -168,7 +161,7 @@ function cmdFindPhase(cwd, phase, raw) {
168
161
 
169
162
  try {
170
163
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
171
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
164
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
172
165
 
173
166
  const match = dirs.find(d => d.startsWith(normalized));
174
167
  if (!match) {
@@ -176,7 +169,7 @@ function cmdFindPhase(cwd, phase, raw) {
176
169
  return;
177
170
  }
178
171
 
179
- const dirMatch = match.match(/^(\d+(?:\.\d+)?)-?(.*)/);
172
+ const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
180
173
  const phaseNumber = dirMatch ? dirMatch[1] : normalized;
181
174
  const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
182
175
 
@@ -187,7 +180,7 @@ function cmdFindPhase(cwd, phase, raw) {
187
180
 
188
181
  const result = {
189
182
  found: true,
190
- directory: path.join('.planning', 'phases', match),
183
+ directory: toPosixPath(path.join('.planning', 'phases', match)),
191
184
  phase_number: phaseNumber,
192
185
  phase_name: phaseName,
193
186
  plans,
@@ -200,6 +193,11 @@ function cmdFindPhase(cwd, phase, raw) {
200
193
  }
201
194
  }
202
195
 
196
+ function extractObjective(content) {
197
+ const m = content.match(/<objective>\s*\n?\s*(.+)/);
198
+ return m ? m[1].trim() : null;
199
+ }
200
+
203
201
  function cmdPhasePlanIndex(cwd, phase, raw) {
204
202
  if (!phase) {
205
203
  error('phase required for phase-plan-index');
@@ -213,7 +211,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
213
211
  let phaseDirName = null;
214
212
  try {
215
213
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
216
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
214
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
217
215
  const match = dirs.find(d => d.startsWith(normalized));
218
216
  if (match) {
219
217
  phaseDir = path.join(phasesDir, match);
@@ -249,9 +247,10 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
249
247
  const content = fs.readFileSync(planPath, 'utf-8');
250
248
  const fm = extractFrontmatter(content);
251
249
 
252
- // Count tasks (## task N patterns)
253
- const taskMatches = content.match(/##\s*task\s*\d+/gi) || [];
254
- const taskCount = taskMatches.length;
250
+ // Count tasks: XML <task> tags (canonical) or ## task N markdown (legacy)
251
+ const xmlTasks = content.match(/<task[\s>]/gi) || [];
252
+ const mdTasks = content.match(/##\s*task\s*\d+/gi) || [];
253
+ const taskCount = xmlTasks.length || mdTasks.length;
255
254
 
256
255
  // Parse wave as integer
257
256
  const wave = parseInt(fm.wave, 10) || 1;
@@ -266,10 +265,11 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
266
265
  hasCheckpoints = true;
267
266
  }
268
267
 
269
- // Parse files-modified
268
+ // Parse files_modified (underscore is canonical; also accept hyphenated for compat)
270
269
  let filesModified = [];
271
- if (fm['files-modified']) {
272
- filesModified = Array.isArray(fm['files-modified']) ? fm['files-modified'] : [fm['files-modified']];
270
+ const fmFiles = fm['files_modified'] || fm['files-modified'];
271
+ if (fmFiles) {
272
+ filesModified = Array.isArray(fmFiles) ? fmFiles : [fmFiles];
273
273
  }
274
274
 
275
275
  const hasSummary = completedPlanIds.has(planId);
@@ -281,7 +281,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
281
281
  id: planId,
282
282
  wave,
283
283
  autonomous,
284
- objective: fm.objective || null,
284
+ objective: extractObjective(content) || fm.objective || null,
285
285
  files_modified: filesModified,
286
286
  task_count: taskCount,
287
287
  has_summary: hasSummary,
@@ -322,7 +322,7 @@ function cmdPhaseAdd(cwd, description, raw) {
322
322
  const slug = generateSlugInternal(description);
323
323
 
324
324
  // Find highest integer phase number
325
- const phasePattern = /#{2,4}\s*Phase\s+(\d+)(?:\.\d+)?:/gi;
325
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
326
326
  let maxPhase = 0;
327
327
  let m;
328
328
  while ((m = phasePattern.exec(content)) !== null) {
@@ -340,7 +340,7 @@ function cmdPhaseAdd(cwd, description, raw) {
340
340
  fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
341
341
 
342
342
  // Build phase entry
343
- const phaseEntry = `\n### Phase ${newPhaseNum}: ${description}\n\n**Goal:** [To be planned]\n**Depends on:** Phase ${maxPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd-plan-phase ${newPhaseNum} to break down)\n`;
343
+ const phaseEntry = `\n### Phase ${newPhaseNum}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${maxPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd-plan-phase ${newPhaseNum} to break down)\n`;
344
344
 
345
345
  // Find insertion point: before last "---" or at end
346
346
  let updatedContent;
@@ -411,7 +411,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
411
411
  fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
412
412
 
413
413
  // Build phase entry
414
- const phaseEntry = `\n### Phase ${decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd-plan-phase ${decimalPhase} to break down)\n`;
414
+ const phaseEntry = `\n### Phase ${decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd-plan-phase ${decimalPhase} to break down)\n`;
415
415
 
416
416
  // Insert after the target phase section
417
417
  const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
@@ -466,7 +466,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
466
466
  let targetDir = null;
467
467
  try {
468
468
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
469
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
469
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
470
470
  targetDir = dirs.find(d => d.startsWith(normalized + '-') || d === normalized);
471
471
  } catch {}
472
472
 
@@ -497,7 +497,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
497
497
 
498
498
  try {
499
499
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
500
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
500
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
501
501
 
502
502
  // Find sibling decimals with higher numbers
503
503
  const decPattern = new RegExp(`^${baseInt}\\.(\\d+)-(.+)$`);
@@ -544,20 +544,21 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
544
544
 
545
545
  try {
546
546
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
547
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
547
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
548
548
 
549
- // Collect directories that need renumbering (integer phases > removed, and their decimals)
549
+ // Collect directories that need renumbering (integer phases > removed, and their decimals/letters)
550
550
  const toRename = [];
551
551
  for (const dir of dirs) {
552
- const dm = dir.match(/^(\d+)(?:\.(\d+))?-(.+)$/);
552
+ const dm = dir.match(/^(\d+)([A-Z])?(?:\.(\d+))?-(.+)$/i);
553
553
  if (!dm) continue;
554
554
  const dirInt = parseInt(dm[1], 10);
555
555
  if (dirInt > removedInt) {
556
556
  toRename.push({
557
557
  dir,
558
558
  oldInt: dirInt,
559
- decimal: dm[2] ? parseInt(dm[2], 10) : null,
560
- slug: dm[3],
559
+ letter: dm[2] ? dm[2].toUpperCase() : '',
560
+ decimal: dm[3] ? parseInt(dm[3], 10) : null,
561
+ slug: dm[4],
561
562
  });
562
563
  }
563
564
  }
@@ -572,9 +573,10 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
572
573
  const newInt = item.oldInt - 1;
573
574
  const newPadded = String(newInt).padStart(2, '0');
574
575
  const oldPadded = String(item.oldInt).padStart(2, '0');
576
+ const letterSuffix = item.letter || '';
575
577
  const decimalSuffix = item.decimal !== null ? `.${item.decimal}` : '';
576
- const oldPrefix = `${oldPadded}${decimalSuffix}`;
577
- const newPrefix = `${newPadded}${decimalSuffix}`;
578
+ const oldPrefix = `${oldPadded}${letterSuffix}${decimalSuffix}`;
579
+ const newPrefix = `${newPadded}${letterSuffix}${decimalSuffix}`;
578
580
  const newDirName = `${newPrefix}-${item.slug}`;
579
581
 
580
582
  // Rename directory
@@ -601,7 +603,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
601
603
  let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
602
604
 
603
605
  // Remove the target phase section
604
- const targetEscaped = targetPhase.replace(/\./g, '\\.');
606
+ const targetEscaped = escapeRegex(targetPhase);
605
607
  const sectionPattern = new RegExp(
606
608
  `\\n?#{2,4}\\s*Phase\\s+${targetEscaped}\\s*:[\\s\\S]*?(?=\\n#{2,4}\\s+Phase\\s+\\d|$)`,
607
609
  'i'
@@ -681,7 +683,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
681
683
  const oldTotal = parseInt(ofMatch[2], 10);
682
684
  stateContent = stateContent.replace(ofPattern, `$1${oldTotal - 1}$3`);
683
685
  }
684
- fs.writeFileSync(statePath, stateContent, 'utf-8');
686
+ writeStateMd(statePath, stateContent, cwd);
685
687
  }
686
688
 
687
689
  const result = {
@@ -722,13 +724,13 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
722
724
 
723
725
  // Checkbox: - [ ] Phase N: → - [x] Phase N: (...completed DATE)
724
726
  const checkboxPattern = new RegExp(
725
- `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseNum.replace('.', '\\.')}[:\\s][^\\n]*)`,
727
+ `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s][^\\n]*)`,
726
728
  'i'
727
729
  );
728
730
  roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
729
731
 
730
732
  // Progress table: update Status to Complete, add date
731
- const phaseEscaped = phaseNum.replace('.', '\\.');
733
+ const phaseEscaped = escapeRegex(phaseNum);
732
734
  const tablePattern = new RegExp(
733
735
  `(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|[^|]*\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`,
734
736
  'i'
@@ -755,7 +757,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
755
757
  if (fs.existsSync(reqPath)) {
756
758
  // Extract Requirements line from roadmap for this phase
757
759
  const reqMatch = roadmapContent.match(
758
- new RegExp(`Phase\\s+${phaseNum.replace('.', '\\.')}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, 'i')
760
+ new RegExp(`Phase\\s+${escapeRegex(phaseNum)}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, 'i')
759
761
  );
760
762
 
761
763
  if (reqMatch) {
@@ -763,14 +765,15 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
763
765
  let reqContent = fs.readFileSync(reqPath, 'utf-8');
764
766
 
765
767
  for (const reqId of reqIds) {
768
+ const reqEscaped = escapeRegex(reqId);
766
769
  // Update checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID**
767
770
  reqContent = reqContent.replace(
768
- new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqId}\\*\\*)`, 'gi'),
771
+ new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi'),
769
772
  '$1x$2'
770
773
  );
771
774
  // Update traceability table: | REQ-ID | Phase N | Pending | → | REQ-ID | Phase N | Complete |
772
775
  reqContent = reqContent.replace(
773
- new RegExp(`(\\|\\s*${reqId}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'),
776
+ new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'),
774
777
  '$1 Complete $2'
775
778
  );
776
779
  }
@@ -780,22 +783,25 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
780
783
  }
781
784
  }
782
785
 
783
- // Find next phase
786
+ // Find next phase — check both filesystem AND roadmap
787
+ // Phases may be defined in ROADMAP.md but not yet scaffolded to disk,
788
+ // so a filesystem-only scan would incorrectly report is_last_phase:true
784
789
  let nextPhaseNum = null;
785
790
  let nextPhaseName = null;
786
791
  let isLastPhase = true;
787
792
 
788
793
  try {
794
+ const isDirInMilestone = getMilestonePhaseFilter(cwd);
789
795
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
790
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
791
- const currentFloat = parseFloat(phaseNum);
796
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name)
797
+ .filter(isDirInMilestone)
798
+ .sort((a, b) => comparePhaseNum(a, b));
792
799
 
793
800
  // Find the next phase directory after current
794
801
  for (const dir of dirs) {
795
- const dm = dir.match(/^(\d+(?:\.\d+)?)-?(.*)/);
802
+ const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
796
803
  if (dm) {
797
- const dirFloat = parseFloat(dm[1]);
798
- if (dirFloat > currentFloat) {
804
+ if (comparePhaseNum(dm[1], phaseNum) > 0) {
799
805
  nextPhaseNum = dm[1];
800
806
  nextPhaseName = dm[2] || null;
801
807
  isLastPhase = false;
@@ -805,6 +811,24 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
805
811
  }
806
812
  } catch {}
807
813
 
814
+ // Fallback: if filesystem found no next phase, check ROADMAP.md
815
+ // for phases that are defined but not yet planned (no directory on disk)
816
+ if (isLastPhase && fs.existsSync(roadmapPath)) {
817
+ try {
818
+ const roadmapForPhases = fs.readFileSync(roadmapPath, 'utf-8');
819
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
820
+ let pm;
821
+ while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
822
+ if (comparePhaseNum(pm[1], phaseNum) > 0) {
823
+ nextPhaseNum = pm[1];
824
+ nextPhaseName = pm[2].replace(/\(INSERTED\)/i, '').trim().toLowerCase().replace(/\s+/g, '-');
825
+ isLastPhase = false;
826
+ break;
827
+ }
828
+ }
829
+ } catch {}
830
+ }
831
+
808
832
  // Update STATE.md
809
833
  if (fs.existsSync(statePath)) {
810
834
  let stateContent = fs.readFileSync(statePath, 'utf-8');
@@ -847,7 +871,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
847
871
  `$1Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`
848
872
  );
849
873
 
850
- fs.writeFileSync(statePath, stateContent, 'utf-8');
874
+ writeStateMd(statePath, stateContent, cwd);
851
875
  }
852
876
 
853
877
  const result = {
@@ -4,7 +4,7 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { normalizePhaseName, output, error, findPhaseInternal } = require('./core.cjs');
7
+ const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal } = require('./core.cjs');
8
8
 
9
9
  function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
10
10
  const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
@@ -18,7 +18,7 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
18
18
  const content = fs.readFileSync(roadmapPath, 'utf-8');
19
19
 
20
20
  // Escape special regex chars in phase number, handle decimal
21
- const escapedPhase = phaseNum.replace(/\./g, '\\.');
21
+ const escapedPhase = escapeRegex(phaseNum);
22
22
 
23
23
  // Match "## Phase X:", "### Phase X:", or "#### Phase X:" with optional name
24
24
  const phasePattern = new RegExp(
@@ -102,7 +102,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
102
102
  const phasesDir = path.join(cwd, '.planning', 'phases');
103
103
 
104
104
  // Extract all phase headings: ## Phase N: Name or ### Phase N: Name
105
- const phasePattern = /#{2,4}\s*Phase\s+(\d+(?:\.\d+)?)\s*:\s*([^\n]+)/gi;
105
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
106
106
  const phases = [];
107
107
  let match;
108
108
 
@@ -153,7 +153,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
153
153
  } catch {}
154
154
 
155
155
  // Check ROADMAP checkbox status
156
- const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseNum.replace('.', '\\.')}`, 'i');
156
+ const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}`, 'i');
157
157
  const checkboxMatch = content.match(checkboxPattern);
158
158
  const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
159
159
 
@@ -192,7 +192,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
192
192
  const completedPhases = phases.filter(p => p.disk_status === 'complete').length;
193
193
 
194
194
  // Detect phases in summary list without detail sections (malformed ROADMAP)
195
- const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+(\d+(?:\.\d+)?)/gi;
195
+ const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+(\d+[A-Z]?(?:\.\d+)*)/gi;
196
196
  const checklistPhases = new Set();
197
197
  let checklistMatch;
198
198
  while ((checklistMatch = checklistPattern.exec(content)) !== null) {
@@ -208,7 +208,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
208
208
  completed_phases: completedPhases,
209
209
  total_plans: totalPlans,
210
210
  total_summaries: totalSummaries,
211
- progress_percent: totalPlans > 0 ? Math.round((totalSummaries / totalPlans) * 100) : 0,
211
+ progress_percent: totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0,
212
212
  current_phase: currentPhase ? currentPhase.number : null,
213
213
  next_phase: nextPhase ? nextPhase.number : null,
214
214
  missing_phase_details: missingDetails.length > 0 ? missingDetails : null,
@@ -247,7 +247,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
247
247
  }
248
248
 
249
249
  let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
250
- const phaseEscaped = phaseNum.replace('.', '\\.');
250
+ const phaseEscaped = escapeRegex(phaseNum);
251
251
 
252
252
  // Progress table row: update Plans column (summaries/plans) and Status column
253
253
  const tablePattern = new RegExp(