gsd-opencode 1.22.0 → 1.30.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 (157) hide show
  1. package/agents/gsd-advisor-researcher.md +112 -0
  2. package/agents/gsd-assumptions-analyzer.md +110 -0
  3. package/agents/gsd-codebase-mapper.md +1 -2
  4. package/agents/gsd-debugger.md +119 -2
  5. package/agents/gsd-executor.md +25 -4
  6. package/agents/gsd-integration-checker.md +1 -2
  7. package/agents/gsd-nyquist-auditor.md +1 -2
  8. package/agents/gsd-phase-researcher.md +151 -5
  9. package/agents/gsd-plan-checker.md +71 -5
  10. package/agents/gsd-planner.md +50 -4
  11. package/agents/gsd-project-researcher.md +29 -3
  12. package/agents/gsd-research-synthesizer.md +1 -2
  13. package/agents/gsd-roadmapper.md +30 -2
  14. package/agents/gsd-ui-auditor.md +445 -0
  15. package/agents/gsd-ui-checker.md +305 -0
  16. package/agents/gsd-ui-researcher.md +368 -0
  17. package/agents/gsd-user-profiler.md +173 -0
  18. package/agents/gsd-verifier.md +124 -4
  19. package/commands/gsd/gsd-add-backlog.md +76 -0
  20. package/commands/gsd/gsd-audit-uat.md +24 -0
  21. package/commands/gsd/gsd-autonomous.md +41 -0
  22. package/commands/gsd/gsd-debug.md +5 -0
  23. package/commands/gsd/gsd-discuss-phase.md +10 -36
  24. package/commands/gsd/gsd-do.md +30 -0
  25. package/commands/gsd/gsd-execute-phase.md +20 -2
  26. package/commands/gsd/gsd-fast.md +30 -0
  27. package/commands/gsd/gsd-forensics.md +56 -0
  28. package/commands/gsd/gsd-list-workspaces.md +19 -0
  29. package/commands/gsd/gsd-manager.md +39 -0
  30. package/commands/gsd/gsd-milestone-summary.md +51 -0
  31. package/commands/gsd/gsd-new-workspace.md +44 -0
  32. package/commands/gsd/gsd-next.md +24 -0
  33. package/commands/gsd/gsd-note.md +34 -0
  34. package/commands/gsd/gsd-plan-phase.md +3 -1
  35. package/commands/gsd/gsd-plant-seed.md +28 -0
  36. package/commands/gsd/gsd-pr-branch.md +25 -0
  37. package/commands/gsd/gsd-profile-user.md +46 -0
  38. package/commands/gsd/gsd-quick.md +4 -2
  39. package/commands/gsd/gsd-reapply-patches.md +10 -6
  40. package/commands/gsd/gsd-remove-workspace.md +26 -0
  41. package/commands/gsd/gsd-research-phase.md +5 -0
  42. package/commands/gsd/gsd-resume-work.md +1 -1
  43. package/commands/gsd/gsd-review-backlog.md +61 -0
  44. package/commands/gsd/gsd-review.md +37 -0
  45. package/commands/gsd/gsd-session-report.md +19 -0
  46. package/commands/gsd/gsd-set-profile.md +24 -23
  47. package/commands/gsd/gsd-ship.md +23 -0
  48. package/commands/gsd/gsd-stats.md +18 -0
  49. package/commands/gsd/gsd-thread.md +127 -0
  50. package/commands/gsd/gsd-ui-phase.md +34 -0
  51. package/commands/gsd/gsd-ui-review.md +32 -0
  52. package/commands/gsd/gsd-workstreams.md +66 -0
  53. package/get-shit-done/bin/gsd-tools.cjs +410 -84
  54. package/get-shit-done/bin/lib/commands.cjs +429 -18
  55. package/get-shit-done/bin/lib/config.cjs +318 -45
  56. package/get-shit-done/bin/lib/core.cjs +822 -84
  57. package/get-shit-done/bin/lib/frontmatter.cjs +78 -41
  58. package/get-shit-done/bin/lib/init.cjs +836 -104
  59. package/get-shit-done/bin/lib/milestone.cjs +44 -33
  60. package/get-shit-done/bin/lib/model-profiles.cjs +68 -0
  61. package/get-shit-done/bin/lib/phase.cjs +293 -306
  62. package/get-shit-done/bin/lib/profile-output.cjs +952 -0
  63. package/get-shit-done/bin/lib/profile-pipeline.cjs +539 -0
  64. package/get-shit-done/bin/lib/roadmap.cjs +55 -24
  65. package/get-shit-done/bin/lib/security.cjs +382 -0
  66. package/get-shit-done/bin/lib/state.cjs +363 -53
  67. package/get-shit-done/bin/lib/template.cjs +2 -2
  68. package/get-shit-done/bin/lib/uat.cjs +282 -0
  69. package/get-shit-done/bin/lib/verify.cjs +104 -36
  70. package/get-shit-done/bin/lib/workstream.cjs +491 -0
  71. package/get-shit-done/references/checkpoints.md +12 -10
  72. package/get-shit-done/references/decimal-phase-calculation.md +2 -3
  73. package/get-shit-done/references/git-integration.md +47 -0
  74. package/get-shit-done/references/model-profile-resolution.md +2 -0
  75. package/get-shit-done/references/model-profiles.md +62 -16
  76. package/get-shit-done/references/phase-argument-parsing.md +2 -2
  77. package/get-shit-done/references/planning-config.md +3 -1
  78. package/get-shit-done/references/user-profiling.md +681 -0
  79. package/get-shit-done/references/workstream-flag.md +58 -0
  80. package/get-shit-done/templates/UAT.md +21 -3
  81. package/get-shit-done/templates/UI-SPEC.md +100 -0
  82. package/get-shit-done/templates/claude-md.md +122 -0
  83. package/get-shit-done/templates/config.json +10 -3
  84. package/get-shit-done/templates/context.md +61 -6
  85. package/get-shit-done/templates/dev-preferences.md +21 -0
  86. package/get-shit-done/templates/discussion-log.md +63 -0
  87. package/get-shit-done/templates/phase-prompt.md +46 -5
  88. package/get-shit-done/templates/project.md +2 -0
  89. package/get-shit-done/templates/state.md +2 -2
  90. package/get-shit-done/templates/user-profile.md +146 -0
  91. package/get-shit-done/workflows/add-phase.md +2 -2
  92. package/get-shit-done/workflows/add-tests.md +4 -4
  93. package/get-shit-done/workflows/add-todo.md +3 -3
  94. package/get-shit-done/workflows/audit-milestone.md +13 -5
  95. package/get-shit-done/workflows/audit-uat.md +109 -0
  96. package/get-shit-done/workflows/autonomous.md +891 -0
  97. package/get-shit-done/workflows/check-todos.md +2 -2
  98. package/get-shit-done/workflows/cleanup.md +4 -4
  99. package/get-shit-done/workflows/complete-milestone.md +9 -6
  100. package/get-shit-done/workflows/diagnose-issues.md +15 -3
  101. package/get-shit-done/workflows/discovery-phase.md +2 -2
  102. package/get-shit-done/workflows/discuss-phase-assumptions.md +653 -0
  103. package/get-shit-done/workflows/discuss-phase.md +411 -38
  104. package/get-shit-done/workflows/do.md +104 -0
  105. package/get-shit-done/workflows/execute-phase.md +405 -18
  106. package/get-shit-done/workflows/execute-plan.md +77 -12
  107. package/get-shit-done/workflows/fast.md +105 -0
  108. package/get-shit-done/workflows/forensics.md +265 -0
  109. package/get-shit-done/workflows/health.md +28 -6
  110. package/get-shit-done/workflows/help.md +124 -7
  111. package/get-shit-done/workflows/insert-phase.md +2 -2
  112. package/get-shit-done/workflows/list-phase-assumptions.md +2 -2
  113. package/get-shit-done/workflows/list-workspaces.md +56 -0
  114. package/get-shit-done/workflows/manager.md +362 -0
  115. package/get-shit-done/workflows/map-codebase.md +74 -13
  116. package/get-shit-done/workflows/milestone-summary.md +223 -0
  117. package/get-shit-done/workflows/new-milestone.md +120 -18
  118. package/get-shit-done/workflows/new-project.md +178 -39
  119. package/get-shit-done/workflows/new-workspace.md +237 -0
  120. package/get-shit-done/workflows/next.md +97 -0
  121. package/get-shit-done/workflows/node-repair.md +92 -0
  122. package/get-shit-done/workflows/note.md +156 -0
  123. package/get-shit-done/workflows/pause-work.md +62 -8
  124. package/get-shit-done/workflows/plan-milestone-gaps.md +4 -5
  125. package/get-shit-done/workflows/plan-phase.md +332 -33
  126. package/get-shit-done/workflows/plant-seed.md +169 -0
  127. package/get-shit-done/workflows/pr-branch.md +129 -0
  128. package/get-shit-done/workflows/profile-user.md +450 -0
  129. package/get-shit-done/workflows/progress.md +145 -20
  130. package/get-shit-done/workflows/quick.md +205 -49
  131. package/get-shit-done/workflows/remove-phase.md +2 -2
  132. package/get-shit-done/workflows/remove-workspace.md +90 -0
  133. package/get-shit-done/workflows/research-phase.md +11 -3
  134. package/get-shit-done/workflows/resume-project.md +35 -16
  135. package/get-shit-done/workflows/review.md +228 -0
  136. package/get-shit-done/workflows/session-report.md +146 -0
  137. package/get-shit-done/workflows/set-profile.md +2 -2
  138. package/get-shit-done/workflows/settings.md +80 -11
  139. package/get-shit-done/workflows/ship.md +228 -0
  140. package/get-shit-done/workflows/stats.md +60 -0
  141. package/get-shit-done/workflows/transition.md +147 -20
  142. package/get-shit-done/workflows/ui-phase.md +302 -0
  143. package/get-shit-done/workflows/ui-review.md +165 -0
  144. package/get-shit-done/workflows/update.md +108 -25
  145. package/get-shit-done/workflows/validate-phase.md +15 -8
  146. package/get-shit-done/workflows/verify-phase.md +16 -5
  147. package/get-shit-done/workflows/verify-work.md +72 -18
  148. package/package.json +1 -1
  149. package/skills/gsd-audit-milestone/SKILL.md +29 -0
  150. package/skills/gsd-cleanup/SKILL.md +19 -0
  151. package/skills/gsd-complete-milestone/SKILL.md +131 -0
  152. package/skills/gsd-discuss-phase/SKILL.md +54 -0
  153. package/skills/gsd-execute-phase/SKILL.md +49 -0
  154. package/skills/gsd-plan-phase/SKILL.md +37 -0
  155. package/skills/gsd-ui-phase/SKILL.md +24 -0
  156. package/skills/gsd-ui-review/SKILL.md +24 -0
  157. package/skills/gsd-verify-work/SKILL.md +30 -0
