gsd-remix 1.0.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 (554) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +939 -0
  3. package/README.zh-CN.md +876 -0
  4. package/agents/gsd-advisor-researcher.md +127 -0
  5. package/agents/gsd-ai-researcher.md +133 -0
  6. package/agents/gsd-assumptions-analyzer.md +105 -0
  7. package/agents/gsd-code-fixer.md +517 -0
  8. package/agents/gsd-code-reviewer.md +371 -0
  9. package/agents/gsd-codebase-mapper.md +781 -0
  10. package/agents/gsd-debug-session-manager.md +314 -0
  11. package/agents/gsd-debugger.md +1452 -0
  12. package/agents/gsd-doc-classifier.md +168 -0
  13. package/agents/gsd-doc-synthesizer.md +204 -0
  14. package/agents/gsd-doc-verifier.md +217 -0
  15. package/agents/gsd-doc-writer.md +615 -0
  16. package/agents/gsd-domain-researcher.md +153 -0
  17. package/agents/gsd-eval-auditor.md +191 -0
  18. package/agents/gsd-eval-planner.md +154 -0
  19. package/agents/gsd-executor.md +603 -0
  20. package/agents/gsd-framework-selector.md +160 -0
  21. package/agents/gsd-integration-checker.md +470 -0
  22. package/agents/gsd-intel-updater.md +334 -0
  23. package/agents/gsd-nyquist-auditor.md +203 -0
  24. package/agents/gsd-pattern-mapper.md +335 -0
  25. package/agents/gsd-phase-researcher.md +841 -0
  26. package/agents/gsd-plan-checker.md +978 -0
  27. package/agents/gsd-planner.md +1251 -0
  28. package/agents/gsd-project-researcher.md +677 -0
  29. package/agents/gsd-research-synthesizer.md +247 -0
  30. package/agents/gsd-roadmapper.md +688 -0
  31. package/agents/gsd-security-auditor.md +155 -0
  32. package/agents/gsd-ui-auditor.md +495 -0
  33. package/agents/gsd-ui-checker.md +309 -0
  34. package/agents/gsd-ui-researcher.md +380 -0
  35. package/agents/gsd-user-profiler.md +171 -0
  36. package/agents/gsd-verifier.md +830 -0
  37. package/bin/install.js +7062 -0
  38. package/commands/gsd/add-backlog.md +79 -0
  39. package/commands/gsd/add-phase.md +43 -0
  40. package/commands/gsd/add-tests.md +41 -0
  41. package/commands/gsd/add-todo.md +47 -0
  42. package/commands/gsd/ai-integration-phase.md +36 -0
  43. package/commands/gsd/analyze-dependencies.md +34 -0
  44. package/commands/gsd/audit-fix.md +33 -0
  45. package/commands/gsd/audit-milestone.md +36 -0
  46. package/commands/gsd/audit-uat.md +24 -0
  47. package/commands/gsd/autonomous.md +46 -0
  48. package/commands/gsd/check-todos.md +45 -0
  49. package/commands/gsd/cleanup.md +23 -0
  50. package/commands/gsd/code-review-fix.md +52 -0
  51. package/commands/gsd/code-review.md +55 -0
  52. package/commands/gsd/complete-milestone.md +136 -0
  53. package/commands/gsd/debug.md +263 -0
  54. package/commands/gsd/discuss-phase.md +69 -0
  55. package/commands/gsd/do.md +30 -0
  56. package/commands/gsd/docs-update.md +48 -0
  57. package/commands/gsd/eval-review.md +32 -0
  58. package/commands/gsd/execute-phase.md +63 -0
  59. package/commands/gsd/explore.md +27 -0
  60. package/commands/gsd/extract_learnings.md +22 -0
  61. package/commands/gsd/fast.md +30 -0
  62. package/commands/gsd/forensics.md +56 -0
  63. package/commands/gsd/from-gsd2.md +47 -0
  64. package/commands/gsd/graphify.md +201 -0
  65. package/commands/gsd/health.md +22 -0
  66. package/commands/gsd/help.md +24 -0
  67. package/commands/gsd/import.md +37 -0
  68. package/commands/gsd/inbox.md +38 -0
  69. package/commands/gsd/ingest-docs.md +42 -0
  70. package/commands/gsd/insert-phase.md +32 -0
  71. package/commands/gsd/intel.md +179 -0
  72. package/commands/gsd/join-discord.md +19 -0
  73. package/commands/gsd/list-phase-assumptions.md +46 -0
  74. package/commands/gsd/list-workspaces.md +19 -0
  75. package/commands/gsd/manager.md +40 -0
  76. package/commands/gsd/map-codebase.md +71 -0
  77. package/commands/gsd/milestone-summary.md +51 -0
  78. package/commands/gsd/new-milestone.md +44 -0
  79. package/commands/gsd/new-project.md +46 -0
  80. package/commands/gsd/new-workspace.md +44 -0
  81. package/commands/gsd/next.md +28 -0
  82. package/commands/gsd/note.md +34 -0
  83. package/commands/gsd/pause-work.md +38 -0
  84. package/commands/gsd/plan-milestone-gaps.md +34 -0
  85. package/commands/gsd/plan-phase.md +52 -0
  86. package/commands/gsd/plan-review-convergence.md +52 -0
  87. package/commands/gsd/plant-seed.md +28 -0
  88. package/commands/gsd/pr-branch.md +25 -0
  89. package/commands/gsd/profile-user.md +46 -0
  90. package/commands/gsd/progress.md +25 -0
  91. package/commands/gsd/quick.md +173 -0
  92. package/commands/gsd/reapply-patches.md +331 -0
  93. package/commands/gsd/remove-phase.md +31 -0
  94. package/commands/gsd/remove-workspace.md +26 -0
  95. package/commands/gsd/research-phase.md +195 -0
  96. package/commands/gsd/resume-work.md +40 -0
  97. package/commands/gsd/review-backlog.md +62 -0
  98. package/commands/gsd/review.md +40 -0
  99. package/commands/gsd/scan.md +26 -0
  100. package/commands/gsd/secure-phase.md +35 -0
  101. package/commands/gsd/session-report.md +19 -0
  102. package/commands/gsd/set-profile.md +12 -0
  103. package/commands/gsd/settings.md +36 -0
  104. package/commands/gsd/ship.md +23 -0
  105. package/commands/gsd/sketch-wrap-up.md +31 -0
  106. package/commands/gsd/sketch.md +49 -0
  107. package/commands/gsd/spec-phase.md +62 -0
  108. package/commands/gsd/spike-wrap-up.md +31 -0
  109. package/commands/gsd/spike.md +46 -0
  110. package/commands/gsd/stats.md +18 -0
  111. package/commands/gsd/sync-skills.md +19 -0
  112. package/commands/gsd/thread.md +227 -0
  113. package/commands/gsd/ui-phase.md +34 -0
  114. package/commands/gsd/ui-review.md +32 -0
  115. package/commands/gsd/ultraplan-phase.md +33 -0
  116. package/commands/gsd/undo.md +34 -0
  117. package/commands/gsd/update.md +37 -0
  118. package/commands/gsd/validate-phase.md +35 -0
  119. package/commands/gsd/verify-work.md +38 -0
  120. package/commands/gsd/workstreams.md +69 -0
  121. package/get-shit-done/bin/gsd-tools.cjs +1263 -0
  122. package/get-shit-done/bin/lib/artifacts.cjs +52 -0
  123. package/get-shit-done/bin/lib/audit.cjs +757 -0
  124. package/get-shit-done/bin/lib/commands.cjs +1023 -0
  125. package/get-shit-done/bin/lib/config-schema.cjs +79 -0
  126. package/get-shit-done/bin/lib/config.cjs +463 -0
  127. package/get-shit-done/bin/lib/core.cjs +1794 -0
  128. package/get-shit-done/bin/lib/docs.cjs +267 -0
  129. package/get-shit-done/bin/lib/frontmatter.cjs +379 -0
  130. package/get-shit-done/bin/lib/graphify.cjs +494 -0
  131. package/get-shit-done/bin/lib/gsd2-import.cjs +511 -0
  132. package/get-shit-done/bin/lib/init.cjs +1878 -0
  133. package/get-shit-done/bin/lib/intel.cjs +639 -0
  134. package/get-shit-done/bin/lib/learnings.cjs +378 -0
  135. package/get-shit-done/bin/lib/milestone.cjs +283 -0
  136. package/get-shit-done/bin/lib/model-profiles.cjs +71 -0
  137. package/get-shit-done/bin/lib/phase.cjs +1058 -0
  138. package/get-shit-done/bin/lib/profile-output.cjs +1080 -0
  139. package/get-shit-done/bin/lib/profile-pipeline.cjs +539 -0
  140. package/get-shit-done/bin/lib/roadmap.cjs +523 -0
  141. package/get-shit-done/bin/lib/schema-detect.cjs +238 -0
  142. package/get-shit-done/bin/lib/security.cjs +504 -0
  143. package/get-shit-done/bin/lib/state.cjs +1649 -0
  144. package/get-shit-done/bin/lib/template.cjs +226 -0
  145. package/get-shit-done/bin/lib/uat.cjs +288 -0
  146. package/get-shit-done/bin/lib/verify.cjs +1184 -0
  147. package/get-shit-done/bin/lib/workstream.cjs +495 -0
  148. package/get-shit-done/bin/repair-sdk.cjs +177 -0
  149. package/get-shit-done/contexts/dev.md +21 -0
  150. package/get-shit-done/contexts/research.md +22 -0
  151. package/get-shit-done/contexts/review.md +22 -0
  152. package/get-shit-done/references/agent-contracts.md +79 -0
  153. package/get-shit-done/references/ai-evals.md +156 -0
  154. package/get-shit-done/references/ai-frameworks.md +186 -0
  155. package/get-shit-done/references/artifact-types.md +131 -0
  156. package/get-shit-done/references/autonomous-smart-discuss.md +277 -0
  157. package/get-shit-done/references/checkpoints.md +808 -0
  158. package/get-shit-done/references/common-bug-patterns.md +114 -0
  159. package/get-shit-done/references/context-budget.md +49 -0
  160. package/get-shit-done/references/continuation-format.md +253 -0
  161. package/get-shit-done/references/debugger-philosophy.md +76 -0
  162. package/get-shit-done/references/decimal-phase-calculation.md +64 -0
  163. package/get-shit-done/references/doc-conflict-engine.md +91 -0
  164. package/get-shit-done/references/domain-probes.md +125 -0
  165. package/get-shit-done/references/executor-examples.md +110 -0
  166. package/get-shit-done/references/few-shot-examples/plan-checker.md +73 -0
  167. package/get-shit-done/references/few-shot-examples/verifier.md +109 -0
  168. package/get-shit-done/references/gate-prompts.md +100 -0
  169. package/get-shit-done/references/gates.md +70 -0
  170. package/get-shit-done/references/git-integration.md +295 -0
  171. package/get-shit-done/references/git-planning-commit.md +40 -0
  172. package/get-shit-done/references/ios-scaffold.md +123 -0
  173. package/get-shit-done/references/mandatory-initial-read.md +2 -0
  174. package/get-shit-done/references/model-profile-resolution.md +38 -0
  175. package/get-shit-done/references/model-profiles.md +145 -0
  176. package/get-shit-done/references/phase-argument-parsing.md +61 -0
  177. package/get-shit-done/references/planner-antipatterns.md +89 -0
  178. package/get-shit-done/references/planner-gap-closure.md +62 -0
  179. package/get-shit-done/references/planner-reviews.md +39 -0
  180. package/get-shit-done/references/planner-revision.md +87 -0
  181. package/get-shit-done/references/planner-source-audit.md +73 -0
  182. package/get-shit-done/references/planning-config.md +460 -0
  183. package/get-shit-done/references/project-skills-discovery.md +19 -0
  184. package/get-shit-done/references/questioning.md +162 -0
  185. package/get-shit-done/references/revision-loop.md +97 -0
  186. package/get-shit-done/references/sketch-interactivity.md +41 -0
  187. package/get-shit-done/references/sketch-theme-system.md +94 -0
  188. package/get-shit-done/references/sketch-tooling.md +45 -0
  189. package/get-shit-done/references/sketch-variant-patterns.md +81 -0
  190. package/get-shit-done/references/tdd.md +330 -0
  191. package/get-shit-done/references/thinking-models-debug.md +44 -0
  192. package/get-shit-done/references/thinking-models-execution.md +50 -0
  193. package/get-shit-done/references/thinking-models-planning.md +62 -0
  194. package/get-shit-done/references/thinking-models-research.md +50 -0
  195. package/get-shit-done/references/thinking-models-verification.md +55 -0
  196. package/get-shit-done/references/thinking-partner.md +96 -0
  197. package/get-shit-done/references/ui-brand.md +160 -0
  198. package/get-shit-done/references/universal-anti-patterns.md +63 -0
  199. package/get-shit-done/references/user-profiling.md +681 -0
  200. package/get-shit-done/references/verification-overrides.md +227 -0
  201. package/get-shit-done/references/verification-patterns.md +612 -0
  202. package/get-shit-done/references/workstream-flag.md +111 -0
  203. package/get-shit-done/templates/AI-SPEC.md +246 -0
  204. package/get-shit-done/templates/DEBUG.md +169 -0
  205. package/get-shit-done/templates/README.md +76 -0
  206. package/get-shit-done/templates/SECURITY.md +61 -0
  207. package/get-shit-done/templates/UAT.md +265 -0
  208. package/get-shit-done/templates/UI-SPEC.md +100 -0
  209. package/get-shit-done/templates/VALIDATION.md +76 -0
  210. package/get-shit-done/templates/claude-md.md +145 -0
  211. package/get-shit-done/templates/codebase/architecture.md +255 -0
  212. package/get-shit-done/templates/codebase/concerns.md +310 -0
  213. package/get-shit-done/templates/codebase/conventions.md +307 -0
  214. package/get-shit-done/templates/codebase/integrations.md +280 -0
  215. package/get-shit-done/templates/codebase/stack.md +186 -0
  216. package/get-shit-done/templates/codebase/structure.md +285 -0
  217. package/get-shit-done/templates/codebase/testing.md +480 -0
  218. package/get-shit-done/templates/config.json +56 -0
  219. package/get-shit-done/templates/context.md +352 -0
  220. package/get-shit-done/templates/continue-here.md +78 -0
  221. package/get-shit-done/templates/copilot-instructions.md +7 -0
  222. package/get-shit-done/templates/debug-subagent-prompt.md +91 -0
  223. package/get-shit-done/templates/dev-preferences.md +21 -0
  224. package/get-shit-done/templates/discovery.md +146 -0
  225. package/get-shit-done/templates/discussion-log.md +63 -0
  226. package/get-shit-done/templates/milestone-archive.md +123 -0
  227. package/get-shit-done/templates/milestone.md +115 -0
  228. package/get-shit-done/templates/phase-prompt.md +610 -0
  229. package/get-shit-done/templates/planner-subagent-prompt.md +117 -0
  230. package/get-shit-done/templates/project.md +186 -0
  231. package/get-shit-done/templates/requirements.md +231 -0
  232. package/get-shit-done/templates/research-project/ARCHITECTURE.md +204 -0
  233. package/get-shit-done/templates/research-project/FEATURES.md +147 -0
  234. package/get-shit-done/templates/research-project/PITFALLS.md +200 -0
  235. package/get-shit-done/templates/research-project/STACK.md +120 -0
  236. package/get-shit-done/templates/research-project/SUMMARY.md +170 -0
  237. package/get-shit-done/templates/research.md +592 -0
  238. package/get-shit-done/templates/retrospective.md +54 -0
  239. package/get-shit-done/templates/roadmap.md +202 -0
  240. package/get-shit-done/templates/spec.md +307 -0
  241. package/get-shit-done/templates/state.md +184 -0
  242. package/get-shit-done/templates/summary-complex.md +59 -0
  243. package/get-shit-done/templates/summary-minimal.md +41 -0
  244. package/get-shit-done/templates/summary-standard.md +48 -0
  245. package/get-shit-done/templates/summary.md +248 -0
  246. package/get-shit-done/templates/user-profile.md +146 -0
  247. package/get-shit-done/templates/user-setup.md +311 -0
  248. package/get-shit-done/templates/verification-report.md +322 -0
  249. package/get-shit-done/workflows/add-phase.md +112 -0
  250. package/get-shit-done/workflows/add-tests.md +354 -0
  251. package/get-shit-done/workflows/add-todo.md +160 -0
  252. package/get-shit-done/workflows/ai-integration-phase.md +284 -0
  253. package/get-shit-done/workflows/analyze-dependencies.md +96 -0
  254. package/get-shit-done/workflows/audit-fix.md +175 -0
  255. package/get-shit-done/workflows/audit-milestone.md +340 -0
  256. package/get-shit-done/workflows/audit-uat.md +109 -0
  257. package/get-shit-done/workflows/autonomous.md +789 -0
  258. package/get-shit-done/workflows/check-todos.md +179 -0
  259. package/get-shit-done/workflows/cleanup.md +154 -0
  260. package/get-shit-done/workflows/code-review-fix.md +497 -0
  261. package/get-shit-done/workflows/code-review.md +515 -0
  262. package/get-shit-done/workflows/complete-milestone.md +847 -0
  263. package/get-shit-done/workflows/diagnose-issues.md +238 -0
  264. package/get-shit-done/workflows/discovery-phase.md +291 -0
  265. package/get-shit-done/workflows/discuss-phase-assumptions.md +670 -0
  266. package/get-shit-done/workflows/discuss-phase-power.md +308 -0
  267. package/get-shit-done/workflows/discuss-phase.md +1378 -0
  268. package/get-shit-done/workflows/do.md +110 -0
  269. package/get-shit-done/workflows/docs-update.md +1155 -0
  270. package/get-shit-done/workflows/eval-review.md +155 -0
  271. package/get-shit-done/workflows/execute-phase.md +1677 -0
  272. package/get-shit-done/workflows/execute-plan.md +533 -0
  273. package/get-shit-done/workflows/explore.md +141 -0
  274. package/get-shit-done/workflows/extract_learnings.md +242 -0
  275. package/get-shit-done/workflows/fast.md +105 -0
  276. package/get-shit-done/workflows/forensics.md +265 -0
  277. package/get-shit-done/workflows/graduation.md +195 -0
  278. package/get-shit-done/workflows/health.md +314 -0
  279. package/get-shit-done/workflows/help.md +667 -0
  280. package/get-shit-done/workflows/import.md +246 -0
  281. package/get-shit-done/workflows/inbox.md +387 -0
  282. package/get-shit-done/workflows/ingest-docs.md +328 -0
  283. package/get-shit-done/workflows/insert-phase.md +130 -0
  284. package/get-shit-done/workflows/list-phase-assumptions.md +178 -0
  285. package/get-shit-done/workflows/list-workspaces.md +56 -0
  286. package/get-shit-done/workflows/manager.md +365 -0
  287. package/get-shit-done/workflows/map-codebase.md +393 -0
  288. package/get-shit-done/workflows/milestone-summary.md +223 -0
  289. package/get-shit-done/workflows/new-milestone.md +611 -0
  290. package/get-shit-done/workflows/new-project.md +1391 -0
  291. package/get-shit-done/workflows/new-workspace.md +239 -0
  292. package/get-shit-done/workflows/next.md +220 -0
  293. package/get-shit-done/workflows/node-repair.md +92 -0
  294. package/get-shit-done/workflows/note.md +158 -0
  295. package/get-shit-done/workflows/pause-work.md +243 -0
  296. package/get-shit-done/workflows/plan-milestone-gaps.md +273 -0
  297. package/get-shit-done/workflows/plan-phase.md +1349 -0
  298. package/get-shit-done/workflows/plan-review-convergence.md +254 -0
  299. package/get-shit-done/workflows/plant-seed.md +172 -0
  300. package/get-shit-done/workflows/pr-branch.md +157 -0
  301. package/get-shit-done/workflows/profile-user.md +452 -0
  302. package/get-shit-done/workflows/progress.md +619 -0
  303. package/get-shit-done/workflows/quick.md +970 -0
  304. package/get-shit-done/workflows/remove-phase.md +155 -0
  305. package/get-shit-done/workflows/remove-workspace.md +92 -0
  306. package/get-shit-done/workflows/research-phase.md +89 -0
  307. package/get-shit-done/workflows/resume-project.md +326 -0
  308. package/get-shit-done/workflows/review.md +344 -0
  309. package/get-shit-done/workflows/scan.md +102 -0
  310. package/get-shit-done/workflows/secure-phase.md +166 -0
  311. package/get-shit-done/workflows/session-report.md +146 -0
  312. package/get-shit-done/workflows/settings.md +319 -0
  313. package/get-shit-done/workflows/ship.md +302 -0
  314. package/get-shit-done/workflows/sketch-wrap-up.md +283 -0
  315. package/get-shit-done/workflows/sketch.md +286 -0
  316. package/get-shit-done/workflows/spec-phase.md +262 -0
  317. package/get-shit-done/workflows/spike-wrap-up.md +281 -0
  318. package/get-shit-done/workflows/spike.md +362 -0
  319. package/get-shit-done/workflows/stats.md +60 -0
  320. package/get-shit-done/workflows/sync-skills.md +182 -0
  321. package/get-shit-done/workflows/transition.md +693 -0
  322. package/get-shit-done/workflows/ui-phase.md +323 -0
  323. package/get-shit-done/workflows/ui-review.md +190 -0
  324. package/get-shit-done/workflows/ultraplan-phase.md +189 -0
  325. package/get-shit-done/workflows/undo.md +314 -0
  326. package/get-shit-done/workflows/update.md +587 -0
  327. package/get-shit-done/workflows/validate-phase.md +176 -0
  328. package/get-shit-done/workflows/verify-phase.md +465 -0
  329. package/get-shit-done/workflows/verify-work.md +740 -0
  330. package/hooks/dist/gsd-check-update-worker.js +108 -0
  331. package/hooks/dist/gsd-check-update.js +64 -0
  332. package/hooks/dist/gsd-context-monitor.js +192 -0
  333. package/hooks/dist/gsd-phase-boundary.sh +28 -0
  334. package/hooks/dist/gsd-prompt-guard.js +97 -0
  335. package/hooks/dist/gsd-read-guard.js +82 -0
  336. package/hooks/dist/gsd-read-injection-scanner.js +152 -0
  337. package/hooks/dist/gsd-session-state.sh +34 -0
  338. package/hooks/dist/gsd-statusline.js +293 -0
  339. package/hooks/dist/gsd-validate-commit.sh +48 -0
  340. package/hooks/dist/gsd-workflow-guard.js +94 -0
  341. package/hooks/gsd-check-update-worker.js +108 -0
  342. package/hooks/gsd-check-update.js +64 -0
  343. package/hooks/gsd-context-monitor.js +192 -0
  344. package/hooks/gsd-phase-boundary.sh +28 -0
  345. package/hooks/gsd-prompt-guard.js +97 -0
  346. package/hooks/gsd-read-guard.js +82 -0
  347. package/hooks/gsd-read-injection-scanner.js +152 -0
  348. package/hooks/gsd-session-state.sh +34 -0
  349. package/hooks/gsd-statusline.js +293 -0
  350. package/hooks/gsd-validate-commit.sh +48 -0
  351. package/hooks/gsd-workflow-guard.js +94 -0
  352. package/package.json +59 -0
  353. package/scripts/base64-scan.sh +262 -0
  354. package/scripts/build-hooks.js +95 -0
  355. package/scripts/gen-inventory-manifest.cjs +109 -0
  356. package/scripts/prompt-injection-scan.sh +201 -0
  357. package/scripts/run-tests.cjs +33 -0
  358. package/scripts/secret-scan.sh +227 -0
  359. package/sdk/package-lock.json +1998 -0
  360. package/sdk/package.json +52 -0
  361. package/sdk/prompts/agents/gsd-executor.md +110 -0
  362. package/sdk/prompts/agents/gsd-phase-researcher.md +158 -0
  363. package/sdk/prompts/agents/gsd-plan-checker.md +160 -0
  364. package/sdk/prompts/agents/gsd-planner.md +214 -0
  365. package/sdk/prompts/agents/gsd-project-researcher.md +323 -0
  366. package/sdk/prompts/agents/gsd-research-synthesizer.md +237 -0
  367. package/sdk/prompts/agents/gsd-roadmapper.md +670 -0
  368. package/sdk/prompts/agents/gsd-verifier.md +159 -0
  369. package/sdk/prompts/templates/project.md +186 -0
  370. package/sdk/prompts/templates/requirements.md +231 -0
  371. package/sdk/prompts/templates/research-project/ARCHITECTURE.md +204 -0
  372. package/sdk/prompts/templates/research-project/FEATURES.md +147 -0
  373. package/sdk/prompts/templates/research-project/PITFALLS.md +200 -0
  374. package/sdk/prompts/templates/research-project/STACK.md +120 -0
  375. package/sdk/prompts/templates/research-project/SUMMARY.md +170 -0
  376. package/sdk/prompts/templates/roadmap.md +202 -0
  377. package/sdk/prompts/templates/state.md +175 -0
  378. package/sdk/prompts/workflows/discuss-phase.md +126 -0
  379. package/sdk/prompts/workflows/execute-plan.md +106 -0
  380. package/sdk/prompts/workflows/plan-phase.md +84 -0
  381. package/sdk/prompts/workflows/research-phase.md +45 -0
  382. package/sdk/prompts/workflows/verify-phase.md +142 -0
  383. package/sdk/src/assembled-prompts.test.ts +349 -0
  384. package/sdk/src/cli-transport.test.ts +388 -0
  385. package/sdk/src/cli-transport.ts +130 -0
  386. package/sdk/src/cli.test.ts +383 -0
  387. package/sdk/src/cli.ts +670 -0
  388. package/sdk/src/config.test.ts +168 -0
  389. package/sdk/src/config.ts +177 -0
  390. package/sdk/src/context-engine.test.ts +295 -0
  391. package/sdk/src/context-engine.ts +170 -0
  392. package/sdk/src/context-truncation.test.ts +163 -0
  393. package/sdk/src/context-truncation.ts +233 -0
  394. package/sdk/src/e2e.integration.test.ts +178 -0
  395. package/sdk/src/errors.ts +72 -0
  396. package/sdk/src/event-stream.test.ts +661 -0
  397. package/sdk/src/event-stream.ts +441 -0
  398. package/sdk/src/failure-memory.test.ts +457 -0
  399. package/sdk/src/failure-memory.ts +1324 -0
  400. package/sdk/src/golden/capture.ts +95 -0
  401. package/sdk/src/golden/fixtures/generate-slug.golden.json +1 -0
  402. package/sdk/src/golden/fixtures/profile-sample-sessions/demo-project/sample.jsonl +3 -0
  403. package/sdk/src/golden/fixtures/summary-extract-sample.md +26 -0
  404. package/sdk/src/golden/fixtures/uat-render-checkpoint-sample.md +15 -0
  405. package/sdk/src/golden/golden-integration-covered.ts +30 -0
  406. package/sdk/src/golden/golden-mutation-covered.ts +7 -0
  407. package/sdk/src/golden/golden-policy.test.ts +8 -0
  408. package/sdk/src/golden/golden-policy.ts +112 -0
  409. package/sdk/src/golden/golden.integration.test.ts +373 -0
  410. package/sdk/src/golden/init-golden-normalize.ts +15 -0
  411. package/sdk/src/golden/read-only-golden-rows.ts +77 -0
  412. package/sdk/src/golden/read-only-parity.integration.test.ts +125 -0
  413. package/sdk/src/golden/registry-canonical-commands.ts +31 -0
  414. package/sdk/src/gsd-tools.test.ts +409 -0
  415. package/sdk/src/gsd-tools.ts +595 -0
  416. package/sdk/src/headless-prompts.test.ts +159 -0
  417. package/sdk/src/index.ts +333 -0
  418. package/sdk/src/init-e2e.integration.test.ts +136 -0
  419. package/sdk/src/init-runner.test.ts +783 -0
  420. package/sdk/src/init-runner.ts +735 -0
  421. package/sdk/src/lifecycle-e2e.integration.test.ts +258 -0
  422. package/sdk/src/logger.test.ts +149 -0
  423. package/sdk/src/logger.ts +113 -0
  424. package/sdk/src/milestone-runner.test.ts +421 -0
  425. package/sdk/src/phase-prompt.test.ts +538 -0
  426. package/sdk/src/phase-prompt.ts +264 -0
  427. package/sdk/src/phase-runner-types.test.ts +421 -0
  428. package/sdk/src/phase-runner.integration.test.ts +377 -0
  429. package/sdk/src/phase-runner.test.ts +2333 -0
  430. package/sdk/src/phase-runner.ts +1203 -0
  431. package/sdk/src/plan-parser.test.ts +528 -0
  432. package/sdk/src/plan-parser.ts +427 -0
  433. package/sdk/src/prompt-builder.test.ts +306 -0
  434. package/sdk/src/prompt-builder.ts +193 -0
  435. package/sdk/src/prompt-sanitizer.test.ts +260 -0
  436. package/sdk/src/prompt-sanitizer.ts +71 -0
  437. package/sdk/src/query/QUERY-HANDLERS.md +317 -0
  438. package/sdk/src/query/audit-open.ts +722 -0
  439. package/sdk/src/query/check-auto-mode.test.ts +77 -0
  440. package/sdk/src/query/check-auto-mode.ts +50 -0
  441. package/sdk/src/query/check-completion.test.ts +113 -0
  442. package/sdk/src/query/check-completion.ts +182 -0
  443. package/sdk/src/query/check-gates.test.ts +103 -0
  444. package/sdk/src/query/check-gates.ts +112 -0
  445. package/sdk/src/query/check-ship-ready.test.ts +77 -0
  446. package/sdk/src/query/check-ship-ready.ts +103 -0
  447. package/sdk/src/query/check-verification-status.test.ts +143 -0
  448. package/sdk/src/query/check-verification-status.ts +160 -0
  449. package/sdk/src/query/commit.test.ts +202 -0
  450. package/sdk/src/query/commit.ts +301 -0
  451. package/sdk/src/query/config-gates.test.ts +89 -0
  452. package/sdk/src/query/config-gates.ts +69 -0
  453. package/sdk/src/query/config-mutation.test.ts +365 -0
  454. package/sdk/src/query/config-mutation.ts +497 -0
  455. package/sdk/src/query/config-query.test.ts +161 -0
  456. package/sdk/src/query/config-query.ts +190 -0
  457. package/sdk/src/query/context-history.test.ts +165 -0
  458. package/sdk/src/query/context-history.ts +467 -0
  459. package/sdk/src/query/decomposed-handlers.test.ts +365 -0
  460. package/sdk/src/query/detect-custom-files.ts +97 -0
  461. package/sdk/src/query/detect-phase-type.test.ts +105 -0
  462. package/sdk/src/query/detect-phase-type.ts +141 -0
  463. package/sdk/src/query/docs-init.ts +257 -0
  464. package/sdk/src/query/failure-capture.ts +58 -0
  465. package/sdk/src/query/frontmatter-array.test.ts +14 -0
  466. package/sdk/src/query/frontmatter-mutation.test.ts +259 -0
  467. package/sdk/src/query/frontmatter-mutation.ts +343 -0
  468. package/sdk/src/query/frontmatter.test.ts +281 -0
  469. package/sdk/src/query/frontmatter.ts +397 -0
  470. package/sdk/src/query/helpers.test.ts +426 -0
  471. package/sdk/src/query/helpers.ts +482 -0
  472. package/sdk/src/query/index.ts +586 -0
  473. package/sdk/src/query/init-complex.test.ts +232 -0
  474. package/sdk/src/query/init-complex.ts +578 -0
  475. package/sdk/src/query/init.test.ts +522 -0
  476. package/sdk/src/query/init.ts +1046 -0
  477. package/sdk/src/query/intel.test.ts +90 -0
  478. package/sdk/src/query/intel.ts +404 -0
  479. package/sdk/src/query/normalize-query-command.test.ts +50 -0
  480. package/sdk/src/query/normalize-query-command.ts +56 -0
  481. package/sdk/src/query/phase-lifecycle.test.ts +1126 -0
  482. package/sdk/src/query/phase-lifecycle.ts +1799 -0
  483. package/sdk/src/query/phase-list-queries.test.ts +88 -0
  484. package/sdk/src/query/phase-list-queries.ts +152 -0
  485. package/sdk/src/query/phase-ready.test.ts +65 -0
  486. package/sdk/src/query/phase-ready.ts +158 -0
  487. package/sdk/src/query/phase.test.ts +307 -0
  488. package/sdk/src/query/phase.ts +340 -0
  489. package/sdk/src/query/pipeline.test.ts +169 -0
  490. package/sdk/src/query/pipeline.ts +243 -0
  491. package/sdk/src/query/plan-execution-route.test.ts +166 -0
  492. package/sdk/src/query/plan-execution-route.ts +209 -0
  493. package/sdk/src/query/plan-task-structure.test.ts +65 -0
  494. package/sdk/src/query/plan-task-structure.ts +63 -0
  495. package/sdk/src/query/profile-extract-messages.ts +247 -0
  496. package/sdk/src/query/profile-output.ts +908 -0
  497. package/sdk/src/query/profile-questionnaire-data.ts +181 -0
  498. package/sdk/src/query/profile-sample.ts +184 -0
  499. package/sdk/src/query/profile-scan-sessions.ts +174 -0
  500. package/sdk/src/query/profile.test.ts +74 -0
  501. package/sdk/src/query/profile.ts +337 -0
  502. package/sdk/src/query/progress.test.ts +156 -0
  503. package/sdk/src/query/progress.ts +566 -0
  504. package/sdk/src/query/registry.test.ts +216 -0
  505. package/sdk/src/query/registry.ts +174 -0
  506. package/sdk/src/query/requirements-extract-from-plans.test.ts +58 -0
  507. package/sdk/src/query/requirements-extract-from-plans.ts +86 -0
  508. package/sdk/src/query/roadmap-update-plan-progress.ts +132 -0
  509. package/sdk/src/query/roadmap.test.ts +359 -0
  510. package/sdk/src/query/roadmap.ts +591 -0
  511. package/sdk/src/query/route-next-action.test.ts +61 -0
  512. package/sdk/src/query/route-next-action.ts +345 -0
  513. package/sdk/src/query/runtime-health.ts +7 -0
  514. package/sdk/src/query/schema-detect.ts +189 -0
  515. package/sdk/src/query/skill-manifest.ts +214 -0
  516. package/sdk/src/query/skills.test.ts +80 -0
  517. package/sdk/src/query/skills.ts +62 -0
  518. package/sdk/src/query/state-mutation.test.ts +450 -0
  519. package/sdk/src/query/state-mutation.ts +1444 -0
  520. package/sdk/src/query/state-project-load.ts +109 -0
  521. package/sdk/src/query/state.test.ts +347 -0
  522. package/sdk/src/query/state.ts +397 -0
  523. package/sdk/src/query/summary.test.ts +95 -0
  524. package/sdk/src/query/summary.ts +296 -0
  525. package/sdk/src/query/template.test.ts +180 -0
  526. package/sdk/src/query/template.ts +242 -0
  527. package/sdk/src/query/uat.test.ts +77 -0
  528. package/sdk/src/query/uat.ts +314 -0
  529. package/sdk/src/query/utils.test.ts +82 -0
  530. package/sdk/src/query/utils.ts +92 -0
  531. package/sdk/src/query/validate.test.ts +656 -0
  532. package/sdk/src/query/validate.ts +807 -0
  533. package/sdk/src/query/verify.test.ts +414 -0
  534. package/sdk/src/query/verify.ts +645 -0
  535. package/sdk/src/query/websearch.test.ts +31 -0
  536. package/sdk/src/query/websearch.ts +82 -0
  537. package/sdk/src/query/workspace.test.ts +119 -0
  538. package/sdk/src/query/workspace.ts +131 -0
  539. package/sdk/src/query/workstream.test.ts +51 -0
  540. package/sdk/src/query/workstream.ts +434 -0
  541. package/sdk/src/research-gate.test.ts +190 -0
  542. package/sdk/src/research-gate.ts +94 -0
  543. package/sdk/src/runtime-health.test.ts +176 -0
  544. package/sdk/src/runtime-health.ts +387 -0
  545. package/sdk/src/session-runner.test.ts +98 -0
  546. package/sdk/src/session-runner.ts +299 -0
  547. package/sdk/src/tool-scoping.test.ts +160 -0
  548. package/sdk/src/tool-scoping.ts +61 -0
  549. package/sdk/src/types.ts +917 -0
  550. package/sdk/src/workstream-utils.ts +33 -0
  551. package/sdk/src/ws-flag.test.ts +285 -0
  552. package/sdk/src/ws-transport.test.ts +161 -0
  553. package/sdk/src/ws-transport.ts +93 -0
  554. package/sdk/tsconfig.json +20 -0
