gsd-antigravity-kit 2.0.0 → 2.1.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 (258) hide show
  1. package/.agent/skills/gsd/SKILL.md +26 -4
  2. package/.agent/skills/gsd/VERSION +1 -1
  3. package/.agent/skills/gsd/assets/templates/AI-SPEC.md +246 -0
  4. package/.agent/skills/gsd/assets/templates/DEBUG.md +7 -2
  5. package/.agent/skills/gsd/assets/templates/config.json +56 -48
  6. package/.agent/skills/gsd/assets/templates/research.md +40 -0
  7. package/.agent/skills/gsd/assets/templates/spec.md +307 -0
  8. package/.agent/skills/gsd/assets/templates/state.md +8 -0
  9. package/.agent/skills/gsd/bin/gsd-tools.cjs +212 -11
  10. package/.agent/skills/gsd/bin/help-manifest.json +8 -2
  11. package/.agent/skills/gsd/bin/hooks/gsd-check-update-worker.js +108 -0
  12. package/.agent/skills/gsd/bin/hooks/gsd-check-update.js +14 -89
  13. package/.agent/skills/gsd/bin/hooks/gsd-context-monitor.js +34 -5
  14. package/.agent/skills/gsd/bin/hooks/gsd-phase-boundary.sh +1 -0
  15. package/.agent/skills/gsd/bin/hooks/gsd-prompt-guard.js +1 -1
  16. package/.agent/skills/gsd/bin/hooks/gsd-read-guard.js +6 -1
  17. package/.agent/skills/gsd/bin/hooks/gsd-session-state.sh +1 -0
  18. package/.agent/skills/gsd/bin/hooks/gsd-statusline.js +150 -16
  19. package/.agent/skills/gsd/bin/hooks/gsd-validate-commit.sh +1 -0
  20. package/.agent/skills/gsd/bin/hooks/gsd-workflow-guard.js +1 -1
  21. package/.agent/skills/gsd/bin/lib/audit.cjs +757 -0
  22. package/.agent/skills/gsd/bin/lib/commands.cjs +17 -7
  23. package/.agent/skills/gsd/bin/lib/config.cjs +66 -20
  24. package/.agent/skills/gsd/bin/lib/core.cjs +212 -12
  25. package/.agent/skills/gsd/bin/lib/frontmatter.cjs +6 -8
  26. package/.agent/skills/gsd/bin/lib/graphify.cjs +494 -0
  27. package/.agent/skills/gsd/bin/lib/gsd2-import.cjs +511 -0
  28. package/.agent/skills/gsd/bin/lib/init.cjs +371 -18
  29. package/.agent/skills/gsd/bin/lib/intel.cjs +9 -30
  30. package/.agent/skills/gsd/bin/lib/milestone.cjs +18 -17
  31. package/.agent/skills/gsd/bin/lib/model-profiles.cjs +1 -0
  32. package/.agent/skills/gsd/bin/lib/phase.cjs +225 -98
  33. package/.agent/skills/gsd/bin/lib/profile-output.cjs +17 -5
  34. package/.agent/skills/gsd/bin/lib/roadmap.cjs +12 -5
  35. package/.agent/skills/gsd/bin/lib/state.cjs +394 -129
  36. package/.agent/skills/gsd/bin/lib/template.cjs +8 -4
  37. package/.agent/skills/gsd/bin/lib/uat.cjs +2 -1
  38. package/.agent/skills/gsd/bin/lib/verify.cjs +111 -42
  39. package/.agent/skills/gsd/migration_report.md +2 -2
  40. package/.agent/skills/gsd/references/agents/gsd-advisor-researcher.md +23 -0
  41. package/.agent/skills/gsd/references/agents/gsd-ai-researcher.md +133 -0
  42. package/.agent/skills/gsd/references/agents/gsd-code-fixer.md +11 -10
  43. package/.agent/skills/gsd/references/agents/gsd-code-reviewer.md +2 -2
  44. package/.agent/skills/gsd/references/agents/gsd-codebase-mapper.md +13 -2
  45. package/.agent/skills/gsd/references/agents/gsd-debug-session-manager.md +314 -0
  46. package/.agent/skills/gsd/references/agents/gsd-debugger.md +147 -76
  47. package/.agent/skills/gsd/references/agents/gsd-doc-verifier.md +1 -1
  48. package/.agent/skills/gsd/references/agents/gsd-doc-writer.md +615 -602
  49. package/.agent/skills/gsd/references/agents/gsd-domain-researcher.md +153 -0
  50. package/.agent/skills/gsd/references/agents/gsd-eval-auditor.md +175 -0
  51. package/.agent/skills/gsd/references/agents/gsd-eval-planner.md +154 -0
  52. package/.agent/skills/gsd/references/agents/gsd-executor.md +108 -38
  53. package/.agent/skills/gsd/references/agents/gsd-framework-selector.md +160 -0
  54. package/.agent/skills/gsd/references/agents/gsd-integration-checker.md +454 -443
  55. package/.agent/skills/gsd/references/agents/gsd-intel-updater.md +40 -20
  56. package/.agent/skills/gsd/references/agents/gsd-nyquist-auditor.md +187 -176
  57. package/.agent/skills/gsd/references/agents/gsd-pattern-mapper.md +335 -0
  58. package/.agent/skills/gsd/references/agents/gsd-phase-researcher.md +112 -13
  59. package/.agent/skills/gsd/references/agents/gsd-plan-checker.md +104 -10
  60. package/.agent/skills/gsd/references/agents/gsd-planner.md +125 -167
  61. package/.agent/skills/gsd/references/agents/gsd-project-researcher.md +25 -2
  62. package/.agent/skills/gsd/references/agents/gsd-research-synthesizer.md +3 -3
  63. package/.agent/skills/gsd/references/agents/gsd-roadmapper.md +12 -1
  64. package/.agent/skills/gsd/references/agents/gsd-security-auditor.md +139 -128
  65. package/.agent/skills/gsd/references/agents/gsd-ui-auditor.md +3 -3
  66. package/.agent/skills/gsd/references/agents/gsd-ui-checker.md +11 -2
  67. package/.agent/skills/gsd/references/agents/gsd-ui-researcher.md +27 -4
  68. package/.agent/skills/gsd/references/agents/gsd-verifier.md +13 -19
  69. package/.agent/skills/gsd/references/commands/atomic/add-todo.md +2 -2
  70. package/.agent/skills/gsd/references/commands/atomic/check-todos.md +2 -2
  71. package/.agent/skills/gsd/references/commands/atomic/cleanup.md +2 -2
  72. package/.agent/skills/gsd/references/commands/atomic/do.md +2 -2
  73. package/.agent/skills/gsd/references/commands/atomic/help.md +2 -2
  74. package/.agent/skills/gsd/references/commands/atomic/join-discord.md +2 -2
  75. package/.agent/skills/gsd/references/commands/atomic/note.md +2 -2
  76. package/.agent/skills/gsd/references/commands/atomic/session-report.md +2 -2
  77. package/.agent/skills/gsd/references/commands/atomic/ship.md +2 -2
  78. package/.agent/skills/gsd/references/commands/atomic/stats.md +2 -2
  79. package/.agent/skills/gsd/references/commands/atomic/thread.md +141 -41
  80. package/.agent/skills/gsd/references/commands/atomic/undo.md +2 -2
  81. package/.agent/skills/gsd/references/commands/milestone/add-backlog.md +15 -12
  82. package/.agent/skills/gsd/references/commands/milestone/audit-milestone.md +2 -2
  83. package/.agent/skills/gsd/references/commands/milestone/complete-milestone.md +2 -2
  84. package/.agent/skills/gsd/references/commands/milestone/milestone-summary.md +2 -2
  85. package/.agent/skills/gsd/references/commands/milestone/new-milestone.md +2 -2
  86. package/.agent/skills/gsd/references/commands/milestone/plan-milestone-gaps.md +2 -2
  87. package/.agent/skills/gsd/references/commands/milestone/plant-seed.md +2 -2
  88. package/.agent/skills/gsd/references/commands/milestone/review-backlog.md +4 -4
  89. package/.agent/skills/gsd/references/commands/misc/ai-integration-phase.md +38 -0
  90. package/.agent/skills/gsd/references/commands/misc/audit-fix.md +2 -2
  91. package/.agent/skills/gsd/references/commands/misc/audit-uat.md +2 -2
  92. package/.agent/skills/gsd/references/commands/misc/eval-review.md +34 -0
  93. package/.agent/skills/gsd/references/commands/misc/extract_learnings.md +24 -0
  94. package/.agent/skills/gsd/references/commands/misc/from-gsd2.md +49 -0
  95. package/.agent/skills/gsd/references/commands/misc/graphify.md +203 -0
  96. package/.agent/skills/gsd/references/commands/misc/inbox.md +40 -0
  97. package/.agent/skills/gsd/references/commands/misc/next.md +5 -3
  98. package/.agent/skills/gsd/references/commands/misc/progress.md +4 -3
  99. package/.agent/skills/gsd/references/commands/misc/sketch-wrap-up.md +33 -0
  100. package/.agent/skills/gsd/references/commands/misc/sketch.md +47 -0
  101. package/.agent/skills/gsd/references/commands/misc/spec-phase.md +64 -0
  102. package/.agent/skills/gsd/references/commands/misc/spike-wrap-up.md +33 -0
  103. package/.agent/skills/gsd/references/commands/misc/spike.md +43 -0
  104. package/.agent/skills/gsd/references/commands/misc/verify-work.md +2 -2
  105. package/.agent/skills/gsd/references/commands/phase/add-phase.md +2 -2
  106. package/.agent/skills/gsd/references/commands/phase/add-tests.md +2 -2
  107. package/.agent/skills/gsd/references/commands/phase/discuss-phase.md +5 -5
  108. package/.agent/skills/gsd/references/commands/phase/execute-phase.md +4 -4
  109. package/.agent/skills/gsd/references/commands/phase/insert-phase.md +2 -2
  110. package/.agent/skills/gsd/references/commands/phase/list-phase-assumptions.md +2 -2
  111. package/.agent/skills/gsd/references/commands/phase/plan-phase.md +3 -3
  112. package/.agent/skills/gsd/references/commands/phase/remove-phase.md +2 -2
  113. package/.agent/skills/gsd/references/commands/phase/research-phase.md +5 -5
  114. package/.agent/skills/gsd/references/commands/phase/secure-phase.md +2 -2
  115. package/.agent/skills/gsd/references/commands/phase/ui-phase.md +2 -2
  116. package/.agent/skills/gsd/references/commands/phase/ui-review.md +2 -2
  117. package/.agent/skills/gsd/references/commands/phase/validate-phase.md +2 -2
  118. package/.agent/skills/gsd/references/commands/phase/workstreams.md +9 -9
  119. package/.agent/skills/gsd/references/commands/project/analyze-dependencies.md +2 -2
  120. package/.agent/skills/gsd/references/commands/project/explore.md +2 -2
  121. package/.agent/skills/gsd/references/commands/project/import.md +2 -2
  122. package/.agent/skills/gsd/references/commands/project/intel.md +10 -10
  123. package/.agent/skills/gsd/references/commands/project/list-workspaces.md +2 -2
  124. package/.agent/skills/gsd/references/commands/project/map-codebase.md +2 -2
  125. package/.agent/skills/gsd/references/commands/project/new-project.md +2 -2
  126. package/.agent/skills/gsd/references/commands/project/new-workspace.md +2 -2
  127. package/.agent/skills/gsd/references/commands/project/remove-workspace.md +2 -2
  128. package/.agent/skills/gsd/references/commands/project/scan.md +2 -2
  129. package/.agent/skills/gsd/references/commands/system/autonomous.md +4 -3
  130. package/.agent/skills/gsd/references/commands/system/code-review-fix.md +3 -3
  131. package/.agent/skills/gsd/references/commands/system/code-review.md +3 -3
  132. package/.agent/skills/gsd/references/commands/system/debug.md +177 -100
  133. package/.agent/skills/gsd/references/commands/system/docs-update.md +2 -2
  134. package/.agent/skills/gsd/references/commands/system/fast.md +2 -2
  135. package/.agent/skills/gsd/references/commands/system/forensics.md +2 -2
  136. package/.agent/skills/gsd/references/commands/system/gsd-tools.md +153 -6
  137. package/.agent/skills/gsd/references/commands/system/health.md +2 -2
  138. package/.agent/skills/gsd/references/commands/system/manager.md +3 -3
  139. package/.agent/skills/gsd/references/commands/system/pause-work.md +2 -2
  140. package/.agent/skills/gsd/references/commands/system/pr-branch.md +2 -2
  141. package/.agent/skills/gsd/references/commands/system/profile-user.md +2 -2
  142. package/.agent/skills/gsd/references/commands/system/quick.md +127 -3
  143. package/.agent/skills/gsd/references/commands/system/reapply-patches.md +45 -6
  144. package/.agent/skills/gsd/references/commands/system/resume-work.md +2 -2
  145. package/.agent/skills/gsd/references/commands/system/review.md +6 -4
  146. package/.agent/skills/gsd/references/commands/system/set-profile.md +3 -3
  147. package/.agent/skills/gsd/references/commands/system/settings.md +2 -2
  148. package/.agent/skills/gsd/references/commands/system/update.md +2 -2
  149. package/.agent/skills/gsd/references/docs/ai-evals.md +156 -0
  150. package/.agent/skills/gsd/references/docs/ai-frameworks.md +186 -0
  151. package/.agent/skills/gsd/references/docs/artifact-types.md +18 -0
  152. package/.agent/skills/gsd/references/docs/autonomous-smart-discuss.md +277 -0
  153. package/.agent/skills/gsd/references/docs/checkpoints.md +30 -0
  154. package/.agent/skills/gsd/references/docs/common-bug-patterns.md +49 -49
  155. package/.agent/skills/gsd/references/docs/continuation-format.md +11 -7
  156. package/.agent/skills/gsd/references/docs/debugger-philosophy.md +76 -0
  157. package/.agent/skills/gsd/references/docs/decimal-phase-calculation.md +64 -64
  158. package/.agent/skills/gsd/references/docs/executor-examples.md +110 -0
  159. package/.agent/skills/gsd/references/docs/git-integration.md +4 -4
  160. package/.agent/skills/gsd/references/docs/git-planning-commit.md +40 -38
  161. package/.agent/skills/gsd/references/docs/ios-scaffold.md +123 -0
  162. package/.agent/skills/gsd/references/docs/mandatory-initial-read.md +2 -0
  163. package/.agent/skills/gsd/references/docs/phase-argument-parsing.md +61 -61
  164. package/.agent/skills/gsd/references/docs/planner-antipatterns.md +89 -0
  165. package/.agent/skills/gsd/references/docs/planner-revision.md +87 -87
  166. package/.agent/skills/gsd/references/docs/planner-source-audit.md +73 -0
  167. package/.agent/skills/gsd/references/docs/planning-config.md +33 -8
  168. package/.agent/skills/gsd/references/docs/project-skills-discovery.md +19 -0
  169. package/.agent/skills/gsd/references/docs/sketch-interactivity.md +41 -0
  170. package/.agent/skills/gsd/references/docs/sketch-theme-system.md +94 -0
  171. package/.agent/skills/gsd/references/docs/sketch-tooling.md +45 -0
  172. package/.agent/skills/gsd/references/docs/sketch-variant-patterns.md +81 -0
  173. package/.agent/skills/gsd/references/docs/tdd.md +67 -0
  174. package/.agent/skills/gsd/references/docs/universal-anti-patterns.md +5 -0
  175. package/.agent/skills/gsd/references/docs/workstream-flag.md +11 -11
  176. package/.agent/skills/gsd/references/mapping.md +1 -1
  177. package/.agent/skills/gsd/references/workflows/add-phase.md +112 -112
  178. package/.agent/skills/gsd/references/workflows/add-tests.md +6 -3
  179. package/.agent/skills/gsd/references/workflows/add-todo.md +5 -3
  180. package/.agent/skills/gsd/references/workflows/ai-integration-phase.md +284 -0
  181. package/.agent/skills/gsd/references/workflows/audit-fix.md +157 -157
  182. package/.agent/skills/gsd/references/workflows/audit-milestone.md +340 -340
  183. package/.agent/skills/gsd/references/workflows/audit-uat.md +109 -109
  184. package/.agent/skills/gsd/references/workflows/autonomous.md +20 -288
  185. package/.agent/skills/gsd/references/workflows/check-todos.md +4 -2
  186. package/.agent/skills/gsd/references/workflows/cleanup.md +3 -1
  187. package/.agent/skills/gsd/references/workflows/code-review-fix.md +497 -497
  188. package/.agent/skills/gsd/references/workflows/code-review.md +515 -515
  189. package/.agent/skills/gsd/references/workflows/complete-milestone.md +97 -24
  190. package/.agent/skills/gsd/references/workflows/diagnose-issues.md +238 -238
  191. package/.agent/skills/gsd/references/workflows/discovery-phase.md +2 -0
  192. package/.agent/skills/gsd/references/workflows/discuss-phase-assumptions.md +11 -11
  193. package/.agent/skills/gsd/references/workflows/discuss-phase.md +143 -19
  194. package/.agent/skills/gsd/references/workflows/do.md +8 -2
  195. package/.agent/skills/gsd/references/workflows/docs-update.md +5 -3
  196. package/.agent/skills/gsd/references/workflows/eval-review.md +155 -0
  197. package/.agent/skills/gsd/references/workflows/execute-phase.md +338 -54
  198. package/.agent/skills/gsd/references/workflows/execute-plan.md +80 -104
  199. package/.agent/skills/gsd/references/workflows/explore.md +3 -1
  200. package/.agent/skills/gsd/references/workflows/extract_learnings.md +232 -0
  201. package/.agent/skills/gsd/references/workflows/forensics.md +3 -3
  202. package/.agent/skills/gsd/references/workflows/health.md +2 -2
  203. package/.agent/skills/gsd/references/workflows/help.md +59 -1
  204. package/.agent/skills/gsd/references/workflows/import.md +3 -1
  205. package/.agent/skills/gsd/references/workflows/inbox.md +387 -384
  206. package/.agent/skills/gsd/references/workflows/insert-phase.md +130 -130
  207. package/.agent/skills/gsd/references/workflows/list-workspaces.md +56 -56
  208. package/.agent/skills/gsd/references/workflows/manager.md +5 -3
  209. package/.agent/skills/gsd/references/workflows/map-codebase.md +19 -5
  210. package/.agent/skills/gsd/references/workflows/milestone-summary.md +6 -6
  211. package/.agent/skills/gsd/references/workflows/new-milestone.md +63 -9
  212. package/.agent/skills/gsd/references/workflows/new-project.md +126 -22
  213. package/.agent/skills/gsd/references/workflows/new-workspace.md +6 -4
  214. package/.agent/skills/gsd/references/workflows/next.md +220 -153
  215. package/.agent/skills/gsd/references/workflows/note.md +2 -0
  216. package/.agent/skills/gsd/references/workflows/pause-work.md +11 -7
  217. package/.agent/skills/gsd/references/workflows/plan-milestone-gaps.md +273 -273
  218. package/.agent/skills/gsd/references/workflows/plan-phase.md +281 -62
  219. package/.agent/skills/gsd/references/workflows/plant-seed.md +4 -1
  220. package/.agent/skills/gsd/references/workflows/pr-branch.md +41 -13
  221. package/.agent/skills/gsd/references/workflows/profile-user.md +15 -13
  222. package/.agent/skills/gsd/references/workflows/progress.md +133 -21
  223. package/.agent/skills/gsd/references/workflows/quick.md +67 -27
  224. package/.agent/skills/gsd/references/workflows/remove-phase.md +155 -155
  225. package/.agent/skills/gsd/references/workflows/remove-workspace.md +4 -2
  226. package/.agent/skills/gsd/references/workflows/research-phase.md +3 -3
  227. package/.agent/skills/gsd/references/workflows/resume-project.md +3 -3
  228. package/.agent/skills/gsd/references/workflows/review.md +71 -8
  229. package/.agent/skills/gsd/references/workflows/scan.md +102 -102
  230. package/.agent/skills/gsd/references/workflows/secure-phase.md +7 -5
  231. package/.agent/skills/gsd/references/workflows/settings.md +24 -7
  232. package/.agent/skills/gsd/references/workflows/ship.md +71 -6
  233. package/.agent/skills/gsd/references/workflows/sketch-wrap-up.md +283 -0
  234. package/.agent/skills/gsd/references/workflows/sketch.md +263 -0
  235. package/.agent/skills/gsd/references/workflows/spec-phase.md +262 -0
  236. package/.agent/skills/gsd/references/workflows/spike-wrap-up.md +273 -0
  237. package/.agent/skills/gsd/references/workflows/spike.md +270 -0
  238. package/.agent/skills/gsd/references/workflows/stats.md +60 -60
  239. package/.agent/skills/gsd/references/workflows/transition.md +671 -671
  240. package/.agent/skills/gsd/references/workflows/ui-phase.md +33 -12
  241. package/.agent/skills/gsd/references/workflows/ui-review.md +6 -4
  242. package/.agent/skills/gsd/references/workflows/undo.md +3 -1
  243. package/.agent/skills/gsd/references/workflows/update.md +113 -2
  244. package/.agent/skills/gsd/references/workflows/validate-phase.md +7 -5
  245. package/.agent/skills/gsd/references/workflows/verify-phase.md +93 -10
  246. package/.agent/skills/gsd/references/workflows/verify-work.md +50 -10
  247. package/.agent/skills/gsd-converter/references/mapping.md +1 -1
  248. package/.agent/skills/gsd-converter/scripts/convert.py +36 -17
  249. package/.agent/skills/gsd-converter/scripts/regression_test.py +68 -33
  250. package/README.md +3 -2
  251. package/package.json +4 -2
  252. package/.agent/skills/release-manager/SKILL.md +0 -162
  253. package/.agent/skills/release-manager/bin/LICENSE +0 -21
  254. package/.agent/skills/release-manager/bin/gh.exe +0 -0
  255. package/.agent/skills/release-manager/references/update_kb_from_fixes.md +0 -29
  256. package/.agent/skills/release-manager/scripts/release.ps1 +0 -222
  257. package/.agent/skills/selectpaste-update/SKILL.md +0 -46
  258. package/.agent/skills/selectpaste-update/scripts/sync-commands.py +0 -317
