gsd-opencode 1.20.3 → 1.22.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 (114) hide show
  1. package/agents/gsd-codebase-mapper.md +9 -1
  2. package/agents/gsd-debugger.md +66 -10
  3. package/agents/gsd-executor.md +36 -16
  4. package/agents/gsd-integration-checker.md +2 -0
  5. package/agents/gsd-nyquist-auditor.md +178 -0
  6. package/agents/gsd-phase-researcher.md +28 -34
  7. package/agents/gsd-plan-checker.md +42 -78
  8. package/agents/gsd-planner.md +139 -24
  9. package/agents/gsd-project-researcher.md +11 -1
  10. package/agents/gsd-research-synthesizer.md +13 -3
  11. package/agents/gsd-roadmapper.md +25 -15
  12. package/agents/gsd-verifier.md +29 -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 +19 -11
  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-oc-commands/allow-read-config.cjs +235 -0
  49. package/get-shit-done/bin/gsd-oc-tools.cjs +11 -5
  50. package/get-shit-done/bin/gsd-tools.cjs +45 -6
  51. package/get-shit-done/bin/lib/commands.cjs +11 -19
  52. package/get-shit-done/bin/lib/config.cjs +8 -1
  53. package/get-shit-done/bin/lib/core.cjs +131 -16
  54. package/get-shit-done/bin/lib/init.cjs +28 -12
  55. package/get-shit-done/bin/lib/milestone.cjs +34 -8
  56. package/get-shit-done/bin/lib/phase.cjs +74 -50
  57. package/get-shit-done/bin/lib/roadmap.cjs +7 -7
  58. package/get-shit-done/bin/lib/state.cjs +294 -63
  59. package/get-shit-done/bin/lib/template.cjs +3 -3
  60. package/get-shit-done/bin/lib/verify.cjs +56 -8
  61. package/get-shit-done/bin/test/allow-read-config.test.cjs +262 -0
  62. package/get-shit-done/references/checkpoints.md +1 -1
  63. package/get-shit-done/references/decimal-phase-calculation.md +6 -6
  64. package/get-shit-done/references/git-integration.md +3 -3
  65. package/get-shit-done/references/git-planning-commit.md +2 -2
  66. package/get-shit-done/references/model-profile-resolution.md +1 -1
  67. package/get-shit-done/references/model-profiles.md +1 -0
  68. package/get-shit-done/references/phase-argument-parsing.md +4 -4
  69. package/get-shit-done/references/planning-config.md +10 -6
  70. package/get-shit-done/references/questioning.md +17 -0
  71. package/get-shit-done/references/verification-patterns.md +1 -1
  72. package/get-shit-done/templates/DEBUG.md +7 -2
  73. package/get-shit-done/templates/VALIDATION.md +18 -46
  74. package/get-shit-done/templates/codebase/structure.md +3 -3
  75. package/get-shit-done/templates/config.json +2 -2
  76. package/get-shit-done/templates/context.md +14 -0
  77. package/get-shit-done/templates/phase-prompt.md +10 -10
  78. package/get-shit-done/templates/retrospective.md +54 -0
  79. package/get-shit-done/templates/roadmap.md +1 -1
  80. package/get-shit-done/workflows/add-phase.md +3 -2
  81. package/get-shit-done/workflows/add-tests.md +351 -0
  82. package/get-shit-done/workflows/add-todo.md +4 -3
  83. package/get-shit-done/workflows/audit-milestone.md +40 -5
  84. package/get-shit-done/workflows/check-todos.md +3 -2
  85. package/get-shit-done/workflows/cleanup.md +1 -1
  86. package/get-shit-done/workflows/complete-milestone.md +69 -5
  87. package/get-shit-done/workflows/diagnose-issues.md +2 -2
  88. package/get-shit-done/workflows/discovery-phase.md +6 -6
  89. package/get-shit-done/workflows/discuss-phase.md +194 -58
  90. package/get-shit-done/workflows/execute-phase.md +29 -23
  91. package/get-shit-done/workflows/execute-plan.md +22 -18
  92. package/get-shit-done/workflows/health.md +5 -2
  93. package/get-shit-done/workflows/help.md +4 -1
  94. package/get-shit-done/workflows/insert-phase.md +3 -2
  95. package/get-shit-done/workflows/map-codebase.md +3 -2
  96. package/get-shit-done/workflows/new-milestone.md +12 -10
  97. package/get-shit-done/workflows/new-project.md +44 -49
  98. package/get-shit-done/workflows/oc-set-profile.md +24 -0
  99. package/get-shit-done/workflows/pause-work.md +2 -2
  100. package/get-shit-done/workflows/plan-milestone-gaps.md +3 -3
  101. package/get-shit-done/workflows/plan-phase.md +155 -73
  102. package/get-shit-done/workflows/progress.md +8 -7
  103. package/get-shit-done/workflows/quick.md +158 -10
  104. package/get-shit-done/workflows/remove-phase.md +5 -4
  105. package/get-shit-done/workflows/research-phase.md +5 -4
  106. package/get-shit-done/workflows/resume-project.md +3 -2
  107. package/get-shit-done/workflows/set-profile.md +3 -2
  108. package/get-shit-done/workflows/settings.md +6 -6
  109. package/get-shit-done/workflows/transition.md +5 -5
  110. package/get-shit-done/workflows/update.md +45 -19
  111. package/get-shit-done/workflows/validate-phase.md +167 -0
  112. package/get-shit-done/workflows/verify-phase.md +10 -9
  113. package/get-shit-done/workflows/verify-work.md +18 -4
  114. package/package.json +1 -1
@@ -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(