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,1799 @@
1
+ /**
2
+ * Phase lifecycle handlers — add, insert, scaffold operations.
3
+ *
4
+ * Ported from get-shit-done/bin/lib/phase.cjs and commands.cjs.
5
+ * Provides phaseAdd (append phase), phaseAddBatch (append multiple phases),
6
+ * phaseInsert (decimal phase insertion), and phaseScaffold (template file/directory creation).
7
+ *
8
+ * Shared helpers replaceInCurrentMilestone and readModifyWriteRoadmapMd
9
+ * are exported for use by downstream handlers (phaseComplete in Plan 03).
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { phaseAdd, phaseInsert, phaseScaffold } from './phase-lifecycle.js';
14
+ *
15
+ * await phaseAdd(['New Feature'], '/project');
16
+ * await phaseInsert(['10', 'Urgent Fix'], '/project');
17
+ * await phaseScaffold(['context', '9'], '/project');
18
+ * ```
19
+ */
20
+
21
+ import { readFile, writeFile, mkdir, readdir, rename, rm } from 'node:fs/promises';
22
+ import { existsSync } from 'node:fs';
23
+ import { join, relative } from 'node:path';
24
+ import { GSDError, ErrorClassification } from '../errors.js';
25
+ import {
26
+ escapeRegex,
27
+ normalizeMd,
28
+ normalizePhaseName,
29
+ comparePhaseNum,
30
+ phaseTokenMatches,
31
+ toPosixPath,
32
+ planningPaths,
33
+ stateExtractField,
34
+ } from './helpers.js';
35
+ import { extractFrontmatter } from './frontmatter.js';
36
+ import { extractCurrentMilestone } from './roadmap.js';
37
+ import { getMilestonePhaseFilter } from './state.js';
38
+ import {
39
+ acquireStateLock,
40
+ readModifyWriteStateMdFull,
41
+ releaseStateLock,
42
+ stateReplaceField,
43
+ } from './state-mutation.js';
44
+ import type { QueryHandler } from './utils.js';
45
+
46
+ // ─── Null byte validation ────────────────────────────────────────────────
47
+
48
+ /** Reject strings containing null bytes (path traversal defense). */
49
+ function assertNoNullBytes(value: string, label: string): void {
50
+ if (value.includes('\0')) {
51
+ throw new GSDError(`${label} contains null byte`, ErrorClassification.Validation);
52
+ }
53
+ }
54
+
55
+ /** Reject `..` or path separators in phase directory names. */
56
+ function assertSafePhaseDirName(dirName: string, label = 'phase directory'): void {
57
+ if (/[/\\]|\.\./.test(dirName)) {
58
+ throw new GSDError(`${label} contains invalid path segments`, ErrorClassification.Validation);
59
+ }
60
+ }
61
+
62
+ function assertSafeProjectCode(code: string): void {
63
+ if (code && /[/\\]|\.\./.test(code)) {
64
+ throw new GSDError('project_code contains invalid characters', ErrorClassification.Validation);
65
+ }
66
+ }
67
+
68
+ // ─── Slug generation (inline) ────────────────────────────────────────────
69
+
70
+ /** Generate kebab-case slug from description. Port of generateSlugInternal. */
71
+ function generateSlugInternal(text: string): string {
72
+ return text
73
+ .toLowerCase()
74
+ .replace(/[^a-z0-9]+/g, '-')
75
+ .replace(/^-+|-+$/g, '')
76
+ .substring(0, 60);
77
+ }
78
+
79
+ // ─── replaceInCurrentMilestone ──────────────────────────────────────────
80
+
81
+ /**
82
+ * Replace a pattern only in the current milestone section of ROADMAP.md.
83
+ *
84
+ * Port of replaceInCurrentMilestone from core.cjs line 1197-1206.
85
+ * If no `</details>` blocks exist, replaces in the entire content.
86
+ * Otherwise, only replaces in content after the last `</details>` close tag.
87
+ *
88
+ * @param content - Full ROADMAP.md content
89
+ * @param pattern - Regex or string pattern to match
90
+ * @param replacement - Replacement string
91
+ * @returns Modified content
92
+ */
93
+ export function replaceInCurrentMilestone(
94
+ content: string,
95
+ pattern: string | RegExp,
96
+ replacement: string,
97
+ ): string {
98
+ const lastDetailsClose = content.lastIndexOf('</details>');
99
+ if (lastDetailsClose === -1) {
100
+ return content.replace(pattern, replacement);
101
+ }
102
+ const offset = lastDetailsClose + '</details>'.length;
103
+ const before = content.slice(0, offset);
104
+ const after = content.slice(offset);
105
+ return before + after.replace(pattern, replacement);
106
+ }
107
+
108
+ // ─── readModifyWriteRoadmapMd ───────────────────────────────────────────
109
+
110
+ /**
111
+ * Atomic read-modify-write for ROADMAP.md.
112
+ *
113
+ * Holds a lockfile across the entire read -> transform -> write cycle.
114
+ * Uses the same acquireStateLock/releaseStateLock mechanism as STATE.md
115
+ * but with a ROADMAP.md-specific lock path.
116
+ *
117
+ * @param projectDir - Project root directory
118
+ * @param modifier - Function to transform ROADMAP.md content
119
+ * @returns The final written content
120
+ */
121
+ export async function readModifyWriteRoadmapMd(
122
+ projectDir: string,
123
+ modifier: (content: string) => string | Promise<string>,
124
+ ): Promise<string> {
125
+ const roadmapPath = planningPaths(projectDir).roadmap;
126
+ const lockPath = await acquireStateLock(roadmapPath);
127
+ try {
128
+ let content: string;
129
+ try {
130
+ content = await readFile(roadmapPath, 'utf-8');
131
+ } catch {
132
+ content = '';
133
+ }
134
+ const modified = await modifier(content);
135
+ await writeFile(roadmapPath, modified, 'utf-8');
136
+ return modified;
137
+ } finally {
138
+ await releaseStateLock(lockPath);
139
+ }
140
+ }
141
+
142
+ // ─── phaseAdd handler ───────────────────────────────────────────────────
143
+
144
+ /**
145
+ * Query handler for phase.add.
146
+ *
147
+ * Port of cmdPhaseAdd from phase.cjs lines 312-392.
148
+ * Creates a new phase directory with .gitkeep, appends a phase section
149
+ * to ROADMAP.md before the last "---" separator.
150
+ *
151
+ * @param args - args[0]: description (required), args[1]: customId (optional)
152
+ * @param projectDir - Project root directory
153
+ * @returns QueryResult with { phase_number, padded, name, slug, directory, naming_mode }
154
+ */
155
+ export const phaseAdd: QueryHandler = async (args, projectDir) => {
156
+ const description = args[0];
157
+ if (!description) {
158
+ throw new GSDError('description required for phase add', ErrorClassification.Validation);
159
+ }
160
+ assertNoNullBytes(description, 'description');
161
+
162
+ const configPath = planningPaths(projectDir).config;
163
+ let config: Record<string, unknown> = {};
164
+ try {
165
+ config = JSON.parse(await readFile(configPath, 'utf-8'));
166
+ } catch { /* use defaults */ }
167
+
168
+ const slug = generateSlugInternal(description);
169
+ const customId = args[1] || null;
170
+
171
+ // Optional project code prefix (e.g., 'CK' -> 'CK-01-foundation')
172
+ const projectCode = (config.project_code as string) || '';
173
+ assertSafeProjectCode(projectCode);
174
+ const prefix = projectCode ? `${projectCode}-` : '';
175
+
176
+ let newPhaseId: number | string = '';
177
+ let dirName = '';
178
+
179
+ await readModifyWriteRoadmapMd(projectDir, async (rawContent) => {
180
+ const content = await extractCurrentMilestone(rawContent, projectDir);
181
+
182
+ if (customId || config.phase_naming === 'custom') {
183
+ // Custom phase naming
184
+ newPhaseId = customId || slug.toUpperCase().replace(/-/g, '_');
185
+ if (!newPhaseId) {
186
+ throw new GSDError('--id required when phase_naming is "custom"', ErrorClassification.Validation);
187
+ }
188
+ assertSafePhaseDirName(String(newPhaseId), 'custom phase id');
189
+ dirName = `${prefix}${newPhaseId}-${slug}`;
190
+ } else {
191
+ // Sequential mode: find highest integer phase number (in current milestone only)
192
+ // Skip 999.x backlog phases — they live outside the active sequence
193
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
194
+ let maxPhase = 0;
195
+ let m: RegExpExecArray | null;
196
+ while ((m = phasePattern.exec(content)) !== null) {
197
+ const num = parseInt(m[1], 10);
198
+ if (num >= 999) continue; // backlog phases use 999.x numbering
199
+ if (num > maxPhase) maxPhase = num;
200
+ }
201
+
202
+ newPhaseId = maxPhase + 1;
203
+ const paddedNum = String(newPhaseId).padStart(2, '0');
204
+ dirName = `${prefix}${paddedNum}-${slug}`;
205
+ }
206
+
207
+ assertSafePhaseDirName(dirName);
208
+
209
+ const dirPath = join(planningPaths(projectDir).phases, dirName);
210
+
211
+ // Create directory with .gitkeep so git tracks empty folders
212
+ await mkdir(dirPath, { recursive: true });
213
+ await writeFile(join(dirPath, '.gitkeep'), '', 'utf-8');
214
+
215
+ // Build phase entry
216
+ const dependsOn = config.phase_naming === 'custom'
217
+ ? ''
218
+ : `\n**Depends on:** Phase ${typeof newPhaseId === 'number' ? newPhaseId - 1 : 'TBD'}`;
219
+ 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`;
220
+
221
+ // Find insertion point: before last "---" or at end
222
+ const lastSeparator = rawContent.lastIndexOf('\n---');
223
+ if (lastSeparator > 0) {
224
+ return rawContent.slice(0, lastSeparator) + phaseEntry + rawContent.slice(lastSeparator);
225
+ }
226
+ return rawContent + phaseEntry;
227
+ });
228
+
229
+ if (!dirName) {
230
+ throw new GSDError('Phase directory name was not computed', ErrorClassification.Execution);
231
+ }
232
+ if (newPhaseId === '') {
233
+ throw new GSDError('Phase ID was not computed', ErrorClassification.Execution);
234
+ }
235
+
236
+ const result = {
237
+ phase_number: typeof newPhaseId === 'number' ? newPhaseId : String(newPhaseId),
238
+ padded: typeof newPhaseId === 'number' ? String(newPhaseId).padStart(2, '0') : String(newPhaseId),
239
+ name: description,
240
+ slug,
241
+ directory: toPosixPath(relative(projectDir, join(planningPaths(projectDir).phases, dirName))),
242
+ naming_mode: config.phase_naming || 'sequential',
243
+ };
244
+
245
+ return { data: result };
246
+ };
247
+
248
+ // ─── phaseAddBatch handler ────────────────────────────────────────────────
249
+
250
+ /**
251
+ * Query handler for phase.add-batch.
252
+ *
253
+ * Port of cmdPhaseAddBatch from phase.cjs lines 411-478.
254
+ * Appends multiple phases in one locked ROADMAP pass (sequential or custom naming).
255
+ *
256
+ * @param args - Either `--descriptions` followed by a JSON array string, or one description per arg (`--raw` ignored)
257
+ */
258
+ export const phaseAddBatch: QueryHandler = async (args, projectDir) => {
259
+ let descriptions: string[];
260
+ const descIdx = args.indexOf('--descriptions');
261
+ if (descIdx !== -1 && args[descIdx + 1] !== undefined) {
262
+ try {
263
+ const parsed = JSON.parse(args[descIdx + 1]) as unknown;
264
+ if (!Array.isArray(parsed)) {
265
+ throw new GSDError('--descriptions must be a JSON array', ErrorClassification.Validation);
266
+ }
267
+ descriptions = parsed.map((x) => String(x));
268
+ } catch (e) {
269
+ if (e instanceof GSDError) throw e;
270
+ throw new GSDError('--descriptions must be a valid JSON array', ErrorClassification.Validation);
271
+ }
272
+ } else {
273
+ descriptions = args.filter((a) => a !== '--raw');
274
+ }
275
+
276
+ if (descriptions.length === 0) {
277
+ throw new GSDError('descriptions array required for phase add-batch', ErrorClassification.Validation);
278
+ }
279
+
280
+ for (const d of descriptions) {
281
+ assertNoNullBytes(d, 'description');
282
+ if (!d.trim()) {
283
+ throw new GSDError('description must be non-empty', ErrorClassification.Validation);
284
+ }
285
+ }
286
+
287
+ const roadmapPath = planningPaths(projectDir).roadmap;
288
+ if (!existsSync(roadmapPath)) {
289
+ throw new GSDError('ROADMAP.md not found', ErrorClassification.Validation);
290
+ }
291
+
292
+ let config: Record<string, unknown> = {};
293
+ try {
294
+ config = JSON.parse(await readFile(planningPaths(projectDir).config, 'utf-8'));
295
+ } catch { /* use defaults */ }
296
+
297
+ const projectCode = (config.project_code as string) || '';
298
+ assertSafeProjectCode(projectCode);
299
+ const prefix = projectCode ? `${projectCode}-` : '';
300
+
301
+ const added: Array<{
302
+ phase_number: string | number;
303
+ padded: string;
304
+ name: string;
305
+ slug: string;
306
+ directory: string;
307
+ naming_mode: unknown;
308
+ }> = [];
309
+
310
+ await readModifyWriteRoadmapMd(projectDir, async (initialContent) => {
311
+ let rawContent = initialContent;
312
+ const content = await extractCurrentMilestone(rawContent, projectDir);
313
+ let maxPhase = 0;
314
+
315
+ if (config.phase_naming !== 'custom') {
316
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
317
+ let m: RegExpExecArray | null;
318
+ while ((m = phasePattern.exec(content)) !== null) {
319
+ const num = parseInt(m[1], 10);
320
+ if (num >= 999) continue;
321
+ if (num > maxPhase) maxPhase = num;
322
+ }
323
+
324
+ const phasesOnDisk = planningPaths(projectDir).phases;
325
+ if (existsSync(phasesOnDisk)) {
326
+ const entries = await readdir(phasesOnDisk, { withFileTypes: true });
327
+ const dirNumPattern = /^(?:[A-Z][A-Z0-9]*-)?(\d+)-/;
328
+ for (const entry of entries) {
329
+ if (!entry.isDirectory()) continue;
330
+ const match = entry.name.match(dirNumPattern);
331
+ if (!match) continue;
332
+ const num = parseInt(match[1], 10);
333
+ if (num >= 999) continue;
334
+ if (num > maxPhase) maxPhase = num;
335
+ }
336
+ }
337
+ }
338
+
339
+ for (const description of descriptions) {
340
+ const slug = generateSlugInternal(description);
341
+ let newPhaseId: number | string;
342
+ let dirName: string;
343
+
344
+ if (config.phase_naming === 'custom') {
345
+ // Match CJS cmdPhaseAddBatch: slug.toUpperCase().replace(/-/g, '-') (identity on hyphens)
346
+ newPhaseId = slug.toUpperCase();
347
+ dirName = `${prefix}${newPhaseId}-${slug}`;
348
+ } else {
349
+ maxPhase += 1;
350
+ newPhaseId = maxPhase;
351
+ dirName = `${prefix}${String(newPhaseId).padStart(2, '0')}-${slug}`;
352
+ }
353
+
354
+ assertSafePhaseDirName(dirName);
355
+ const dirPath = join(planningPaths(projectDir).phases, dirName);
356
+ await mkdir(dirPath, { recursive: true });
357
+ await writeFile(join(dirPath, '.gitkeep'), '', 'utf-8');
358
+
359
+ const dependsOn =
360
+ config.phase_naming === 'custom'
361
+ ? ''
362
+ : `\n**Depends on:** Phase ${typeof newPhaseId === 'number' ? newPhaseId - 1 : 'TBD'}`;
363
+ 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`;
364
+
365
+ const lastSeparator = rawContent.lastIndexOf('\n---');
366
+ rawContent =
367
+ lastSeparator > 0
368
+ ? rawContent.slice(0, lastSeparator) + phaseEntry + rawContent.slice(lastSeparator)
369
+ : rawContent + phaseEntry;
370
+
371
+ added.push({
372
+ phase_number: typeof newPhaseId === 'number' ? newPhaseId : String(newPhaseId),
373
+ padded: typeof newPhaseId === 'number' ? String(newPhaseId).padStart(2, '0') : String(newPhaseId),
374
+ name: description,
375
+ slug,
376
+ directory: toPosixPath(relative(projectDir, join(planningPaths(projectDir).phases, dirName))),
377
+ naming_mode: config.phase_naming || 'sequential',
378
+ });
379
+ }
380
+
381
+ return rawContent;
382
+ });
383
+
384
+ return { data: { phases: added, count: added.length } };
385
+ };
386
+
387
+ // ─── phaseInsert handler ────────────────────────────────────────────────
388
+
389
+ /**
390
+ * Query handler for phase.insert.
391
+ *
392
+ * Port of cmdPhaseInsert from phase.cjs lines 394-492.
393
+ * Creates a decimal phase directory after a target phase, inserting
394
+ * the phase section in ROADMAP.md after the target.
395
+ *
396
+ * @param args - args[0]: afterPhase (required), args[1]: description (required)
397
+ * @param projectDir - Project root directory
398
+ * @returns QueryResult with { phase_number, after_phase, name, slug, directory }
399
+ */
400
+ export const phaseInsert: QueryHandler = async (args, projectDir) => {
401
+ const afterPhase = args[0];
402
+ const description = args[1];
403
+
404
+ if (!afterPhase || !description) {
405
+ throw new GSDError('after-phase and description required for phase insert', ErrorClassification.Validation);
406
+ }
407
+ assertNoNullBytes(afterPhase, 'afterPhase');
408
+ assertNoNullBytes(description, 'description');
409
+
410
+ const slug = generateSlugInternal(description);
411
+ let decimalPhase = '';
412
+ let dirName = '';
413
+
414
+ await readModifyWriteRoadmapMd(projectDir, async (rawContent) => {
415
+ const content = await extractCurrentMilestone(rawContent, projectDir);
416
+
417
+ // Normalize input then strip leading zeros for flexible matching
418
+ const normalizedAfter = normalizePhaseName(afterPhase);
419
+ const unpadded = normalizedAfter.replace(/^0+/, '');
420
+ const afterPhaseEscaped = unpadded.replace(/\./g, '\\.');
421
+ const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:`, 'i');
422
+ if (!targetPattern.test(content)) {
423
+ throw new GSDError(`Phase ${afterPhase} not found in ROADMAP.md`, ErrorClassification.Validation);
424
+ }
425
+
426
+ // Calculate next decimal by scanning both directories AND ROADMAP.md entries
427
+ const phasesDir = planningPaths(projectDir).phases;
428
+ const normalizedBase = normalizePhaseName(afterPhase);
429
+ const decimalSet = new Set<number>();
430
+
431
+ try {
432
+ const entries = await readdir(phasesDir, { withFileTypes: true });
433
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
434
+ const decimalPattern = new RegExp(`^(?:[A-Z]{1,6}-)?${escapeRegex(normalizedBase)}\\.(\\d+)`);
435
+ for (const dir of dirs) {
436
+ const dm = dir.match(decimalPattern);
437
+ if (dm) decimalSet.add(parseInt(dm[1], 10));
438
+ }
439
+ } catch { /* intentionally empty */ }
440
+
441
+ // Also scan ROADMAP.md content for decimal entries
442
+ const rmPhasePattern = new RegExp(
443
+ `#{2,4}\\s*Phase\\s+0*${escapeRegex(normalizedBase)}\\.(\\d+)\\s*:`, 'gi'
444
+ );
445
+ let rmMatch: RegExpExecArray | null;
446
+ while ((rmMatch = rmPhasePattern.exec(rawContent)) !== null) {
447
+ decimalSet.add(parseInt(rmMatch[1], 10));
448
+ }
449
+
450
+ const nextDecimal = decimalSet.size === 0 ? 1 : Math.max(...decimalSet) + 1;
451
+ decimalPhase = `${normalizedBase}.${nextDecimal}`;
452
+
453
+ // Optional project code prefix
454
+ let insertConfig: Record<string, unknown> = {};
455
+ try {
456
+ insertConfig = JSON.parse(await readFile(planningPaths(projectDir).config, 'utf-8'));
457
+ } catch { /* use defaults */ }
458
+ const projectCode = (insertConfig.project_code as string) || '';
459
+ assertSafeProjectCode(projectCode);
460
+ const pfx = projectCode ? `${projectCode}-` : '';
461
+ dirName = `${pfx}${decimalPhase}-${slug}`;
462
+ assertSafePhaseDirName(dirName);
463
+ const dirPath = join(phasesDir, dirName);
464
+
465
+ // Create directory with .gitkeep
466
+ await mkdir(dirPath, { recursive: true });
467
+ await writeFile(join(dirPath, '.gitkeep'), '', 'utf-8');
468
+
469
+ // Build phase entry
470
+ 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`;
471
+
472
+ // Insert after the target phase section
473
+ const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
474
+ const headerMatch = rawContent.match(headerPattern);
475
+ if (!headerMatch) {
476
+ throw new GSDError(`Could not find Phase ${afterPhase} header`, ErrorClassification.Execution);
477
+ }
478
+
479
+ const headerIdx = rawContent.indexOf(headerMatch[0]);
480
+ const afterHeader = rawContent.slice(headerIdx + headerMatch[0].length);
481
+ const nextPhaseMatch = afterHeader.match(/\n#{2,4}\s+Phase\s+\d/i);
482
+
483
+ let insertIdx: number;
484
+ if (nextPhaseMatch && nextPhaseMatch.index !== undefined) {
485
+ insertIdx = headerIdx + headerMatch[0].length + nextPhaseMatch.index;
486
+ } else {
487
+ insertIdx = rawContent.length;
488
+ }
489
+
490
+ return rawContent.slice(0, insertIdx) + phaseEntry + rawContent.slice(insertIdx);
491
+ });
492
+
493
+ if (!decimalPhase) {
494
+ throw new GSDError('Decimal phase was not computed', ErrorClassification.Execution);
495
+ }
496
+ if (!dirName) {
497
+ throw new GSDError('Phase directory name was not computed', ErrorClassification.Execution);
498
+ }
499
+
500
+ const result = {
501
+ phase_number: decimalPhase,
502
+ after_phase: afterPhase,
503
+ name: description,
504
+ slug,
505
+ directory: toPosixPath(relative(projectDir, join(planningPaths(projectDir).phases, dirName))),
506
+ };
507
+
508
+ return { data: result };
509
+ };
510
+
511
+ // ─── phaseScaffold handler ──────────────────────────────────────────────
512
+
513
+ /**
514
+ * Internal helper: find phase directory matching a phase identifier.
515
+ *
516
+ * Reuses the same logic as findPhase handler but returns just the directory info.
517
+ */
518
+ async function findPhaseDir(
519
+ projectDir: string,
520
+ phase: string,
521
+ ): Promise<{ dirPath: string; dirName: string; phaseName: string | null } | null> {
522
+ const phasesDir = planningPaths(projectDir).phases;
523
+ const normalized = normalizePhaseName(phase);
524
+
525
+ try {
526
+ const entries = await readdir(phasesDir, { withFileTypes: true });
527
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
528
+ const match = dirs.find(d => phaseTokenMatches(d, normalized));
529
+ if (!match) return null;
530
+
531
+ // Extract phase name from directory
532
+ const dirMatch = match.match(/^(?:[A-Z]{1,6}-)?\d+[A-Z]?(?:\.\d+)*-(.+)/i);
533
+ const phaseName = dirMatch ? dirMatch[1] : null;
534
+
535
+ return {
536
+ dirPath: join(phasesDir, match),
537
+ dirName: match,
538
+ phaseName,
539
+ };
540
+ } catch {
541
+ return null;
542
+ }
543
+ }
544
+
545
+ /**
546
+ * Query handler for phase.scaffold.
547
+ *
548
+ * Port of cmdScaffold from commands.cjs lines 750-806.
549
+ * Creates template files (context, uat, verification) or phase directories.
550
+ *
551
+ * @param args - Positional `[type, phase, name?]` **or** gsd-tools style
552
+ * `[type, '--phase', N, '--name', title]` (name may be multiple words).
553
+ * @param projectDir - Project root directory
554
+ * @returns QueryResult with { created, path } or { created: false, reason: 'already_exists' }
555
+ */
556
+ function normalizeScaffoldArgs(args: string[]): string[] {
557
+ const type = args[0];
558
+ if (!type || !args.includes('--phase')) {
559
+ return args;
560
+ }
561
+ const phaseIdx = args.indexOf('--phase');
562
+ const phase = phaseIdx !== -1 && args[phaseIdx + 1] && !args[phaseIdx + 1].startsWith('--')
563
+ ? args[phaseIdx + 1]
564
+ : '';
565
+ const nameIdx = args.indexOf('--name');
566
+ let name: string | undefined;
567
+ if (nameIdx !== -1) {
568
+ const tail = args.slice(nameIdx + 1);
569
+ const stop = tail.findIndex(a => a.startsWith('--'));
570
+ const parts = stop === -1 ? tail : tail.slice(0, stop);
571
+ name = parts.join(' ').trim() || undefined;
572
+ }
573
+ return [type, phase, ...(name !== undefined && name !== '' ? [name] : [])];
574
+ }
575
+
576
+ export const phaseScaffold: QueryHandler = async (args, projectDir) => {
577
+ const normalized = normalizeScaffoldArgs(args);
578
+ const type = normalized[0];
579
+ const phase = normalized[1];
580
+ const name = normalized[2] || undefined;
581
+
582
+ if (!type) {
583
+ throw new GSDError('type required for scaffold', ErrorClassification.Validation);
584
+ }
585
+
586
+ const validTypes = new Set(['context', 'uat', 'verification', 'phase-dir']);
587
+ if (!validTypes.has(type)) {
588
+ throw new GSDError(
589
+ `Unknown scaffold type: ${type}. Available: context, uat, verification, phase-dir`,
590
+ ErrorClassification.Validation,
591
+ );
592
+ }
593
+
594
+ if (phase) {
595
+ assertNoNullBytes(phase, 'phase');
596
+ }
597
+ if (name) {
598
+ assertNoNullBytes(name, 'name');
599
+ }
600
+
601
+ const padded = phase ? normalizePhaseName(phase) : '00';
602
+ const today = new Date().toISOString().split('T')[0];
603
+
604
+ // Handle phase-dir type separately
605
+ if (type === 'phase-dir') {
606
+ if (!phase || !name) {
607
+ throw new GSDError('phase and name required for phase-dir scaffold', ErrorClassification.Validation);
608
+ }
609
+ const slug = generateSlugInternal(name);
610
+ const dirNameNew = `${padded}-${slug}`;
611
+ assertSafePhaseDirName(dirNameNew, 'scaffold phase directory');
612
+ const phasesParent = planningPaths(projectDir).phases;
613
+ await mkdir(phasesParent, { recursive: true });
614
+ const dirPath = join(phasesParent, dirNameNew);
615
+ await mkdir(dirPath, { recursive: true });
616
+ await writeFile(join(dirPath, '.gitkeep'), '', 'utf-8');
617
+ return {
618
+ data: {
619
+ created: true,
620
+ directory: toPosixPath(relative(projectDir, dirPath)),
621
+ path: dirPath,
622
+ },
623
+ };
624
+ }
625
+
626
+ // For context/uat/verification types, find the phase directory
627
+ const phaseInfo = phase ? await findPhaseDir(projectDir, phase) : null;
628
+ if (phase && !phaseInfo) {
629
+ throw new GSDError(`Phase ${phase} directory not found`, ErrorClassification.Blocked);
630
+ }
631
+
632
+ const phaseDir = phaseInfo!.dirPath;
633
+ const phaseName = name || phaseInfo?.phaseName || 'Unnamed';
634
+
635
+ let filePath: string;
636
+ let content: string;
637
+
638
+ switch (type) {
639
+ case 'context': {
640
+ filePath = join(phaseDir, `${padded}-CONTEXT.md`);
641
+ content = `---\nphase: "${padded}"\nname: "${phaseName}"\ncreated: ${today}\n---\n\n# Phase ${phase}: ${phaseName} — Context\n\n## Decisions\n\n_Decisions will be captured during /gsd-discuss-phase ${phase}_\n\n## Discretion Areas\n\n_Areas where the executor can use judgment_\n\n## Deferred Ideas\n\n_Ideas to consider later_\n`;
642
+ break;
643
+ }
644
+ case 'uat': {
645
+ filePath = join(phaseDir, `${padded}-UAT.md`);
646
+ content = `---\nphase: "${padded}"\nname: "${phaseName}"\ncreated: ${today}\nstatus: pending\n---\n\n# Phase ${phase}: ${phaseName} — User Acceptance Testing\n\n## Test Results\n\n| # | Test | Status | Notes |\n|---|------|--------|-------|\n\n## Summary\n\n_Pending UAT_\n`;
647
+ break;
648
+ }
649
+ case 'verification': {
650
+ filePath = join(phaseDir, `${padded}-VERIFICATION.md`);
651
+ content = `---\nphase: "${padded}"\nname: "${phaseName}"\ncreated: ${today}\nstatus: pending\n---\n\n# Phase ${phase}: ${phaseName} — Verification\n\n## Goal-Backward Verification\n\n**Phase Goal:** [From ROADMAP.md]\n\n## Checks\n\n| # | Requirement | Status | Evidence |\n|---|------------|--------|----------|\n\n## Result\n\n_Pending verification_\n`;
652
+ break;
653
+ }
654
+ default:
655
+ throw new GSDError(`Unknown scaffold type: ${type}`, ErrorClassification.Validation);
656
+ }
657
+
658
+ // Check if file already exists
659
+ if (existsSync(filePath)) {
660
+ return {
661
+ data: {
662
+ created: false,
663
+ reason: 'already_exists',
664
+ path: filePath,
665
+ },
666
+ };
667
+ }
668
+
669
+ await writeFile(filePath, content, 'utf-8');
670
+ const relPath = toPosixPath(relative(projectDir, filePath));
671
+ return { data: { created: true, path: relPath } };
672
+ };
673
+
674
+ // ─── renameDecimalPhases ───────────────────────────────────────────────
675
+
676
+ /**
677
+ * Renumber sibling decimal phases after a decimal phase is removed.
678
+ *
679
+ * Port of renameDecimalPhases from phase.cjs lines 499-524.
680
+ * e.g. removing 06.2 -> 06.3 becomes 06.2, 06.4 becomes 06.3, etc.
681
+ * Renames directories AND files inside them that contain the old phase ID.
682
+ *
683
+ * CRITICAL: Sorted in DESCENDING order to avoid rename conflicts.
684
+ *
685
+ * @param phasesDir - Path to the phases directory
686
+ * @param baseInt - The integer part of the decimal phase (e.g. "06")
687
+ * @param removedDecimal - The decimal part that was removed (e.g. 2 for 06.2)
688
+ * @returns { renamedDirs, renamedFiles }
689
+ */
690
+ async function renameDecimalPhases(
691
+ phasesDir: string,
692
+ baseInt: string,
693
+ removedDecimal: number,
694
+ ): Promise<{ renamedDirs: Array<{ from: string; to: string }>; renamedFiles: Array<{ from: string; to: string }> }> {
695
+ const renamedDirs: Array<{ from: string; to: string }> = [];
696
+ const renamedFiles: Array<{ from: string; to: string }> = [];
697
+
698
+ const decPattern = new RegExp(`^${escapeRegex(baseInt)}\\.(\\d+)-(.+)$`);
699
+ const entries = await readdir(phasesDir, { withFileTypes: true });
700
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
701
+
702
+ const toRename = dirs
703
+ .map(dir => {
704
+ const m = dir.match(decPattern);
705
+ return m ? { dir, oldDecimal: parseInt(m[1], 10), slug: m[2] } : null;
706
+ })
707
+ .filter((item): item is NonNullable<typeof item> => item !== null && item.oldDecimal > removedDecimal)
708
+ .sort((a, b) => b.oldDecimal - a.oldDecimal); // DESCENDING to avoid conflicts
709
+
710
+ for (const item of toRename) {
711
+ const newDecimal = item.oldDecimal - 1;
712
+ const oldPhaseId = `${baseInt}.${item.oldDecimal}`;
713
+ const newPhaseId = `${baseInt}.${newDecimal}`;
714
+ const newDirName = `${baseInt}.${newDecimal}-${item.slug}`;
715
+
716
+ await rename(join(phasesDir, item.dir), join(phasesDir, newDirName));
717
+ renamedDirs.push({ from: item.dir, to: newDirName });
718
+
719
+ // Rename files inside that contain the old phase ID
720
+ const files = await readdir(join(phasesDir, newDirName));
721
+ for (const f of files) {
722
+ if (f.includes(oldPhaseId)) {
723
+ const newFileName = f.replace(oldPhaseId, newPhaseId);
724
+ await rename(join(phasesDir, newDirName, f), join(phasesDir, newDirName, newFileName));
725
+ renamedFiles.push({ from: f, to: newFileName });
726
+ }
727
+ }
728
+ }
729
+
730
+ return { renamedDirs, renamedFiles };
731
+ }
732
+
733
+ // ─── renameIntegerPhases ───────────────────────────────────────────────
734
+
735
+ /**
736
+ * Renumber all integer phases after a removed integer phase.
737
+ *
738
+ * Port of renameIntegerPhases from phase.cjs lines 531-564.
739
+ * e.g. removing phase 5 -> phase 6 becomes 5, phase 7 becomes 6, etc.
740
+ * Handles letter suffixes (12A) and decimals (6.1).
741
+ *
742
+ * CRITICAL: Sorted in DESCENDING order to avoid rename conflicts.
743
+ *
744
+ * @param phasesDir - Path to the phases directory
745
+ * @param removedInt - The integer phase number that was removed
746
+ * @returns { renamedDirs, renamedFiles }
747
+ */
748
+ async function renameIntegerPhases(
749
+ phasesDir: string,
750
+ removedInt: number,
751
+ ): Promise<{ renamedDirs: Array<{ from: string; to: string }>; renamedFiles: Array<{ from: string; to: string }> }> {
752
+ const renamedDirs: Array<{ from: string; to: string }> = [];
753
+ const renamedFiles: Array<{ from: string; to: string }> = [];
754
+
755
+ const entries = await readdir(phasesDir, { withFileTypes: true });
756
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
757
+
758
+ const toRename = dirs
759
+ .map(dir => {
760
+ const m = dir.match(/^(\d+)([A-Z])?(?:\.(\d+))?-(.+)$/i);
761
+ if (!m) return null;
762
+ const dirInt = parseInt(m[1], 10);
763
+ if (dirInt <= removedInt) return null;
764
+ return {
765
+ dir,
766
+ oldInt: dirInt,
767
+ letter: m[2] ? m[2].toUpperCase() : '',
768
+ decimal: m[3] !== undefined ? parseInt(m[3], 10) : null,
769
+ slug: m[4],
770
+ };
771
+ })
772
+ .filter((item): item is NonNullable<typeof item> => item !== null)
773
+ .sort((a, b) => a.oldInt !== b.oldInt
774
+ ? b.oldInt - a.oldInt
775
+ : (b.decimal ?? 0) - (a.decimal ?? 0)); // DESCENDING
776
+
777
+ for (const item of toRename) {
778
+ const newInt = item.oldInt - 1;
779
+ const newPadded = String(newInt).padStart(2, '0');
780
+ const oldPadded = String(item.oldInt).padStart(2, '0');
781
+ const letterSuffix = item.letter || '';
782
+ const decimalSuffix = item.decimal !== null ? `.${item.decimal}` : '';
783
+ const oldPrefix = `${oldPadded}${letterSuffix}${decimalSuffix}`;
784
+ const newPrefix = `${newPadded}${letterSuffix}${decimalSuffix}`;
785
+ const newDirName = `${newPrefix}-${item.slug}`;
786
+
787
+ await rename(join(phasesDir, item.dir), join(phasesDir, newDirName));
788
+ renamedDirs.push({ from: item.dir, to: newDirName });
789
+
790
+ // Rename files that start with the old prefix
791
+ const files = await readdir(join(phasesDir, newDirName));
792
+ for (const f of files) {
793
+ if (f.startsWith(oldPrefix)) {
794
+ const newFileName = newPrefix + f.slice(oldPrefix.length);
795
+ await rename(join(phasesDir, newDirName, f), join(phasesDir, newDirName, newFileName));
796
+ renamedFiles.push({ from: f, to: newFileName });
797
+ }
798
+ }
799
+ }
800
+
801
+ return { renamedDirs, renamedFiles };
802
+ }
803
+
804
+ // ─── updateRoadmapAfterPhaseRemoval ────────────────────────────────────
805
+
806
+ /**
807
+ * Remove a phase section from ROADMAP.md and renumber subsequent integer phases.
808
+ *
809
+ * Port of updateRoadmapAfterPhaseRemoval from phase.cjs lines 569-595.
810
+ * Uses readModifyWriteRoadmapMd for atomic writes.
811
+ *
812
+ * @param projectDir - Project root directory
813
+ * @param targetPhase - Phase identifier that was removed
814
+ * @param isDecimal - Whether the removed phase was a decimal phase
815
+ * @param removedInt - The integer part of the removed phase
816
+ */
817
+ async function updateRoadmapAfterPhaseRemoval(
818
+ projectDir: string,
819
+ targetPhase: string,
820
+ isDecimal: boolean,
821
+ removedInt: number,
822
+ ): Promise<void> {
823
+ await readModifyWriteRoadmapMd(projectDir, (content) => {
824
+ const escaped = escapeRegex(targetPhase);
825
+
826
+ // Remove the phase section (header + body until next phase header or end)
827
+ content = content.replace(
828
+ new RegExp(`\\n?#{2,4}\\s*Phase\\s+${escaped}\\s*:[\\s\\S]*?(?=\\n#{2,4}\\s+Phase\\s+\\d|$)`, 'i'),
829
+ '',
830
+ );
831
+
832
+ // Remove checkbox lines referencing the phase
833
+ content = content.replace(
834
+ new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${escaped}[:\\s][^\\n]*`, 'gi'),
835
+ '',
836
+ );
837
+
838
+ // Remove table rows referencing the phase
839
+ content = content.replace(
840
+ new RegExp(`\\n?\\|\\s*${escaped}\\.?\\s[^|]*\\|[^\\n]*`, 'gi'),
841
+ '',
842
+ );
843
+
844
+ // For integer phase removal, renumber all subsequent phases in ROADMAP text
845
+ if (!isDecimal) {
846
+ const MAX_PHASE = 99;
847
+ for (let oldNum = MAX_PHASE; oldNum > removedInt; oldNum--) {
848
+ const newNum = oldNum - 1;
849
+ const oldStr = String(oldNum);
850
+ const newStr = String(newNum);
851
+ const oldPad = oldStr.padStart(2, '0');
852
+ const newPad = newStr.padStart(2, '0');
853
+
854
+ // Renumber phase headers: ### Phase N:
855
+ content = content.replace(
856
+ new RegExp(`(#{2,4}\\s*Phase\\s+)${escapeRegex(oldStr)}(\\s*:)`, 'gi'),
857
+ `$1${newStr}$2`,
858
+ );
859
+
860
+ // Renumber inline Phase N references
861
+ content = content.replace(
862
+ new RegExp(`(Phase\\s+)${escapeRegex(oldStr)}([:\\s])`, 'g'),
863
+ `$1${newStr}$2`,
864
+ );
865
+
866
+ // Renumber padded plan references: 07-01 -> 06-01
867
+ content = content.replace(
868
+ new RegExp(`${escapeRegex(oldPad)}-(\\d{2})`, 'g'),
869
+ `${newPad}-$1`,
870
+ );
871
+
872
+ // Renumber table row phase numbers: | 7. -> | 6.
873
+ content = content.replace(
874
+ new RegExp(`(\\|\\s*)${escapeRegex(oldStr)}\\.\\s`, 'g'),
875
+ `$1${newStr}. `,
876
+ );
877
+
878
+ // Renumber depends-on references
879
+ content = content.replace(
880
+ new RegExp(`(\\*\\*Depends on:\\*\\*\\s*Phase\\s+)${escapeRegex(oldStr)}\\b`, 'gi'),
881
+ `$1${newStr}`,
882
+ );
883
+ }
884
+ }
885
+
886
+ return content;
887
+ });
888
+ }
889
+
890
+ // ─── phaseRemove handler ───────────────────────────────────────────────
891
+
892
+ /**
893
+ * Query handler for phase.remove.
894
+ *
895
+ * Port of cmdPhaseRemove from phase.cjs lines 597-661.
896
+ * Deletes phase directory, renumbers subsequent phases on disk,
897
+ * updates ROADMAP.md (removes section + renumbers), and decrements
898
+ * STATE.md total_phases count.
899
+ *
900
+ * @param args - args[0]: targetPhase (required), args[1]: '--force' (optional)
901
+ * @param projectDir - Project root directory
902
+ * @returns QueryResult with { removed, directory_deleted, renamed_directories, renamed_files, roadmap_updated, state_updated }
903
+ */
904
+ export const phaseRemove: QueryHandler = async (args, projectDir) => {
905
+ const targetPhase = args[0];
906
+ if (!targetPhase) {
907
+ throw new GSDError('phase number required for phase remove', ErrorClassification.Validation);
908
+ }
909
+ assertNoNullBytes(targetPhase, 'targetPhase');
910
+
911
+ const paths = planningPaths(projectDir);
912
+ const phasesDir = paths.phases;
913
+
914
+ if (!existsSync(paths.roadmap)) {
915
+ throw new GSDError('ROADMAP.md not found', ErrorClassification.Validation);
916
+ }
917
+
918
+ const normalized = normalizePhaseName(targetPhase);
919
+ const isDecimal = targetPhase.includes('.');
920
+ const force = args[1] === '--force';
921
+
922
+ // Find target directory
923
+ const entries = await readdir(phasesDir, { withFileTypes: true });
924
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
925
+ const targetDir = dirs.find(d => phaseTokenMatches(d, normalized)) ?? null;
926
+
927
+ // Guard against removing executed work
928
+ if (targetDir && !force) {
929
+ const files = await readdir(join(phasesDir, targetDir));
930
+ const summaries = files.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
931
+ if (summaries.length > 0) {
932
+ throw new GSDError(
933
+ `Phase ${targetPhase} has ${summaries.length} executed plan(s). Use --force to remove anyway.`,
934
+ ErrorClassification.Validation,
935
+ );
936
+ }
937
+ }
938
+
939
+ // Delete directory
940
+ if (targetDir) {
941
+ await rm(join(phasesDir, targetDir), { recursive: true, force: true });
942
+ }
943
+
944
+ // Renumber subsequent phases on disk
945
+ let renamedDirs: Array<{ from: string; to: string }> = [];
946
+ let renamedFiles: Array<{ from: string; to: string }> = [];
947
+ try {
948
+ let renamed: { renamedDirs: Array<{ from: string; to: string }>; renamedFiles: Array<{ from: string; to: string }> };
949
+ if (isDecimal) {
950
+ const parts = normalized.split('.');
951
+ if (parts.length < 2 || !parts[1]) {
952
+ throw new GSDError(`Invalid decimal phase identifier: ${targetPhase}`, ErrorClassification.Validation);
953
+ }
954
+ const decimalPart = parseInt(parts[1], 10);
955
+ if (isNaN(decimalPart)) {
956
+ throw new GSDError(`Invalid decimal part in phase: ${targetPhase}`, ErrorClassification.Validation);
957
+ }
958
+ renamed = await renameDecimalPhases(phasesDir, parts[0], decimalPart);
959
+ } else {
960
+ renamed = await renameIntegerPhases(phasesDir, parseInt(normalized, 10));
961
+ }
962
+ renamedDirs = renamed.renamedDirs;
963
+ renamedFiles = renamed.renamedFiles;
964
+ } catch { /* intentionally empty — renaming is best-effort */ }
965
+
966
+ // Update ROADMAP.md
967
+ await updateRoadmapAfterPhaseRemoval(projectDir, targetPhase, isDecimal, parseInt(normalized, 10));
968
+
969
+ // Update STATE.md: decrement total_phases
970
+ let stateUpdated = false;
971
+ const statePath = paths.state;
972
+ if (existsSync(statePath)) {
973
+ const lockPath = await acquireStateLock(statePath);
974
+ try {
975
+ let stateContent = await readFile(statePath, 'utf-8');
976
+
977
+ // Decrement total_phases in frontmatter
978
+ const totalPhasesMatch = stateContent.match(/total_phases:\s*(\d+)/);
979
+ if (totalPhasesMatch) {
980
+ const oldTotal = parseInt(totalPhasesMatch[1], 10);
981
+ stateContent = stateContent.replace(
982
+ /total_phases:\s*\d+/,
983
+ `total_phases: ${oldTotal - 1}`,
984
+ );
985
+ }
986
+
987
+ // Decrement "of N" pattern in body (e.g., "Plan: 2 of 3")
988
+ const ofMatch = stateContent.match(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i);
989
+ if (ofMatch) {
990
+ stateContent = stateContent.replace(
991
+ /(\bof\s+)(\d+)(\s*(?:\(|phases?))/i,
992
+ `$1${parseInt(ofMatch[2], 10) - 1}$3`,
993
+ );
994
+ }
995
+
996
+ // Also try stateReplaceField for "Total Phases" field
997
+ const totalRaw = stateExtractField(stateContent, 'Total Phases');
998
+ if (totalRaw) {
999
+ const replaced = stateReplaceField(stateContent, 'Total Phases', String(parseInt(totalRaw, 10) - 1));
1000
+ if (replaced) stateContent = replaced;
1001
+ }
1002
+
1003
+ await writeFile(statePath, stateContent, 'utf-8');
1004
+ stateUpdated = true;
1005
+ } finally {
1006
+ await releaseStateLock(lockPath);
1007
+ }
1008
+ }
1009
+
1010
+ return {
1011
+ data: {
1012
+ removed: targetPhase,
1013
+ directory_deleted: targetDir,
1014
+ renamed_directories: renamedDirs,
1015
+ renamed_files: renamedFiles,
1016
+ roadmap_updated: true,
1017
+ state_updated: stateUpdated,
1018
+ },
1019
+ };
1020
+ };
1021
+
1022
+ // ─── stateReplaceFieldWithFallback (inline) ────────────────────────────────
1023
+
1024
+ /**
1025
+ * Replace a field with fallback field name support.
1026
+ *
1027
+ * Tries primary first, then fallback. Returns content unchanged if neither matches.
1028
+ * Reimplemented here because state-mutation.ts keeps it module-private.
1029
+ */
1030
+ function stateReplaceFieldWithFallback(
1031
+ content: string,
1032
+ primary: string,
1033
+ fallback: string | null,
1034
+ value: string,
1035
+ ): string {
1036
+ let result = stateReplaceField(content, primary, value);
1037
+ if (result) return result;
1038
+ if (fallback) {
1039
+ result = stateReplaceField(content, fallback, value);
1040
+ if (result) return result;
1041
+ }
1042
+ return content;
1043
+ }
1044
+
1045
+ // ─── updatePerformanceMetricsSection ───────────────────────────────────────
1046
+
1047
+ /**
1048
+ * Update the Performance Metrics section in STATE.md content.
1049
+ *
1050
+ * Port of updatePerformanceMetricsSection from state.cjs lines 1125-1156.
1051
+ * Updates "Total plans completed" counter and upserts a row in the By Phase table.
1052
+ *
1053
+ * @param content - STATE.md content
1054
+ * @param phaseNum - Phase number being completed
1055
+ * @param planCount - Total number of plans in the phase
1056
+ * @param summaryCount - Number of completed summaries
1057
+ * @returns Modified content
1058
+ */
1059
+ function updatePerformanceMetricsSection(
1060
+ content: string,
1061
+ phaseNum: string,
1062
+ planCount: number,
1063
+ summaryCount: number,
1064
+ ): string {
1065
+ // Update Velocity: Total plans completed
1066
+ const totalMatch = content.match(/Total plans completed:\s*(\d+|\[N\])/);
1067
+ const prevTotal = totalMatch && totalMatch[1] !== '[N]' ? parseInt(totalMatch[1], 10) : 0;
1068
+ const newTotal = prevTotal + summaryCount;
1069
+ content = content.replace(
1070
+ /Total plans completed:\s*(\d+|\[N\])/,
1071
+ `Total plans completed: ${newTotal}`,
1072
+ );
1073
+
1074
+ // Update By Phase table — upsert row for this phase
1075
+ const byPhaseTablePattern = /(\|\s*Phase\s*\|\s*Plans\s*\|\s*Total\s*\|\s*Avg\/Plan\s*\|[ \t]*\n\|(?:[- :\t]+\|)+[ \t]*\n)((?:[ \t]*\|[^\n]*\n)*)(?=\n|$)/i;
1076
+ const byPhaseMatch = content.match(byPhaseTablePattern);
1077
+ if (byPhaseMatch) {
1078
+ let tableBody = byPhaseMatch[2].trim();
1079
+ const phaseRowPattern = new RegExp(`^\\|\\s*${escapeRegex(String(phaseNum))}\\s*\\|.*$`, 'm');
1080
+ const newRow = `| ${phaseNum} | ${summaryCount} | - | - |`;
1081
+
1082
+ if (phaseRowPattern.test(tableBody)) {
1083
+ // Update existing row
1084
+ tableBody = tableBody.replace(new RegExp(`^\\|\\s*${escapeRegex(String(phaseNum))}\\s*\\|.*$`, 'm'), newRow);
1085
+ } else {
1086
+ // Remove placeholder row and add new row
1087
+ tableBody = tableBody.replace(/^\|\s*-\s*\|\s*-\s*\|\s*-\s*\|\s*-\s*\|$/m, '').trim();
1088
+ tableBody = tableBody ? tableBody + '\n' + newRow : newRow;
1089
+ }
1090
+
1091
+ content = content.replace(byPhaseTablePattern, `$1${tableBody}\n`);
1092
+ }
1093
+
1094
+ return content;
1095
+ }
1096
+
1097
+ // ─── phaseComplete handler ────────────────────────────────────────────────
1098
+
1099
+ /**
1100
+ * Query handler for phase.complete.
1101
+ *
1102
+ * Port of cmdPhaseComplete from phase.cjs lines 663-932.
1103
+ * Marks a phase as done — updates ROADMAP.md (checkbox, progress table,
1104
+ * plan count, plan checkboxes), REQUIREMENTS.md (requirement checkboxes,
1105
+ * traceability table), and STATE.md (current phase, status, progress,
1106
+ * performance metrics) atomically with per-file locks.
1107
+ *
1108
+ * @param args - args[0]: phaseNum (required)
1109
+ * @param projectDir - Project root directory
1110
+ * @returns QueryResult with completion details and warnings
1111
+ */
1112
+ export const phaseComplete: QueryHandler = async (args, projectDir) => {
1113
+ const phaseNum = args[0];
1114
+ if (!phaseNum) {
1115
+ throw new GSDError('phase number required for phase complete', ErrorClassification.Validation);
1116
+ }
1117
+ assertNoNullBytes(phaseNum, 'phaseNum');
1118
+
1119
+ const paths = planningPaths(projectDir);
1120
+ const today = new Date().toISOString().split('T')[0];
1121
+
1122
+ // Step A: Validate phase exists and get info
1123
+ const phaseInfo = await findPhaseDir(projectDir, phaseNum);
1124
+ if (!phaseInfo) {
1125
+ throw new GSDError(`Phase ${phaseNum} not found`, ErrorClassification.Validation);
1126
+ }
1127
+
1128
+ const phaseDir = phaseInfo.dirPath;
1129
+ let phaseFiles: string[];
1130
+ try {
1131
+ phaseFiles = await readdir(phaseDir);
1132
+ } catch {
1133
+ phaseFiles = [];
1134
+ }
1135
+
1136
+ const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
1137
+ const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
1138
+ const planCount = plans.length;
1139
+ const summaryCount = summaries.length;
1140
+ let requirementsUpdated = false;
1141
+
1142
+ // Step B: Check for verification warnings (non-blocking)
1143
+ const warnings: string[] = [];
1144
+ for (const file of phaseFiles.filter(f => f.includes('-UAT') && f.endsWith('.md'))) {
1145
+ try {
1146
+ const content = await readFile(join(phaseDir, file), 'utf-8');
1147
+ if (/result: pending/.test(content)) warnings.push(`${file}: has pending tests`);
1148
+ if (/result: blocked/.test(content)) warnings.push(`${file}: has blocked tests`);
1149
+ if (/status: partial/.test(content)) warnings.push(`${file}: testing incomplete (partial)`);
1150
+ if (/status: diagnosed/.test(content)) warnings.push(`${file}: has diagnosed gaps`);
1151
+ } catch { /* intentionally empty */ }
1152
+ }
1153
+ for (const file of phaseFiles.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md'))) {
1154
+ try {
1155
+ const content = await readFile(join(phaseDir, file), 'utf-8');
1156
+ if (/status: human_needed/.test(content)) warnings.push(`${file}: needs human verification`);
1157
+ if (/status: gaps_found/.test(content)) warnings.push(`${file}: has unresolved gaps`);
1158
+ } catch { /* intentionally empty */ }
1159
+ }
1160
+
1161
+ // Step C: Update ROADMAP.md atomically
1162
+ if (existsSync(paths.roadmap)) {
1163
+ await readModifyWriteRoadmapMd(projectDir, async (roadmapContent) => {
1164
+ const phaseEscaped = escapeRegex(phaseNum);
1165
+
1166
+ // Checkbox: - [ ] Phase N: -> - [x] Phase N: (...completed DATE)
1167
+ const checkboxPattern = new RegExp(
1168
+ `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}[:\\s][^\\n]*)`,
1169
+ 'i',
1170
+ );
1171
+ roadmapContent = replaceInCurrentMilestone(roadmapContent, checkboxPattern, `$1x$2 (completed ${today})`);
1172
+
1173
+ // Progress table: update Status to Complete, add date
1174
+ const tableRowPattern = new RegExp(
1175
+ `^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
1176
+ 'im',
1177
+ );
1178
+ roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
1179
+ const cells = fullRow.split('|').slice(1, -1);
1180
+ if (cells.length === 5) {
1181
+ cells[2] = ` ${summaryCount}/${planCount} `;
1182
+ cells[3] = ' Complete ';
1183
+ cells[4] = ` ${today} `;
1184
+ } else if (cells.length === 4) {
1185
+ cells[1] = ` ${summaryCount}/${planCount} `;
1186
+ cells[2] = ' Complete ';
1187
+ cells[3] = ` ${today} `;
1188
+ }
1189
+ return '|' + cells.join('|') + '|';
1190
+ });
1191
+
1192
+ // Update plan count in phase section
1193
+ const planCountPattern = new RegExp(
1194
+ `(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
1195
+ 'i',
1196
+ );
1197
+ roadmapContent = replaceInCurrentMilestone(
1198
+ roadmapContent, planCountPattern,
1199
+ `$1${summaryCount}/${planCount} plans complete`,
1200
+ );
1201
+
1202
+ // Mark completed plan checkboxes
1203
+ for (const summaryFile of summaries) {
1204
+ const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
1205
+ if (!planId) continue;
1206
+ const planEscaped = escapeRegex(planId);
1207
+ const planCheckboxPattern = new RegExp(
1208
+ `(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`,
1209
+ 'i',
1210
+ );
1211
+ roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
1212
+ }
1213
+
1214
+ // Step D: Update REQUIREMENTS.md
1215
+ const reqPath = paths.requirements;
1216
+ if (existsSync(reqPath)) {
1217
+ const currentMilestoneRoadmap = await extractCurrentMilestone(roadmapContent, projectDir);
1218
+ const phaseSectionMatch = currentMilestoneRoadmap.match(
1219
+ new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEscaped}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i'),
1220
+ );
1221
+
1222
+ const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : '';
1223
+ const reqMatch = sectionText.match(/\*\*Requirements\*?\*?:?\s*([^\n]+)/i);
1224
+
1225
+ if (reqMatch) {
1226
+ const reqIds = reqMatch[1].replace(/[[\]]/g, '').split(/[,\s]+/).map(r => r.trim()).filter(Boolean);
1227
+ let reqContent = await readFile(reqPath, 'utf-8');
1228
+
1229
+ for (const reqId of reqIds) {
1230
+ const reqEscaped = escapeRegex(reqId);
1231
+ // Update checkbox: - [ ] **REQ-ID** -> - [x] **REQ-ID**
1232
+ reqContent = reqContent.replace(
1233
+ new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi'),
1234
+ '$1x$2',
1235
+ );
1236
+ // Update traceability table: Pending/In Progress -> Complete
1237
+ reqContent = reqContent.replace(
1238
+ new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*(?:Pending|In Progress)\\s*(\\|)`, 'gi'),
1239
+ '$1 Complete $2',
1240
+ );
1241
+ }
1242
+
1243
+ await writeFile(reqPath, reqContent, 'utf-8');
1244
+ requirementsUpdated = true;
1245
+ }
1246
+ }
1247
+
1248
+ return roadmapContent;
1249
+ });
1250
+ }
1251
+
1252
+ // Step E: Find next phase — filesystem first, then ROADMAP.md fallback
1253
+ let nextPhaseNum: string | null = null;
1254
+ let nextPhaseName: string | null = null;
1255
+ let isLastPhase = true;
1256
+
1257
+ try {
1258
+ const isDirInMilestone = await getMilestonePhaseFilter(projectDir);
1259
+ const entries = await readdir(paths.phases, { withFileTypes: true });
1260
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name)
1261
+ .filter(isDirInMilestone)
1262
+ .sort((a, b) => comparePhaseNum(a, b));
1263
+
1264
+ for (const dir of dirs) {
1265
+ const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
1266
+ if (dm) {
1267
+ if (comparePhaseNum(dm[1], phaseNum) > 0) {
1268
+ nextPhaseNum = dm[1];
1269
+ nextPhaseName = dm[2] || null;
1270
+ isLastPhase = false;
1271
+ break;
1272
+ }
1273
+ }
1274
+ }
1275
+ } catch { /* intentionally empty */ }
1276
+
1277
+ // Fallback: check ROADMAP.md for phases not yet scaffolded
1278
+ if (isLastPhase && existsSync(paths.roadmap)) {
1279
+ try {
1280
+ const roadmapContent = await readFile(paths.roadmap, 'utf-8');
1281
+ const roadmapForPhases = await extractCurrentMilestone(roadmapContent, projectDir);
1282
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
1283
+ let pm: RegExpExecArray | null;
1284
+ while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
1285
+ if (comparePhaseNum(pm[1], phaseNum) > 0) {
1286
+ nextPhaseNum = pm[1];
1287
+ nextPhaseName = pm[2].replace(/\(INSERTED\)/i, '').trim().toLowerCase().replace(/\s+/g, '-');
1288
+ isLastPhase = false;
1289
+ break;
1290
+ }
1291
+ }
1292
+ } catch { /* intentionally empty */ }
1293
+ }
1294
+
1295
+ // Step F: Update STATE.md atomically
1296
+ let stateUpdated = false;
1297
+ if (existsSync(paths.state)) {
1298
+ const lockPath = await acquireStateLock(paths.state);
1299
+ try {
1300
+ const rawState = await readFile(paths.state, 'utf-8');
1301
+
1302
+ // Split into frontmatter and body to prevent field replacement from
1303
+ // matching YAML keys (e.g., `status:` in frontmatter vs `Status:` in body).
1304
+ // Pattern 11: Strip frontmatter before modifier (from Phase 11 decisions).
1305
+ const fmMatch = rawState.match(/^(---\r?\n[\s\S]*?\r?\n---)\s*/);
1306
+ let frontmatter = fmMatch ? fmMatch[1] : '';
1307
+ let body = fmMatch ? rawState.slice(fmMatch[0].length) : rawState;
1308
+
1309
+ // Update Current Phase — preserve "X of Y (Name)" compound format
1310
+ const phaseValue = nextPhaseNum || phaseNum;
1311
+ const existingPhaseField = stateExtractField(body, 'Current Phase')
1312
+ || stateExtractField(body, 'Phase');
1313
+ let newPhaseValue = String(phaseValue);
1314
+ if (existingPhaseField) {
1315
+ const totalMatch = existingPhaseField.match(/of\s+(\d+)/);
1316
+ const nameMatch = existingPhaseField.match(/\(([^)]+)\)/);
1317
+ if (totalMatch) {
1318
+ const total = totalMatch[1];
1319
+ const nameStr = nextPhaseName
1320
+ ? ` (${nextPhaseName.replace(/-/g, ' ')})`
1321
+ : (nameMatch ? ` (${nameMatch[1]})` : '');
1322
+ newPhaseValue = `${phaseValue} of ${total}${nameStr}`;
1323
+ }
1324
+ }
1325
+ body = stateReplaceFieldWithFallback(body, 'Current Phase', 'Phase', newPhaseValue);
1326
+
1327
+ // Update Status
1328
+ body = stateReplaceFieldWithFallback(body, 'Status', null,
1329
+ isLastPhase ? 'Milestone complete' : 'Ready to plan');
1330
+
1331
+ // Update Current Plan
1332
+ body = stateReplaceFieldWithFallback(body, 'Current Plan', 'Plan', 'Not started');
1333
+
1334
+ // Update Last Activity
1335
+ body = stateReplaceFieldWithFallback(body, 'Last Activity', 'Last activity', today);
1336
+
1337
+ // Update Performance Metrics section (operates on body only)
1338
+ body = updatePerformanceMetricsSection(body, phaseNum, planCount, summaryCount);
1339
+
1340
+ // Update frontmatter fields separately
1341
+ // Increment completed_phases
1342
+ const completedFmMatch = frontmatter.match(/completed_phases:\s*(\d+)/);
1343
+ if (completedFmMatch) {
1344
+ const newCompleted = parseInt(completedFmMatch[1], 10) + 1;
1345
+ frontmatter = frontmatter.replace(
1346
+ /completed_phases:\s*\d+/,
1347
+ `completed_phases: ${newCompleted}`,
1348
+ );
1349
+
1350
+ // Recalculate percent
1351
+ const totalFmMatch = frontmatter.match(/total_phases:\s*(\d+)/);
1352
+ if (totalFmMatch) {
1353
+ const totalPhases = parseInt(totalFmMatch[1], 10);
1354
+ if (totalPhases > 0) {
1355
+ const newPercent = Math.round((newCompleted / totalPhases) * 100);
1356
+ frontmatter = frontmatter.replace(
1357
+ /(percent:\s*)\d+/,
1358
+ `$1${newPercent}`,
1359
+ );
1360
+ }
1361
+ }
1362
+ }
1363
+
1364
+ // Update frontmatter status field
1365
+ frontmatter = frontmatter.replace(
1366
+ /status:\s*.+/,
1367
+ `status: ${isLastPhase ? 'milestone_complete' : 'ready_to_plan'}`,
1368
+ );
1369
+
1370
+ // Reassemble and write
1371
+ const stateContent = frontmatter + '\n\n' + body;
1372
+ await writeFile(paths.state, stateContent, 'utf-8');
1373
+ stateUpdated = true;
1374
+ } finally {
1375
+ await releaseStateLock(lockPath);
1376
+ }
1377
+ }
1378
+
1379
+ // Step G: Return result
1380
+ return {
1381
+ data: {
1382
+ completed_phase: phaseNum,
1383
+ phase_name: phaseInfo.phaseName,
1384
+ plans_executed: `${summaryCount}/${planCount}`,
1385
+ next_phase: nextPhaseNum,
1386
+ next_phase_name: nextPhaseName,
1387
+ is_last_phase: isLastPhase,
1388
+ date: today,
1389
+ roadmap_updated: existsSync(paths.roadmap),
1390
+ state_updated: stateUpdated,
1391
+ requirements_updated: requirementsUpdated,
1392
+ warnings,
1393
+ has_warnings: warnings.length > 0,
1394
+ },
1395
+ };
1396
+ };
1397
+
1398
+ // ─── phasesClear handler ──────────────────────────────────────────────────
1399
+
1400
+ /**
1401
+ * Query handler for phases.clear.
1402
+ *
1403
+ * Port of cmdPhasesClear from milestone.cjs lines 250-277.
1404
+ * Deletes all phase directories except 999.x backlog phases.
1405
+ * Requires --confirm flag to proceed.
1406
+ *
1407
+ * @param args - args[0]: '--confirm' to proceed (optional)
1408
+ * @param projectDir - Project root directory
1409
+ * @returns QueryResult with { cleared: count }
1410
+ */
1411
+ export const phasesClear: QueryHandler = async (args, projectDir) => {
1412
+ const phasesDir = planningPaths(projectDir).phases;
1413
+ const confirm = Array.isArray(args) && args.includes('--confirm');
1414
+ let cleared = 0;
1415
+
1416
+ if (existsSync(phasesDir)) {
1417
+ const entries = await readdir(phasesDir, { withFileTypes: true });
1418
+ const dirs = entries.filter(e => e.isDirectory() && !/^999(?:\.|$)/.test(e.name));
1419
+
1420
+ if (dirs.length > 0 && !confirm) {
1421
+ throw new GSDError(
1422
+ `phases clear would delete ${dirs.length} phase director${dirs.length === 1 ? 'y' : 'ies'}. ` +
1423
+ `Pass --confirm to proceed.`,
1424
+ ErrorClassification.Validation,
1425
+ );
1426
+ }
1427
+
1428
+ for (const entry of dirs) {
1429
+ await rm(join(phasesDir, entry.name), { recursive: true, force: true });
1430
+ cleared++;
1431
+ }
1432
+ }
1433
+
1434
+ return { data: { cleared } };
1435
+ };
1436
+
1437
+ // ─── phasesArchive handler ────────────────────────────────────────────────
1438
+
1439
+ /**
1440
+ * Query handler for phases.archive.
1441
+ *
1442
+ * Extracted from cmdMilestoneComplete, milestone.cjs lines 210-227.
1443
+ * Moves milestone phase directories to milestones/{version}-phases/.
1444
+ *
1445
+ * @param args - args[0]: version string (e.g., "v3.0")
1446
+ * @param projectDir - Project root directory
1447
+ * @returns QueryResult with { archived: count, version, archive_directory }
1448
+ */
1449
+ export const phasesList: QueryHandler = async (args, projectDir) => {
1450
+ const paths = planningPaths(projectDir);
1451
+ const phasesDir = paths.phases;
1452
+
1453
+ const typeIdx = args.indexOf('--type');
1454
+ const phaseIdx = args.indexOf('--phase');
1455
+ const type = typeIdx !== -1 ? args[typeIdx + 1] : null;
1456
+ const phase = phaseIdx !== -1 ? args[phaseIdx + 1] : null;
1457
+ const includeArchived = args.includes('--include-archived');
1458
+
1459
+ if (!existsSync(phasesDir)) {
1460
+ return { data: type ? { files: [], count: 0 } : { directories: [], count: 0 } };
1461
+ }
1462
+
1463
+ const entries = await readdir(phasesDir, { withFileTypes: true });
1464
+ let dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
1465
+
1466
+ if (includeArchived) {
1467
+ const milestonesDir = join(paths.planning, 'milestones');
1468
+ if (existsSync(milestonesDir)) {
1469
+ const milestoneEntries = await readdir(milestonesDir, { withFileTypes: true });
1470
+ for (const mDir of milestoneEntries.filter(e => e.isDirectory() && e.name.endsWith('-phases'))) {
1471
+ const milestone = mDir.name.replace(/-phases$/, '');
1472
+ const archivedEntries = await readdir(join(milestonesDir, mDir.name), { withFileTypes: true });
1473
+ for (const a of archivedEntries.filter(e => e.isDirectory())) {
1474
+ dirs.push(`${a.name} [${milestone}]`);
1475
+ }
1476
+ }
1477
+ }
1478
+ }
1479
+
1480
+ dirs.sort((a, b) => comparePhaseNum(a, b));
1481
+
1482
+ if (phase) {
1483
+ const normalized = normalizePhaseName(phase);
1484
+ const match = dirs.find(d => phaseTokenMatches(d, normalized));
1485
+ if (!match) {
1486
+ return { data: { files: [], count: 0, phase_dir: null, error: 'Phase not found' } };
1487
+ }
1488
+ dirs = [match];
1489
+ }
1490
+
1491
+ if (type) {
1492
+ const files: string[] = [];
1493
+ for (const dir of dirs) {
1494
+ const dirPath = join(phasesDir, dir);
1495
+ if (!existsSync(dirPath)) continue;
1496
+ const dirFiles = await readdir(dirPath);
1497
+ let filtered: string[];
1498
+ if (type === 'plans') {
1499
+ filtered = dirFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
1500
+ } else if (type === 'summaries') {
1501
+ filtered = dirFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
1502
+ } else {
1503
+ filtered = dirFiles;
1504
+ }
1505
+ files.push(...filtered.sort());
1506
+ }
1507
+ return { data: { files, count: files.length, phase_dir: phase ? dirs[0]?.replace(/^\d+(?:\.\d+)*-?/, '') : null } };
1508
+ }
1509
+
1510
+ return { data: { directories: dirs, count: dirs.length } };
1511
+ };
1512
+
1513
+ export const phaseNextDecimal: QueryHandler = async (args, projectDir) => {
1514
+ const basePhase = args[0];
1515
+ if (!basePhase) {
1516
+ throw new GSDError('base phase number required', ErrorClassification.Validation);
1517
+ }
1518
+ assertNoNullBytes(basePhase, 'basePhase');
1519
+
1520
+ const paths = planningPaths(projectDir);
1521
+ const phasesDir = paths.phases;
1522
+ const normalized = normalizePhaseName(basePhase);
1523
+ const decimalSet = new Set<number>();
1524
+ let baseExists = false;
1525
+
1526
+ if (existsSync(phasesDir)) {
1527
+ const entries = await readdir(phasesDir, { withFileTypes: true });
1528
+ const dirNames = entries.filter(e => e.isDirectory()).map(e => e.name);
1529
+ baseExists = dirNames.some(d => phaseTokenMatches(d, normalized));
1530
+
1531
+ const dirPattern = new RegExp(`^(?:[A-Z]{1,6}-)?${escapeRegex(normalized)}\\.(\\d+)`);
1532
+ for (const dir of dirNames) {
1533
+ const match = dir.match(dirPattern);
1534
+ if (match) decimalSet.add(parseInt(match[1], 10));
1535
+ }
1536
+ }
1537
+
1538
+ const roadmapPath = paths.roadmap;
1539
+ if (existsSync(roadmapPath)) {
1540
+ try {
1541
+ const roadmapContent = await readFile(roadmapPath, 'utf-8');
1542
+ const phasePattern = new RegExp(
1543
+ `#{2,4}\\s*Phase\\s+0*${escapeRegex(normalized)}\\.(\\d+)\\s*:`, 'gi',
1544
+ );
1545
+ let pm;
1546
+ while ((pm = phasePattern.exec(roadmapContent)) !== null) {
1547
+ decimalSet.add(parseInt(pm[1], 10));
1548
+ }
1549
+ } catch { /* ROADMAP.md read failure is non-fatal */ }
1550
+ }
1551
+
1552
+ const existingDecimals = Array.from(decimalSet)
1553
+ .sort((a, b) => a - b)
1554
+ .map(n => `${normalized}.${n}`);
1555
+
1556
+ const nextDecimal = decimalSet.size === 0
1557
+ ? `${normalized}.1`
1558
+ : `${normalized}.${Math.max(...decimalSet) + 1}`;
1559
+
1560
+ return {
1561
+ data: {
1562
+ found: baseExists,
1563
+ base_phase: normalized,
1564
+ next: nextDecimal,
1565
+ existing: existingDecimals,
1566
+ },
1567
+ };
1568
+ };
1569
+
1570
+ export const phasesArchive: QueryHandler = async (args, projectDir) => {
1571
+ const version = args[0];
1572
+ if (!version) {
1573
+ throw new GSDError('version required for phases archive', ErrorClassification.Validation);
1574
+ }
1575
+ assertNoNullBytes(version, 'version');
1576
+
1577
+ const paths = planningPaths(projectDir);
1578
+ const phasesDir = paths.phases;
1579
+ const isDirInMilestone = await getMilestonePhaseFilter(projectDir);
1580
+
1581
+ const archiveDir = join(paths.planning, 'milestones', `${version}-phases`);
1582
+ await mkdir(archiveDir, { recursive: true });
1583
+
1584
+ let archivedCount = 0;
1585
+ if (existsSync(phasesDir)) {
1586
+ const entries = await readdir(phasesDir, { withFileTypes: true });
1587
+ const phaseDirNames = entries.filter(e => e.isDirectory()).map(e => e.name);
1588
+
1589
+ for (const dir of phaseDirNames) {
1590
+ if (!isDirInMilestone(dir)) continue;
1591
+ await rename(join(phasesDir, dir), join(archiveDir, dir));
1592
+ archivedCount++;
1593
+ }
1594
+ }
1595
+
1596
+ return {
1597
+ data: {
1598
+ archived: archivedCount,
1599
+ version,
1600
+ archive_directory: toPosixPath(relative(projectDir, archiveDir)),
1601
+ },
1602
+ };
1603
+ };
1604
+
1605
+ // ─── milestoneComplete ────────────────────────────────────────────────────
1606
+
1607
+ /** Port of `parseMultiwordArg` in `gsd-tools.cjs`. */
1608
+ function parseMultiwordArg(args: string[], flag: string): string | null {
1609
+ const idx = args.indexOf(`--${flag}`);
1610
+ if (idx === -1) return null;
1611
+ const tokens: string[] = [];
1612
+ for (let i = idx + 1; i < args.length; i++) {
1613
+ if (args[i]!.startsWith('--')) break;
1614
+ tokens.push(args[i]!);
1615
+ }
1616
+ return tokens.length > 0 ? tokens.join(' ') : null;
1617
+ }
1618
+
1619
+ /** Port of `extractOneLinerFromBody` from `core.cjs` / `summary.ts`. */
1620
+ function extractOneLinerFromBody(content: string): string | null {
1621
+ if (!content) return null;
1622
+ const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n*/, '');
1623
+ const match = body.match(/^#[^\n]*\n+\*\*([^*]+)\*\*/m);
1624
+ return match ? match[1]!.trim() : null;
1625
+ }
1626
+
1627
+ /**
1628
+ * Query handler for `milestone.complete` — port of `cmdMilestoneComplete` from `milestone.cjs`.
1629
+ */
1630
+ export const milestoneComplete: QueryHandler = async (args, projectDir) => {
1631
+ const version = args[0];
1632
+ if (!version) {
1633
+ throw new GSDError('version required for milestone complete (e.g., v1.0)', ErrorClassification.Validation);
1634
+ }
1635
+ assertNoNullBytes(version, 'version');
1636
+
1637
+ const nameOpt = parseMultiwordArg(args, 'name');
1638
+ const archivePhases = args.includes('--archive-phases');
1639
+
1640
+ const paths = planningPaths(projectDir);
1641
+ const roadmapPath = paths.roadmap;
1642
+ const reqPath = paths.requirements;
1643
+ const statePath = paths.state;
1644
+ const milestonesPath = join(paths.planning, 'MILESTONES.md');
1645
+ const archiveDir = join(paths.planning, 'milestones');
1646
+ const phasesDir = paths.phases;
1647
+ const today = new Date().toISOString().split('T')[0]!;
1648
+ const milestoneName = nameOpt || version;
1649
+
1650
+ await mkdir(archiveDir, { recursive: true });
1651
+
1652
+ const isDirInMilestone = await getMilestonePhaseFilter(projectDir);
1653
+
1654
+ let phaseCount = 0;
1655
+ let totalPlans = 0;
1656
+ let totalTasks = 0;
1657
+ const accomplishments: string[] = [];
1658
+
1659
+ try {
1660
+ const entries = await readdir(phasesDir, { withFileTypes: true });
1661
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
1662
+
1663
+ for (const dir of dirs) {
1664
+ if (!isDirInMilestone(dir)) continue;
1665
+
1666
+ phaseCount++;
1667
+ const phaseFiles = await readdir(join(phasesDir, dir));
1668
+ const plans = phaseFiles.filter((f) => f.endsWith('-PLAN.md') || f === 'PLAN.md');
1669
+ const summaries = phaseFiles.filter((f) => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
1670
+ totalPlans += plans.length;
1671
+
1672
+ for (const s of summaries) {
1673
+ try {
1674
+ const content = await readFile(join(phasesDir, dir, s), 'utf-8');
1675
+ const fm = extractFrontmatter(content);
1676
+ const oneLiner =
1677
+ (fm['one-liner'] as string | undefined) || extractOneLinerFromBody(content);
1678
+ if (oneLiner) {
1679
+ accomplishments.push(oneLiner);
1680
+ }
1681
+ const tasksFieldMatch = content.match(/\*\*Tasks:\*\*\s*(\d+)/);
1682
+ if (tasksFieldMatch) {
1683
+ totalTasks += parseInt(tasksFieldMatch[1]!, 10);
1684
+ } else {
1685
+ const xmlTaskMatches = content.match(/<task[\s>]/gi) || [];
1686
+ const mdTaskMatches = content.match(/##\s*Task\s*\d+/gi) || [];
1687
+ totalTasks += xmlTaskMatches.length || mdTaskMatches.length;
1688
+ }
1689
+ } catch {
1690
+ /* intentionally empty */
1691
+ }
1692
+ }
1693
+ }
1694
+ } catch {
1695
+ /* intentionally empty */
1696
+ }
1697
+
1698
+ if (existsSync(roadmapPath)) {
1699
+ const roadmapContent = await readFile(roadmapPath, 'utf-8');
1700
+ await writeFile(join(archiveDir, `${version}-ROADMAP.md`), roadmapContent, 'utf-8');
1701
+ }
1702
+
1703
+ if (existsSync(reqPath)) {
1704
+ const reqContent = await readFile(reqPath, 'utf-8');
1705
+ const archiveHeader =
1706
+ `# Requirements Archive: ${version} ${milestoneName}\n\n` +
1707
+ `**Archived:** ${today}\n**Status:** SHIPPED\n\n` +
1708
+ `For current requirements, see \`.planning/REQUIREMENTS.md\`.\n\n---\n\n`;
1709
+ await writeFile(join(archiveDir, `${version}-REQUIREMENTS.md`), archiveHeader + reqContent, 'utf-8');
1710
+ }
1711
+
1712
+ const auditFile = join(projectDir, '.planning', `${version}-MILESTONE-AUDIT.md`);
1713
+ if (existsSync(auditFile)) {
1714
+ await rename(auditFile, join(archiveDir, `${version}-MILESTONE-AUDIT.md`));
1715
+ }
1716
+
1717
+ const accomplishmentsList = accomplishments.map((a) => `- ${a}`).join('\n');
1718
+ const milestoneEntry =
1719
+ `## ${version} ${milestoneName} (Shipped: ${today})\n\n` +
1720
+ `**Phases completed:** ${phaseCount} phases, ${totalPlans} plans, ${totalTasks} tasks\n\n` +
1721
+ `**Key accomplishments:**\n${accomplishmentsList || '- (none recorded)'}\n\n---\n\n`;
1722
+
1723
+ if (existsSync(milestonesPath)) {
1724
+ const existing = await readFile(milestonesPath, 'utf-8');
1725
+ if (!existing.trim()) {
1726
+ await writeFile(milestonesPath, normalizeMd(`# Milestones\n\n${milestoneEntry}`), 'utf-8');
1727
+ } else {
1728
+ const headerMatch = existing.match(/^(#{1,3}\s+[^\n]*\n\n?)/);
1729
+ if (headerMatch) {
1730
+ const header = headerMatch[1]!;
1731
+ const rest = existing.slice(header.length);
1732
+ await writeFile(milestonesPath, normalizeMd(header + milestoneEntry + rest), 'utf-8');
1733
+ } else {
1734
+ await writeFile(milestonesPath, normalizeMd(milestoneEntry + existing), 'utf-8');
1735
+ }
1736
+ }
1737
+ } else {
1738
+ await writeFile(milestonesPath, normalizeMd(`# Milestones\n\n${milestoneEntry}`), 'utf-8');
1739
+ }
1740
+
1741
+ if (existsSync(statePath)) {
1742
+ await readModifyWriteStateMdFull(projectDir, (stateContent) => {
1743
+ let next = stateReplaceFieldWithFallback(
1744
+ stateContent,
1745
+ 'Status',
1746
+ null,
1747
+ `${version} milestone complete`,
1748
+ );
1749
+ next = stateReplaceFieldWithFallback(next, 'Last Activity', 'Last activity', today);
1750
+ next = stateReplaceFieldWithFallback(
1751
+ next,
1752
+ 'Last Activity Description',
1753
+ null,
1754
+ `${version} milestone completed and archived`,
1755
+ );
1756
+ return next;
1757
+ });
1758
+ }
1759
+
1760
+ let phasesArchived = false;
1761
+ if (archivePhases) {
1762
+ try {
1763
+ const phaseArchiveDir = join(archiveDir, `${version}-phases`);
1764
+ await mkdir(phaseArchiveDir, { recursive: true });
1765
+
1766
+ const phaseEntries = await readdir(phasesDir, { withFileTypes: true });
1767
+ const phaseDirNames = phaseEntries.filter((e) => e.isDirectory()).map((e) => e.name);
1768
+ let archivedCount = 0;
1769
+ for (const dir of phaseDirNames) {
1770
+ if (!isDirInMilestone(dir)) continue;
1771
+ await rename(join(phasesDir, dir), join(phaseArchiveDir, dir));
1772
+ archivedCount++;
1773
+ }
1774
+ phasesArchived = archivedCount > 0;
1775
+ } catch {
1776
+ /* intentionally empty */
1777
+ }
1778
+ }
1779
+
1780
+ return {
1781
+ data: {
1782
+ version,
1783
+ name: milestoneName,
1784
+ date: today,
1785
+ phases: phaseCount,
1786
+ plans: totalPlans,
1787
+ tasks: totalTasks,
1788
+ accomplishments,
1789
+ archived: {
1790
+ roadmap: existsSync(join(archiveDir, `${version}-ROADMAP.md`)),
1791
+ requirements: existsSync(join(archiveDir, `${version}-REQUIREMENTS.md`)),
1792
+ audit: existsSync(join(archiveDir, `${version}-MILESTONE-AUDIT.md`)),
1793
+ phases: phasesArchived,
1794
+ },
1795
+ milestones_updated: true,
1796
+ state_updated: existsSync(statePath),
1797
+ },
1798
+ };
1799
+ };