@@ -4,7 +4,7 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { escapeRegex, getMilestonePhaseFilter, extractOneLinerFromBody, normalizeMd, planningPaths, output, error } = require('./core.cjs');
7
+ const { escapeRegex, getMilestonePhaseFilter, extractOneLinerFromBody, normalizeMd, planningPaths, output, error, atomicWriteFileSync } = require('./core.cjs');
8
8
  const { extractFrontmatter } = require('./frontmatter.cjs');
9
9
  const { writeStateMd, stateReplaceFieldWithFallback } = require('./state.cjs');
10
10
 
@@ -41,29 +41,30 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
41
41
  const reqEscaped = escapeRegex(reqId);
42
42
 
43
43
  // Update checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID**
44
+ // Use replace() directly and compare — avoids test()+replace() global regex
45
+ // lastIndex bug where test() advances state and replace() misses matches.
44
46
  const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi');
45
- if (checkboxPattern.test(reqContent)) {
46
- reqContent = reqContent.replace(checkboxPattern, '$1x$2');
47
+ const afterCheckbox = reqContent.replace(checkboxPattern, '$1x$2');
48
+ if (afterCheckbox !== reqContent) {
49
+ reqContent = afterCheckbox;
47
50
  found = true;
48
51
  }
49
52
 
50
53
  // Update traceability table: | REQ-ID | Phase N | Pending | → | REQ-ID | Phase N | Complete |
51
54
  const tablePattern = new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi');
52
- if (tablePattern.test(reqContent)) {
53
- // Re-read since test() advances lastIndex for global regex
54
- reqContent = reqContent.replace(
55
- new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'),
56
- '$1 Complete $2'
57
- );
55
+ const afterTable = reqContent.replace(tablePattern, '$1 Complete $2');
56
+ if (afterTable !== reqContent) {
57
+ reqContent = afterTable;
58
58
  found = true;
59
59
  }
60
60
 
61
61
  if (found) {
62
62
  updated.push(reqId);
63
63
  } else {
64
- // Check if already complete before declaring not_found
65
- const doneCheckbox = new RegExp(`-\\s*\\[x\\]\\s*\\*\\*${reqEscaped}\\*\\*`, 'gi');
66
- const doneTable = new RegExp(`\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|\\s*Complete\\s*\\|`, 'gi');
64
+ // Check if already complete before declaring not_found.
65
+ // Non-global flag is fine here — we only need to know if a match exists.
66
+ const doneCheckbox = new RegExp(`-\\s*\\[x\\]\\s*\\*\\*${reqEscaped}\\*\\*`, 'i');
67
+ const doneTable = new RegExp(`\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|\\s*Complete\\s*\\|`, 'i');
67
68
  if (doneCheckbox.test(reqContent) || doneTable.test(reqContent)) {
68
69
  alreadyComplete.push(reqId);
69
70
  } else {
@@ -73,7 +74,7 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
73
74
  }
74
75
 
75
76
  if (updated.length > 0) {
76
- fs.writeFileSync(reqPath, reqContent, 'utf-8');
77
+ atomicWriteFileSync(reqPath, reqContent);
77
78
  }
78
79
 
79
80
  output({
@@ -177,21 +178,21 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
177
178
  const existing = fs.readFileSync(milestonesPath, 'utf-8');
178
179
  if (!existing.trim()) {
179
180
  // Empty file — treat like new
180
- fs.writeFileSync(milestonesPath, normalizeMd(`# Milestones\n\n${milestoneEntry}`), 'utf-8');
181
+ atomicWriteFileSync(milestonesPath, normalizeMd(`# Milestones\n\n${milestoneEntry}`));
181
182
  } else {
182
183
  // Insert after the header line(s) for reverse chronological order (newest first)
183
184
  const headerMatch = existing.match(/^(#{1,3}\s+[^\n]*\n\n?)/);
184
185
  if (headerMatch) {
185
186
  const header = headerMatch[1];
186
187
  const rest = existing.slice(header.length);
187
- fs.writeFileSync(milestonesPath, normalizeMd(header + milestoneEntry + rest), 'utf-8');
188
+ atomicWriteFileSync(milestonesPath, normalizeMd(header + milestoneEntry + rest));
188
189
  } else {
189
190
  // No recognizable header — prepend the entry
190
- fs.writeFileSync(milestonesPath, normalizeMd(milestoneEntry + existing), 'utf-8');
191
+ atomicWriteFileSync(milestonesPath, normalizeMd(milestoneEntry + existing));
191
192
  }
192
193
  }
193
194
  } else {
194
- fs.writeFileSync(milestonesPath, normalizeMd(`# Milestones\n\n${milestoneEntry}`), 'utf-8');
195
+ atomicWriteFileSync(milestonesPath, normalizeMd(`# Milestones\n\n${milestoneEntry}`));
195
196
  }
196
197
 
197
198
  // Update STATE.md — use shared helpers that handle both **bold:** and plain Field: formats
@@ -19,6 +19,7 @@ const MODEL_PROFILES = {
19
19
  'gsd-plan-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku', adaptive: 'haiku' },
20
20
  'gsd-integration-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku', adaptive: 'haiku' },
21
21
  'gsd-nyquist-auditor': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku', adaptive: 'haiku' },
22
+ 'gsd-pattern-mapper': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku', adaptive: 'haiku' },
22
23
  'gsd-ui-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku', adaptive: 'sonnet' },
23
24
  'gsd-ui-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku', adaptive: 'haiku' },
24
25
  'gsd-ui-auditor': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku', adaptive: 'haiku' },
@@ -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, withPlanningLock, output, error, readSubdirectories, phaseTokenMatches } = require('./core.cjs');
7
+ const { escapeRegex, loadConfig, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, planningDir, withPlanningLock, output, error, readSubdirectories, phaseTokenMatches, atomicWriteFileSync } = require('./core.cjs');
8
8
  const { extractFrontmatter } = require('./frontmatter.cjs');
9
- const { writeStateMd, stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, updatePerformanceMetricsSection } = 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');
@@ -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);
92
+ let baseExists = false;
93
+ const decimalSet = new Set();
109
94
 
110
- // Check if base phase exists
111
- const baseExists = dirs.some(d => phaseTokenMatches(d, normalized));
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));
112
100
 
