@bhargavvc/sdd-cc 1.30.1 → 1.35.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 (242) hide show
  1. package/README.ja-JP.md +144 -110
  2. package/README.ko-KR.md +143 -107
  3. package/README.md +183 -112
  4. package/README.pt-BR.md +90 -52
  5. package/README.zh-CN.md +141 -101
  6. package/agents/sdd-advisor-researcher.md +23 -0
  7. package/agents/sdd-ai-researcher.md +133 -0
  8. package/agents/sdd-code-fixer.md +516 -0
  9. package/agents/sdd-code-reviewer.md +355 -0
  10. package/agents/sdd-codebase-mapper.md +3 -3
  11. package/agents/sdd-debugger.md +17 -5
  12. package/agents/sdd-doc-verifier.md +201 -0
  13. package/agents/sdd-doc-writer.md +602 -0
  14. package/agents/sdd-domain-researcher.md +153 -0
  15. package/agents/sdd-eval-auditor.md +164 -0
  16. package/agents/sdd-eval-planner.md +154 -0
  17. package/agents/sdd-executor.md +87 -4
  18. package/agents/sdd-framework-selector.md +160 -0
  19. package/agents/sdd-intel-updater.md +314 -0
  20. package/agents/sdd-nyquist-auditor.md +1 -1
  21. package/agents/sdd-phase-researcher.md +71 -4
  22. package/agents/sdd-plan-checker.md +100 -6
  23. package/agents/sdd-planner.md +145 -206
  24. package/agents/sdd-project-researcher.md +25 -2
  25. package/agents/sdd-research-synthesizer.md +3 -3
  26. package/agents/sdd-roadmapper.md +6 -6
  27. package/agents/sdd-security-auditor.md +128 -0
  28. package/agents/sdd-ui-auditor.md +43 -3
  29. package/agents/sdd-ui-checker.md +5 -5
  30. package/agents/sdd-ui-researcher.md +27 -4
  31. package/agents/sdd-user-profiler.md +2 -2
  32. package/agents/sdd-verifier.md +142 -22
  33. package/bin/install.js +2145 -545
  34. package/commands/sdd/add-backlog.md +5 -5
  35. package/commands/sdd/add-tests.md +2 -2
  36. package/commands/sdd/ai-integration-phase.md +36 -0
  37. package/commands/sdd/analyze-dependencies.md +34 -0
  38. package/commands/sdd/audit-fix.md +33 -0
  39. package/commands/sdd/autonomous.md +7 -2
  40. package/commands/sdd/cleanup.md +5 -0
  41. package/commands/sdd/code-review-fix.md +52 -0
  42. package/commands/sdd/code-review.md +55 -0
  43. package/commands/sdd/complete-milestone.md +6 -6
  44. package/commands/sdd/debug.md +22 -9
  45. package/commands/sdd/discuss-phase.md +7 -2
  46. package/commands/sdd/do.md +1 -1
  47. package/commands/sdd/docs-update.md +48 -0
  48. package/commands/sdd/eval-review.md +32 -0
  49. package/commands/sdd/execute-phase.md +4 -0
  50. package/commands/sdd/explore.md +27 -0
  51. package/commands/sdd/fast.md +2 -2
  52. package/commands/sdd/from-sdd2.md +45 -0
  53. package/commands/sdd/help.md +2 -0
  54. package/commands/sdd/import.md +36 -0
  55. package/commands/sdd/intel.md +179 -0
  56. package/commands/sdd/join-discord.md +2 -1
  57. package/commands/sdd/manager.md +1 -0
  58. package/commands/sdd/map-codebase.md +3 -3
  59. package/commands/sdd/new-milestone.md +1 -1
  60. package/commands/sdd/new-project.md +5 -1
  61. package/commands/sdd/new-workspace.md +1 -1
  62. package/commands/sdd/next.md +2 -0
  63. package/commands/sdd/plan-milestone-gaps.md +2 -2
  64. package/commands/sdd/plan-phase.md +6 -1
  65. package/commands/sdd/plant-seed.md +1 -1
  66. package/commands/sdd/profile-user.md +1 -1
  67. package/commands/sdd/quick.md +5 -3
  68. package/commands/sdd/reapply-patches.md +230 -42
  69. package/commands/sdd/research-phase.md +3 -3
  70. package/commands/sdd/review-backlog.md +1 -0
  71. package/commands/sdd/review.md +6 -3
  72. package/commands/sdd/scan.md +26 -0
  73. package/commands/sdd/secure-phase.md +35 -0
  74. package/commands/sdd/ship.md +1 -1
  75. package/commands/sdd/thread.md +5 -5
  76. package/commands/sdd/undo.md +34 -0
  77. package/commands/sdd/verify-work.md +1 -1
  78. package/commands/sdd/workstreams.md +17 -11
  79. package/hooks/dist/sdd-check-update.js +33 -8
  80. package/hooks/dist/sdd-context-monitor.js +17 -8
  81. package/hooks/dist/sdd-phase-boundary.sh +27 -0
  82. package/hooks/dist/sdd-prompt-guard.js +1 -0
  83. package/hooks/dist/sdd-read-guard.js +82 -0
  84. package/hooks/dist/sdd-session-state.sh +33 -0
  85. package/hooks/dist/sdd-statusline.js +137 -15
  86. package/hooks/dist/sdd-validate-commit.sh +47 -0
  87. package/hooks/dist/sdd-workflow-guard.js +4 -4
  88. package/hooks/sdd-check-update.js +139 -0
  89. package/hooks/sdd-context-monitor.js +165 -0
  90. package/hooks/sdd-phase-boundary.sh +27 -0
  91. package/hooks/sdd-prompt-guard.js +97 -0
  92. package/hooks/sdd-read-guard.js +82 -0
  93. package/hooks/sdd-session-state.sh +33 -0
  94. package/hooks/sdd-statusline.js +241 -0
  95. package/hooks/sdd-validate-commit.sh +47 -0
  96. package/hooks/sdd-workflow-guard.js +94 -0
  97. package/package.json +3 -3
  98. package/scripts/build-hooks.js +18 -7
  99. package/scripts/prompt-injection-scan.sh +1 -0
  100. package/scripts/rebrand-gsd-to-sdd.sh +221 -220
  101. package/scripts/run-tests.cjs +5 -1
  102. package/scripts/sync-upstream.sh +1 -1
  103. package/sdd/bin/lib/commands.cjs +79 -17
  104. package/sdd/bin/lib/config.cjs +90 -48
  105. package/sdd/bin/lib/core.cjs +452 -87
  106. package/sdd/bin/lib/docs.cjs +267 -0
  107. package/sdd/bin/lib/frontmatter.cjs +381 -336
  108. package/sdd/bin/lib/init.cjs +110 -16
  109. package/sdd/bin/lib/intel.cjs +660 -0
  110. package/sdd/bin/lib/learnings.cjs +378 -0
  111. package/sdd/bin/lib/milestone.cjs +42 -11
  112. package/sdd/bin/lib/model-profiles.cjs +17 -15
  113. package/sdd/bin/lib/phase.cjs +367 -288
  114. package/sdd/bin/lib/profile-output.cjs +106 -10
  115. package/sdd/bin/lib/roadmap.cjs +146 -115
  116. package/sdd/bin/lib/schema-detect.cjs +238 -0
  117. package/sdd/bin/lib/sdd2-import.cjs +511 -0
  118. package/sdd/bin/lib/security.cjs +124 -3
  119. package/sdd/bin/lib/state.cjs +648 -264
  120. package/sdd/bin/lib/template.cjs +8 -4
  121. package/sdd/bin/lib/verify.cjs +209 -28
  122. package/sdd/bin/lib/workstream.cjs +7 -3
  123. package/sdd/bin/sdd-tools.cjs +184 -12
  124. package/sdd/contexts/dev.md +21 -0
  125. package/sdd/contexts/research.md +22 -0
  126. package/sdd/contexts/review.md +22 -0
  127. package/sdd/references/agent-contracts.md +79 -0
  128. package/sdd/references/ai-evals.md +156 -0
  129. package/sdd/references/ai-frameworks.md +186 -0
  130. package/sdd/references/artifact-types.md +113 -0
  131. package/sdd/references/common-bug-patterns.md +114 -0
  132. package/sdd/references/context-budget.md +49 -0
  133. package/sdd/references/continuation-format.md +25 -25
  134. package/sdd/references/domain-probes.md +125 -0
  135. package/sdd/references/few-shot-examples/plan-checker.md +73 -0
  136. package/sdd/references/few-shot-examples/verifier.md +109 -0
  137. package/sdd/references/gate-prompts.md +100 -0
  138. package/sdd/references/gates.md +70 -0
  139. package/sdd/references/git-integration.md +1 -1
  140. package/sdd/references/ios-scaffold.md +123 -0
  141. package/sdd/references/model-profile-resolution.md +2 -0
  142. package/sdd/references/model-profiles.md +24 -18
  143. package/sdd/references/planner-gap-closure.md +62 -0
  144. package/sdd/references/planner-reviews.md +39 -0
  145. package/sdd/references/planner-revision.md +87 -0
  146. package/sdd/references/planning-config.md +252 -0
  147. package/sdd/references/revision-loop.md +97 -0
  148. package/sdd/references/thinking-models-debug.md +44 -0
  149. package/sdd/references/thinking-models-execution.md +50 -0
  150. package/sdd/references/thinking-models-planning.md +62 -0
  151. package/sdd/references/thinking-models-research.md +50 -0
  152. package/sdd/references/thinking-models-verification.md +55 -0
  153. package/sdd/references/thinking-partner.md +96 -0
  154. package/sdd/references/ui-brand.md +4 -4
  155. package/sdd/references/universal-anti-patterns.md +63 -0
  156. package/sdd/references/verification-overrides.md +227 -0
  157. package/sdd/references/workstream-flag.md +56 -3
  158. package/sdd/templates/AI-SPEC.md +246 -0
  159. package/sdd/templates/DEBUG.md +1 -1
  160. package/sdd/templates/SECURITY.md +61 -0
  161. package/sdd/templates/UAT.md +4 -4
  162. package/sdd/templates/VALIDATION.md +4 -4
  163. package/sdd/templates/claude-md.md +32 -9
  164. package/sdd/templates/config.json +4 -0
  165. package/sdd/templates/debug-subagent-prompt.md +1 -1
  166. package/sdd/templates/dev-preferences.md +1 -1
  167. package/sdd/templates/discovery.md +2 -2
  168. package/sdd/templates/phase-prompt.md +1 -1
  169. package/sdd/templates/planner-subagent-prompt.md +3 -3
  170. package/sdd/templates/project.md +1 -1
  171. package/sdd/templates/research.md +1 -1
  172. package/sdd/templates/state.md +2 -2
  173. package/sdd/workflows/add-phase.md +8 -8
  174. package/sdd/workflows/add-tests.md +12 -9
  175. package/sdd/workflows/add-todo.md +5 -3
  176. package/sdd/workflows/ai-integration-phase.md +284 -0
  177. package/sdd/workflows/analyze-dependencies.md +96 -0
  178. package/sdd/workflows/audit-fix.md +157 -0
  179. package/sdd/workflows/audit-milestone.md +11 -11
  180. package/sdd/workflows/audit-uat.md +2 -2
  181. package/sdd/workflows/autonomous.md +195 -27
  182. package/sdd/workflows/check-todos.md +12 -10
  183. package/sdd/workflows/cleanup.md +2 -0
  184. package/sdd/workflows/code-review-fix.md +497 -0
  185. package/sdd/workflows/code-review.md +515 -0
  186. package/sdd/workflows/complete-milestone.md +56 -22
  187. package/sdd/workflows/diagnose-issues.md +10 -3
  188. package/sdd/workflows/discovery-phase.md +5 -3
  189. package/sdd/workflows/discuss-phase-assumptions.md +24 -6
  190. package/sdd/workflows/discuss-phase-power.md +291 -0
  191. package/sdd/workflows/discuss-phase.md +173 -21
  192. package/sdd/workflows/do.md +23 -21
  193. package/sdd/workflows/docs-update.md +1155 -0
  194. package/sdd/workflows/eval-review.md +155 -0
  195. package/sdd/workflows/execute-phase.md +594 -38
  196. package/sdd/workflows/execute-plan.md +67 -96
  197. package/sdd/workflows/explore.md +139 -0
  198. package/sdd/workflows/fast.md +5 -5
  199. package/sdd/workflows/forensics.md +2 -2
  200. package/sdd/workflows/health.md +4 -4
  201. package/sdd/workflows/help.md +122 -119
  202. package/sdd/workflows/import.md +276 -0
  203. package/sdd/workflows/inbox.md +387 -0
  204. package/sdd/workflows/insert-phase.md +7 -7
  205. package/sdd/workflows/list-phase-assumptions.md +4 -4
  206. package/sdd/workflows/list-workspaces.md +2 -2
  207. package/sdd/workflows/manager.md +35 -32
  208. package/sdd/workflows/map-codebase.md +7 -5
  209. package/sdd/workflows/milestone-summary.md +2 -2
  210. package/sdd/workflows/new-milestone.md +17 -9
  211. package/sdd/workflows/new-project.md +50 -25
  212. package/sdd/workflows/new-workspace.md +7 -5
  213. package/sdd/workflows/next.md +67 -11
  214. package/sdd/workflows/note.md +9 -7
  215. package/sdd/workflows/pause-work.md +75 -12
  216. package/sdd/workflows/plan-milestone-gaps.md +8 -8
  217. package/sdd/workflows/plan-phase.md +294 -42
  218. package/sdd/workflows/plant-seed.md +6 -3
  219. package/sdd/workflows/pr-branch.md +42 -14
  220. package/sdd/workflows/profile-user.md +9 -7
  221. package/sdd/workflows/progress.md +45 -45
  222. package/sdd/workflows/quick.md +195 -47
  223. package/sdd/workflows/remove-phase.md +6 -6
  224. package/sdd/workflows/remove-workspace.md +3 -1
  225. package/sdd/workflows/research-phase.md +2 -2
  226. package/sdd/workflows/resume-project.md +12 -12
  227. package/sdd/workflows/review.md +109 -9
  228. package/sdd/workflows/scan.md +102 -0
  229. package/sdd/workflows/secure-phase.md +166 -0
  230. package/sdd/workflows/session-report.md +2 -2
  231. package/sdd/workflows/settings.md +38 -12
  232. package/sdd/workflows/ship.md +21 -9
  233. package/sdd/workflows/stats.md +1 -1
  234. package/sdd/workflows/transition.md +23 -23
  235. package/sdd/workflows/ui-phase.md +15 -7
  236. package/sdd/workflows/ui-review.md +29 -4
  237. package/sdd/workflows/undo.md +314 -0
  238. package/sdd/workflows/update.md +171 -20
  239. package/sdd/workflows/validate-phase.md +6 -4
  240. package/sdd/workflows/verify-phase.md +210 -6
  241. package/sdd/workflows/verify-work.md +83 -9
  242. package/sdd/commands/sdd/workstreams.md +0 -63