@@ -4,12 +4,12 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, toPosixPath, output, error } = require('./core.cjs');
7
+ const { escapeRegex, loadConfig, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, planningDir, output, error, readSubdirectories } = require('./core.cjs');
8
8
  const { extractFrontmatter } = require('./frontmatter.cjs');
9
- const { writeStateMd } = require('./state.cjs');
9
+ const { writeStateMd, stateExtractField, stateReplaceField, stateReplaceFieldWithFallback } = require('./state.cjs');
10
10
 
11
11
  function cmdPhasesList(cwd, options, raw) {
12
- const phasesDir = path.join(cwd, '.planning', 'phases');
12
+ const phasesDir = path.join(planningDir(cwd), 'phases');
13
13
  const { type, phase, includeArchived } = options;
14
14
 
15
15
  // If no phases directory, return empty
@@ -85,7 +85,7 @@ function cmdPhasesList(cwd, options, raw) {
85
85
  }
86
86
 
87
87
  function cmdPhaseNextDecimal(cwd, basePhase, raw) {
88
- const phasesDir = path.join(cwd, '.planning', 'phases');
88
+ const phasesDir = path.join(planningDir(cwd), 'phases');
89
89
  const normalized = normalizePhaseName(basePhase);
90
90
 
91
91
  // Check if phases directory exists
@@ -154,7 +154,7 @@ function cmdFindPhase(cwd, phase, raw) {
154
154
  error('phase identifier required');
155
155
  }
156
156
 
157
- const phasesDir = path.join(cwd, '.planning', 'phases');
157
+ const phasesDir = path.join(planningDir(cwd), 'phases');
158
158
  const normalized = normalizePhaseName(phase);
159
159
 
160
160
  const notFound = { found: false, directory: null, phase_number: null, phase_name: null, plans: [], summaries: [] };
@@ -180,7 +180,7 @@ function cmdFindPhase(cwd, phase, raw) {
180
180
 
181
181
  const result = {
182
182
  found: true,
183
- directory: toPosixPath(path.join('.planning', 'phases', match)),
183
+ directory: toPosixPath(path.join(path.relative(cwd, planningDir(cwd)), 'phases', match)),
184
184
  phase_number: phaseNumber,
185
185
  phase_name: phaseName,
186
186
  plans,
@@ -203,7 +203,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
203
203
  error('phase required for phase-plan-index');
204
204
  }
205
205
 
206
- const phasesDir = path.join(cwd, '.planning', 'phases');
206
+ const phasesDir = path.join(planningDir(cwd), 'phases');
207
207
  const normalized = normalizePhaseName(phase);
208
208
 
209
209
  // Find phase directory
@@ -308,60 +308,75 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
308
308
  output(result, raw);
309
309
  }
310
310
 
311
- function cmdPhaseAdd(cwd, description, raw) {
311
+ function cmdPhaseAdd(cwd, description, raw, customId) {
312
312
  if (!description) {
313
313
  error('description required for phase add');
314
314
  }
315
315
 
316
- const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
316
+ const config = loadConfig(cwd);
317
+ const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
317
318
  if (!fs.existsSync(roadmapPath)) {
318
319
  error('ROADMAP.md not found');
319
320
  }
320
321
 
321
- const content = fs.readFileSync(roadmapPath, 'utf-8');
322
+ const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
323
+ const content = extractCurrentMilestone(rawContent, cwd);
322
324
  const slug = generateSlugInternal(description);
323
325
 
324
- // Find highest integer phase number
325
- const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
326
- let maxPhase = 0;
327
- let m;
328
- while ((m = phasePattern.exec(content)) !== null) {
329
- const num = parseInt(m[1], 10);
330
- if (num > maxPhase) maxPhase = num;
326
+ let newPhaseId;
327
+ let dirName;
328
+
329
+ if (customId || config.phase_naming === 'custom') {
330
+ // Custom phase naming: use provided ID or generate from description
331
+ newPhaseId = customId || slug.toUpperCase().replace(/-/g, '-');
332
+ if (!newPhaseId) error('--id required when phase_naming is "custom"');
333
+ dirName = `${newPhaseId}-${slug}`;
334
+ } else {
335
+ // Sequential mode: find highest integer phase number (in current milestone only)
336
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
337
+ let maxPhase = 0;
338
+ let m;
339
+ while ((m = phasePattern.exec(content)) !== null) {
340
+ const num = parseInt(m[1], 10);
341
+ if (num > maxPhase) maxPhase = num;
342
+ }
343
+
344
+ newPhaseId = maxPhase + 1;
345
+ const paddedNum = String(newPhaseId).padStart(2, '0');
346
+ dirName = `${paddedNum}-${slug}`;
331
347
  }
332
348
 
333
- const newPhaseNum = maxPhase + 1;
334
- const paddedNum = String(newPhaseNum).padStart(2, '0');
335
- const dirName = `${paddedNum}-${slug}`;
336
- const dirPath = path.join(cwd, '.planning', 'phases', dirName);
349
+ const dirPath = path.join(planningDir(cwd), 'phases', dirName);
337
350
 
338
351
  // Create directory with .gitkeep so git tracks empty folders
339
352
  fs.mkdirSync(dirPath, { recursive: true });
340
353
  fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
341
354
 
342
355
  // Build phase entry
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`;
356
+ const dependsOn = config.phase_naming === 'custom' ? '' : `\n**Depends on:** Phase ${typeof newPhaseId === 'number' ? newPhaseId - 1 : 'TBD'}`;
357
+ const phaseEntry = `\n### Phase ${newPhaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd-plan-phase ${newPhaseId} to break down)\n`;
344
358
 
345
359
  // Find insertion point: before last "---" or at end
346
360
  let updatedContent;
347
- const lastSeparator = content.lastIndexOf('\n---');
361
+ const lastSeparator = rawContent.lastIndexOf('\n---');
348
362
  if (lastSeparator > 0) {
349
- updatedContent = content.slice(0, lastSeparator) + phaseEntry + content.slice(lastSeparator);
363
+ updatedContent = rawContent.slice(0, lastSeparator) + phaseEntry + rawContent.slice(lastSeparator);
350
364
  } else {
351
- updatedContent = content + phaseEntry;
365
+ updatedContent = rawContent + phaseEntry;
352
366
  }
353
367
 
354
368
  fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
355
369
 
356
370
  const result = {
357
- phase_number: newPhaseNum,
358
- padded: paddedNum,
371
+ phase_number: typeof newPhaseId === 'number' ? newPhaseId : String(newPhaseId),
372
+ padded: typeof newPhaseId === 'number' ? String(newPhaseId).padStart(2, '0') : String(newPhaseId),
359
373
  name: description,
360
374
  slug,
361
- directory: `.planning/phases/${dirName}`,
375
+ directory: toPosixPath(path.join(path.relative(cwd, planningDir(cwd)), 'phases', dirName)),
376
+ naming_mode: config.phase_naming,
362
377
  };
363
378
 
364
- output(result, raw, paddedNum);
379
+ output(result, raw, result.padded);
365
380
  }
366
381
 
367
382
  function cmdPhaseInsert(cwd, afterPhase, description, raw) {
@@ -369,12 +384,13 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
369
384
  error('after-phase and description required for phase insert');
370
385
  }
371
386
 
372
- const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
387
+ const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
373
388
  if (!fs.existsSync(roadmapPath)) {
374
389
  error('ROADMAP.md not found');
375
390
  }
376
391
 
377
- const content = fs.readFileSync(roadmapPath, 'utf-8');
392
+ const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
393
+ const content = extractCurrentMilestone(rawContent, cwd);
378
394
  const slug = generateSlugInternal(description);
379
395
 
380
396
  // Normalize input then strip leading zeros for flexible matching
@@ -387,7 +403,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
387
403
  }
388
404
 
389
405
  // Calculate next decimal using existing logic
390
- const phasesDir = path.join(cwd, '.planning', 'phases');
406
+ const phasesDir = path.join(planningDir(cwd), 'phases');
391
407
  const normalizedBase = normalizePhaseName(afterPhase);
392
408
  let existingDecimals = [];
393
409
 
@@ -399,12 +415,12 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
399
415
  const dm = dir.match(decimalPattern);
400
416
  if (dm) existingDecimals.push(parseInt(dm[1], 10));
401
417
  }
402
- } catch {}
418
+ } catch { /* intentionally empty */ }
403
419
 