113
- // Find existing decimal phases for this base
114
- const decimalPattern = new RegExp(`^${normalized}\\.(\\d+)`);
115
- const existingDecimals = [];
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));
105
+ }
106
+ }
116
107
 
117
- for (const dir of dirs) {
118
- const match = dir.match(decimalPattern);
119
- if (match) {
120
- existingDecimals.push(`${normalized}.${match[1]}`);
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));
121
119
  }
120
+ } catch { /* ROADMAP.md read failure is non-fatal */ }
122
121
  }
123
122
 
124
- // Sort numerically
125
- existingDecimals.sort((a, b) => comparePhaseNum(a, b));
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(
@@ -341,15 +340,34 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
341
340
  if (!_newPhaseId) error('--id required when phase_naming is "custom"');
342
341
  _dirName = `${prefix}${_newPhaseId}-${slug}`;
343
342
  } else {
344
- // Sequential mode: find highest integer phase number (in current milestone only)
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
345
347
  const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
346
348
  let maxPhase = 0;
347
349
  let m;
348
350
  while ((m = phasePattern.exec(content)) !== null) {
349
351
  const num = parseInt(m[1], 10);
352
+ if (num >= 999) continue; // backlog phases use 999.x numbering
350
353
  if (num > maxPhase) maxPhase = num;
351
354
  }
352
355
 
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
+ }
370
+
353
371
  _newPhaseId = maxPhase + 1;