@@ -0,0 +1,1058 @@
1
+ /**
2
+ * Phase — Phase CRUD, query, and lifecycle operations
3
+ */
4
+
5
+ const fs = require('fs');
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, atomicWriteFileSync } = require('./core.cjs');
8
+ const { extractFrontmatter } = require('./frontmatter.cjs');
9
+ const { writeStateMd, readModifyWriteStateMd, stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, updatePerformanceMetricsSection } = require('./state.cjs');
10
+
11
+ function cmdPhasesList(cwd, options, raw) {
12
+ const phasesDir = path.join(planningDir(cwd), 'phases');
13
+ const { type, phase, includeArchived } = options;
14
+
15
+ // If no phases directory, return empty
16
+ if (!fs.existsSync(phasesDir)) {
17
+ if (type) {
18
+ output({ files: [], count: 0 }, raw, '');
19
+ } else {
20
+ output({ directories: [], count: 0 }, raw, '');
21
+ }
22
+ return;
23
+ }
24
+
25
+ try {
26
+ // Get all phase directories
27
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
28
+ let dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
29
+
30
+ // Include archived phases if requested
31
+ if (includeArchived) {
32
+ const archived = getArchivedPhaseDirs(cwd);
33
+ for (const a of archived) {
34
+ dirs.push(`${a.name} [${a.milestone}]`);
35
+ }
36
+ }
37
+
38
+ // Sort numerically (handles integers, decimals, letter-suffix, hybrids)
39
+ dirs.sort((a, b) => comparePhaseNum(a, b));
40
+
41
+ // If filtering by phase number
42
+ if (phase) {
43
+ const normalized = normalizePhaseName(phase);
44
+ const match = dirs.find(d => phaseTokenMatches(d, normalized));
45
+ if (!match) {
46
+ output({ files: [], count: 0, phase_dir: null, error: 'Phase not found' }, raw, '');
47
+ return;
48
+ }
49
+ dirs = [match];
50
+ }
51
+
52
+ // If listing files of a specific type
53
+ if (type) {
54
+ const files = [];
55
+ for (const dir of dirs) {
56
+ const dirPath = path.join(phasesDir, dir);
57
+ const dirFiles = fs.readdirSync(dirPath);
58
+
59
+ let filtered;
60
+ if (type === 'plans') {
61
+ filtered = dirFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
62
+ } else if (type === 'summaries') {
63
+ filtered = dirFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
64
+ } else {
65
+ filtered = dirFiles;
66
+ }
67
+
68
+ files.push(...filtered.sort());
69
+ }
70
+
71
+ const result = {
72
+ files,
73
+ count: files.length,
74
+ phase_dir: phase ? dirs[0].replace(/^\d+(?:\.\d+)*-?/, '') : null,
75
+ };
76
+ output(result, raw, files.join('\n'));
77
+ return;
78
+ }
79
+
80
+ // Default: list directories
81
+ output({ directories: dirs, count: dirs.length }, raw, dirs.join('\n'));
82
+ } catch (e) {
83
+ error('Failed to list phases: ' + e.message);
84
+ }
85
+ }
86
+
87
+ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
88
+ const phasesDir = path.join(planningDir(cwd), 'phases');
89
+ const normalized = normalizePhaseName(basePhase);
90
+
91
+ try {
92
+ let baseExists = false;
93
+ const decimalSet = new Set();
94
+
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));
100
+
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
+ }
107
+
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}`);
127
+
128
+ // Calculate next decimal
129
+ let nextDecimal;
130
+ if (decimalSet.size === 0) {
131
+ nextDecimal = `${normalized}.1`;
132
+ } else {
133
+ nextDecimal = `${normalized}.${Math.max(...decimalSet) + 1}`;
134
+ }
135
+
136
+ output(
137
+ {
138
+ found: baseExists,
139
+ base_phase: normalized,
140
+ next: nextDecimal,
141
+ existing: existingDecimals,
142
+ },
143
+ raw,
144
+ nextDecimal
145
+ );
146
+ } catch (e) {
147
+ error('Failed to calculate next decimal phase: ' + e.message);
148
+ }
149
+ }
150
+
151
+ function cmdFindPhase(cwd, phase, raw) {
152
+ if (!phase) {
153
+ error('phase identifier required');
154
+ }
155
+
156
+ const phasesDir = path.join(planningDir(cwd), 'phases');
157
+ const normalized = normalizePhaseName(phase);
158
+
159
+ const notFound = { found: false, directory: null, phase_number: null, phase_name: null, plans: [], summaries: [] };
160
+
161
+ try {
162
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
163
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
164
+
165
+ const match = dirs.find(d => phaseTokenMatches(d, normalized));
166
+ if (!match) {
167
+ output(notFound, raw, '');
168
+ return;
169
+ }
170
+
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);
174
+ const phaseNumber = dirMatch ? dirMatch[1] : normalized;
175
+ const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
176
+
177
+ const phaseDir = path.join(phasesDir, match);
178
+ const phaseFiles = fs.readdirSync(phaseDir);
179
+ const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').sort();
180
+ const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').sort();
181
+
182
+ const result = {
183
+ found: true,
184
+ directory: toPosixPath(path.join(path.relative(cwd, planningDir(cwd)), 'phases', match)),
185
+ phase_number: phaseNumber,
186
+ phase_name: phaseName,
187
+ plans,
188
+ summaries,
189
+ };
190
+
191
+ output(result, raw, result.directory);
192
+ } catch {
193
+ output(notFound, raw, '');
194
+ }
195
+ }
196
+
197
+ function extractObjective(content) {
198
+ const m = content.match(/<objective>\s*\n?\s*(.+)/);
199
+ return m ? m[1].trim() : null;
200
+ }
201
+
202
+ function cmdPhasePlanIndex(cwd, phase, raw) {
203
+ if (!phase) {
204
+ error('phase required for phase-plan-index');
205
+ }
206
+
207
+ const phasesDir = path.join(planningDir(cwd), 'phases');
208
+ const normalized = normalizePhaseName(phase);
209
+
210
+ // Find phase directory
211
+ let phaseDir = null;
212
+ let phaseDirName = null;
213
+ try {
214
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
215
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
216
+ const match = dirs.find(d => phaseTokenMatches(d, normalized));
217
+ if (match) {
218
+ phaseDir = path.join(phasesDir, match);
219
+ phaseDirName = match;
220
+ }
221
+ } catch {
222
+ // phases dir doesn't exist
223
+ }
224
+
225
+ if (!phaseDir) {
226
+ output({ phase: normalized, error: 'Phase not found', plans: [], waves: {}, incomplete: [], has_checkpoints: false }, raw);
227
+ return;
228
+ }
229
+
230
+ // Get all files in phase directory
231
+ const phaseFiles = fs.readdirSync(phaseDir);
232
+ const planFiles = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').sort();
233
+ const summaryFiles = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
234
+
235
+ // Build set of plan IDs with summaries
236
+ const completedPlanIds = new Set(
237
+ summaryFiles.map(s => s.replace('-SUMMARY.md', '').replace('SUMMARY.md', ''))
238
+ );
239
+
240
+ const plans = [];
241
+ const waves = {};
242
+ const incomplete = [];
243
+ let hasCheckpoints = false;
244
+
245
+ for (const planFile of planFiles) {
246
+ const planId = planFile.replace('-PLAN.md', '').replace('PLAN.md', '');
247
+ const planPath = path.join(phaseDir, planFile);
248
+ const content = fs.readFileSync(planPath, 'utf-8');
249
+ const fm = extractFrontmatter(content);
250
+
251
+ // Count tasks: XML <task> tags (canonical) or ## Task N markdown (legacy)
252
+ const xmlTasks = content.match(/<task[\s>]/gi) || [];
253
+ const mdTasks = content.match(/##\s*Task\s*\d+/gi) || [];
254
+ const taskCount = xmlTasks.length || mdTasks.length;
255
+
256
+ // Parse wave as integer
257
+ const wave = parseInt(fm.wave, 10) || 1;
258
+
259
+ // Parse autonomous (default true if not specified)
260
+ let autonomous = true;
261
+ if (fm.autonomous !== undefined) {
262
+ autonomous = fm.autonomous === 'true' || fm.autonomous === true;
263
+ }
264
+
265
+ if (!autonomous) {
266
+ hasCheckpoints = true;
267
+ }
268
+
269
+ // Parse files_modified (underscore is canonical; also accept hyphenated for compat)
270
+ let filesModified = [];
271
+ const fmFiles = fm['files_modified'] || fm['files-modified'];
272
+ if (fmFiles) {
273
+ filesModified = Array.isArray(fmFiles) ? fmFiles : [fmFiles];
274
+ }
275
+
276
+ const hasSummary = completedPlanIds.has(planId);
277
+ if (!hasSummary) {
278
+ incomplete.push(planId);
279
+ }
280
+
281
+ const plan = {
282
+ id: planId,
283
+ wave,
284
+ autonomous,
285
+ objective: extractObjective(content) || fm.objective || null,
286
+ files_modified: filesModified,
287
+ task_count: taskCount,
288
+ has_summary: hasSummary,
289
+ };
290
+
291
+ plans.push(plan);
292
+
293
+ // Group by wave
294
+ const waveKey = String(wave);
295
+ if (!waves[waveKey]) {
296
+ waves[waveKey] = [];
297
+ }
298
+ waves[waveKey].push(planId);
299
+ }
300
+
301
+ const result = {
302
+ phase: normalized,
303
+ plans,
304
+ waves,
305
+ incomplete,
306
+ has_checkpoints: hasCheckpoints,
307
+ };
308
+
309
+ output(result, raw);
310
+ }
311
+
312
+ function cmdPhaseAdd(cwd, description, raw, customId) {
313
+ if (!description) {
314
+ error('description required for phase add');
315
+ }
316
+
317
+ const config = loadConfig(cwd);
318
+ const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
319
+ if (!fs.existsSync(roadmapPath)) {
320
+ error('ROADMAP.md not found');
321
+ }
322
+
323
+ const slug = generateSlugInternal(description);
324
+
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);
329
+
330
+ // Optional project code prefix (e.g., 'CK' → 'CK-01-foundation')
331
+ const projectCode = config.project_code || '';
332
+ const prefix = projectCode ? `${projectCode}-` : '';
333
+
334
+ let _newPhaseId;
335
+ let _dirName;
336
+
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
+ }
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
+
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);
377
+
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 /gsd-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
+ atomicWriteFileSync(roadmapPath, updatedContent);
396
+ return { newPhaseId: _newPhaseId, dirName: _dirName };
397
+ });
398
+
399
+ const result = {
400
+ phase_number: typeof newPhaseId === 'number' ? newPhaseId : String(newPhaseId),
401
+ padded: typeof newPhaseId === 'number' ? String(newPhaseId).padStart(2, '0') : String(newPhaseId),
402
+ name: description,
403
+ slug,
404
+ directory: toPosixPath(path.join(path.relative(cwd, planningDir(cwd)), 'phases', dirName)),
405
+ naming_mode: config.phase_naming,
406
+ };
407
+
408
+ output(result, raw, result.padded);
409
+ }
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
+
481
+ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
482
+ if (!afterPhase || !description) {
483
+ error('after-phase and description required for phase insert');
484
+ }
485
+
486
+ const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
487
+ if (!fs.existsSync(roadmapPath)) {
488
+ error('ROADMAP.md not found');
489
+ }
490
+
491
+ const slug = generateSlugInternal(description);
492
+
493
+ // Wrap entire read-modify-write in lock to prevent concurrent corruption
494
+ const { decimalPhase, dirName } = withPlanningLock(cwd, () => {
495
+ const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
496
+ const content = extractCurrentMilestone(rawContent, cwd);
497
+
498
+ // Normalize input then strip leading zeros for flexible matching
499
+ const normalizedAfter = normalizePhaseName(afterPhase);
500
+ const unpadded = normalizedAfter.replace(/^0+/, '');
501
+ const afterPhaseEscaped = unpadded.replace(/\./g, '\\.');
502
+ const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:`, 'i');
503
+ if (!targetPattern.test(content)) {
504
+ error(`Phase ${afterPhase} not found in ROADMAP.md`);
505
+ }
506
+
507
+ // Calculate next decimal by scanning both directories AND ROADMAP.md entries
508
+ const phasesDir = path.join(planningDir(cwd), 'phases');
509
+ const normalizedBase = normalizePhaseName(afterPhase);
510
+ const decimalSet = new Set();
511
+
512
+ try {
513
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
514
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
515
+ const decimalPattern = new RegExp(`^(?:[A-Z]{1,6}-)?${escapeRegex(normalizedBase)}\\.(\\d+)`);
516
+ for (const dir of dirs) {
517
+ const dm = dir.match(decimalPattern);
518
+ if (dm) decimalSet.add(parseInt(dm[1], 10));
519
+ }
520
+ } catch { /* intentionally empty */ }
521
+
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;
532
+ const _decimalPhase = `${normalizedBase}.${nextDecimal}`;
533
+ // Optional project code prefix
534
+ const insertConfig = loadConfig(cwd);
535
+ const projectCode = insertConfig.project_code || '';
536
+ const pfx = projectCode ? `${projectCode}-` : '';
537
+ const _dirName = `${pfx}${_decimalPhase}-${slug}`;
538
+ const dirPath = path.join(planningDir(cwd), 'phases', _dirName);
539
+
540
+ // Create directory with .gitkeep so git tracks empty folders
541
+ fs.mkdirSync(dirPath, { recursive: true });
542
+ fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
543
+
544
+ // Build phase entry
545
+ const phaseEntry = `\n### Phase ${_decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd-plan-phase ${_decimalPhase} to break down)\n`;
546
+
547
+ // Insert after the target phase section
548
+ const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
549
+ const headerMatch = rawContent.match(headerPattern);
550
+ if (!headerMatch) {
551
+ error(`Could not find Phase ${afterPhase} header`);
552
+ }
553
+
554
+ const headerIdx = rawContent.indexOf(headerMatch[0]);
555
+ const afterHeader = rawContent.slice(headerIdx + headerMatch[0].length);
556
+ const nextPhaseMatch = afterHeader.match(/\n#{2,4}\s+Phase\s+\d/i);
557
+
558
+ let insertIdx;
559
+ if (nextPhaseMatch) {
560
+ insertIdx = headerIdx + headerMatch[0].length + nextPhaseMatch.index;
561
+ } else {
562
+ insertIdx = rawContent.length;
563
+ }
564
+
565
+ const updatedContent = rawContent.slice(0, insertIdx) + phaseEntry + rawContent.slice(insertIdx);
566
+ atomicWriteFileSync(roadmapPath, updatedContent);
567
+ return { decimalPhase: _decimalPhase, dirName: _dirName };
568
+ });
569
+
570
+ const result = {
571
+ phase_number: decimalPhase,
572
+ after_phase: afterPhase,
573
+ name: description,
574
+ slug,
575
+ directory: toPosixPath(path.join(path.relative(cwd, planningDir(cwd)), 'phases', dirName)),
576
+ };
577
+
578
+ output(result, raw, decimalPhase);
579
+ }
580
+
581
+ /**
582
+ * Renumber sibling decimal phases after a decimal phase is removed.
583
+ * e.g. removing 06.2 → 06.3 becomes 06.2, 06.4 becomes 06.3, etc.
584
+ * Returns { renamedDirs, renamedFiles }.
585
+ */
586
+ function renameDecimalPhases(phasesDir, baseInt, removedDecimal) {
587
+ const renamedDirs = [], renamedFiles = [];
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+)-(.+)$`);
591
+ const dirs = readSubdirectories(phasesDir, true);
592
+ const toRename = dirs
593
+ .map(dir => { const m = dir.match(decPattern); return m ? { dir, prefix: m[1], oldDecimal: parseInt(m[2], 10), slug: m[3] } : null; })
594
+ .filter(item => item && item.oldDecimal > removedDecimal)
595
+ .sort((a, b) => b.oldDecimal - a.oldDecimal); // descending to avoid conflicts
596
+
597
+ for (const item of toRename) {
598
+ const newDecimal = item.oldDecimal - 1;
599
+ const oldPhaseId = `${baseInt}.${item.oldDecimal}`;
600
+ const newPhaseId = `${baseInt}.${newDecimal}`;
601
+ const newDirName = `${item.prefix}.${newDecimal}-${item.slug}`;
602
+ fs.renameSync(path.join(phasesDir, item.dir), path.join(phasesDir, newDirName));
603
+ renamedDirs.push({ from: item.dir, to: newDirName });
604
+ for (const f of fs.readdirSync(path.join(phasesDir, newDirName))) {
605
+ if (f.includes(oldPhaseId)) {
606
+ const newFileName = f.replace(oldPhaseId, newPhaseId);
607
+ fs.renameSync(path.join(phasesDir, newDirName, f), path.join(phasesDir, newDirName, newFileName));
608
+ renamedFiles.push({ from: f, to: newFileName });
609
+ }
610
+ }
611
+ }
612
+ return { renamedDirs, renamedFiles };
613
+ }
614
+
615
+ /**
616
+ * Renumber all integer phases after removedInt.
617
+ * e.g. removing phase 5 → phase 6 becomes 5, phase 7 becomes 6, etc.
618
+ * Returns { renamedDirs, renamedFiles }.
619
+ */
620
+ function renameIntegerPhases(phasesDir, removedInt) {
621
+ const renamedDirs = [], renamedFiles = [];
622
+ const dirs = readSubdirectories(phasesDir, true);
623
+ const toRename = dirs
624
+ .map(dir => {
625
+ const m = dir.match(/^(\d+)([A-Z])?(?:\.(\d+))?-(.+)$/i);
626
+ if (!m) return null;
627
+ const dirInt = parseInt(m[1], 10);
628
+ return (dirInt > removedInt && dirInt < 999) ? { dir, oldInt: dirInt, letter: m[2] ? m[2].toUpperCase() : '', decimal: m[3] ? parseInt(m[3], 10) : null, slug: m[4] } : null;
629
+ })
630
+ .filter(Boolean)
631
+ .sort((a, b) => a.oldInt !== b.oldInt ? b.oldInt - a.oldInt : (b.decimal || 0) - (a.decimal || 0));
632
+
633
+ for (const item of toRename) {
634
+ const newInt = item.oldInt - 1;
635
+ const newPadded = String(newInt).padStart(2, '0');
636
+ const oldPadded = String(item.oldInt).padStart(2, '0');
637
+ const letterSuffix = item.letter || '';
638
+ const decimalSuffix = item.decimal !== null ? `.${item.decimal}` : '';
639
+ const oldPrefix = `${oldPadded}${letterSuffix}${decimalSuffix}`;
640
+ const newPrefix = `${newPadded}${letterSuffix}${decimalSuffix}`;
641
+ const newDirName = `${newPrefix}-${item.slug}`;
642
+ fs.renameSync(path.join(phasesDir, item.dir), path.join(phasesDir, newDirName));
643
+ renamedDirs.push({ from: item.dir, to: newDirName });
644
+ for (const f of fs.readdirSync(path.join(phasesDir, newDirName))) {
645
+ if (f.startsWith(oldPrefix)) {
646
+ const newFileName = newPrefix + f.slice(oldPrefix.length);
647
+ fs.renameSync(path.join(phasesDir, newDirName, f), path.join(phasesDir, newDirName, newFileName));
648
+ renamedFiles.push({ from: f, to: newFileName });
649
+ }
650
+ }
651
+ }
652
+ return { renamedDirs, renamedFiles };
653
+ }
654
+
655
+ /**
656
+ * Remove a phase section from ROADMAP.md and renumber all subsequent integer phases.
657
+ */
658
+ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, removedInt, cwd) {
659
+ // Wrap entire read-modify-write in lock to prevent concurrent corruption
660
+ withPlanningLock(cwd, () => {
661
+ let content = fs.readFileSync(roadmapPath, 'utf-8');
662
+ const escaped = escapeRegex(targetPhase);
663
+
664
+ content = content.replace(new RegExp(`\\n?#{2,4}\\s*Phase\\s+${escaped}\\s*:[\\s\\S]*?(?=\\n#{2,4}\\s+Phase\\s+\\d|$)`, 'i'), '');
665
+ content = content.replace(new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${escaped}[:\\s][^\\n]*`, 'gi'), '');
666
+ content = content.replace(new RegExp(`\\n?\\|\\s*${escaped}\\.?\\s[^|]*\\|[^\\n]*`, 'gi'), '');
667
+
668
+ if (!isDecimal) {
669
+ const MAX_PHASE = 99;
670
+ for (let oldNum = MAX_PHASE; oldNum > removedInt; oldNum--) {
671
+ const newNum = oldNum - 1;
672
+ const oldStr = String(oldNum), newStr = String(newNum);
673
+ const oldPad = oldStr.padStart(2, '0'), newPad = newStr.padStart(2, '0');
674
+ content = content.replace(new RegExp(`(#{2,4}\\s*Phase\\s+)${oldStr}(\\s*:)`, 'gi'), `$1${newStr}$2`);
675
+ content = content.replace(new RegExp(`(Phase\\s+)${oldStr}([:\\s])`, 'g'), `$1${newStr}$2`);
676
+ content = content.replace(new RegExp(`(?<![0-9-])${oldPad}-(\\d{2})(?![0-9-])`, 'g'), `${newPad}-$1`);
677
+ content = content.replace(new RegExp(`(\\|\\s*)${oldStr}\\.\\s`, 'g'), `$1${newStr}. `);
678
+ content = content.replace(new RegExp(`(Depends on:\\*\\*\\s*Phase\\s+)${oldStr}\\b`, 'gi'), `$1${newStr}`);
679
+ }
680
+ }
681
+
682
+ atomicWriteFileSync(roadmapPath, content);
683
+ });
684
+ }
685
+
686
+ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
687
+ if (!targetPhase) error('phase number required for phase remove');
688
+
689
+ const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
690
+ const phasesDir = path.join(planningDir(cwd), 'phases');
691
+
692
+ if (!fs.existsSync(roadmapPath)) error('ROADMAP.md not found');
693
+
694
+ const normalized = normalizePhaseName(targetPhase);
695
+ const isDecimal = targetPhase.includes('.');
696
+ const force = options.force || false;
697
+
698
+ // Find target directory
699
+ const targetDir = readSubdirectories(phasesDir, true)
700
+ .find(d => phaseTokenMatches(d, normalized)) || null;
701
+
702
+ // Guard against removing executed work
703
+ if (targetDir && !force) {
704
+ const files = fs.readdirSync(path.join(phasesDir, targetDir));
705
+ const summaries = files.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
706
+ if (summaries.length > 0) {
707
+ error(`Phase ${targetPhase} has ${summaries.length} executed plan(s). Use --force to remove anyway.`);
708
+ }
709
+ }
710
+
711
+ if (targetDir) fs.rmSync(path.join(phasesDir, targetDir), { recursive: true, force: true });
712
+
713
+ // Renumber subsequent phases on disk
714
+ let renamedDirs = [], renamedFiles = [];
715
+ try {
716
+ const renamed = isDecimal
717
+ ? renameDecimalPhases(phasesDir, parseInt(normalized.split('.')[0], 10), parseInt(normalized.split('.')[1], 10))
718
+ : renameIntegerPhases(phasesDir, parseInt(normalized, 10));
719
+ renamedDirs = renamed.renamedDirs;
720
+ renamedFiles = renamed.renamedFiles;
721
+ } catch { /* intentionally empty */ }
722
+
723
+ // Update ROADMAP.md
724
+ updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, parseInt(normalized, 10), cwd);
725
+
726
+ // Update STATE.md phase count atomically (#P4.4)
727
+ const statePath = path.join(planningDir(cwd), 'STATE.md');
728
+ if (fs.existsSync(statePath)) {
729
+ readModifyWriteStateMd(statePath, (stateContent) => {
730
+ const totalRaw = stateExtractField(stateContent, 'Total Phases');
731
+ if (totalRaw) {
732
+ stateContent = stateReplaceField(stateContent, 'Total Phases', String(parseInt(totalRaw, 10) - 1)) || stateContent;
733
+ }
734
+ const ofMatch = stateContent.match(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i);
735
+ if (ofMatch) {
736
+ stateContent = stateContent.replace(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i, `$1${parseInt(ofMatch[2], 10) - 1}$3`);
737
+ }
738
+ return stateContent;
739
+ }, cwd);
740
+ }
741
+
742
+ output({
743
+ removed: targetPhase,
744
+ directory_deleted: targetDir,
745
+ renamed_directories: renamedDirs,
746
+ renamed_files: renamedFiles,
747
+ roadmap_updated: true,
748
+ state_updated: fs.existsSync(statePath),
749
+ }, raw);
750
+ }
751
+
752
+ function cmdPhaseComplete(cwd, phaseNum, raw) {
753
+ if (!phaseNum) {
754
+ error('phase number required for phase complete');
755
+ }
756
+
757
+ const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
758
+ const statePath = path.join(planningDir(cwd), 'STATE.md');
759
+ const phasesDir = path.join(planningDir(cwd), 'phases');
760
+ const normalized = normalizePhaseName(phaseNum);
761
+ const today = new Date().toISOString().split('T')[0];
762
+
763
+ // Verify phase info
764
+ const phaseInfo = findPhaseInternal(cwd, phaseNum);
765
+ if (!phaseInfo) {
766
+ error(`Phase ${phaseNum} not found`);
767
+ }
768
+
769
+ const planCount = phaseInfo.plans.length;
770
+ const summaryCount = phaseInfo.summaries.length;
771
+ let requirementsUpdated = false;
772
+
773
+ // Check for unresolved verification debt (non-blocking warnings)
774
+ const warnings = [];
775
+ try {
776
+ const phaseFullDir = path.join(cwd, phaseInfo.directory);
777
+ const phaseFiles = fs.readdirSync(phaseFullDir);
778
+
779
+ for (const file of phaseFiles.filter(f => f.includes('-UAT') && f.endsWith('.md'))) {
780
+ const content = fs.readFileSync(path.join(phaseFullDir, file), 'utf-8');
781
+ if (/result: pending/.test(content)) warnings.push(`${file}: has pending tests`);
782
+ if (/result: blocked/.test(content)) warnings.push(`${file}: has blocked tests`);
783
+ if (/status: partial/.test(content)) warnings.push(`${file}: testing incomplete (partial)`);
784
+ if (/status: diagnosed/.test(content)) warnings.push(`${file}: has diagnosed gaps`);
785
+ }
786
+
787
+ for (const file of phaseFiles.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md'))) {
788
+ const content = fs.readFileSync(path.join(phaseFullDir, file), 'utf-8');
789
+ if (/status: human_needed/.test(content)) warnings.push(`${file}: needs human verification`);
790
+ if (/status: gaps_found/.test(content)) warnings.push(`${file}: has unresolved gaps`);
791
+ }
792
+ } catch {}
793
+
794
+ // Update ROADMAP.md and REQUIREMENTS.md atomically under lock
795
+ if (fs.existsSync(roadmapPath)) {
796
+ withPlanningLock(cwd, () => {
797
+ let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
798
+
799
+ // Checkbox: - [ ] Phase N: → - [x] Phase N: (...completed DATE)
800
+ const checkboxPattern = new RegExp(
801
+ `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s][^\\n]*)`,
802
+ 'i'
803
+ );
804
+ roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
805
+
806
+ // Progress table: update Status to Complete, add date (handles 4 or 5 column tables)
807
+ const phaseEscaped = escapeRegex(phaseNum);
808
+ const tableRowPattern = new RegExp(
809
+ `^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
810
+ 'im'
811
+ );
812
+ roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
813
+ const cells = fullRow.split('|').slice(1, -1);
814
+ if (cells.length === 5) {
815
+ // 5-col: Phase | Milestone | Plans | Status | Completed
816
+ cells[2] = ` ${summaryCount}/${planCount} `;
817
+ cells[3] = ' Complete ';
818
+ cells[4] = ` ${today} `;
819
+ } else if (cells.length === 4) {
820
+ // 4-col: Phase | Plans | Status | Completed
821
+ cells[1] = ` ${summaryCount}/${planCount} `;
822
+ cells[2] = ' Complete ';
823
+ cells[3] = ` ${today} `;
824
+ }
825
+ return '|' + cells.join('|') + '|';
826
+ });
827
+
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).
836
+ const planCountPattern = new RegExp(
837
+ `(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
838
+ 'i'
839
+ );
840
+ roadmapContent = roadmapContent.replace(
841
+ planCountPattern,
842
+ `$1${summaryCount}/${planCount} plans complete`
843
+ );
844
+
845
+ // Mark completed plan checkboxes (safety net for missed per-plan updates)
846
+ // Handles both plain IDs ("- [ ] 01-01-PLAN.md") and bold-wrapped IDs ("- [ ] **01-01**")
847
+ for (const summaryFile of phaseInfo.summaries) {
848
+ const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
849
+ if (!planId) continue;
850
+ const planEscaped = escapeRegex(planId);
851
+ const planCheckboxPattern = new RegExp(
852
+ `(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`,
853
+ 'i'
854
+ );
855
+ roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
856
+ }
857
+
858
+ atomicWriteFileSync(roadmapPath, roadmapContent);
859
+
860
+ // Update REQUIREMENTS.md traceability for this phase's requirements
861
+ const reqPath = path.join(planningDir(cwd), 'REQUIREMENTS.md');
862
+ if (fs.existsSync(reqPath)) {
863
+ // Extract the current phase section from roadmap (scoped to avoid cross-phase matching)
864
+ const phaseEsc = escapeRegex(phaseNum);
865
+ const currentMilestoneRoadmap = extractCurrentMilestone(roadmapContent, cwd);
866
+ const phaseSectionMatch = currentMilestoneRoadmap.match(
867
+ new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEsc}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i')
868
+ );
869
+
870
+ const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : '';
871
+ const reqMatch = sectionText.match(/\*\*Requirements:\*\*\s*([^\n]+)/i);
872
+
873
+ if (reqMatch) {
874
+ const reqIds = reqMatch[1].replace(/[\[\]]/g, '').split(/[,\s]+/).map(r => r.trim()).filter(Boolean);
875
+ let reqContent = fs.readFileSync(reqPath, 'utf-8');
876
+
877
+ for (const reqId of reqIds) {
878
+ const reqEscaped = escapeRegex(reqId);
879
+ // Update checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID**
880
+ reqContent = reqContent.replace(
881
+ new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi'),
882
+ '$1x$2'
883
+ );
884
+ // Update traceability table: | REQ-ID | Phase N | Pending/In Progress | → | REQ-ID | Phase N | Complete |
885
+ reqContent = reqContent.replace(
886
+ new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*(?:Pending|In Progress)\\s*(\\|)`, 'gi'),
887
+ '$1 Complete $2'
888
+ );
889
+ }
890
+
891
+ atomicWriteFileSync(reqPath, reqContent);
892
+ requirementsUpdated = true;
893
+ }
894
+ }
895
+ });
896
+ }
897
+
898
+ // Find next phase — check both filesystem AND roadmap
899
+ // Phases may be defined in ROADMAP.md but not yet scaffolded to disk,
900
+ // so a filesystem-only scan would incorrectly report is_last_phase:true
901
+ let nextPhaseNum = null;
902
+ let nextPhaseName = null;
903
+ let isLastPhase = true;
904
+
905
+ try {
906
+ const isDirInMilestone = getMilestonePhaseFilter(cwd);
907
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
908
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name)
909
+ .filter(isDirInMilestone)
910
+ .sort((a, b) => comparePhaseNum(a, b));
911
+
912
+ // Find the next phase directory after current
913
+ // Skip backlog phases (999.x) — they are parked ideas, not sequential work (#2129)
914
+ for (const dir of dirs) {
915
+ const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
916
+ if (dm) {
917
+ if (/^999(?:\.|$)/.test(dm[1])) continue;
918
+ if (comparePhaseNum(dm[1], phaseNum) > 0) {
919
+ nextPhaseNum = dm[1];
920
+ nextPhaseName = dm[2] || null;
921
+ isLastPhase = false;
922
+ break;
923
+ }
924
+ }
925
+ }
926
+ } catch { /* intentionally empty */ }
927
+
928
+ // Fallback: if filesystem found no next phase, check ROADMAP.md
929
+ // for phases that are defined but not yet planned (no directory on disk)
930
+ if (isLastPhase && fs.existsSync(roadmapPath)) {
931
+ try {
932
+ const roadmapForPhases = extractCurrentMilestone(fs.readFileSync(roadmapPath, 'utf-8'), cwd);
933
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
934
+ let pm;
935
+ while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
936
+ if (comparePhaseNum(pm[1], phaseNum) > 0) {
937
+ nextPhaseNum = pm[1];
938
+ nextPhaseName = pm[2].replace(/\(INSERTED\)/i, '').trim().toLowerCase().replace(/\s+/g, '-');
939
+ isLastPhase = false;
940
+ break;
941
+ }
942
+ }
943
+ } catch { /* intentionally empty */ }
944
+ }
945
+
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.
949
+ if (fs.existsSync(statePath)) {
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')
954
+ || stateExtractField(stateContent, 'Phase');
955
+ let newPhaseValue = String(phaseValue);
956
+ if (existingPhaseField) {
957
+ const totalMatch = existingPhaseField.match(/of\s+(\d+)/);
958
+ const nameMatch = existingPhaseField.match(/\(([^)]+)\)/);
959
+ if (totalMatch) {
960
+ const total = totalMatch[1];
961
+ const nameStr = nextPhaseName ? ` (${nextPhaseName.replace(/-/g, ' ')})` : (nameMatch ? ` (${nameMatch[1]})` : '');
962
+ newPhaseValue = `${phaseValue} of ${total}${nameStr}`;
963
+ }
964
+ }
965
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase', 'Phase', newPhaseValue);
966
+
967
+ // Update Current Phase Name
968
+ if (nextPhaseName) {
969
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase Name', null, nextPhaseName.replace(/-/g, ' '));
970
+ }
971
+
972
+ // Update Status
973
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null,
974
+ isLastPhase ? 'Milestone complete' : 'Ready to plan');
975
+
976
+ // Update Current Plan
977
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Plan', 'Plan', 'Not started');
978
+
979
+ // Update Last Activity
980
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity', 'Last activity', today);
981
+
982
+ // Update Last Activity Description
983
+ stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity Description', null,
984
+ `Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`);
985
+
986
+ // Increment Completed Phases counter (#956)
987
+ const completedRaw = stateExtractField(stateContent, 'Completed Phases');
988
+ if (completedRaw) {
989
+ const newCompleted = parseInt(completedRaw, 10) + 1;
990
+ stateContent = stateReplaceField(stateContent, 'Completed Phases', String(newCompleted)) || stateContent;
991
+
992
+ // Recalculate percent based on completed / total (#956)
993
+ const totalRaw = stateExtractField(stateContent, 'Total Phases');
994
+ if (totalRaw) {
995
+ const totalPhases = parseInt(totalRaw, 10);
996
+ if (totalPhases > 0) {
997
+ const newPercent = Math.round((newCompleted / totalPhases) * 100);
998
+ stateContent = stateReplaceField(stateContent, 'Progress', `${newPercent}%`) || stateContent;
999
+ stateContent = stateContent.replace(
1000
+ /(percent:\s*)\d+/,
1001
+ `$1${newPercent}`
1002
+ );
1003
+ }
1004
+ }
1005
+ }
1006
+
1007
+ // Gate 4: Update Performance Metrics section (#1627)
1008
+ stateContent = updatePerformanceMetricsSection(stateContent, cwd, phaseNum, planCount, summaryCount);
1009
+
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
+ }
1026
+ }
1027
+ } catch { /* intentionally empty — auto-prune is best-effort */ }
1028
+
1029
+ const result = {
1030
+ completed_phase: phaseNum,
1031
+ phase_name: phaseInfo.phase_name,
1032
+ plans_executed: `${summaryCount}/${planCount}`,
1033
+ next_phase: nextPhaseNum,
1034
+ next_phase_name: nextPhaseName,
1035
+ is_last_phase: isLastPhase,
1036
+ date: today,
1037
+ roadmap_updated: fs.existsSync(roadmapPath),
1038
+ state_updated: fs.existsSync(statePath),
1039
+ requirements_updated: requirementsUpdated,
1040
+ auto_pruned: autoPruned,
1041
+ warnings,
1042
+ has_warnings: warnings.length > 0,
1043
+ };
1044
+
1045
+ output(result, raw);
1046
+ }
1047
+
1048
+ module.exports = {
1049
+ cmdPhasesList,
1050
+ cmdPhaseNextDecimal,
1051
+ cmdFindPhase,
1052
+ cmdPhasePlanIndex,
1053
+ cmdPhaseAdd,
1054
+ cmdPhaseAddBatch,
1055
+ cmdPhaseInsert,
1056
+ cmdPhaseRemove,
1057
+ cmdPhaseComplete,
1058
+ };