404
420
  const nextDecimal = existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1;
405
421
  const decimalPhase = `${normalizedBase}.${nextDecimal}`;
406
422
  const dirName = `${decimalPhase}-${slug}`;
407
- const dirPath = path.join(cwd, '.planning', 'phases', dirName);
423
+ const dirPath = path.join(planningDir(cwd), 'phases', dirName);
408
424
 
409
425
  // Create directory with .gitkeep so git tracks empty folders
410
426
  fs.mkdirSync(dirPath, { recursive: true });
@@ -415,23 +431,23 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
415
431
 
416
432
  // Insert after the target phase section
417
433
  const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
418
- const headerMatch = content.match(headerPattern);
434
+ const headerMatch = rawContent.match(headerPattern);
419
435
  if (!headerMatch) {
420
436
  error(`Could not find Phase ${afterPhase} header`);
421
437
  }
422
438
 
423
- const headerIdx = content.indexOf(headerMatch[0]);
424
- const afterHeader = content.slice(headerIdx + headerMatch[0].length);
439
+ const headerIdx = rawContent.indexOf(headerMatch[0]);
440
+ const afterHeader = rawContent.slice(headerIdx + headerMatch[0].length);
425
441
  const nextPhaseMatch = afterHeader.match(/\n#{2,4}\s+Phase\s+\d/i);
426
442
 
427
443
  let insertIdx;
428
444
  if (nextPhaseMatch) {
429
445
  insertIdx = headerIdx + headerMatch[0].length + nextPhaseMatch.index;
430
446
  } else {
431
- insertIdx = content.length;
447
+ insertIdx = rawContent.length;
432
448
  }
433
449
 
434
- const updatedContent = content.slice(0, insertIdx) + phaseEntry + content.slice(insertIdx);
450
+ const updatedContent = rawContent.slice(0, insertIdx) + phaseEntry + rawContent.slice(insertIdx);
435
451
  fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
436
452
 
437
453
  const result = {
@@ -439,263 +455,175 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
439
455
  after_phase: afterPhase,
440
456
  name: description,
441
457
  slug,
442
- directory: `.planning/phases/${dirName}`,
458
+ directory: toPosixPath(path.join(path.relative(cwd, planningDir(cwd)), 'phases', dirName)),
443
459
  };
444
460
 
445
461
  output(result, raw, decimalPhase);
446
462
  }