354
372
  const paddedNum = String(_newPhaseId).padStart(2, '0');
355
373
  _dirName = `${prefix}${paddedNum}-${slug}`;
@@ -374,7 +392,7 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
374
392
  updatedContent = rawContent + phaseEntry;
375
393
  }
376
394
 
377
- fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
395
+ atomicWriteFileSync(roadmapPath, updatedContent);
378
396
  return { newPhaseId: _newPhaseId, dirName: _dirName };
379
397
  });
380
398
 
@@ -390,6 +408,76 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
390
408
  output(result, raw, result.padded);
391
409
  }
392
410
 
411
+ function cmdPhaseAddBatch(cwd, descriptions, raw) {
412
+ if (!Array.isArray(descriptions) || descriptions.length === 0) {
413
+ error('descriptions array required for phase add-batch');
414
+ }
415
+ const config = loadConfig(cwd);
416
+ const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
417
+ if (!fs.existsSync(roadmapPath)) { error('ROADMAP.md not found'); }
418
+ const projectCode = config.project_code || '';
419
+ const prefix = projectCode ? `${projectCode}-` : '';
420
+
421
+ const results = withPlanningLock(cwd, () => {
422
+ let rawContent = fs.readFileSync(roadmapPath, 'utf-8');
423
+ const content = extractCurrentMilestone(rawContent, cwd);
424
+ let maxPhase = 0;
425
+ if (config.phase_naming !== 'custom') {
426
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
427
+ let m;
428
+ while ((m = phasePattern.exec(content)) !== null) {
429
+ const num = parseInt(m[1], 10);
430
+ if (num >= 999) continue;
431
+ if (num > maxPhase) maxPhase = num;
432
+ }
433
+ const phasesOnDisk = path.join(planningDir(cwd), 'phases');
434
+ if (fs.existsSync(phasesOnDisk)) {
435
+ const dirNumPattern = /^(?:[A-Z][A-Z0-9]*-)?(\d+)-/;
436
+ for (const entry of fs.readdirSync(phasesOnDisk)) {
437
+ const match = entry.match(dirNumPattern);
438
+ if (!match) continue;
439
+ const num = parseInt(match[1], 10);
440
+ if (num >= 999) continue;
441
+ if (num > maxPhase) maxPhase = num;
442
+ }
443
+ }
444
+ }
445
+ const added = [];
446
+ for (const description of descriptions) {
447
+ const slug = generateSlugInternal(description);
448
+ let newPhaseId, dirName;
449
+ if (config.phase_naming === 'custom') {
450
+ newPhaseId = slug.toUpperCase().replace(/-/g, '-');
451
+ dirName = `${prefix}${newPhaseId}-${slug}`;
452
+ } else {
453
+ maxPhase += 1;
454
+ newPhaseId = maxPhase;
455
+ dirName = `${prefix}${String(newPhaseId).padStart(2, '0')}-${slug}`;
456
+ }
457
+ const dirPath = path.join(planningDir(cwd), 'phases', dirName);
458
+ fs.mkdirSync(dirPath, { recursive: true });
459
+ fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
460
+ const dependsOn = config.phase_naming === 'custom' ? '' : `\n**Depends on:** Phase ${typeof newPhaseId === 'number' ? newPhaseId - 1 : 'TBD'}`;
461
+ 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`;
462
+ const lastSeparator = rawContent.lastIndexOf('\n---');
463
+ rawContent = lastSeparator > 0
464
+ ? rawContent.slice(0, lastSeparator) + phaseEntry + rawContent.slice(lastSeparator)
465
+ : rawContent + phaseEntry;
466
+ added.push({
467
+ phase_number: typeof newPhaseId === 'number' ? newPhaseId : String(newPhaseId),
468
+ padded: typeof newPhaseId === 'number' ? String(newPhaseId).padStart(2, '0') : String(newPhaseId),
469
+ name: description,
470
+ slug,
471
+ directory: toPosixPath(path.join(path.relative(cwd, planningDir(cwd)), 'phases', dirName)),
472
+ naming_mode: config.phase_naming,
473
+ });
474
+ }
475
+ atomicWriteFileSync(roadmapPath, rawContent);
476
+ return added;
477
+ });
478
+ output({ phases: results, count: results.length }, raw);
479
+ }
480
+
393
481
  function cmdPhaseInsert(cwd, afterPhase, description, raw) {
394
482
  if (!afterPhase || !description) {
395
483
  error('after-phase and description required for phase insert');
@@ -416,22 +504,31 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
416
504
  error(`Phase ${afterPhase} not found in ROADMAP.md`);
417
505
  }
418
506
 
419
- // Calculate next decimal using existing logic
507
+ // Calculate next decimal by scanning both directories AND ROADMAP.md entries
420
508
  const phasesDir = path.join(planningDir(cwd), 'phases');
421
509
  const normalizedBase = normalizePhaseName(afterPhase);
422
- let existingDecimals = [];
510
+ const decimalSet = new Set();
423
511
 
424
512
  try {
425
513
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
426
514
  const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
427
- const decimalPattern = new RegExp(`^(?:[A-Z]{1,6}-)?${normalizedBase}\\.(\\d+)`);
515
+ const decimalPattern = new RegExp(`^(?:[A-Z]{1,6}-)?${escapeRegex(normalizedBase)}\\.(\\d+)`);
428
516
  for (const dir of dirs) {
429
517
  const dm = dir.match(decimalPattern);
430
- if (dm) existingDecimals.push(parseInt(dm[1], 10));
518
+ if (dm) decimalSet.add(parseInt(dm[1], 10));
431
519
  }
432
520
  } catch { /* intentionally empty */ }
433
521
 
434
- const nextDecimal = existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1;
522
+ // Also scan ROADMAP.md content (already loaded) for decimal entries
523
+ const rmPhasePattern = new RegExp(
524
+ `#{2,4}\\s*Phase\\s+0*${escapeRegex(normalizedBase)}\\.(\\d+)\\s*:`, 'gi'
525
+ );
526
+ let rmMatch;
527
+ while ((rmMatch = rmPhasePattern.exec(rawContent)) !== null) {
528
+ decimalSet.add(parseInt(rmMatch[1], 10));
529
+ }
530
+
531
+ const nextDecimal = decimalSet.size === 0 ? 1 : Math.max(...decimalSet) + 1;
435
532
  const _decimalPhase = `${normalizedBase}.${nextDecimal}`;
436
533
  // Optional project code prefix
437
534
  const insertConfig = loadConfig(cwd);
@@ -466,7 +563,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
466
563
  }
467
564
 
468
565
  const updatedContent = rawContent.slice(0, insertIdx) + phaseEntry + rawContent.slice(insertIdx);
469
- fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
566
+ atomicWriteFileSync(roadmapPath, updatedContent);
470
567
  return { decimalPhase: _decimalPhase, dirName: _dirName };
471
568
  });