@@ -4,9 +4,9 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { escapeRegex, loadConfig, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, planningDir, output, error, readSubdirectories } = require('./core.cjs');
7
+ const { escapeRegex, loadConfig, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, planningDir, withPlanningLock, output, error, readSubdirectories, phaseTokenMatches } = require('./core.cjs');
8
8
  const { extractFrontmatter } = require('./frontmatter.cjs');
9
- const { writeStateMd, stateExtractField, stateReplaceField, stateReplaceFieldWithFallback } = require('./state.cjs');
9
+ const { writeStateMd, readModifyWriteStateMd, stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, updatePerformanceMetricsSection } = require('./state.cjs');
10
10
 
11
11
  function cmdPhasesList(cwd, options, raw) {
12
12
  const phasesDir = path.join(planningDir(cwd), 'phases');
@@ -41,7 +41,7 @@ function cmdPhasesList(cwd, options, raw) {
41
41
  // If filtering by phase number
42
42
  if (phase) {
43
43
  const normalized = normalizePhaseName(phase);
44
- const match = dirs.find(d => d.startsWith(normalized));
44
+ const match = dirs.find(d => phaseTokenMatches(d, normalized));
45
45
  if (!match) {
46
46
  output({ files: [], count: 0, phase_dir: null, error: 'Phase not found' }, raw, '');
47
47
  return;
@@ -88,50 +88,49 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
88
88
  const phasesDir = path.join(planningDir(cwd), 'phases');
89
89
  const normalized = normalizePhaseName(basePhase);
90
90
 
91
- // Check if phases directory exists
92
- if (!fs.existsSync(phasesDir)) {
93
- output(
94
- {
95
- found: false,
96
- base_phase: normalized,
97
- next: `${normalized}.1`,
98
- existing: [],
99
- },
100
- raw,
101
- `${normalized}.1`
102
- );
103
- return;
104
- }
105
-
106
91
  try {
107
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
108
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
109
-
110
- // Check if base phase exists
111
- const baseExists = dirs.some(d => d.startsWith(normalized + '-') || d === normalized);
92
+ let baseExists = false;
93
+ const decimalSet = new Set();
112
94
 
113
- // Find existing decimal phases for this base
114
- const decimalPattern = new RegExp(`^${normalized}\\.(\\d+)`);
115
- const existingDecimals = [];
95
+ // Scan directory names for existing decimal phases
96
+ if (fs.existsSync(phasesDir)) {
97
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
98
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
99
+ baseExists = dirs.some(d => phaseTokenMatches(d, normalized));
116
100
 
117
- for (const dir of dirs) {
118
- const match = dir.match(decimalPattern);
119
- if (match) {
120
- existingDecimals.push(`${normalized}.${match[1]}`);
101
+ const dirPattern = new RegExp(`^(?:[A-Z]{1,6}-)?${escapeRegex(normalized)}\\.(\\d+)`);
102
+ for (const dir of dirs) {
103
+ const match = dir.match(dirPattern);
104
+ if (match) decimalSet.add(parseInt(match[1], 10));
121
105
  }
122
106
  }
123
107
 
124
- // Sort numerically
125
- existingDecimals.sort((a, b) => comparePhaseNum(a, b));
108
+ // Also scan ROADMAP.md for phase entries that may not have directories yet
109
+ const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
110
+ if (fs.existsSync(roadmapPath)) {
111
+ try {
112
+ const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
113
+ const phasePattern = new RegExp(
114
+ `#{2,4}\\s*Phase\\s+0*${escapeRegex(normalized)}\\.(\\d+)\\s*:`, 'gi'
115
+ );
116
+ let pm;
117
+ while ((pm = phasePattern.exec(roadmapContent)) !== null) {
118
+ decimalSet.add(parseInt(pm[1], 10));
119
+ }
120
+ } catch { /* ROADMAP.md read failure is non-fatal */ }
121
+ }
122
+
123
+ // Build sorted list of existing decimals
124
+ const existingDecimals = Array.from(decimalSet)
125
+ .sort((a, b) => a - b)
126
+ .map(n => `${normalized}.${n}`);
126
127
 
127
128
  // Calculate next decimal
128
129
  let nextDecimal;
129
- if (existingDecimals.length === 0) {
130
+ if (decimalSet.size === 0) {
130
131
  nextDecimal = `${normalized}.1`;
131
132
  } else {
132
- const lastDecimal = existingDecimals[existingDecimals.length - 1];
133
- const lastNum = parseInt(lastDecimal.split('.')[1], 10);
134
- nextDecimal = `${normalized}.${lastNum + 1}`;
133
+ nextDecimal = `${normalized}.${Math.max(...decimalSet) + 1}`;
135
134
  }
136
135
 
137
136
  output(
@@ -163,13 +162,15 @@ function cmdFindPhase(cwd, phase, raw) {
163
162
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
164
163
  const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
165
164
 
166
- const match = dirs.find(d => d.startsWith(normalized));
165
+ const match = dirs.find(d => phaseTokenMatches(d, normalized));
167
166
  if (!match) {
168
167
  output(notFound, raw, '');
169
168
  return;
170
169
  }
171
170
 
172
- const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
171
+ // Extract phase number — supports project-code-prefixed (CK-01-name), numeric (01-name), and custom IDs
172
+ const dirMatch = match.match(/^(?:[A-Z]{1,6}-)(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i)
173
+ || match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
173
174
  const phaseNumber = dirMatch ? dirMatch[1] : normalized;
174
175
  const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
175
176
 
@@ -212,7 +213,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
212
213
  try {
213
214
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
214
215
  const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
215
- const match = dirs.find(d => d.startsWith(normalized));
216
+ const match = dirs.find(d => phaseTokenMatches(d, normalized));
216
217
  if (match) {
217
218
  phaseDir = path.join(phasesDir, match);
218
219
  phaseDirName = match;
@@ -319,53 +320,81 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
319
320
  error('ROADMAP.md not found');
320
321
  }
321
322
 
322
- const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
323
- const content = extractCurrentMilestone(rawContent, cwd);
324
323
  const slug = generateSlugInternal(description);
325
324
 
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
- }
325
+ // Wrap entire read-modify-write in lock to prevent concurrent corruption
326
+ const { newPhaseId, dirName } = withPlanningLock(cwd, () => {
327
+ const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
328
+ const content = extractCurrentMilestone(rawContent, cwd);
343
329
 
344
- newPhaseId = maxPhase + 1;
345
- const paddedNum = String(newPhaseId).padStart(2, '0');
346
- dirName = `${paddedNum}-${slug}`;
347
- }
330
+ // Optional project code prefix (e.g., 'CK' → 'CK-01-foundation')
331
+ const projectCode = config.project_code || '';
332
+ const prefix = projectCode ? `${projectCode}-` : '';
348
333
 
349
- const dirPath = path.join(planningDir(cwd), 'phases', dirName);
334
+ let _newPhaseId;
335
+ let _dirName;
350
336
 
351
- // Create directory with .gitkeep so git tracks empty folders
352
- fs.mkdirSync(dirPath, { recursive: true });
353
- fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
337
+ if (customId || config.phase_naming === 'custom') {
338
+ // Custom phase naming: use provided ID or generate from description
339
+ _newPhaseId = customId || slug.toUpperCase().replace(/-/g, '-');
340
+ if (!_newPhaseId) error('--id required when phase_naming is "custom"');
341
+ _dirName = `${prefix}${_newPhaseId}-${slug}`;
342
+ } else {
343
+ // Sequential mode: find highest integer phase number from two sources:
344
+ // 1. ROADMAP.md (current milestone only)
345
+ // 2. .planning/phases/ on disk (orphan directories not tracked in roadmap)
346
+ // Skip 999.x backlog phases — they live outside the active sequence
347
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
348
+ let maxPhase = 0;
349
+ let m;
350
+ while ((m = phasePattern.exec(content)) !== null) {
351
+ const num = parseInt(m[1], 10);
352
+ if (num >= 999) continue; // backlog phases use 999.x numbering
353
+ if (num > maxPhase) maxPhase = num;
354
+ }
354
355
 
355
- // Build phase entry
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 /sdd:plan-phase ${newPhaseId} to break down)\n`;
356
+ // Also scan .planning/phases/ for orphan directories not tracked in ROADMAP.
357
+ // Directory names follow: [PREFIX-]NN-slug (e.g. 03-api or CK-05-old-feature).
358
+ // Strip the optional project_code prefix before extracting the leading integer.
359
+ const phasesOnDisk = path.join(planningDir(cwd), 'phases');
360
+ if (fs.existsSync(phasesOnDisk)) {
361
+ const dirNumPattern = /^(?:[A-Z][A-Z0-9]*-)?(\d+)-/;
362
+ for (const entry of fs.readdirSync(phasesOnDisk)) {
363
+ const match = entry.match(dirNumPattern);
364
+ if (!match) continue;
365
+ const num = parseInt(match[1], 10);
366
+ if (num >= 999) continue; // skip backlog orphans
367
+ if (num > maxPhase) maxPhase = num;
368
+ }
369
+ }
358
370
 
359
- // Find insertion point: before last "---" or at end
360
- let updatedContent;
361
- const lastSeparator = rawContent.lastIndexOf('\n---');
362
- if (lastSeparator > 0) {
363
- updatedContent = rawContent.slice(0, lastSeparator) + phaseEntry + rawContent.slice(lastSeparator);
364
- } else {
365
- updatedContent = rawContent + phaseEntry;
366
- }
371
+ _newPhaseId = maxPhase + 1;
372
+ const paddedNum = String(_newPhaseId).padStart(2, '0');
373
+ _dirName = `${prefix}${paddedNum}-${slug}`;
374
+ }
375
+
376
+ const dirPath = path.join(planningDir(cwd), 'phases', _dirName);
367
377
 
368
- fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
378
+ // Create directory with .gitkeep so git tracks empty folders
379
+ fs.mkdirSync(dirPath, { recursive: true });
380
+ fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
381
+
382
+ // Build phase entry
383
+ const dependsOn = config.phase_naming === 'custom' ? '' : `\n**Depends on:** Phase ${typeof _newPhaseId === 'number' ? _newPhaseId - 1 : 'TBD'}`;
384
+ 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 /sdd-plan-phase ${_newPhaseId} to break down)\n`;
385
+
386
+ // Find insertion point: before last "---" or at end
387
+ let updatedContent;
388
+ const lastSeparator = rawContent.lastIndexOf('\n---');
389
+ if (lastSeparator > 0) {
390
+ updatedContent = rawContent.slice(0, lastSeparator) + phaseEntry + rawContent.slice(lastSeparator);
391
+ } else {
392
+ updatedContent = rawContent + phaseEntry;
393
+ }
394
+
395
+ fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
396
+ return { newPhaseId: _newPhaseId, dirName: _dirName };
397
+ });
369
398
 
370
399
  const result = {
371
400
  phase_number: typeof newPhaseId === 'number' ? newPhaseId : String(newPhaseId),
@@ -389,66 +418,84 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
389
418
  error('ROADMAP.md not found');
390
419
  }
391
420
 
392
- const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
393
- const content = extractCurrentMilestone(rawContent, cwd);
394
421
  const slug = generateSlugInternal(description);
395
422
 
396
- // Normalize input then strip leading zeros for flexible matching
397
- const normalizedAfter = normalizePhaseName(afterPhase);
398
- const unpadded = normalizedAfter.replace(/^0+/, '');
399
- const afterPhaseEscaped = unpadded.replace(/\./g, '\\.');
400
- const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:`, 'i');
401
- if (!targetPattern.test(content)) {
402
- error(`Phase ${afterPhase} not found in ROADMAP.md`);
403
- }
404
-
405
- // Calculate next decimal using existing logic
406
- const phasesDir = path.join(planningDir(cwd), 'phases');
407
- const normalizedBase = normalizePhaseName(afterPhase);
408
- let existingDecimals = [];
409
-
410
- try {
411
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
412
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
413
- const decimalPattern = new RegExp(`^${normalizedBase}\\.(\\d+)`);
414
- for (const dir of dirs) {
415
- const dm = dir.match(decimalPattern);
416
- if (dm) existingDecimals.push(parseInt(dm[1], 10));
423
+ // Wrap entire read-modify-write in lock to prevent concurrent corruption
424
+ const { decimalPhase, dirName } = withPlanningLock(cwd, () => {
425
+ const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
426
+ const content = extractCurrentMilestone(rawContent, cwd);
427
+
428
+ // Normalize input then strip leading zeros for flexible matching
429
+ const normalizedAfter = normalizePhaseName(afterPhase);
430
+ const unpadded = normalizedAfter.replace(/^0+/, '');
431
+ const afterPhaseEscaped = unpadded.replace(/\./g, '\\.');
432
+ const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:`, 'i');
433
+ if (!targetPattern.test(content)) {
434
+ error(`Phase ${afterPhase} not found in ROADMAP.md`);
417
435
  }
418
- } catch { /* intentionally empty */ }
419
436
 
420
- const nextDecimal = existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1;
421
- const decimalPhase = `${normalizedBase}.${nextDecimal}`;
422
- const dirName = `${decimalPhase}-${slug}`;
423
- const dirPath = path.join(planningDir(cwd), 'phases', dirName);
437
+ // Calculate next decimal by scanning both directories AND ROADMAP.md entries
438
+ const phasesDir = path.join(planningDir(cwd), 'phases');
439
+ const normalizedBase = normalizePhaseName(afterPhase);
440
+ const decimalSet = new Set();
424
441
 
425
- // Create directory with .gitkeep so git tracks empty folders
426
- fs.mkdirSync(dirPath, { recursive: true });
427
- fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
442
+ try {
443
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
444
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
445
+ const decimalPattern = new RegExp(`^(?:[A-Z]{1,6}-)?${escapeRegex(normalizedBase)}\\.(\\d+)`);
446
+ for (const dir of dirs) {
447
+ const dm = dir.match(decimalPattern);
448
+ if (dm) decimalSet.add(parseInt(dm[1], 10));
449
+ }
450
+ } catch { /* intentionally empty */ }
428
451
 
429
- // Build phase entry
430
- 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 /sdd:plan-phase ${decimalPhase} to break down)\n`;
452
+ // Also scan ROADMAP.md content (already loaded) for decimal entries
453
+ const rmPhasePattern = new RegExp(
454
+ `#{2,4}\\s*Phase\\s+0*${escapeRegex(normalizedBase)}\\.(\\d+)\\s*:`, 'gi'
455
+ );
456
+ let rmMatch;
457
+ while ((rmMatch = rmPhasePattern.exec(rawContent)) !== null) {
458
+ decimalSet.add(parseInt(rmMatch[1], 10));
459
+ }
431
460
 
432
- // Insert after the target phase section
433
- const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
434
- const headerMatch = rawContent.match(headerPattern);
435
- if (!headerMatch) {
436
- error(`Could not find Phase ${afterPhase} header`);
437
- }
461
+ const nextDecimal = decimalSet.size === 0 ? 1 : Math.max(...decimalSet) + 1;
462
+ const _decimalPhase = `${normalizedBase}.${nextDecimal}`;
463
+ // Optional project code prefix
464
+ const insertConfig = loadConfig(cwd);
465
+ const projectCode = insertConfig.project_code || '';
466
+ const pfx = projectCode ? `${projectCode}-` : '';
467
+ const _dirName = `${pfx}${_decimalPhase}-${slug}`;
468
+ const dirPath = path.join(planningDir(cwd), 'phases', _dirName);
469
+
470
+ // Create directory with .gitkeep so git tracks empty folders
471
+ fs.mkdirSync(dirPath, { recursive: true });
472
+ fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
473
+
474
+ // Build phase entry
475
+ 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 /sdd-plan-phase ${_decimalPhase} to break down)\n`;
476
+
477
+ // Insert after the target phase section
478
+ const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
479
+ const headerMatch = rawContent.match(headerPattern);
480
+ if (!headerMatch) {
481
+ error(`Could not find Phase ${afterPhase} header`);
482
+ }
438
483
 
439
- const headerIdx = rawContent.indexOf(headerMatch[0]);
440
- const afterHeader = rawContent.slice(headerIdx + headerMatch[0].length);
441
- const nextPhaseMatch = afterHeader.match(/\n#{2,4}\s+Phase\s+\d/i);
484
+ const headerIdx = rawContent.indexOf(headerMatch[0]);
485
+ const afterHeader = rawContent.slice(headerIdx + headerMatch[0].length);
486
+ const nextPhaseMatch = afterHeader.match(/\n#{2,4}\s+Phase\s+\d/i);
442
487
 
443
- let insertIdx;
444
- if (nextPhaseMatch) {
445
- insertIdx = headerIdx + headerMatch[0].length + nextPhaseMatch.index;
446
- } else {
447
- insertIdx = rawContent.length;
448
- }
488
+ let insertIdx;
489
+ if (nextPhaseMatch) {
490
+ insertIdx = headerIdx + headerMatch[0].length + nextPhaseMatch.index;
491
+ } else {
492
+ insertIdx = rawContent.length;
493
+ }
449
494
 
450
- const updatedContent = rawContent.slice(0, insertIdx) + phaseEntry + rawContent.slice(insertIdx);
451
- fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
495
+ const updatedContent = rawContent.slice(0, insertIdx) + phaseEntry + rawContent.slice(insertIdx);
496
+ fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
497
+ return { decimalPhase: _decimalPhase, dirName: _dirName };
498
+ });
452
499
 
453
500
  const result = {
454
501
  phase_number: decimalPhase,
@@ -536,29 +583,32 @@ function renameIntegerPhases(phasesDir, removedInt) {
536
583
  /**
537
584
  * Remove a phase section from ROADMAP.md and renumber all subsequent integer phases.
538
585
  */
539
- function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, removedInt) {
540
- let content = fs.readFileSync(roadmapPath, 'utf-8');
541
- const escaped = escapeRegex(targetPhase);
542
-
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'), '');
546
-
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}`);
586
+ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, removedInt, cwd) {
587
+ // Wrap entire read-modify-write in lock to prevent concurrent corruption
588
+ withPlanningLock(cwd, () => {
589
+ let content = fs.readFileSync(roadmapPath, 'utf-8');
590
+ const escaped = escapeRegex(targetPhase);
591
+
592
+ content = content.replace(new RegExp(`\\n?#{2,4}\\s*Phase\\s+${escaped}\\s*:[\\s\\S]*?(?=\\n#{2,4}\\s+Phase\\s+\\d|$)`, 'i'), '');
593
+ content = content.replace(new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${escaped}[:\\s][^\\n]*`, 'gi'), '');
594
+ content = content.replace(new RegExp(`\\n?\\|\\s*${escaped}\\.?\\s[^|]*\\|[^\\n]*`, 'gi'), '');
595
+
596
+ if (!isDecimal) {
597
+ const MAX_PHASE = 99;
598
+ for (let oldNum = MAX_PHASE; oldNum > removedInt; oldNum--) {
599
+ const newNum = oldNum - 1;
600
+ const oldStr = String(oldNum), newStr = String(newNum);
601
+ const oldPad = oldStr.padStart(2, '0'), newPad = newStr.padStart(2, '0');
602
+ content = content.replace(new RegExp(`(#{2,4}\\s*Phase\\s+)${oldStr}(\\s*:)`, 'gi'), `$1${newStr}$2`);
603
+ content = content.replace(new RegExp(`(Phase\\s+)${oldStr}([:\\s])`, 'g'), `$1${newStr}$2`);
604
+ content = content.replace(new RegExp(`${oldPad}-(\\d{2})`, 'g'), `${newPad}-$1`);
605
+ content = content.replace(new RegExp(`(\\|\\s*)${oldStr}\\.\\s`, 'g'), `$1${newStr}. `);
606
+ content = content.replace(new RegExp(`(Depends on:\\*\\*\\s*Phase\\s+)${oldStr}\\b`, 'gi'), `$1${newStr}`);
607
+ }
558
608
  }
559
- }
560
609
 
561
- fs.writeFileSync(roadmapPath, content, 'utf-8');
610
+ fs.writeFileSync(roadmapPath, content, 'utf-8');
611
+ });
562
612
  }
563
613
 
564
614
  function cmdPhaseRemove(cwd, targetPhase, options, raw) {
@@ -575,7 +625,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
575
625
 
576
626
  // Find target directory
577
627
  const targetDir = readSubdirectories(phasesDir, true)
578
- .find(d => d.startsWith(normalized + '-') || d === normalized) || null;
628
+ .find(d => phaseTokenMatches(d, normalized)) || null;
579
629
 
580
630
  // Guard against removing executed work
581
631
  if (targetDir && !force) {
@@ -599,21 +649,22 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
599
649
  } catch { /* intentionally empty */ }
600
650
 
601
651
  // Update ROADMAP.md
602
- updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, parseInt(normalized, 10));
652
+ updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, parseInt(normalized, 10), cwd);
603
653
 
604
- // Update STATE.md phase count
654
+ // Update STATE.md phase count atomically (#P4.4)
605
655
  const statePath = path.join(planningDir(cwd), 'STATE.md');
606
656
  if (fs.existsSync(statePath)) {
607
- let stateContent = fs.readFileSync(statePath, 'utf-8');
608
- const totalRaw = stateExtractField(stateContent, 'Total Phases');
609
- if (totalRaw) {
610
- stateContent = stateReplaceField(stateContent, 'Total Phases', String(parseInt(totalRaw, 10) - 1)) || stateContent;
611
- }
612
- const ofMatch = stateContent.match(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i);
613
- if (ofMatch) {
614
- stateContent = stateContent.replace(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i, `$1${parseInt(ofMatch[2], 10) - 1}$3`);
615
- }
616
- writeStateMd(statePath, stateContent, cwd);
657
+ readModifyWriteStateMd(statePath, (stateContent) => {
658
+ const totalRaw = stateExtractField(stateContent, 'Total Phases');
659
+ if (totalRaw) {
660
+ stateContent = stateReplaceField(stateContent, 'Total Phases', String(parseInt(totalRaw, 10) - 1)) || stateContent;
661
+ }
662
+ const ofMatch = stateContent.match(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i);
663
+ if (ofMatch) {
664
+ stateContent = stateContent.replace(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i, `$1${parseInt(ofMatch[2], 10) - 1}$3`);
665
+ }
666
+ return stateContent;
667
+ }, cwd);
617
668
  }
618
669
 
619
670
  output({
@@ -668,84 +719,108 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
668
719
  }
669
720
  } catch {}
670
721
 
671
- // Update ROADMAP.md: mark phase complete
722
+ // Update ROADMAP.md and REQUIREMENTS.md atomically under lock
672
723
  if (fs.existsSync(roadmapPath)) {
673
- let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
674
-
675
- // Checkbox: - [ ] Phase N: → - [x] Phase N: (...completed DATE)
676
- const checkboxPattern = new RegExp(
677
- `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s][^\\n]*)`,
678
- 'i'
679
- );
680
- roadmapContent = replaceInCurrentMilestone(roadmapContent, checkboxPattern, `$1x$2 (completed ${today})`);
681
-
682
- // Progress table: update Status to Complete, add date (handles 4 or 5 column tables)
683
- const phaseEscaped = escapeRegex(phaseNum);
684
- const tableRowPattern = new RegExp(
685
- `^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
686
- 'im'
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
- });
701
-
702
- // Update plan count in phase section
703
- const planCountPattern = new RegExp(
704
- `(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
705
- 'i'
706
- );
707
- roadmapContent = replaceInCurrentMilestone(
708
- roadmapContent, planCountPattern,
709
- `$1${summaryCount}/${planCount} plans complete`
710
- );
724
+ withPlanningLock(cwd, () => {
725
+ let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
711
726
 
712
- fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
713
-
714
- // Update REQUIREMENTS.md traceability for this phase's requirements
715
- const reqPath = path.join(planningDir(cwd), 'REQUIREMENTS.md');
716
- if (fs.existsSync(reqPath)) {
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')
727
+ // Checkbox: - [ ] Phase N: → - [x] Phase N: (...completed DATE)
728
+ const checkboxPattern = new RegExp(
729
+ `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s][^\\n]*)`,
730
+ 'i'
722
731
  );
732
+ roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
723
733
 
724
- const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : '';
725
- const reqMatch = sectionText.match(/\*\*Requirements:\*\*\s*([^\n]+)/i);
726
-
727
- if (reqMatch) {
728
- const reqIds = reqMatch[1].replace(/[\[\]]/g, '').split(/[,\s]+/).map(r => r.trim()).filter(Boolean);
729
- let reqContent = fs.readFileSync(reqPath, 'utf-8');
730
-
731
- for (const reqId of reqIds) {
732
- const reqEscaped = escapeRegex(reqId);
733
- // Update checkbox: - [ ] **REQ-ID** - [x] **REQ-ID**
734
- reqContent = reqContent.replace(
735
- new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi'),
736
- '$1x$2'
737
- );
738
- // Update traceability table: | REQ-ID | Phase N | Pending/In Progress | | REQ-ID | Phase N | Complete |
739
- reqContent = reqContent.replace(
740
- new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*(?:Pending|In Progress)\\s*(\\|)`, 'gi'),
741
- '$1 Complete $2'
742
- );
734
+ // Progress table: update Status to Complete, add date (handles 4 or 5 column tables)
735
+ const phaseEscaped = escapeRegex(phaseNum);
736
+ const tableRowPattern = new RegExp(
737
+ `^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
738
+ 'im'
739
+ );
740
+ roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
741
+ const cells = fullRow.split('|').slice(1, -1);
742
+ if (cells.length === 5) {
743
+ // 5-col: Phase | Milestone | Plans | Status | Completed
744
+ cells[2] = ` ${summaryCount}/${planCount} `;
745
+ cells[3] = ' Complete ';
746
+ cells[4] = ` ${today} `;
747
+ } else if (cells.length === 4) {
748
+ // 4-col: Phase | Plans | Status | Completed
749
+ cells[1] = ` ${summaryCount}/${planCount} `;
750
+ cells[2] = ' Complete ';
751
+ cells[3] = ` ${today} `;
743
752
  }
753
+ return '|' + cells.join('|') + '|';
754
+ });
755
+
756
+ // Update plan count in phase section.
757
+ // Use direct .replace() rather than replaceInCurrentMilestone() so this
758
+ // works when the current milestone section is itself inside a <details>
759
+ // block (the standard /sdd-new-project layout). replaceInCurrentMilestone
760
+ // scopes to content after the last </details>, which misses content inside
761
+ // the current milestone's own <details> wrapper (#2005).
762
+ // The phase-scoped heading pattern is specific enough to avoid matching
763
+ // archived phases (which belong to different milestones).
764
+ const planCountPattern = new RegExp(
765
+ `(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
766
+ 'i'
767
+ );
768
+ roadmapContent = roadmapContent.replace(
769
+ planCountPattern,
770
+ `$1${summaryCount}/${planCount} plans complete`
771
+ );
744
772
 
745
- fs.writeFileSync(reqPath, reqContent, 'utf-8');
746
- requirementsUpdated = true;
773
+ // Mark completed plan checkboxes (safety net for missed per-plan updates)
774
+ // Handles both plain IDs ("- [ ] 01-01-PLAN.md") and bold-wrapped IDs ("- [ ] **01-01**")
775
+ for (const summaryFile of phaseInfo.summaries) {
776
+ const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
777
+ if (!planId) continue;
778
+ const planEscaped = escapeRegex(planId);
779
+ const planCheckboxPattern = new RegExp(
780
+ `(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`,
781
+ 'i'
782
+ );
783
+ roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
747
784
  }
748
- }
785
+
786
+ fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
787
+
788
+ // Update REQUIREMENTS.md traceability for this phase's requirements
789
+ const reqPath = path.join(planningDir(cwd), 'REQUIREMENTS.md');
790
+ if (fs.existsSync(reqPath)) {
791
+ // Extract the current phase section from roadmap (scoped to avoid cross-phase matching)
792
+ const phaseEsc = escapeRegex(phaseNum);
793
+ const currentMilestoneRoadmap = extractCurrentMilestone(roadmapContent, cwd);
794
+ const phaseSectionMatch = currentMilestoneRoadmap.match(
795
+ new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEsc}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i')
796
+ );
797
+
798
+ const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : '';
799
+ const reqMatch = sectionText.match(/\*\*Requirements:\*\*\s*([^\n]+)/i);
800
+
801
+ if (reqMatch) {
802
+ const reqIds = reqMatch[1].replace(/[\[\]]/g, '').split(/[,\s]+/).map(r => r.trim()).filter(Boolean);
803
+ let reqContent = fs.readFileSync(reqPath, 'utf-8');
804
+
805
+ for (const reqId of reqIds) {
806
+ const reqEscaped = escapeRegex(reqId);
807
+ // Update checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID**
808
+ reqContent = reqContent.replace(
809
+ new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi'),
810
+ '$1x$2'
811
+ );
812
+ // Update traceability table: | REQ-ID | Phase N | Pending/In Progress | → | REQ-ID | Phase N | Complete |
813
+ reqContent = reqContent.replace(
814
+ new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*(?:Pending|In Progress)\\s*(\\|)`, 'gi'),
815
+ '$1 Complete $2'
816
+ );
817
+ }
818
+
819
+ fs.writeFileSync(reqPath, reqContent, 'utf-8');
820
+ requirementsUpdated = true;
821
+ }
822
+ }
823
+ });
749
824
  }