447
463
 
448
- function cmdPhaseRemove(cwd, targetPhase, options, raw) {
449
- if (!targetPhase) {
450
- error('phase number required for phase remove');
464
+ /**
465
+ * Renumber sibling decimal phases after a decimal phase is removed.
466
+ * e.g. removing 06.2 → 06.3 becomes 06.2, 06.4 becomes 06.3, etc.
467
+ * Returns { renamedDirs, renamedFiles }.
468
+ */
469
+ function renameDecimalPhases(phasesDir, baseInt, removedDecimal) {
470
+ const renamedDirs = [], renamedFiles = [];
471
+ const decPattern = new RegExp(`^${baseInt}\\.(\\d+)-(.+)$`);
472
+ const dirs = readSubdirectories(phasesDir, true);
473
+ const toRename = dirs
474
+ .map(dir => { const m = dir.match(decPattern); return m ? { dir, oldDecimal: parseInt(m[1], 10), slug: m[2] } : null; })
475
+ .filter(item => item && item.oldDecimal > removedDecimal)
476
+ .sort((a, b) => b.oldDecimal - a.oldDecimal); // descending to avoid conflicts
477
+
478
+ for (const item of toRename) {
479
+ const newDecimal = item.oldDecimal - 1;
480
+ const oldPhaseId = `${baseInt}.${item.oldDecimal}`;
481
+ const newPhaseId = `${baseInt}.${newDecimal}`;
482
+ const newDirName = `${baseInt}.${newDecimal}-${item.slug}`;
483
+ fs.renameSync(path.join(phasesDir, item.dir), path.join(phasesDir, newDirName));
484
+ renamedDirs.push({ from: item.dir, to: newDirName });
485
+ for (const f of fs.readdirSync(path.join(phasesDir, newDirName))) {
486
+ if (f.includes(oldPhaseId)) {
487
+ const newFileName = f.replace(oldPhaseId, newPhaseId);
488
+ fs.renameSync(path.join(phasesDir, newDirName, f), path.join(phasesDir, newDirName, newFileName));
489
+ renamedFiles.push({ from: f, to: newFileName });
490
+ }
491
+ }
451
492
  }
493
+ return { renamedDirs, renamedFiles };
494
+ }
452
495
 
453
- const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
454
- const phasesDir = path.join(cwd, '.planning', 'phases');
455
- const force = options.force || false;
456
-
457
- if (!fs.existsSync(roadmapPath)) {
458
- error('ROADMAP.md not found');
496
+ /**
497
+ * Renumber all integer phases after removedInt.
498
+ * e.g. removing phase 5 → phase 6 becomes 5, phase 7 becomes 6, etc.
499
+ * Returns { renamedDirs, renamedFiles }.
500
+ */
501
+ function renameIntegerPhases(phasesDir, removedInt) {
502
+ const renamedDirs = [], renamedFiles = [];
503
+ const dirs = readSubdirectories(phasesDir, true);
504
+ const toRename = dirs
505
+ .map(dir => {
506
+ const m = dir.match(/^(\d+)([A-Z])?(?:\.(\d+))?-(.+)$/i);
507
+ if (!m) return null;
508
+ const dirInt = parseInt(m[1], 10);
509
+ return dirInt > removedInt ? { dir, oldInt: dirInt, letter: m[2] ? m[2].toUpperCase() : '', decimal: m[3] ? parseInt(m[3], 10) : null, slug: m[4] } : null;
510
+ })
511
+ .filter(Boolean)
512
+ .sort((a, b) => a.oldInt !== b.oldInt ? b.oldInt - a.oldInt : (b.decimal || 0) - (a.decimal || 0));
513
+
514
+ for (const item of toRename) {
515
+ const newInt = item.oldInt - 1;
516
+ const newPadded = String(newInt).padStart(2, '0');
517
+ const oldPadded = String(item.oldInt).padStart(2, '0');
518
+ const letterSuffix = item.letter || '';
519
+ const decimalSuffix = item.decimal !== null ? `.${item.decimal}` : '';
520
+ const oldPrefix = `${oldPadded}${letterSuffix}${decimalSuffix}`;
521
+ const newPrefix = `${newPadded}${letterSuffix}${decimalSuffix}`;
522
+ const newDirName = `${newPrefix}-${item.slug}`;
523
+ fs.renameSync(path.join(phasesDir, item.dir), path.join(phasesDir, newDirName));
524
+ renamedDirs.push({ from: item.dir, to: newDirName });
525
+ for (const f of fs.readdirSync(path.join(phasesDir, newDirName))) {
526
+ if (f.startsWith(oldPrefix)) {
527
+ const newFileName = newPrefix + f.slice(oldPrefix.length);
528
+ fs.renameSync(path.join(phasesDir, newDirName, f), path.join(phasesDir, newDirName, newFileName));
529
+ renamedFiles.push({ from: f, to: newFileName });
530
+ }
531
+ }
459
532
  }
533
+ return { renamedDirs, renamedFiles };
534
+ }
460
535
 
461
- // Normalize the target
462
- const normalized = normalizePhaseName(targetPhase);
463
- const isDecimal = targetPhase.includes('.');
536
+ /**
537
+ * Remove a phase section from ROADMAP.md and renumber all subsequent integer phases.
538
+ */
539
+ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, removedInt) {
540
+ let content = fs.readFileSync(roadmapPath, 'utf-8');
541
+ const escaped = escapeRegex(targetPhase);
464
542
 
465
- // Find and validate target directory
466
- let targetDir = null;
467
- try {
468
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
469
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
470
- targetDir = dirs.find(d => d.startsWith(normalized + '-') || d === normalized);
471
- } catch {}
543
+ content = content.replace(new RegExp(`\\n?#{2,4}\\s*Phase\\s+${escaped}\\s*:[\\s\\S]*?(?=\\n#{2,4}\\s+Phase\\s+\\d|$)`, 'i'), '');
544
+ content = content.replace(new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${escaped}[:\\s][^\\n]*`, 'gi'), '');
545
+ content = content.replace(new RegExp(`\\n?\\|\\s*${escaped}\\.?\\s[^|]*\\|[^\\n]*`, 'gi'), '');
472
546
 
473
- // Check for executed work (SUMMARY.md files)
474
- if (targetDir && !force) {
475
- const targetPath = path.join(phasesDir, targetDir);
476
- const files = fs.readdirSync(targetPath);
477
- const summaries = files.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
478
- if (summaries.length > 0) {
479
- error(`Phase ${targetPhase} has ${summaries.length} executed plan(s). Use --force to remove anyway.`);
547
+ if (!isDecimal) {
548
+ const MAX_PHASE = 99;
549
+ for (let oldNum = MAX_PHASE; oldNum > removedInt; oldNum--) {
550
+ const newNum = oldNum - 1;
551
+ const oldStr = String(oldNum), newStr = String(newNum);
552
+ const oldPad = oldStr.padStart(2, '0'), newPad = newStr.padStart(2, '0');
553
+ content = content.replace(new RegExp(`(#{2,4}\\s*Phase\\s+)${oldStr}(\\s*:)`, 'gi'), `$1${newStr}$2`);
554
+ content = content.replace(new RegExp(`(Phase\\s+)${oldStr}([:\\s])`, 'g'), `$1${newStr}$2`);
555
+ content = content.replace(new RegExp(`${oldPad}-(\\d{2})`, 'g'), `${newPad}-$1`);
556
+ content = content.replace(new RegExp(`(\\|\\s*)${oldStr}\\.\\s`, 'g'), `$1${newStr}. `);
557
+ content = content.replace(new RegExp(`(Depends on:\\*\\*\\s*Phase\\s+)${oldStr}\\b`, 'gi'), `$1${newStr}`);
480
558
  }
481
559
  }
482
560
 
483
- // Delete target directory
484
- if (targetDir) {
485
- fs.rmSync(path.join(phasesDir, targetDir), { recursive: true, force: true });
486
- }
487
-
488
- // Renumber subsequent phases
489
- const renamedDirs = [];
490
- const renamedFiles = [];
491
-
492
- if (isDecimal) {
493
- // Decimal removal: renumber sibling decimals (e.g., removing 06.2 → 06.3 becomes 06.2)
494
- const baseParts = normalized.split('.');
495
- const baseInt = baseParts[0];
496
- const removedDecimal = parseInt(baseParts[1], 10);
497
-
498
- try {
499
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
500
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
561
+ fs.writeFileSync(roadmapPath, content, 'utf-8');
562
+ }
501
563
 
502
- // Find sibling decimals with higher numbers
503
- const decPattern = new RegExp(`^${baseInt}\\.(\\d+)-(.+)$`);
504
- const toRename = [];
505
- for (const dir of dirs) {
506
- const dm = dir.match(decPattern);
507
- if (dm && parseInt(dm[1], 10) > removedDecimal) {
508
- toRename.push({ dir, oldDecimal: parseInt(dm[1], 10), slug: dm[2] });
509
- }
510
- }
564
+ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
565
+ if (!targetPhase) error('phase number required for phase remove');
511
566
 
512
- // Sort descending to avoid conflicts
513
- toRename.sort((a, b) => b.oldDecimal - a.oldDecimal);
514
-
515
- for (const item of toRename) {
516
- const newDecimal = item.oldDecimal - 1;
517
- const oldPhaseId = `${baseInt}.${item.oldDecimal}`;
518
- const newPhaseId = `${baseInt}.${newDecimal}`;
519
- const newDirName = `${baseInt}.${newDecimal}-${item.slug}`;
520
-
521
- // Rename directory
522
- fs.renameSync(path.join(phasesDir, item.dir), path.join(phasesDir, newDirName));
523
- renamedDirs.push({ from: item.dir, to: newDirName });
524
-
525
- // Rename files inside
526
- const dirFiles = fs.readdirSync(path.join(phasesDir, newDirName));
527
- for (const f of dirFiles) {
528
- // Files may have phase prefix like "06.2-01-PLAN.md"
529
- if (f.includes(oldPhaseId)) {
530
- const newFileName = f.replace(oldPhaseId, newPhaseId);
531
- fs.renameSync(
532
- path.join(phasesDir, newDirName, f),
533
- path.join(phasesDir, newDirName, newFileName)
534
- );
535
- renamedFiles.push({ from: f, to: newFileName });
536
- }
537
- }
538
- }
539
- } catch {}
567
+ const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
568
+ const phasesDir = path.join(planningDir(cwd), 'phases');
540
569
 
541
- } else {
542
- // Integer removal: renumber all subsequent integer phases
543
- const removedInt = parseInt(normalized, 10);
570
+ if (!fs.existsSync(roadmapPath)) error('ROADMAP.md not found');
544
571
 
545
- try {
546
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
547
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
572
+ const normalized = normalizePhaseName(targetPhase);
573
+ const isDecimal = targetPhase.includes('.');
574
+ const force = options.force || false;
548
575
 
549
- // Collect directories that need renumbering (integer phases > removed, and their decimals/letters)
550
- const toRename = [];
551
- for (const dir of dirs) {
552
- const dm = dir.match(/^(\d+)([A-Z])?(?:\.(\d+))?-(.+)$/i);
553
- if (!dm) continue;
554
- const dirInt = parseInt(dm[1], 10);
555
- if (dirInt > removedInt) {
556
- toRename.push({
557
- dir,
558
- oldInt: dirInt,
559
- letter: dm[2] ? dm[2].toUpperCase() : '',
560
- decimal: dm[3] ? parseInt(dm[3], 10) : null,
561
- slug: dm[4],
562
- });
563
- }
564
- }
576
+ // Find target directory
577
+ const targetDir = readSubdirectories(phasesDir, true)
578
+ .find(d => d.startsWith(normalized + '-') || d === normalized) || null;
565
579
 
566
- // Sort descending to avoid conflicts
567
- toRename.sort((a, b) => {
568
- if (a.oldInt !== b.oldInt) return b.oldInt - a.oldInt;
569
- return (b.decimal || 0) - (a.decimal || 0);
570
- });
571
-
572
- for (const item of toRename) {
573
- const newInt = item.oldInt - 1;
574
- const newPadded = String(newInt).padStart(2, '0');
575
- const oldPadded = String(item.oldInt).padStart(2, '0');
576
- const letterSuffix = item.letter || '';
577
- const decimalSuffix = item.decimal !== null ? `.${item.decimal}` : '';
578
- const oldPrefix = `${oldPadded}${letterSuffix}${decimalSuffix}`;
579
- const newPrefix = `${newPadded}${letterSuffix}${decimalSuffix}`;
580
- const newDirName = `${newPrefix}-${item.slug}`;
581
-
582
- // Rename directory
583
- fs.renameSync(path.join(phasesDir, item.dir), path.join(phasesDir, newDirName));
584
- renamedDirs.push({ from: item.dir, to: newDirName });
585
-
586
- // Rename files inside
587
- const dirFiles = fs.readdirSync(path.join(phasesDir, newDirName));
588
- for (const f of dirFiles) {
589
- if (f.startsWith(oldPrefix)) {
590
- const newFileName = newPrefix + f.slice(oldPrefix.length);
591
- fs.renameSync(
592
- path.join(phasesDir, newDirName, f),
593
- path.join(phasesDir, newDirName, newFileName)
594
- );
595
- renamedFiles.push({ from: f, to: newFileName });
596
- }
597
- }
598
- }
599
- } catch {}
580
+ // Guard against removing executed work
581
+ if (targetDir && !force) {
582
+ const files = fs.readdirSync(path.join(phasesDir, targetDir));
583
+ const summaries = files.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
584
+ if (summaries.length > 0) {
585
+ error(`Phase ${targetPhase} has ${summaries.length} executed plan(s). Use --force to remove anyway.`);
586
+ }
600
587
  }
601
588
 
602
- // Update ROADMAP.md
603
- let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
604
-
605
- // Remove the target phase section
606
- const targetEscaped = escapeRegex(targetPhase);
607
- const sectionPattern = new RegExp(
608
- `\\n?#{2,4}\\s*Phase\\s+${targetEscaped}\\s*:[\\s\\S]*?(?=\\n#{2,4}\\s+Phase\\s+\\d|$)`,
609
- 'i'
610
- );
611
- roadmapContent = roadmapContent.replace(sectionPattern, '');
612
-
613
- // Remove from phase list (checkbox)
614
- const checkboxPattern = new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${targetEscaped}[:\\s][^\\n]*`, 'gi');
615
- roadmapContent = roadmapContent.replace(checkboxPattern, '');
616
-
617
- // Remove from progress table
618
- const tableRowPattern = new RegExp(`\\n?\\|\\s*${targetEscaped}\\.?\\s[^|]*\\|[^\\n]*`, 'gi');
619
- roadmapContent = roadmapContent.replace(tableRowPattern, '');
589
+ if (targetDir) fs.rmSync(path.join(phasesDir, targetDir), { recursive: true, force: true });
620
590
 
621
- // Renumber references in ROADMAP for subsequent phases
622
- if (!isDecimal) {
623
- const removedInt = parseInt(normalized, 10);
624
-
625
- // Collect all integer phases > removedInt
626
- const maxPhase = 99; // reasonable upper bound
627
- for (let oldNum = maxPhase; oldNum > removedInt; oldNum--) {
628
- const newNum = oldNum - 1;
629
- const oldStr = String(oldNum);
630
- const newStr = String(newNum);
631
- const oldPad = oldStr.padStart(2, '0');
632
- const newPad = newStr.padStart(2, '0');
633
-
634
- // Phase headings: ## Phase 18: or ### Phase 18: → ## Phase 17: or ### Phase 17:
635
- roadmapContent = roadmapContent.replace(
636
- new RegExp(`(#{2,4}\\s*Phase\\s+)${oldStr}(\\s*:)`, 'gi'),
637
- `$1${newStr}$2`
638
- );
639
-
640
- // Checkbox items: - [ ] **Phase 18:** → - [ ] **Phase 17:**
641
- roadmapContent = roadmapContent.replace(
642
- new RegExp(`(Phase\\s+)${oldStr}([:\\s])`, 'g'),
643
- `$1${newStr}$2`
644
- );
645
-
646
- // Plan references: 18-01 → 17-01
647
- roadmapContent = roadmapContent.replace(
648
- new RegExp(`${oldPad}-(\\d{2})`, 'g'),
649
- `${newPad}-$1`
650
- );
651
-
652
- // Table rows: | 18. → | 17.
653
- roadmapContent = roadmapContent.replace(
654
- new RegExp(`(\\|\\s*)${oldStr}\\.\\s`, 'g'),
655
- `$1${newStr}. `
656
- );
657
-
658
- // Depends on references
659
- roadmapContent = roadmapContent.replace(
660
- new RegExp(`(Depends on:\\*\\*\\s*Phase\\s+)${oldStr}\\b`, 'gi'),
661
- `$1${newStr}`
662
- );
663
- }
664
- }
591
+ // Renumber subsequent phases on disk
592
+ let renamedDirs = [], renamedFiles = [];
593
+ try {
594
+ const renamed = isDecimal
595
+ ? renameDecimalPhases(phasesDir, normalized.split('.')[0], parseInt(normalized.split('.')[1], 10))
596
+ : renameIntegerPhases(phasesDir, parseInt(normalized, 10));
597
+ renamedDirs = renamed.renamedDirs;
598
+ renamedFiles = renamed.renamedFiles;
599
+ } catch { /* intentionally empty */ }
665
600
 
666
- fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
601
+ // Update ROADMAP.md
602
+ updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, parseInt(normalized, 10));
667
603
 