472
569
 
@@ -488,10 +585,12 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
488
585
  */
489
586
  function renameDecimalPhases(phasesDir, baseInt, removedDecimal) {
490
587
  const renamedDirs = [], renamedFiles = [];
491
- const decPattern = new RegExp(`^${baseInt}\\.(\\d+)-(.+)$`);
588
+ // Capture the zero-padded prefix (e.g. "06" from "06.3-slug") so the renamed
589
+ // directory preserves the original padding format.
590
+ const decPattern = new RegExp(`^(0*${baseInt})\\.(\\d+)-(.+)$`);
492
591
  const dirs = readSubdirectories(phasesDir, true);
493
592
  const toRename = dirs
494
- .map(dir => { const m = dir.match(decPattern); return m ? { dir, oldDecimal: parseInt(m[1], 10), slug: m[2] } : null; })
593
+ .map(dir => { const m = dir.match(decPattern); return m ? { dir, prefix: m[1], oldDecimal: parseInt(m[2], 10), slug: m[3] } : null; })
495
594
  .filter(item => item && item.oldDecimal > removedDecimal)
496
595
  .sort((a, b) => b.oldDecimal - a.oldDecimal); // descending to avoid conflicts
497
596
 
@@ -499,7 +598,7 @@ function renameDecimalPhases(phasesDir, baseInt, removedDecimal) {
499
598
  const newDecimal = item.oldDecimal - 1;
500
599
  const oldPhaseId = `${baseInt}.${item.oldDecimal}`;
501
600
  const newPhaseId = `${baseInt}.${newDecimal}`;
502
- const newDirName = `${baseInt}.${newDecimal}-${item.slug}`;
601
+ const newDirName = `${item.prefix}.${newDecimal}-${item.slug}`;
503
602
  fs.renameSync(path.join(phasesDir, item.dir), path.join(phasesDir, newDirName));
504
603
  renamedDirs.push({ from: item.dir, to: newDirName });
505
604
  for (const f of fs.readdirSync(path.join(phasesDir, newDirName))) {
@@ -580,7 +679,7 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem
580
679
  }
581
680
  }
582
681
 
583
- fs.writeFileSync(roadmapPath, content, 'utf-8');
682
+ atomicWriteFileSync(roadmapPath, content);
584
683
  });