750
825
 
751
826
  // Find next phase — check both filesystem AND roadmap
@@ -794,68 +869,72 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
794
869
  } catch { /* intentionally empty */ }
795
870
  }
796
871
 
797
- // Update STATE.md — use shared helpers that handle both **bold:** and plain Field: formats
872
+ // Update STATE.md atomically hold lock across read-modify-write (#P4.4).
873
+ // Previously read outside the lock; a crash between the ROADMAP update
874
+ // (locked above) and this write left ROADMAP/STATE inconsistent.
798
875
  if (fs.existsSync(statePath)) {
799
- let stateContent = fs.readFileSync(statePath, 'utf-8');
800
-
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}`;
876
+ readModifyWriteStateMd(statePath, (stateContent) => {
877
+ // Update Current Phase — preserve "X of Y (Name)" compound format
878
+ const phaseValue = nextPhaseNum || phaseNum;
879
+ const existingPhaseField = stateExtractField(stateContent, 'Current Phase')
880
+ || stateExtractField(stateContent, 'Phase');
881
+ let newPhaseValue = String(phaseValue);
882
+ if (existingPhaseField) {
883
+ const totalMatch = existingPhaseField.match(/of\s+(\d+)/);
884
+ const nameMatch = existingPhaseField.match(/\(([^)]+)\)/);
885
+ if (totalMatch) {
886
+ const total = totalMatch[1];
887
+ const nameStr = nextPhaseName ? ` (${nextPhaseName.replace(/-/g, ' ')})` : (nameMatch ? ` (${nameMatch[1]})` : '');
888
+ newPhaseValue = `${phaseValue} of ${total}${nameStr}`;
889
+ }
813
890
  }
814
- }
815
- stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase', 'Phase', newPhaseValue);
891
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase', 'Phase', newPhaseValue);
816
892
 
817
- // Update Current Phase Name
818
- if (nextPhaseName) {
819
- stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase Name', null, nextPhaseName.replace(/-/g, ' '));
820
- }
821
-
822
- // Update Status
823
- stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null,
824
- isLastPhase ? 'Milestone complete' : 'Ready to plan');
825
-
826
- // Update Current Plan
827
- stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Plan', 'Plan', 'Not started');
828
-
829
- // Update Last Activity
830
- stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity', 'Last activity', today);
831
-
832
- // Update Last Activity Description
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;
893
+ // Update Current Phase Name
894
+ if (nextPhaseName) {
895
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase Name', null, nextPhaseName.replace(/-/g, ' '));
896
+ }
841
897
 
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
- );
898
+ // Update Status
899
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null,
900
+ isLastPhase ? 'Milestone complete' : 'Ready to plan');
901
+
902
+ // Update Current Plan
903
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Plan', 'Plan', 'Not started');
904
+
905
+ // Update Last Activity
906
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity', 'Last activity', today);
907
+
908
+ // Update Last Activity Description
909
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity Description', null,
910
+ `Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`);
911
+
912
+ // Increment Completed Phases counter (#956)
913
+ const completedRaw = stateExtractField(stateContent, 'Completed Phases');
914
+ if (completedRaw) {
915
+ const newCompleted = parseInt(completedRaw, 10) + 1;
916
+ stateContent = stateReplaceField(stateContent, 'Completed Phases', String(newCompleted)) || stateContent;
917
+
918
+ // Recalculate percent based on completed / total (#956)
919
+ const totalRaw = stateExtractField(stateContent, 'Total Phases');
920
+ if (totalRaw) {
921
+ const totalPhases = parseInt(totalRaw, 10);
922
+ if (totalPhases > 0) {
923
+ const newPercent = Math.round((newCompleted / totalPhases) * 100);
924
+ stateContent = stateReplaceField(stateContent, 'Progress', `${newPercent}%`) || stateContent;
925
+ stateContent = stateContent.replace(
926
+ /(percent:\s*)\d+/,
927
+ `$1${newPercent}`
928
+ );
929
+ }
854
930
  }
855
931
  }
856
- }
857
932
 
858
- writeStateMd(statePath, stateContent, cwd);
933
+ // Gate 4: Update Performance Metrics section (#1627)
934
+ stateContent = updatePerformanceMetricsSection(stateContent, cwd, phaseNum, planCount, summaryCount);
935
+
936
+ return stateContent;
937
+ }, cwd);
859
938
  }
860
939
 
861
940
  const result = {