668
604
  // Update STATE.md phase count
669
- const statePath = path.join(cwd, '.planning', 'STATE.md');
605
+ const statePath = path.join(planningDir(cwd), 'STATE.md');
670
606
  if (fs.existsSync(statePath)) {
671
607
  let stateContent = fs.readFileSync(statePath, 'utf-8');
672
- // Update "Total Phases" field
673
- const totalPattern = /(\*\*Total Phases:\*\*\s*)(\d+)/;
674
- const totalMatch = stateContent.match(totalPattern);
675
- if (totalMatch) {
676
- const oldTotal = parseInt(totalMatch[2], 10);
677
- stateContent = stateContent.replace(totalPattern, `$1${oldTotal - 1}`);
608
+ const totalRaw = stateExtractField(stateContent, 'Total Phases');
609
+ if (totalRaw) {
610
+ stateContent = stateReplaceField(stateContent, 'Total Phases', String(parseInt(totalRaw, 10) - 1)) || stateContent;
678
611
  }
679
- // Update "Phase: X of Y" pattern
680
- const ofPattern = /(\bof\s+)(\d+)(\s*(?:\(|phases?))/i;
681
- const ofMatch = stateContent.match(ofPattern);
612
+ const ofMatch = stateContent.match(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i);
682
613
  if (ofMatch) {
683
- const oldTotal = parseInt(ofMatch[2], 10);
684
- stateContent = stateContent.replace(ofPattern, `$1${oldTotal - 1}$3`);
614
+ stateContent = stateContent.replace(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i, `$1${parseInt(ofMatch[2], 10) - 1}$3`);
685
615
  }
686
616
  writeStateMd(statePath, stateContent, cwd);
687
617
  }
688
618
 
689
- const result = {
619
+ output({
690
620
  removed: targetPhase,
691
- directory_deleted: targetDir || null,
621
+ directory_deleted: targetDir,
692
622
  renamed_directories: renamedDirs,
693
623
  renamed_files: renamedFiles,
694
624
  roadmap_updated: true,
695
625
  state_updated: fs.existsSync(statePath),
696
- };
697
-
698
- output(result, raw);
626
+ }, raw);
699
627
  }
700
628
 
701
629
  function cmdPhaseComplete(cwd, phaseNum, raw) {
@@ -703,9 +631,9 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
703
631
  error('phase number required for phase complete');
704
632
  }
705
633
 
706
- const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
707
- const statePath = path.join(cwd, '.planning', 'STATE.md');
708
- const phasesDir = path.join(cwd, '.planning', 'phases');
634
+ const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
635
+ const statePath = path.join(planningDir(cwd), 'STATE.md');
636
+ const phasesDir = path.join(planningDir(cwd), 'phases');
709
637
  const normalized = normalizePhaseName(phaseNum);
710
638
  const today = new Date().toISOString().split('T')[0];
711
639
 
@@ -717,6 +645,28 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
717
645
 
718
646
  const planCount = phaseInfo.plans.length;
719
647
  const summaryCount = phaseInfo.summaries.length;
648
+ let requirementsUpdated = false;
649
+
650
+ // Check for unresolved verification debt (non-blocking warnings)
651
+ const warnings = [];
652
+ try {
653
+ const phaseFullDir = path.join(cwd, phaseInfo.directory);
654
+ const phaseFiles = fs.readdirSync(phaseFullDir);
655
+
656
+ for (const file of phaseFiles.filter(f => f.includes('-UAT') && f.endsWith('.md'))) {
657
+ const content = fs.readFileSync(path.join(phaseFullDir, file), 'utf-8');
658
+ if (/result: pending/.test(content)) warnings.push(`${file}: has pending tests`);
659
+ if (/result: blocked/.test(content)) warnings.push(`${file}: has blocked tests`);
660
+ if (/status: partial/.test(content)) warnings.push(`${file}: testing incomplete (partial)`);
661
+ if (/status: diagnosed/.test(content)) warnings.push(`${file}: has diagnosed gaps`);
662
+ }
663
+
664
+ for (const file of phaseFiles.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md'))) {
665
+ const content = fs.readFileSync(path.join(phaseFullDir, file), 'utf-8');
666
+ if (/status: human_needed/.test(content)) warnings.push(`${file}: needs human verification`);
667
+ if (/status: gaps_found/.test(content)) warnings.push(`${file}: has unresolved gaps`);
668
+ }
669
+ } catch {}
720
670
 
721
671
  // Update ROADMAP.md: mark phase complete
722
672
  if (fs.existsSync(roadmapPath)) {
@@ -727,39 +677,53 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
727
677
  `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s][^\\n]*)`,
728
678
  'i'
729
679
  );
730
- roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
680
+ roadmapContent = replaceInCurrentMilestone(roadmapContent, checkboxPattern, `$1x$2 (completed ${today})`);
731
681
 
732
- // Progress table: update Status to Complete, add date
682
+ // Progress table: update Status to Complete, add date (handles 4 or 5 column tables)
733
683
  const phaseEscaped = escapeRegex(phaseNum);
734
- const tablePattern = new RegExp(
735
- `(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|[^|]*\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`,
736
- 'i'
737
- );
738
- roadmapContent = roadmapContent.replace(
739
- tablePattern,
740
- `$1 Complete $2 ${today} $3`
684
+ const tableRowPattern = new RegExp(
685
+ `^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
686
+ 'im'
741
687
  );
688
+ roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
689
+ const cells = fullRow.split('|').slice(1, -1);
690
+ if (cells.length === 5) {
691
+ // 5-col: Phase | Milestone | Plans | Status | Completed
692
+ cells[3] = ' Complete ';
693
+ cells[4] = ` ${today} `;
694
+ } else if (cells.length === 4) {
695
+ // 4-col: Phase | Plans | Status | Completed
696
+ cells[2] = ' Complete ';
697
+ cells[3] = ` ${today} `;
698
+ }
699
+ return '|' + cells.join('|') + '|';
700
+ });
742
701
 
743
702
  // Update plan count in phase section
744
703
  const planCountPattern = new RegExp(
745
704
  `(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
746
705
  'i'
747
706
  );
748
- roadmapContent = roadmapContent.replace(
749
- planCountPattern,
707
+ roadmapContent = replaceInCurrentMilestone(
708
+ roadmapContent, planCountPattern,
750
709
  `$1${summaryCount}/${planCount} plans complete`
751
710
  );
752
711
 
753
712
  fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
754
713
 
755
714
  // Update REQUIREMENTS.md traceability for this phase's requirements
756
- const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md');
715
+ const reqPath = path.join(planningDir(cwd), 'REQUIREMENTS.md');
757
716
  if (fs.existsSync(reqPath)) {
758
- // Extract Requirements line from roadmap for this phase
759
- const reqMatch = roadmapContent.match(
760
- new RegExp(`Phase\\s+${escapeRegex(phaseNum)}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, 'i')
717
+ // Extract the current phase section from roadmap (scoped to avoid cross-phase matching)
718
+ const phaseEsc = escapeRegex(phaseNum);
719
+ const currentMilestoneRoadmap = extractCurrentMilestone(roadmapContent, cwd);
720
+ const phaseSectionMatch = currentMilestoneRoadmap.match(
721
+ new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEsc}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i')
761
722
  );
762
723
 
724
+ const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : '';
725
+ const reqMatch = sectionText.match(/\*\*Requirements:\*\*\s*([^\n]+)/i);
726
+
763
727
  if (reqMatch) {
764
728
  const reqIds = reqMatch[1].replace(/[\[\]]/g, '').split(/[,\s]+/).map(r => r.trim()).filter(Boolean);
765
729
  let reqContent = fs.readFileSync(reqPath, 'utf-8');
@@ -771,14 +735,15 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
771
735
  new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi'),
772
736
  '$1x$2'
773
737
  );
774
- // Update traceability table: | REQ-ID | Phase N | Pending | → | REQ-ID | Phase N | Complete |
738
+ // Update traceability table: | REQ-ID | Phase N | Pending/In Progress | → | REQ-ID | Phase N | Complete |
775
739
  reqContent = reqContent.replace(
776
- new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'),
740
+ new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*(?:Pending|In Progress)\\s*(\\|)`, 'gi'),
777
741
  '$1 Complete $2'
778
742
  );
779
743
  }
780
744
 
781
745
  fs.writeFileSync(reqPath, reqContent, 'utf-8');
746
+ requirementsUpdated = true;
782
747
  }
783
748
  }
784
749
  }
@@ -809,13 +774,13 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
809
774
  }