585
684
  }
586
685
 
@@ -615,7 +714,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
615
714
  let renamedDirs = [], renamedFiles = [];
616
715
  try {
617
716
  const renamed = isDecimal
618
- ? renameDecimalPhases(phasesDir, normalized.split('.')[0], parseInt(normalized.split('.')[1], 10))
717
+ ? renameDecimalPhases(phasesDir, parseInt(normalized.split('.')[0], 10), parseInt(normalized.split('.')[1], 10))
619
718
  : renameIntegerPhases(phasesDir, parseInt(normalized, 10));
620
719
  renamedDirs = renamed.renamedDirs;
621
720
  renamedFiles = renamed.renamedFiles;
@@ -624,19 +723,20 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
624
723
  // Update ROADMAP.md
625
724
  updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, parseInt(normalized, 10), cwd);
626
725
 
627
- // Update STATE.md phase count
726
+ // Update STATE.md phase count atomically (#P4.4)
628
727
  const statePath = path.join(planningDir(cwd), 'STATE.md');
629
728
  if (fs.existsSync(statePath)) {
630
- let stateContent = fs.readFileSync(statePath, 'utf-8');
631
- const totalRaw = stateExtractField(stateContent, 'Total Phases');
632
- if (totalRaw) {
729
+ readModifyWriteStateMd(statePath, (stateContent) => {
730
+ const totalRaw = stateExtractField(stateContent, 'Total Phases');
731
+ if (totalRaw) {
633
732
  stateContent = stateReplaceField(stateContent, 'Total Phases', String(parseInt(totalRaw, 10) - 1)) || stateContent;
634
- }
635
- const ofMatch = stateContent.match(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i);
636
- if (ofMatch) {
733
+ }
734
+ const ofMatch = stateContent.match(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i);
735
+ if (ofMatch) {
637
736
  stateContent = stateContent.replace(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i, `$1${parseInt(ofMatch[2], 10) - 1}$3`);
638
- }
639
- writeStateMd(statePath, stateContent, cwd);
737
+ }
738
+ return stateContent;
739
+ }, cwd);
640
740
  }