810
775
  }
811
776
  }
812
- } catch {}
777
+ } catch { /* intentionally empty */ }
813
778
 
814
779
  // Fallback: if filesystem found no next phase, check ROADMAP.md
815
780
  // for phases that are defined but not yet planned (no directory on disk)
816
781
  if (isLastPhase && fs.existsSync(roadmapPath)) {
817
782
  try {
818
- const roadmapForPhases = fs.readFileSync(roadmapPath, 'utf-8');
783
+ const roadmapForPhases = extractCurrentMilestone(fs.readFileSync(roadmapPath, 'utf-8'), cwd);
819
784
  const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
820
785
  let pm;
821
786
  while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
@@ -826,50 +791,69 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
826
791
  break;
827
792
  }
828
793
  }
829
- } catch {}
794
+ } catch { /* intentionally empty */ }
830
795
  }
831
796
 
832
- // Update STATE.md
797
+ // Update STATE.md — use shared helpers that handle both **bold:** and plain Field: formats
833
798
  if (fs.existsSync(statePath)) {
834
799
  let stateContent = fs.readFileSync(statePath, 'utf-8');
835
800
 
836
- // Update Current Phase
837
- stateContent = stateContent.replace(
838
- /(\*\*Current Phase:\*\*\s*).*/,
839
- `$1${nextPhaseNum || phaseNum}`
840
- );
801
+ // Update Current Phase — preserve "X of Y (Name)" compound format
802
+ const phaseValue = nextPhaseNum || phaseNum;
803
+ const existingPhaseField = stateExtractField(stateContent, 'Current Phase')
804
+ || stateExtractField(stateContent, 'Phase');
805
+ let newPhaseValue = String(phaseValue);
806
+ if (existingPhaseField) {
807
+ const totalMatch = existingPhaseField.match(/of\s+(\d+)/);
808
+ const nameMatch = existingPhaseField.match(/\(([^)]+)\)/);
809
+ if (totalMatch) {
810
+ const total = totalMatch[1];
811
+ const nameStr = nextPhaseName ? ` (${nextPhaseName.replace(/-/g, ' ')})` : (nameMatch ? ` (${nameMatch[1]})` : '');
812
+ newPhaseValue = `${phaseValue} of ${total}${nameStr}`;
813
+ }
814
+ }
815
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase', 'Phase', newPhaseValue);
841
816
 
842
817
  // Update Current Phase Name
843
818
  if (nextPhaseName) {
844
- stateContent = stateContent.replace(
845
- /(\*\*Current Phase Name:\*\*\s*).*/,
846
- `$1${nextPhaseName.replace(/-/g, ' ')}`
847
- );
819
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase Name', null, nextPhaseName.replace(/-/g, ' '));
848
820
  }
849
821
 
850
822
  // Update Status
851
- stateContent = stateContent.replace(
852
- /(\*\*Status:\*\*\s*).*/,
853
- `$1${isLastPhase ? 'Milestone complete' : 'Ready to plan'}`
854
- );
823
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null,
824
+ isLastPhase ? 'Milestone complete' : 'Ready to plan');
855
825
 
856
826
  // Update Current Plan
857
- stateContent = stateContent.replace(
858
- /(\*\*Current Plan:\*\*\s*).*/,
859
- `$1Not started`
860
- );
827
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Plan', 'Plan', 'Not started');
861
828
 
862
829
  // Update Last Activity
863
- stateContent = stateContent.replace(
864
- /(\*\*Last Activity:\*\*\s*).*/,
865
- `$1${today}`
866
- );
830
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity', 'Last activity', today);
867
831
 
868
832
  // Update Last Activity Description
869
- stateContent = stateContent.replace(
870
- /(\*\*Last Activity Description:\*\*\s*).*/,
871
- `$1Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`
872
- );
833
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity Description', null,
834
+ `Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`);
835
+
836
+ // Increment Completed Phases counter (#956)
837
+ const completedRaw = stateExtractField(stateContent, 'Completed Phases');
838
+ if (completedRaw) {
839
+ const newCompleted = parseInt(completedRaw, 10) + 1;
840
+ stateContent = stateReplaceField(stateContent, 'Completed Phases', String(newCompleted)) || stateContent;
841
+
842
+ // Recalculate percent based on completed / total (#956)
843
+ const totalRaw = stateExtractField(stateContent, 'Total Phases');
844
+ if (totalRaw) {
845
+ const totalPhases = parseInt(totalRaw, 10);
846
+ if (totalPhases > 0) {
847
+ const newPercent = Math.round((newCompleted / totalPhases) * 100);
848
+ stateContent = stateReplaceField(stateContent, 'Progress', `${newPercent}%`) || stateContent;
849
+ // Also update percent field if it exists separately
850
+ stateContent = stateContent.replace(
851
+ /(percent:\s*)\d+/,
852
+ `$1${newPercent}`
853
+ );
854
+ }
855
+ }
856
+ }
873
857
 
874
858
  writeStateMd(statePath, stateContent, cwd);
875
859
  }
@@ -884,6 +868,9 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
884
868
  date: today,
885
869
  roadmap_updated: fs.existsSync(roadmapPath),
886
870
  state_updated: fs.existsSync(statePath),
871
+ requirements_updated: requirementsUpdated,
872
+ warnings,
873
+ has_warnings: warnings.length > 0,
887
874
  };
888
875
 
889
876
  output(result, raw);