641
741
 
642
742
  output({
@@ -701,7 +801,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
701
801
  `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s][^\\n]*)`,
702
802
  'i'
703
803
  );
704
- roadmapContent = replaceInCurrentMilestone(roadmapContent, checkboxPattern, `$1x$2 (completed ${today})`);
804
+ roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
705
805
 
706
806
  // Progress table: update Status to Complete, add date (handles 4 or 5 column tables)
707
807
  const phaseEscaped = escapeRegex(phaseNum);
@@ -725,13 +825,20 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
725
825
  return '|' + cells.join('|') + '|';
726
826
  });
727
827
 
728
- // Update plan count in phase section
828
+ // Update plan count in phase section.
829
+ // Use direct .replace() rather than replaceInCurrentMilestone() so this
830
+ // works when the current milestone section is itself inside a <details>
831
+ // block (the standard /gsd-new-project layout). replaceInCurrentMilestone
832
+ // scopes to content after the last </details>, which misses content inside
833
+ // the current milestone's own <details> wrapper (#2005).
834
+ // The phase-scoped heading pattern is specific enough to avoid matching
835
+ // archived phases (which belong to different milestones).
729
836
  const planCountPattern = new RegExp(
730
837
  `(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
731
838
  'i'
732
839
  );
733
- roadmapContent = replaceInCurrentMilestone(
734
- roadmapContent, planCountPattern,
840
+ roadmapContent = roadmapContent.replace(
841
+ planCountPattern,
735
842
  `$1${summaryCount}/${planCount} plans complete`
736
843
  );
737
844
 
@@ -748,7 +855,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
748
855
  roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
749
856
  }
750
857
 
751
- fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
858
+ atomicWriteFileSync(roadmapPath, roadmapContent);
752
859
 
753
860
  // Update REQUIREMENTS.md traceability for this phase's requirements
754
861
  const reqPath = path.join(planningDir(cwd), 'REQUIREMENTS.md');
@@ -781,7 +888,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
781
888
  );
782
889
  }
783
890
 
784
- fs.writeFileSync(reqPath, reqContent, 'utf-8');
891
+ atomicWriteFileSync(reqPath, reqContent);
785
892
  requirementsUpdated = true;
786
893
  }
787
894
  }
@@ -803,9 +910,11 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
803
910
  .sort((a, b) => comparePhaseNum(a, b));
804
911
 
805
912
  // Find the next phase directory after current
913
+ // Skip backlog phases (999.x) — they are parked ideas, not sequential work (#2129)
806
914
  for (const dir of dirs) {
807
915
  const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
808
916
  if (dm) {
917
+ if (/^999(?:\.|$)/.test(dm[1])) continue;
809
918
  if (comparePhaseNum(dm[1], phaseNum) > 0) {
810
919
  nextPhaseNum = dm[1];
811
920
  nextPhaseName = dm[2] || null;
@@ -834,72 +943,88 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
834
943
  } catch { /* intentionally empty */ }
835
944
  }
836
945
 
837
- // Update STATE.md — use shared helpers that handle both **bold:** and plain Field: formats
946
+ // Update STATE.md atomically hold lock across read-modify-write (#P4.4).
947
+ // Previously read outside the lock; a crash between the ROADMAP update
948
+ // (locked above) and this write left ROADMAP/STATE inconsistent.
838
949
  if (fs.existsSync(statePath)) {
839
- let stateContent = fs.readFileSync(statePath, 'utf-8');
840
-
841
- // Update Current Phase preserve "X of Y (Name)" compound format
842
- const phaseValue = nextPhaseNum || phaseNum;
843
- const existingPhaseField = stateExtractField(stateContent, 'Current Phase')
950
+ readModifyWriteStateMd(statePath, (stateContent) => {
951
+ // Update Current Phase — preserve "X of Y (Name)" compound format
952
+ const phaseValue = nextPhaseNum || phaseNum;
953
+ const existingPhaseField = stateExtractField(stateContent, 'Current Phase')
844
954
  || stateExtractField(stateContent, 'Phase');
845
- let newPhaseValue = String(phaseValue);
846
- if (existingPhaseField) {
955
+ let newPhaseValue = String(phaseValue);
956
+ if (existingPhaseField) {
847
957
  const totalMatch = existingPhaseField.match(/of\s+(\d+)/);
848
958
  const nameMatch = existingPhaseField.match(/\(([^)]+)\)/);
849
959
  if (totalMatch) {
850
- const total = totalMatch[1];
851
- const nameStr = nextPhaseName ? ` (${nextPhaseName.replace(/-/g, ' ')})` : (nameMatch ? ` (${nameMatch[1]})` : '');
852
- newPhaseValue = `${phaseValue} of ${total}${nameStr}`;
960
+ const total = totalMatch[1];
961
+ const nameStr = nextPhaseName ? ` (${nextPhaseName.replace(/-/g, ' ')})` : (nameMatch ? ` (${nameMatch[1]})` : '');
962
+ newPhaseValue = `${phaseValue} of ${total}${nameStr}`;
853
963
  }
854
- }
855
- stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase', 'Phase', newPhaseValue);
964
+ }
965
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase', 'Phase', newPhaseValue);
856
966
 
857
- // Update Current Phase Name
858
- if (nextPhaseName) {
967
+ // Update Current Phase Name
968
+ if (nextPhaseName) {
859
969
  stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase Name', null, nextPhaseName.replace(/-/g, ' '));
860
- }
970
+ }
861
971
 
862
- // Update Status
863
- stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null,
972
+ // Update Status
973
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null,
864
974
  isLastPhase ? 'Milestone complete' : 'Ready to plan');
865
975
 
866
- // Update Current Plan
867
- stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Plan', 'Plan', 'Not started');
976
+ // Update Current Plan
977
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Plan', 'Plan', 'Not started');
868
978
 
869
- // Update Last Activity
870
- stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity', 'Last activity', today);
979
+ // Update Last Activity
980
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity', 'Last activity', today);
871
981
 
872
- // Update Last Activity Description
873
- stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity Description', null,
982
+ // Update Last Activity Description
983
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity Description', null,
874
984
  `Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`);
875
985
 
876
- // Increment Completed Phases counter (#956)
877
- const completedRaw = stateExtractField(stateContent, 'Completed Phases');
878
- if (completedRaw) {
986
+ // Increment Completed Phases counter (#956)
987
+ const completedRaw = stateExtractField(stateContent, 'Completed Phases');
988
+ if (completedRaw) {
879
989
  const newCompleted = parseInt(completedRaw, 10) + 1;
880
990
  stateContent = stateReplaceField(stateContent, 'Completed Phases', String(newCompleted)) || stateContent;
881
991
 
882
992
  // Recalculate percent based on completed / total (#956)
883
993
  const totalRaw = stateExtractField(stateContent, 'Total Phases');
884
994
  if (totalRaw) {
885
- const totalPhases = parseInt(totalRaw, 10);
886
- if (totalPhases > 0) {
995
+ const totalPhases = parseInt(totalRaw, 10);
996
+ if (totalPhases > 0) {
887
997
  const newPercent = Math.round((newCompleted / totalPhases) * 100);
888
998
  stateContent = stateReplaceField(stateContent, 'Progress', `${newPercent}%`) || stateContent;
889
- // Also update percent field if it exists separately
890
999
  stateContent = stateContent.replace(
891
- /(percent:\s*)\d+/,
892
- `$1${newPercent}`
1000
+ /(percent:\s*)\d+/,
1001
+ `$1${newPercent}`
893
1002
  );
1003
+ }
894
1004
  }
895
1005
  }
896
- }
897
1006
 
898
- // Gate 4: Update Performance Metrics section (#1627)
899
- stateContent = updatePerformanceMetricsSection(stateContent, cwd, phaseNum, planCount, summaryCount);
1007
+ // Gate 4: Update Performance Metrics section (#1627)
1008
+ stateContent = updatePerformanceMetricsSection(stateContent, cwd, phaseNum, planCount, summaryCount);
900
1009
 
901
- writeStateMd(statePath, stateContent, cwd);
1010
+ return stateContent;
1011
+ }, cwd);
1012
+ }
1013
+
1014
+ // Auto-prune STATE.md on phase boundary when configured (#2087)
1015
+ let autoPruned = false;
1016
+ try {
1017
+ const configPath = path.join(planningDir(cwd), 'config.json');
1018
+ if (fs.existsSync(configPath)) {
1019
+ const rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1020
+ const autoPruneEnabled = rawConfig.workflow && rawConfig.workflow.auto_prune_state === true;
1021
+ if (autoPruneEnabled && fs.existsSync(statePath)) {
1022
+ const { cmdStatePrune } = require('./state.cjs');
1023
+ cmdStatePrune(cwd, { keepRecent: '3', dryRun: false, silent: true }, true);
1024
+ autoPruned = true;
1025
+ }
902
1026
  }
1027
+ } catch { /* intentionally empty — auto-prune is best-effort */ }
903
1028
 
904
1029
  const result = {
905
1030
  completed_phase: phaseNum,
@@ -912,6 +1037,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
912
1037
  roadmap_updated: fs.existsSync(roadmapPath),
913
1038
  state_updated: fs.existsSync(statePath),
914
1039
  requirements_updated: requirementsUpdated,
1040
+ auto_pruned: autoPruned,
915
1041
  warnings,
916
1042
  has_warnings: warnings.length > 0,
917
1043
  };
@@ -925,6 +1051,7 @@ module.exports = {
925
1051
  cmdFindPhase,
926
1052
  cmdPhasePlanIndex,
927
1053
  cmdPhaseAdd,
1054
+ cmdPhaseAddBatch,
928
1055
  cmdPhaseInsert,
929
1056
  cmdPhaseRemove,
930
1057
  cmdPhaseComplete,