@opengsd/gsd-core 1.3.0-rc.1 → 1.3.1

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 (521) hide show
  1. package/README.ja-JP.md +44 -790
  2. package/README.ko-KR.md +41 -778
  3. package/README.md +39 -180
  4. package/README.pt-BR.md +45 -413
  5. package/README.zh-CN.md +43 -761
  6. package/agents/gsd-ai-researcher.md +1 -1
  7. package/agents/gsd-debugger.md +8 -8
  8. package/agents/gsd-domain-researcher.md +14 -1
  9. package/agents/gsd-eval-auditor.md +1 -1
  10. package/agents/gsd-eval-planner.md +1 -1
  11. package/agents/gsd-executor.md +21 -8
  12. package/agents/gsd-framework-selector.md +1 -1
  13. package/agents/gsd-intel-updater.md +4 -4
  14. package/agents/gsd-phase-researcher.md +18 -5
  15. package/agents/gsd-plan-checker.md +3 -3
  16. package/agents/gsd-planner.md +36 -23
  17. package/agents/gsd-project-researcher.md +13 -0
  18. package/agents/gsd-research-synthesizer.md +6 -2
  19. package/agents/gsd-roadmapper.md +6 -6
  20. package/agents/gsd-ui-researcher.md +15 -2
  21. package/agents/gsd-user-profiler.md +2 -2
  22. package/agents/gsd-verifier.md +7 -7
  23. package/bin/install.js +169 -59
  24. package/commands/gsd/add-tests.md +1 -1
  25. package/commands/gsd/ai-integration-phase.md +3 -3
  26. package/commands/gsd/audit-fix.md +1 -1
  27. package/commands/gsd/audit-milestone.md +1 -1
  28. package/commands/gsd/audit-uat.md +1 -1
  29. package/commands/gsd/autonomous.md +2 -2
  30. package/commands/gsd/capture.md +6 -6
  31. package/commands/gsd/cleanup.md +1 -1
  32. package/commands/gsd/code-review.md +1 -1
  33. package/commands/gsd/complete-milestone.md +2 -2
  34. package/commands/gsd/config.md +3 -3
  35. package/commands/gsd/debug.md +1 -1
  36. package/commands/gsd/discuss-phase.md +5 -4
  37. package/commands/gsd/docs-update.md +1 -1
  38. package/commands/gsd/eval-review.md +2 -2
  39. package/commands/gsd/execute-phase.md +2 -2
  40. package/commands/gsd/explore.md +1 -1
  41. package/commands/gsd/extract-learnings.md +2 -2
  42. package/commands/gsd/fast.md +1 -1
  43. package/commands/gsd/forensics.md +1 -1
  44. package/commands/gsd/graphify.md +9 -9
  45. package/commands/gsd/health.md +1 -1
  46. package/commands/gsd/help.md +2 -2
  47. package/commands/gsd/import.md +5 -5
  48. package/commands/gsd/inbox.md +1 -1
  49. package/commands/gsd/ingest-docs.md +4 -4
  50. package/commands/gsd/manager.md +3 -3
  51. package/commands/gsd/map-codebase.md +1 -1
  52. package/commands/gsd/milestone-summary.md +1 -1
  53. package/commands/gsd/mvp-phase.md +4 -4
  54. package/commands/gsd/new-milestone.md +5 -5
  55. package/commands/gsd/new-project.md +5 -5
  56. package/commands/gsd/pause-work.md +2 -2
  57. package/commands/gsd/phase.md +4 -4
  58. package/commands/gsd/plan-phase.md +2 -2
  59. package/commands/gsd/plan-review-convergence.md +4 -4
  60. package/commands/gsd/pr-branch.md +1 -1
  61. package/commands/gsd/profile-user.md +2 -2
  62. package/commands/gsd/progress.md +4 -4
  63. package/commands/gsd/quick.md +1 -1
  64. package/commands/gsd/resume-work.md +1 -1
  65. package/commands/gsd/review.md +1 -1
  66. package/commands/gsd/secure-phase.md +1 -1
  67. package/commands/gsd/settings.md +1 -1
  68. package/commands/gsd/ship.md +2 -2
  69. package/commands/gsd/sketch.md +7 -7
  70. package/commands/gsd/spec-phase.md +2 -2
  71. package/commands/gsd/spike.md +3 -3
  72. package/commands/gsd/stats.md +1 -1
  73. package/commands/gsd/surface.md +4 -4
  74. package/commands/gsd/thread.md +1 -1
  75. package/commands/gsd/ui-phase.md +2 -2
  76. package/commands/gsd/ui-review.md +2 -2
  77. package/commands/gsd/ultraplan-phase.md +2 -2
  78. package/commands/gsd/undo.md +3 -3
  79. package/commands/gsd/update.md +3 -3
  80. package/commands/gsd/validate-phase.md +1 -1
  81. package/commands/gsd/verify-work.md +2 -2
  82. package/commands/gsd/workspace.md +4 -4
  83. package/{get-shit-done → gsd-core}/bin/check-latest-version.cjs +1 -1
  84. package/{get-shit-done → gsd-core}/bin/gsd-tools.cjs +13 -3
  85. package/gsd-core/bin/lib/active-workstream-store.cjs +291 -0
  86. package/gsd-core/bin/lib/adr-parser.cjs +399 -0
  87. package/gsd-core/bin/lib/agent-command-router.cjs +68 -0
  88. package/gsd-core/bin/lib/artifacts.cjs +51 -0
  89. package/gsd-core/bin/lib/audit.cjs +743 -0
  90. package/gsd-core/bin/lib/check-command-router.cjs +342 -0
  91. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +81 -0
  92. package/gsd-core/bin/lib/clock.cjs +95 -0
  93. package/gsd-core/bin/lib/clusters.cjs +128 -0
  94. package/gsd-core/bin/lib/code-review-flags.cjs +59 -0
  95. package/gsd-core/bin/lib/command-aliases.cjs +809 -0
  96. package/gsd-core/bin/lib/command-arg-projection.cjs +55 -0
  97. package/gsd-core/bin/lib/command-routing-hub.cjs +300 -0
  98. package/gsd-core/bin/lib/commands.cjs +1202 -0
  99. package/gsd-core/bin/lib/config-schema.cjs +29 -0
  100. package/{get-shit-done → gsd-core}/bin/lib/config-types.cjs +1 -1
  101. package/gsd-core/bin/lib/config.cjs +714 -0
  102. package/{get-shit-done → gsd-core}/bin/lib/configuration.cjs +76 -85
  103. package/gsd-core/bin/lib/context-utilization.cjs +48 -0
  104. package/gsd-core/bin/lib/core.cjs +2007 -0
  105. package/{get-shit-done → gsd-core}/bin/lib/decisions.cjs +9 -7
  106. package/gsd-core/bin/lib/docs.cjs +252 -0
  107. package/gsd-core/bin/lib/drift.cjs +364 -0
  108. package/gsd-core/bin/lib/fallow-runner.cjs +115 -0
  109. package/gsd-core/bin/lib/frontmatter.cjs +442 -0
  110. package/gsd-core/bin/lib/gap-checker.cjs +257 -0
  111. package/gsd-core/bin/lib/graphify.cjs +496 -0
  112. package/gsd-core/bin/lib/gsd2-import.cjs +456 -0
  113. package/gsd-core/bin/lib/init-command-router.cjs +62 -0
  114. package/gsd-core/bin/lib/init.cjs +1799 -0
  115. package/gsd-core/bin/lib/install-profiles.cjs +526 -0
  116. package/gsd-core/bin/lib/installer-migration-authoring.cjs +122 -0
  117. package/gsd-core/bin/lib/installer-migration-report.cjs +347 -0
  118. package/gsd-core/bin/lib/installer-migrations/000-first-time-baseline.cjs +218 -0
  119. package/gsd-core/bin/lib/installer-migrations/001-legacy-orphan-files.cjs +48 -0
  120. package/gsd-core/bin/lib/installer-migrations/002-codex-legacy-hooks-json.cjs +94 -0
  121. package/gsd-core/bin/lib/installer-migrations/003-rename-get-shit-done-to-gsd-core.cjs +108 -0
  122. package/gsd-core/bin/lib/installer-migrations.cjs +823 -0
  123. package/gsd-core/bin/lib/intel.cjs +607 -0
  124. package/gsd-core/bin/lib/learnings.cjs +270 -0
  125. package/gsd-core/bin/lib/legacy-cleanup.cjs +253 -0
  126. package/gsd-core/bin/lib/milestone.cjs +373 -0
  127. package/gsd-core/bin/lib/model-catalog.cjs +154 -0
  128. package/gsd-core/bin/lib/model-profiles.cjs +24 -0
  129. package/gsd-core/bin/lib/observability/event.cjs +51 -0
  130. package/gsd-core/bin/lib/observability/logger.cjs +146 -0
  131. package/{get-shit-done → gsd-core}/bin/lib/observability/redaction.cjs +19 -21
  132. package/{get-shit-done → gsd-core}/bin/lib/package-identity.cjs +5 -1
  133. package/gsd-core/bin/lib/phase-command-router.cjs +189 -0
  134. package/gsd-core/bin/lib/phase-lifecycle.cjs +74 -0
  135. package/gsd-core/bin/lib/phase.cjs +1307 -0
  136. package/gsd-core/bin/lib/phases-command-router.cjs +43 -0
  137. package/{get-shit-done → gsd-core}/bin/lib/plan-scan.cjs +18 -24
  138. package/gsd-core/bin/lib/planning-workspace.cjs +245 -0
  139. package/gsd-core/bin/lib/profile-output.cjs +1120 -0
  140. package/gsd-core/bin/lib/profile-pipeline.cjs +517 -0
  141. package/gsd-core/bin/lib/project-root.cjs +119 -0
  142. package/gsd-core/bin/lib/prompt-budget.cjs +305 -0
  143. package/gsd-core/bin/lib/review-reviewer-selection.cjs +121 -0
  144. package/gsd-core/bin/lib/roadmap-command-router.cjs +166 -0
  145. package/gsd-core/bin/lib/roadmap-upgrade.cjs +491 -0
  146. package/gsd-core/bin/lib/roadmap.cjs +600 -0
  147. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +254 -0
  148. package/gsd-core/bin/lib/runtime-homes.cjs +169 -0
  149. package/gsd-core/bin/lib/runtime-name-policy.cjs +96 -0
  150. package/gsd-core/bin/lib/runtime-slash.cjs +119 -0
  151. package/{get-shit-done → gsd-core}/bin/lib/schema-detect.cjs +20 -26
  152. package/gsd-core/bin/lib/secrets.cjs +34 -0
  153. package/gsd-core/bin/lib/security.cjs +407 -0
  154. package/{get-shit-done → gsd-core}/bin/lib/semver-compare.cjs +1 -1
  155. package/gsd-core/bin/lib/shell-command-projection.cjs +524 -0
  156. package/gsd-core/bin/lib/state-command-router.cjs +160 -0
  157. package/{get-shit-done → gsd-core}/bin/lib/state-document.cjs +78 -78
  158. package/gsd-core/bin/lib/state.cjs +2010 -0
  159. package/gsd-core/bin/lib/surface.cjs +406 -0
  160. package/gsd-core/bin/lib/task-command-router.cjs +85 -0
  161. package/gsd-core/bin/lib/template.cjs +237 -0
  162. package/gsd-core/bin/lib/uat.cjs +297 -0
  163. package/gsd-core/bin/lib/ui-safety-gate.cjs +98 -0
  164. package/gsd-core/bin/lib/update-context.cjs +218 -0
  165. package/gsd-core/bin/lib/validate-command-router.cjs +91 -0
  166. package/gsd-core/bin/lib/validate.cjs +112 -0
  167. package/gsd-core/bin/lib/verify-command-router.cjs +44 -0
  168. package/gsd-core/bin/lib/verify.cjs +1451 -0
  169. package/{get-shit-done → gsd-core}/bin/lib/workstream-inventory-builder.cjs +19 -12
  170. package/gsd-core/bin/lib/workstream-inventory.cjs +147 -0
  171. package/{get-shit-done → gsd-core}/bin/lib/workstream-name-policy.cjs +28 -31
  172. package/gsd-core/bin/lib/workstream.cjs +380 -0
  173. package/gsd-core/bin/lib/worktree-safety.cjs +912 -0
  174. package/{get-shit-done → gsd-core}/bin/shared/config-schema.manifest.json +5 -0
  175. package/{get-shit-done → gsd-core}/references/artifact-types.md +1 -1
  176. package/{get-shit-done → gsd-core}/references/few-shot-examples/plan-checker.md +2 -2
  177. package/{get-shit-done → gsd-core}/references/few-shot-examples/verifier.md +12 -12
  178. package/{get-shit-done → gsd-core}/references/model-profile-resolution.md +1 -1
  179. package/{get-shit-done → gsd-core}/references/mvp-concepts.md +1 -1
  180. package/{get-shit-done → gsd-core}/references/planner-graphify-auto-update.md +2 -2
  181. package/{get-shit-done → gsd-core}/references/planner-mvp-mode.md +1 -1
  182. package/{get-shit-done → gsd-core}/references/verification-patterns.md +1 -1
  183. package/gsd-core/references/worktree-branch-check.md +38 -0
  184. package/{get-shit-done → gsd-core}/references/worktree-path-safety.md +4 -26
  185. package/{get-shit-done → gsd-core}/templates/README.md +2 -2
  186. package/{get-shit-done → gsd-core}/templates/codebase/structure.md +10 -10
  187. package/{get-shit-done → gsd-core}/templates/copilot-instructions.md +1 -1
  188. package/{get-shit-done → gsd-core}/templates/phase-prompt.md +10 -10
  189. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -0
  190. package/{get-shit-done → gsd-core}/workflows/add-backlog.md +1 -1
  191. package/{get-shit-done → gsd-core}/workflows/add-phase.md +1 -1
  192. package/{get-shit-done → gsd-core}/workflows/add-tests.md +1 -1
  193. package/{get-shit-done → gsd-core}/workflows/add-todo.md +1 -1
  194. package/{get-shit-done → gsd-core}/workflows/ai-integration-phase.md +4 -4
  195. package/{get-shit-done → gsd-core}/workflows/audit-fix.md +1 -1
  196. package/{get-shit-done → gsd-core}/workflows/audit-milestone.md +1 -1
  197. package/{get-shit-done → gsd-core}/workflows/audit-uat.md +1 -1
  198. package/{get-shit-done → gsd-core}/workflows/autonomous.md +4 -4
  199. package/{get-shit-done → gsd-core}/workflows/check-todos.md +1 -1
  200. package/{get-shit-done → gsd-core}/workflows/cleanup.md +1 -1
  201. package/{get-shit-done → gsd-core}/workflows/code-review-fix.md +1 -1
  202. package/{get-shit-done → gsd-core}/workflows/code-review.md +4 -4
  203. package/{get-shit-done → gsd-core}/workflows/complete-milestone.md +2 -2
  204. package/{get-shit-done → gsd-core}/workflows/debug.md +1 -1
  205. package/{get-shit-done → gsd-core}/workflows/diagnose-issues.md +4 -2
  206. package/{get-shit-done → gsd-core}/workflows/discovery-phase.md +5 -5
  207. package/{get-shit-done → gsd-core}/workflows/discuss-phase/modes/advisor.md +4 -4
  208. package/{get-shit-done → gsd-core}/workflows/discuss-phase/modes/auto.md +1 -1
  209. package/{get-shit-done → gsd-core}/workflows/discuss-phase/modes/chain.md +1 -1
  210. package/{get-shit-done → gsd-core}/workflows/discuss-phase/modes/power.md +1 -1
  211. package/{get-shit-done → gsd-core}/workflows/discuss-phase-assumptions.md +2 -2
  212. package/{get-shit-done → gsd-core}/workflows/discuss-phase.md +6 -6
  213. package/{get-shit-done → gsd-core}/workflows/do.md +1 -1
  214. package/{get-shit-done → gsd-core}/workflows/docs-update.md +1 -1
  215. package/{get-shit-done → gsd-core}/workflows/edit-phase.md +1 -1
  216. package/{get-shit-done → gsd-core}/workflows/eval-review.md +2 -2
  217. package/{get-shit-done → gsd-core}/workflows/execute-phase/steps/codebase-drift-gate.md +15 -2
  218. package/{get-shit-done → gsd-core}/workflows/execute-phase/steps/post-merge-gate.md +1 -1
  219. package/{get-shit-done → gsd-core}/workflows/execute-phase.md +68 -46
  220. package/{get-shit-done → gsd-core}/workflows/execute-plan.md +8 -8
  221. package/{get-shit-done → gsd-core}/workflows/explore.md +3 -3
  222. package/{get-shit-done → gsd-core}/workflows/extract-learnings.md +1 -1
  223. package/{get-shit-done → gsd-core}/workflows/forensics.md +1 -1
  224. package/{get-shit-done → gsd-core}/workflows/graduation.md +1 -1
  225. package/{get-shit-done → gsd-core}/workflows/health.md +1 -1
  226. package/{get-shit-done → gsd-core}/workflows/help/modes/full.md +1 -1
  227. package/{get-shit-done → gsd-core}/workflows/import.md +1 -1
  228. package/{get-shit-done → gsd-core}/workflows/ingest-docs.md +3 -3
  229. package/{get-shit-done → gsd-core}/workflows/insert-phase.md +1 -1
  230. package/{get-shit-done → gsd-core}/workflows/list-workspaces.md +1 -1
  231. package/{get-shit-done → gsd-core}/workflows/manager.md +1 -1
  232. package/{get-shit-done → gsd-core}/workflows/map-codebase.md +1 -1
  233. package/{get-shit-done → gsd-core}/workflows/milestone-summary.md +1 -1
  234. package/{get-shit-done → gsd-core}/workflows/mvp-phase.md +7 -7
  235. package/{get-shit-done → gsd-core}/workflows/new-milestone.md +3 -3
  236. package/{get-shit-done → gsd-core}/workflows/new-project.md +6 -6
  237. package/{get-shit-done → gsd-core}/workflows/new-workspace.md +1 -1
  238. package/{get-shit-done → gsd-core}/workflows/next.md +1 -1
  239. package/{get-shit-done → gsd-core}/workflows/pause-work.md +1 -1
  240. package/{get-shit-done → gsd-core}/workflows/plan-milestone-gaps.md +1 -1
  241. package/{get-shit-done → gsd-core}/workflows/plan-phase.md +15 -15
  242. package/{get-shit-done → gsd-core}/workflows/plan-review-convergence.md +7 -7
  243. package/{get-shit-done → gsd-core}/workflows/plant-seed.md +1 -1
  244. package/{get-shit-done → gsd-core}/workflows/profile-user.md +11 -11
  245. package/{get-shit-done → gsd-core}/workflows/progress.md +1 -1
  246. package/{get-shit-done → gsd-core}/workflows/quick.md +2 -26
  247. package/{get-shit-done → gsd-core}/workflows/reapply-patches.md +2 -2
  248. package/{get-shit-done → gsd-core}/workflows/remove-phase.md +1 -1
  249. package/{get-shit-done → gsd-core}/workflows/remove-workspace.md +1 -1
  250. package/{get-shit-done → gsd-core}/workflows/resume-project.md +2 -2
  251. package/{get-shit-done → gsd-core}/workflows/review.md +1 -1
  252. package/{get-shit-done → gsd-core}/workflows/scan.md +1 -1
  253. package/{get-shit-done → gsd-core}/workflows/secure-phase.md +3 -3
  254. package/{get-shit-done → gsd-core}/workflows/settings-advanced.md +1 -1
  255. package/{get-shit-done → gsd-core}/workflows/settings-integrations.md +2 -2
  256. package/{get-shit-done → gsd-core}/workflows/settings.md +1 -1
  257. package/{get-shit-done → gsd-core}/workflows/ship.md +56 -4
  258. package/{get-shit-done → gsd-core}/workflows/sketch-wrap-up.md +1 -1
  259. package/{get-shit-done → gsd-core}/workflows/sketch.md +5 -5
  260. package/{get-shit-done → gsd-core}/workflows/spec-phase.md +2 -2
  261. package/{get-shit-done → gsd-core}/workflows/spike-wrap-up.md +1 -1
  262. package/{get-shit-done → gsd-core}/workflows/spike.md +1 -1
  263. package/{get-shit-done → gsd-core}/workflows/stats.md +1 -1
  264. package/{get-shit-done → gsd-core}/workflows/sync-skills.md +3 -3
  265. package/{get-shit-done → gsd-core}/workflows/thread.md +1 -1
  266. package/{get-shit-done → gsd-core}/workflows/transition.md +2 -2
  267. package/{get-shit-done → gsd-core}/workflows/ui-phase.md +3 -3
  268. package/{get-shit-done → gsd-core}/workflows/ui-review.md +2 -2
  269. package/{get-shit-done → gsd-core}/workflows/ultraplan-phase.md +1 -1
  270. package/{get-shit-done → gsd-core}/workflows/undo.md +2 -2
  271. package/{get-shit-done → gsd-core}/workflows/update.md +41 -16
  272. package/{get-shit-done → gsd-core}/workflows/validate-phase.md +3 -3
  273. package/{get-shit-done → gsd-core}/workflows/verify-phase.md +4 -4
  274. package/{get-shit-done → gsd-core}/workflows/verify-work.md +5 -5
  275. package/hooks/dist/gsd-check-update-worker.js +16 -3
  276. package/hooks/dist/gsd-check-update.js +7 -5
  277. package/hooks/dist/gsd-context-monitor.js +2 -2
  278. package/hooks/dist/gsd-statusline.js +39 -21
  279. package/hooks/dist/gsd-update-banner.js +6 -2
  280. package/hooks/dist/managed-hooks-registry.cjs +35 -0
  281. package/hooks/gsd-check-update-worker.js +16 -3
  282. package/hooks/gsd-check-update.js +7 -5
  283. package/hooks/gsd-context-monitor.js +2 -2
  284. package/hooks/gsd-statusline.js +39 -21
  285. package/hooks/gsd-update-banner.js +6 -2
  286. package/package.json +9 -6
  287. package/scripts/affected-tests-lib.cjs +1 -1
  288. package/scripts/base64-scan.sh +12 -0
  289. package/scripts/build-hooks.js +4 -1
  290. package/scripts/changeset/cli.cjs +2 -2
  291. package/scripts/changeset/github-release-notes.cjs +1 -1
  292. package/scripts/changeset/lint.cjs +1 -1
  293. package/scripts/check-alias-drift.cjs +8 -8
  294. package/scripts/check-env.cjs +5 -1
  295. package/scripts/ci-test-scope.cjs +25 -10
  296. package/scripts/command-contract-helpers.cjs +1 -1
  297. package/scripts/fix-slash-commands.cjs +5 -5
  298. package/scripts/gen-inventory-manifest.cjs +3 -3
  299. package/scripts/generate-package-identity.cjs +28 -7
  300. package/scripts/lib/allowlist-ratchet.cjs +136 -0
  301. package/scripts/lint-command-contract.cjs +1 -1
  302. package/scripts/lint-legacy-dir-name.cjs +156 -0
  303. package/scripts/lint-package-identity-drift.cjs +8 -8
  304. package/scripts/lint-pr-check-project-dir.cjs +0 -1
  305. package/scripts/lint-shell-command-projection-drift.cjs +1 -1
  306. package/scripts/lint-skill-deps.cjs +1 -1
  307. package/scripts/lint-test-file-count.allowlist.json +129 -32
  308. package/scripts/lint-test-file-count.cjs +85 -30
  309. package/scripts/mutation-matrix.cjs +219 -0
  310. package/scripts/prompt-injection-scan.sh +5 -1
  311. package/scripts/release-notes/format-github-release-notes.cjs +256 -0
  312. package/scripts/release-tarball-smoke.cjs +9 -9
  313. package/scripts/run-tests.cjs +11 -3
  314. package/scripts/strip-prose-atrefs.cjs +2 -2
  315. package/scripts/sync-runtime-launcher.cjs +2 -2
  316. package/scripts/verify-npm-publish.cjs +252 -0
  317. package/get-shit-done/bin/lib/active-workstream-store.cjs +0 -302
  318. package/get-shit-done/bin/lib/adr-parser.cjs +0 -394
  319. package/get-shit-done/bin/lib/agent-command-router.cjs +0 -65
  320. package/get-shit-done/bin/lib/artifacts.cjs +0 -53
  321. package/get-shit-done/bin/lib/audit.cjs +0 -755
  322. package/get-shit-done/bin/lib/check-command-router.cjs +0 -333
  323. package/get-shit-done/bin/lib/cjs-command-router-adapter.cjs +0 -118
  324. package/get-shit-done/bin/lib/clock.cjs +0 -96
  325. package/get-shit-done/bin/lib/clusters.cjs +0 -135
  326. package/get-shit-done/bin/lib/code-review-flags.cjs +0 -74
  327. package/get-shit-done/bin/lib/command-aliases.cjs +0 -831
  328. package/get-shit-done/bin/lib/command-arg-projection.cjs +0 -62
  329. package/get-shit-done/bin/lib/command-routing-hub.cjs +0 -388
  330. package/get-shit-done/bin/lib/commands.cjs +0 -1292
  331. package/get-shit-done/bin/lib/config-schema.cjs +0 -31
  332. package/get-shit-done/bin/lib/config.cjs +0 -728
  333. package/get-shit-done/bin/lib/context-utilization.cjs +0 -47
  334. package/get-shit-done/bin/lib/core.cjs +0 -2513
  335. package/get-shit-done/bin/lib/docs.cjs +0 -270
  336. package/get-shit-done/bin/lib/drift.cjs +0 -388
  337. package/get-shit-done/bin/lib/fallow-runner.cjs +0 -109
  338. package/get-shit-done/bin/lib/frontmatter.cjs +0 -389
  339. package/get-shit-done/bin/lib/gap-checker.cjs +0 -254
  340. package/get-shit-done/bin/lib/graphify.cjs +0 -592
  341. package/get-shit-done/bin/lib/gsd2-import.cjs +0 -514
  342. package/get-shit-done/bin/lib/init-command-router.cjs +0 -58
  343. package/get-shit-done/bin/lib/init.cjs +0 -2112
  344. package/get-shit-done/bin/lib/install-profiles.cjs +0 -603
  345. package/get-shit-done/bin/lib/installer-migration-authoring.cjs +0 -117
  346. package/get-shit-done/bin/lib/installer-migration-report.cjs +0 -355
  347. package/get-shit-done/bin/lib/installer-migrations/000-first-time-baseline.cjs +0 -220
  348. package/get-shit-done/bin/lib/installer-migrations/001-legacy-orphan-files.cjs +0 -41
  349. package/get-shit-done/bin/lib/installer-migrations/002-codex-legacy-hooks-json.cjs +0 -80
  350. package/get-shit-done/bin/lib/installer-migrations.cjs +0 -778
  351. package/get-shit-done/bin/lib/intel.cjs +0 -708
  352. package/get-shit-done/bin/lib/learnings.cjs +0 -421
  353. package/get-shit-done/bin/lib/milestone.cjs +0 -372
  354. package/get-shit-done/bin/lib/model-catalog.cjs +0 -229
  355. package/get-shit-done/bin/lib/model-profiles.cjs +0 -31
  356. package/get-shit-done/bin/lib/observability/event.cjs +0 -82
  357. package/get-shit-done/bin/lib/observability/logger.cjs +0 -174
  358. package/get-shit-done/bin/lib/phase-command-router.cjs +0 -191
  359. package/get-shit-done/bin/lib/phase-lifecycle.cjs +0 -79
  360. package/get-shit-done/bin/lib/phase.cjs +0 -1607
  361. package/get-shit-done/bin/lib/phases-command-router.cjs +0 -39
  362. package/get-shit-done/bin/lib/planning-workspace.cjs +0 -238
  363. package/get-shit-done/bin/lib/profile-output.cjs +0 -1141
  364. package/get-shit-done/bin/lib/profile-pipeline.cjs +0 -539
  365. package/get-shit-done/bin/lib/project-root.cjs +0 -112
  366. package/get-shit-done/bin/lib/prompt-budget.cjs +0 -399
  367. package/get-shit-done/bin/lib/review-reviewer-selection.cjs +0 -126
  368. package/get-shit-done/bin/lib/roadmap-command-router.cjs +0 -162
  369. package/get-shit-done/bin/lib/roadmap-upgrade.cjs +0 -569
  370. package/get-shit-done/bin/lib/roadmap.cjs +0 -651
  371. package/get-shit-done/bin/lib/runtime-artifact-layout.cjs +0 -301
  372. package/get-shit-done/bin/lib/runtime-homes.cjs +0 -222
  373. package/get-shit-done/bin/lib/runtime-name-policy.cjs +0 -83
  374. package/get-shit-done/bin/lib/runtime-slash.cjs +0 -112
  375. package/get-shit-done/bin/lib/secrets.cjs +0 -32
  376. package/get-shit-done/bin/lib/security.cjs +0 -600
  377. package/get-shit-done/bin/lib/shell-command-projection.cjs +0 -535
  378. package/get-shit-done/bin/lib/state-command-router.cjs +0 -252
  379. package/get-shit-done/bin/lib/state.cjs +0 -2073
  380. package/get-shit-done/bin/lib/surface.cjs +0 -470
  381. package/get-shit-done/bin/lib/task-command-router.cjs +0 -81
  382. package/get-shit-done/bin/lib/template.cjs +0 -228
  383. package/get-shit-done/bin/lib/uat.cjs +0 -289
  384. package/get-shit-done/bin/lib/ui-safety-gate.cjs +0 -111
  385. package/get-shit-done/bin/lib/update-context.cjs +0 -209
  386. package/get-shit-done/bin/lib/validate-command-router.cjs +0 -83
  387. package/get-shit-done/bin/lib/validate.cjs +0 -118
  388. package/get-shit-done/bin/lib/verify-command-router.cjs +0 -40
  389. package/get-shit-done/bin/lib/verify.cjs +0 -1614
  390. package/get-shit-done/bin/lib/workstream-inventory.cjs +0 -146
  391. package/get-shit-done/bin/lib/workstream.cjs +0 -389
  392. package/get-shit-done/bin/lib/worktree-safety.cjs +0 -985
  393. package/get-shit-done/workflows/_runtime-launcher.snippet.sh +0 -1
  394. package/scripts/lint-no-source-grep-extras.cjs +0 -81
  395. package/scripts/lint-no-source-grep.cjs +0 -174
  396. /package/{get-shit-done → gsd-core}/bin/shared/config-defaults.manifest.json +0 -0
  397. /package/{get-shit-done → gsd-core}/bin/shared/model-catalog.json +0 -0
  398. /package/{get-shit-done → gsd-core}/bin/shared/runtime-aliases.manifest.json +0 -0
  399. /package/{get-shit-done → gsd-core}/bin/verify-reapply-patches.cjs +0 -0
  400. /package/{get-shit-done → gsd-core}/contexts/dev.md +0 -0
  401. /package/{get-shit-done → gsd-core}/contexts/research.md +0 -0
  402. /package/{get-shit-done → gsd-core}/contexts/review.md +0 -0
  403. /package/{get-shit-done → gsd-core}/references/agent-contracts.md +0 -0
  404. /package/{get-shit-done → gsd-core}/references/ai-evals.md +0 -0
  405. /package/{get-shit-done → gsd-core}/references/ai-frameworks.md +0 -0
  406. /package/{get-shit-done → gsd-core}/references/autonomous-smart-discuss.md +0 -0
  407. /package/{get-shit-done → gsd-core}/references/checkpoints.md +0 -0
  408. /package/{get-shit-done → gsd-core}/references/common-bug-patterns.md +0 -0
  409. /package/{get-shit-done → gsd-core}/references/context-budget.md +0 -0
  410. /package/{get-shit-done → gsd-core}/references/continuation-format.md +0 -0
  411. /package/{get-shit-done → gsd-core}/references/debugger-philosophy.md +0 -0
  412. /package/{get-shit-done → gsd-core}/references/decimal-phase-calculation.md +0 -0
  413. /package/{get-shit-done → gsd-core}/references/doc-conflict-engine.md +0 -0
  414. /package/{get-shit-done → gsd-core}/references/domain-probes.md +0 -0
  415. /package/{get-shit-done → gsd-core}/references/execute-mvp-tdd.md +0 -0
  416. /package/{get-shit-done → gsd-core}/references/executor-examples.md +0 -0
  417. /package/{get-shit-done → gsd-core}/references/gate-prompts.md +0 -0
  418. /package/{get-shit-done → gsd-core}/references/gates.md +0 -0
  419. /package/{get-shit-done → gsd-core}/references/git-integration.md +0 -0
  420. /package/{get-shit-done → gsd-core}/references/git-planning-commit.md +0 -0
  421. /package/{get-shit-done → gsd-core}/references/ios-scaffold.md +0 -0
  422. /package/{get-shit-done → gsd-core}/references/mandatory-initial-read.md +0 -0
  423. /package/{get-shit-done → gsd-core}/references/model-profiles.md +0 -0
  424. /package/{get-shit-done → gsd-core}/references/phase-argument-parsing.md +0 -0
  425. /package/{get-shit-done → gsd-core}/references/planner-antipatterns.md +0 -0
  426. /package/{get-shit-done → gsd-core}/references/planner-chunked.md +0 -0
  427. /package/{get-shit-done → gsd-core}/references/planner-gap-closure.md +0 -0
  428. /package/{get-shit-done → gsd-core}/references/planner-human-verify-mode.md +0 -0
  429. /package/{get-shit-done → gsd-core}/references/planner-interface-context.md +0 -0
  430. /package/{get-shit-done → gsd-core}/references/planner-reviews.md +0 -0
  431. /package/{get-shit-done → gsd-core}/references/planner-revision.md +0 -0
  432. /package/{get-shit-done → gsd-core}/references/planner-source-audit.md +0 -0
  433. /package/{get-shit-done → gsd-core}/references/planning-config.md +0 -0
  434. /package/{get-shit-done → gsd-core}/references/project-skills-discovery.md +0 -0
  435. /package/{get-shit-done → gsd-core}/references/questioning.md +0 -0
  436. /package/{get-shit-done → gsd-core}/references/revision-loop.md +0 -0
  437. /package/{get-shit-done → gsd-core}/references/scout-codebase.md +0 -0
  438. /package/{get-shit-done → gsd-core}/references/skeleton-template.md +0 -0
  439. /package/{get-shit-done → gsd-core}/references/sketch-interactivity.md +0 -0
  440. /package/{get-shit-done → gsd-core}/references/sketch-theme-system.md +0 -0
  441. /package/{get-shit-done → gsd-core}/references/sketch-tooling.md +0 -0
  442. /package/{get-shit-done → gsd-core}/references/sketch-variant-patterns.md +0 -0
  443. /package/{get-shit-done → gsd-core}/references/spidr-splitting.md +0 -0
  444. /package/{get-shit-done → gsd-core}/references/tdd.md +0 -0
  445. /package/{get-shit-done → gsd-core}/references/thinking-models-debug.md +0 -0
  446. /package/{get-shit-done → gsd-core}/references/thinking-models-execution.md +0 -0
  447. /package/{get-shit-done → gsd-core}/references/thinking-models-planning.md +0 -0
  448. /package/{get-shit-done → gsd-core}/references/thinking-models-research.md +0 -0
  449. /package/{get-shit-done → gsd-core}/references/thinking-models-verification.md +0 -0
  450. /package/{get-shit-done → gsd-core}/references/thinking-partner.md +0 -0
  451. /package/{get-shit-done → gsd-core}/references/ui-brand.md +0 -0
  452. /package/{get-shit-done → gsd-core}/references/universal-anti-patterns.md +0 -0
  453. /package/{get-shit-done → gsd-core}/references/user-profiling.md +0 -0
  454. /package/{get-shit-done → gsd-core}/references/user-story-template.md +0 -0
  455. /package/{get-shit-done → gsd-core}/references/verification-overrides.md +0 -0
  456. /package/{get-shit-done → gsd-core}/references/verify-mvp-mode.md +0 -0
  457. /package/{get-shit-done → gsd-core}/references/workstream-flag.md +0 -0
  458. /package/{get-shit-done → gsd-core}/templates/AI-SPEC.md +0 -0
  459. /package/{get-shit-done → gsd-core}/templates/DEBUG.md +0 -0
  460. /package/{get-shit-done → gsd-core}/templates/SECURITY.md +0 -0
  461. /package/{get-shit-done → gsd-core}/templates/UAT.md +0 -0
  462. /package/{get-shit-done → gsd-core}/templates/UI-SPEC.md +0 -0
  463. /package/{get-shit-done → gsd-core}/templates/VALIDATION.md +0 -0
  464. /package/{get-shit-done → gsd-core}/templates/claude-md.md +0 -0
  465. /package/{get-shit-done → gsd-core}/templates/codebase/architecture.md +0 -0
  466. /package/{get-shit-done → gsd-core}/templates/codebase/concerns.md +0 -0
  467. /package/{get-shit-done → gsd-core}/templates/codebase/conventions.md +0 -0
  468. /package/{get-shit-done → gsd-core}/templates/codebase/integrations.md +0 -0
  469. /package/{get-shit-done → gsd-core}/templates/codebase/stack.md +0 -0
  470. /package/{get-shit-done → gsd-core}/templates/codebase/testing.md +0 -0
  471. /package/{get-shit-done → gsd-core}/templates/config.json +0 -0
  472. /package/{get-shit-done → gsd-core}/templates/context.md +0 -0
  473. /package/{get-shit-done → gsd-core}/templates/continue-here.md +0 -0
  474. /package/{get-shit-done → gsd-core}/templates/debug-subagent-prompt.md +0 -0
  475. /package/{get-shit-done → gsd-core}/templates/dev-preferences.md +0 -0
  476. /package/{get-shit-done → gsd-core}/templates/discovery.md +0 -0
  477. /package/{get-shit-done → gsd-core}/templates/discussion-log.md +0 -0
  478. /package/{get-shit-done → gsd-core}/templates/milestone-archive.md +0 -0
  479. /package/{get-shit-done → gsd-core}/templates/milestone.md +0 -0
  480. /package/{get-shit-done → gsd-core}/templates/planner-subagent-prompt.md +0 -0
  481. /package/{get-shit-done → gsd-core}/templates/project.md +0 -0
  482. /package/{get-shit-done → gsd-core}/templates/requirements.md +0 -0
  483. /package/{get-shit-done → gsd-core}/templates/research-project/ARCHITECTURE.md +0 -0
  484. /package/{get-shit-done → gsd-core}/templates/research-project/FEATURES.md +0 -0
  485. /package/{get-shit-done → gsd-core}/templates/research-project/PITFALLS.md +0 -0
  486. /package/{get-shit-done → gsd-core}/templates/research-project/STACK.md +0 -0
  487. /package/{get-shit-done → gsd-core}/templates/research-project/SUMMARY.md +0 -0
  488. /package/{get-shit-done → gsd-core}/templates/research.md +0 -0
  489. /package/{get-shit-done → gsd-core}/templates/retrospective.md +0 -0
  490. /package/{get-shit-done → gsd-core}/templates/roadmap.md +0 -0
  491. /package/{get-shit-done → gsd-core}/templates/spec.md +0 -0
  492. /package/{get-shit-done → gsd-core}/templates/state.md +0 -0
  493. /package/{get-shit-done → gsd-core}/templates/summary-complex.md +0 -0
  494. /package/{get-shit-done → gsd-core}/templates/summary-minimal.md +0 -0
  495. /package/{get-shit-done → gsd-core}/templates/summary-standard.md +0 -0
  496. /package/{get-shit-done → gsd-core}/templates/summary.md +0 -0
  497. /package/{get-shit-done → gsd-core}/templates/user-profile.md +0 -0
  498. /package/{get-shit-done → gsd-core}/templates/user-setup.md +0 -0
  499. /package/{get-shit-done → gsd-core}/templates/verification-report.md +0 -0
  500. /package/{get-shit-done → gsd-core}/workflows/analyze-dependencies.md +0 -0
  501. /package/{get-shit-done → gsd-core}/workflows/discuss-phase/modes/all.md +0 -0
  502. /package/{get-shit-done → gsd-core}/workflows/discuss-phase/modes/analyze.md +0 -0
  503. /package/{get-shit-done → gsd-core}/workflows/discuss-phase/modes/batch.md +0 -0
  504. /package/{get-shit-done → gsd-core}/workflows/discuss-phase/modes/default.md +0 -0
  505. /package/{get-shit-done → gsd-core}/workflows/discuss-phase/modes/text.md +0 -0
  506. /package/{get-shit-done → gsd-core}/workflows/discuss-phase/templates/checkpoint.json +0 -0
  507. /package/{get-shit-done → gsd-core}/workflows/discuss-phase/templates/context.md +0 -0
  508. /package/{get-shit-done → gsd-core}/workflows/discuss-phase/templates/discussion-log.md +0 -0
  509. /package/{get-shit-done → gsd-core}/workflows/discuss-phase-power.md +0 -0
  510. /package/{get-shit-done → gsd-core}/workflows/execute-phase/steps/per-plan-worktree-gate.md +0 -0
  511. /package/{get-shit-done → gsd-core}/workflows/fast.md +0 -0
  512. /package/{get-shit-done → gsd-core}/workflows/help/modes/brief.md +0 -0
  513. /package/{get-shit-done → gsd-core}/workflows/help/modes/default.md +0 -0
  514. /package/{get-shit-done → gsd-core}/workflows/help/modes/topic.md +0 -0
  515. /package/{get-shit-done → gsd-core}/workflows/help.md +0 -0
  516. /package/{get-shit-done → gsd-core}/workflows/inbox.md +0 -0
  517. /package/{get-shit-done → gsd-core}/workflows/list-phase-assumptions.md +0 -0
  518. /package/{get-shit-done → gsd-core}/workflows/node-repair.md +0 -0
  519. /package/{get-shit-done → gsd-core}/workflows/note.md +0 -0
  520. /package/{get-shit-done → gsd-core}/workflows/pr-branch.md +0 -0
  521. /package/{get-shit-done → gsd-core}/workflows/session-report.md +0 -0
@@ -0,0 +1,1202 @@
1
+ "use strict";
2
+ /**
3
+ * Commands — Standalone utility commands
4
+ *
5
+ * ADR-457 build-at-publish: the hand-written bin/lib/commands.cjs collapsed
6
+ * to a TypeScript source of truth. Behaviour is preserved byte-for-behaviour
7
+ * from the prior hand-written .cjs; only strict types are added.
8
+ */
9
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ const node_fs_1 = __importDefault(require("node:fs"));
13
+ const node_path_1 = __importDefault(require("node:path"));
14
+ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
15
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
16
+ const core = require("./core.cjs");
17
+ const { loadConfig, isGitIgnored, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier, stripShippedMilestones: _stripShippedMilestones, extractCurrentMilestone, toPosixPath, output, error, findPhaseInternal, extractOneLinerFromBody, getRoadmapPhaseInternal, extractPhaseToken, resolveGranularityInternal, } = core;
18
+ const model_catalog_cjs_1 = require("./model-catalog.cjs");
19
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
20
+ const planningWorkspace = require("./planning-workspace.cjs");
21
+ const { planningDir, planningPaths } = planningWorkspace;
22
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
23
+ const frontmatter = require("./frontmatter.cjs");
24
+ const { extractFrontmatter } = frontmatter;
25
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
26
+ const modelProfiles = require("./model-profiles.cjs");
27
+ const { MODEL_PROFILES, VALID_PHASE_TYPES } = modelProfiles;
28
+ const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
29
+ // ─── Phase Status ─────────────────────────────────────────────────────────────
30
+ /**
31
+ * Determine phase status by checking plan/summary counts AND verification state.
32
+ * Introduces "Executed" for phases with all summaries but no passing verification.
33
+ */
34
+ function determinePhaseStatus(plans, summaries, phaseDir, defaultPending) {
35
+ if (plans === 0)
36
+ return defaultPending;
37
+ if (summaries < plans && summaries > 0)
38
+ return 'In Progress';
39
+ if (summaries < plans)
40
+ return 'Planned';
41
+ // summaries >= plans — check verification
42
+ try {
43
+ const files = node_fs_1.default.readdirSync(phaseDir);
44
+ const verificationFile = files.find(f => f === 'VERIFICATION.md' || f.endsWith('-VERIFICATION.md'));
45
+ if (verificationFile) {
46
+ const content = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(phaseDir, verificationFile)) || '';
47
+ if (/status:\s*passed/i.test(content))
48
+ return 'Complete';
49
+ if (/status:\s*human_needed/i.test(content))
50
+ return 'Needs Review';
51
+ if (/status:\s*gaps_found/i.test(content))
52
+ return 'Executed';
53
+ // Verification exists but unrecognized status — treat as executed
54
+ return 'Executed';
55
+ }
56
+ }
57
+ catch { /* directory read failed — fall through */ }
58
+ // No verification file — executed but not verified
59
+ return 'Executed';
60
+ }
61
+ function cmdGenerateSlug(text, raw) {
62
+ if (!text) {
63
+ error('text required for slug generation');
64
+ }
65
+ const slug = text
66
+ .toLowerCase()
67
+ .replace(/[^a-z0-9]+/g, '-')
68
+ .replace(/^-+|-+$/g, '')
69
+ .substring(0, 60);
70
+ const result = { slug };
71
+ output(result, raw, slug);
72
+ }
73
+ function cmdCurrentTimestamp(format, raw) {
74
+ const now = new Date();
75
+ let result;
76
+ switch (format) {
77
+ case 'date':
78
+ result = now.toISOString().split('T')[0];
79
+ break;
80
+ case 'filename':
81
+ result = now.toISOString().replace(/:/g, '-').replace(/\..+/, '');
82
+ break;
83
+ case 'full':
84
+ default:
85
+ result = now.toISOString();
86
+ break;
87
+ }
88
+ output({ timestamp: result }, raw, result);
89
+ }
90
+ function cmdListTodos(cwd, area, raw) {
91
+ const pendingDir = node_path_1.default.join(planningDir(cwd), 'todos', 'pending');
92
+ let count = 0;
93
+ const todos = [];
94
+ try {
95
+ const files = node_fs_1.default.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
96
+ for (const file of files) {
97
+ const content = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(pendingDir, file));
98
+ if (content === null)
99
+ continue;
100
+ const createdMatch = content.match(/^created:\s*(.+)$/m);
101
+ const titleMatch = content.match(/^title:\s*(.+)$/m);
102
+ const areaMatch = content.match(/^area:\s*(.+)$/m);
103
+ const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
104
+ // Apply area filter if specified
105
+ if (area && todoArea !== area)
106
+ continue;
107
+ count++;
108
+ todos.push({
109
+ file,
110
+ created: createdMatch ? createdMatch[1].trim() : 'unknown',
111
+ title: titleMatch ? titleMatch[1].trim() : 'Untitled',
112
+ area: todoArea,
113
+ path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(pendingDir, file))),
114
+ });
115
+ }
116
+ }
117
+ catch { /* intentionally empty */ }
118
+ const result = { count, todos };
119
+ output(result, raw, count.toString());
120
+ }
121
+ function cmdVerifyPathExists(cwd, targetPath, raw) {
122
+ if (!targetPath) {
123
+ error('path required for verification');
124
+ }
125
+ // Reject null bytes and validate path does not contain traversal attempts
126
+ if (targetPath.includes('\0')) {
127
+ error('path contains null bytes');
128
+ }
129
+ const fullPath = node_path_1.default.isAbsolute(targetPath) ? targetPath : node_path_1.default.join(cwd, targetPath);
130
+ try {
131
+ const stats = node_fs_1.default.statSync(fullPath);
132
+ const type = stats.isDirectory() ? 'directory' : stats.isFile() ? 'file' : 'other';
133
+ const result = { exists: true, type };
134
+ output(result, raw, 'true');
135
+ }
136
+ catch {
137
+ const result = { exists: false, type: null };
138
+ output(result, raw, 'false');
139
+ }
140
+ }
141
+ function cmdHistoryDigest(cwd, raw) {
142
+ const phasesDir = planningPaths(cwd).phases;
143
+ const digest = { phases: {}, decisions: [], tech_stack: new Set() };
144
+ // Collect all phase directories: archived + current
145
+ const allPhaseDirs = [];
146
+ // Add archived phases first (oldest milestones first)
147
+ const archived = getArchivedPhaseDirs(cwd);
148
+ for (const a of archived) {
149
+ allPhaseDirs.push({ name: a.name, fullPath: a.fullPath, milestone: a.milestone });
150
+ }
151
+ // Add current phases
152
+ if (node_fs_1.default.existsSync(phasesDir)) {
153
+ try {
154
+ const currentDirs = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true })
155
+ .filter(e => e.isDirectory())
156
+ .map(e => e.name)
157
+ .sort();
158
+ for (const dir of currentDirs) {
159
+ allPhaseDirs.push({ name: dir, fullPath: node_path_1.default.join(phasesDir, dir), milestone: null });
160
+ }
161
+ }
162
+ catch { /* intentionally empty */ }
163
+ }
164
+ if (allPhaseDirs.length === 0) {
165
+ digest.tech_stack = [];
166
+ output(digest, raw, undefined);
167
+ return;
168
+ }
169
+ try {
170
+ for (const { name: dir, fullPath: dirPath } of allPhaseDirs) {
171
+ const summaries = node_fs_1.default.readdirSync(dirPath).filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
172
+ for (const summary of summaries) {
173
+ const content = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(dirPath, summary));
174
+ if (content === null)
175
+ continue;
176
+ try {
177
+ const fm = extractFrontmatter(content);
178
+ const phaseNum = fm['phase'] || dir.split('-')[0];
179
+ if (!digest.phases[phaseNum]) {
180
+ digest.phases[phaseNum] = {
181
+ name: fm['name'] || dir.split('-').slice(1).join(' ') || 'Unknown',
182
+ provides: new Set(),
183
+ affects: new Set(),
184
+ patterns: new Set(),
185
+ };
186
+ }
187
+ // Merge provides
188
+ const depGraph = fm['dependency-graph'];
189
+ if (depGraph && depGraph['provides']) {
190
+ depGraph['provides'].forEach((p) => digest.phases[phaseNum].provides.add(p));
191
+ }
192
+ else if (fm['provides']) {
193
+ fm['provides'].forEach((p) => digest.phases[phaseNum].provides.add(p));
194
+ }
195
+ // Merge affects
196
+ if (depGraph && depGraph['affects']) {
197
+ depGraph['affects'].forEach((a) => digest.phases[phaseNum].affects.add(a));
198
+ }
199
+ // Merge patterns
200
+ if (fm['patterns-established']) {
201
+ fm['patterns-established'].forEach((p) => digest.phases[phaseNum].patterns.add(p));
202
+ }
203
+ // Merge decisions
204
+ if (fm['key-decisions']) {
205
+ fm['key-decisions'].forEach((d) => {
206
+ digest.decisions.push({ phase: phaseNum, decision: d });
207
+ });
208
+ }
209
+ // Merge tech stack
210
+ const techStack = fm['tech-stack'];
211
+ if (techStack && techStack['added']) {
212
+ techStack['added'].forEach((t) => digest.tech_stack.add(typeof t === 'string' ? t : t.name));
213
+ }
214
+ }
215
+ catch {
216
+ // Skip malformed summaries
217
+ }
218
+ }
219
+ }
220
+ // Convert Sets to Arrays for JSON output
221
+ Object.keys(digest.phases).forEach(p => {
222
+ digest.phases[p].provides = [...digest.phases[p].provides];
223
+ digest.phases[p].affects = [...digest.phases[p].affects];
224
+ digest.phases[p].patterns = [...digest.phases[p].patterns];
225
+ });
226
+ digest.tech_stack = [...digest.tech_stack];
227
+ output(digest, raw, undefined);
228
+ }
229
+ catch (e) {
230
+ error('Failed to generate history digest: ' + e.message);
231
+ }
232
+ }
233
+ function cmdResolveModel(cwd, agentType, raw) {
234
+ if (!agentType) {
235
+ error('agent-type required');
236
+ }
237
+ const config = loadConfig(cwd);
238
+ const profile = config['model_profile'] || 'balanced';
239
+ const model = resolveModelInternal(cwd, agentType);
240
+ const effort = resolveEffortInternal(cwd, agentType);
241
+ const agentModels = MODEL_PROFILES[agentType];
242
+ const result = agentModels
243
+ ? { model, profile, effort }
244
+ : { model, profile, effort, unknown_agent: true };
245
+ output(result, raw, model);
246
+ }
247
+ function cmdResolveGranularity(cwd, phaseType, raw) {
248
+ if (!phaseType) {
249
+ error('phase-type required');
250
+ }
251
+ const granularity = resolveGranularityInternal(cwd, phaseType);
252
+ const result = (VALID_PHASE_TYPES).has(phaseType)
253
+ ? { granularity, phase_type: phaseType }
254
+ : { granularity, phase_type: phaseType, unknown_phase_type: true };
255
+ output(result, raw, granularity);
256
+ }
257
+ /**
258
+ * #443 — Superset execution query: model + unified effort + fast_mode.
259
+ *
260
+ * Emits JSON:
261
+ * { model, profile, effort, effort_rendered, effort_param, effort_propagation,
262
+ * fast_mode, fast_mode_supported, [unknown_agent] }
263
+ *
264
+ * Flags: --effort <level>, --fast-mode <true|false>, --attempt <n>
265
+ */
266
+ function cmdResolveExecution(cwd, agentType, raw, opts) {
267
+ if (!agentType) {
268
+ error('agent-type required');
269
+ }
270
+ opts = opts || {};
271
+ const config = loadConfig(cwd);
272
+ const profile = config['model_profile'] || 'balanced';
273
+ const model = resolveModelInternal(cwd, agentType);
274
+ const effortOpts = {};
275
+ if (typeof opts.effortOverride === 'string')
276
+ effortOpts['override'] = opts.effortOverride;
277
+ const fastModeOpts = {};
278
+ if (typeof opts.fastModeOverride === 'boolean')
279
+ fastModeOpts['override'] = opts.fastModeOverride;
280
+ const effort = (opts.attempt !== undefined && opts.attempt !== null)
281
+ ? resolveEffortForTier(cwd, agentType, opts.attempt)
282
+ : resolveEffortInternal(cwd, agentType, effortOpts);
283
+ const fastMode = resolveFastModeInternal(cwd, agentType, fastModeOpts);
284
+ const runtime = config['runtime'] || 'claude';
285
+ const rendered = (0, model_catalog_cjs_1.renderEffortForRuntime)(runtime, effort);
286
+ const fastModeSupported = model_catalog_cjs_1.RUNTIMES_WITH_FAST_MODE.has(runtime);
287
+ const agentModels = MODEL_PROFILES[agentType];
288
+ const result = {
289
+ model,
290
+ profile,
291
+ effort,
292
+ effort_rendered: rendered.value,
293
+ effort_param: rendered.param,
294
+ effort_propagation: rendered.channel,
295
+ fast_mode: fastMode,
296
+ fast_mode_supported: fastModeSupported,
297
+ };
298
+ if (!agentModels)
299
+ result['unknown_agent'] = true;
300
+ output(result, raw, effort);
301
+ }
302
+ /**
303
+ * #488 — Replace or inject the `effort:` value in YAML frontmatter.
304
+ * Unlike injectEffortFrontmatter (install.js), this overwrites an existing value.
305
+ */
306
+ function setEffortFrontmatter(content, effortValue) {
307
+ const eol = /^---\r\n/.test(content) ? '\r\n' : '\n';
308
+ const fmRe = /^---\r?\n([\s\S]*?)^---\r?$/m;
309
+ const match = fmRe.exec(content);
310
+ if (!match)
311
+ return content;
312
+ const fmBody = match[1];
313
+ if (/^effort:/m.test(fmBody)) {
314
+ return content.replace(/^(effort:)[ \t]*.*$/m, `$1 ${effortValue}`);
315
+ }
316
+ const openLen = 3 + eol.length;
317
+ const closingStart = match.index + openLen + fmBody.length;
318
+ return content.slice(0, closingStart) + `effort: ${effortValue}${eol}` + content.slice(closingStart);
319
+ }
320
+ /**
321
+ * #488 — Re-sync effort: frontmatter in all installed gsd-*.md agent files to
322
+ * match the current effort config, without requiring a full reinstall.
323
+ *
324
+ * Uses install-time resolution (readGsdEffectiveEffortConfig + resolveInstallTimeEffort
325
+ * from bin/install.js) rather than the runtime resolver (resolveEffortInternal), because
326
+ * the sync must mirror what install actually wrote: home defaults merged with project config.
327
+ * The runtime resolver (loadConfig) does not merge ~/.gsd/defaults.json when a project
328
+ * .planning/config.json exists, so it would silently ignore home-level effort changes.
329
+ */
330
+ function cmdEffortSync(cwd, raw, opts) {
331
+ opts = opts || {};
332
+ const dryRun = opts.dryRun !== false;
333
+ const config = loadConfig(cwd);
334
+ const runtime = opts.runtime || config['runtime'] || 'claude';
335
+ if (runtime !== 'claude') {
336
+ output({ synced: 0, skipped: 0, changes: [], dry_run: dryRun, reason: `runtime '${runtime}' does not use effort: frontmatter` }, raw, '');
337
+ return;
338
+ }
339
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
340
+ const { getGlobalConfigDir } = require('./runtime-homes.cjs');
341
+ // Use install-time resolvers: they merge ~/.gsd/defaults.json with project config,
342
+ // matching the exact logic used when agents were originally installed.
343
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
344
+ const { readGsdEffectiveEffortConfig, resolveInstallTimeEffort } = require('../../../bin/install.js');
345
+ const effortCfg = readGsdEffectiveEffortConfig(cwd);
346
+ const agentsDir = node_path_1.default.join(opts.configDir || getGlobalConfigDir(runtime), 'agents');
347
+ if (!node_fs_1.default.existsSync(agentsDir)) {
348
+ output({ synced: 0, skipped: 0, changes: [], dry_run: dryRun, agents_dir: agentsDir, reason: 'agents directory not found' }, raw, '');
349
+ return;
350
+ }
351
+ // Skip symlinks — only write regular files to avoid clobbering symlink targets.
352
+ const files = node_fs_1.default.readdirSync(agentsDir).filter(f => {
353
+ if (!f.startsWith('gsd-') || !f.endsWith('.md'))
354
+ return false;
355
+ try {
356
+ return node_fs_1.default.lstatSync(node_path_1.default.join(agentsDir, f)).isFile();
357
+ }
358
+ catch {
359
+ return false;
360
+ }
361
+ });
362
+ const changes = [];
363
+ let synced = 0;
364
+ let skipped = 0;
365
+ for (const file of files) {
366
+ const agentName = file.replace(/\.md$/, '');
367
+ const filePath = node_path_1.default.join(agentsDir, file);
368
+ const content = node_fs_1.default.readFileSync(filePath, 'utf8');
369
+ // Resolve using install-time logic: home defaults merged with project config.
370
+ const universalEffort = resolveInstallTimeEffort(effortCfg, agentName);
371
+ const rendered = (0, model_catalog_cjs_1.renderEffortForRuntime)(runtime, universalEffort);
372
+ const newEffortValue = rendered.value;
373
+ const fmMatch = /^---\r?\n([\s\S]*?)^---\r?$/m.exec(content);
374
+ if (!fmMatch) {
375
+ skipped++;
376
+ continue;
377
+ }
378
+ const effortMatch = /^effort:[ \t]*(.+?)[ \t]*$/m.exec(fmMatch[1]);
379
+ const currentEffort = effortMatch ? effortMatch[1] : null;
380
+ if (currentEffort === newEffortValue) {
381
+ skipped++;
382
+ continue;
383
+ }
384
+ changes.push({ agent: agentName, from: currentEffort, to: newEffortValue });
385
+ synced++;
386
+ if (!dryRun) {
387
+ node_fs_1.default.writeFileSync(filePath, setEffortFrontmatter(content, newEffortValue));
388
+ }
389
+ }
390
+ output({ synced, skipped, changes, dry_run: dryRun, agents_dir: agentsDir }, raw, synced > 0 ? 'changed' : 'ok');
391
+ }
392
+ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
393
+ if (!message && !amend) {
394
+ error('commit message required');
395
+ }
396
+ // Sanitize commit message: strip invisible chars and injection markers
397
+ // that could hijack agent context when commit messages are read back
398
+ let sanitizedMessage = message;
399
+ if (sanitizedMessage) {
400
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
401
+ const { sanitizeForPrompt } = require('./security.cjs');
402
+ sanitizedMessage = sanitizeForPrompt(sanitizedMessage);
403
+ }
404
+ const config = loadConfig(cwd);
405
+ // Check commit_docs config
406
+ // `skipped: true` is explicit so agent prompts can match on a first-class
407
+ // success signal rather than inferring "skip" from "committed is missing"
408
+ // and improvising raw git fallbacks (#3678).
409
+ if (!config['commit_docs']) {
410
+ const result = { committed: false, skipped: true, hash: null, reason: 'skipped_commit_docs_false' };
411
+ output(result, raw, 'skipped');
412
+ return;
413
+ }
414
+ // Check if .planning is gitignored
415
+ if (isGitIgnored(cwd, '.planning')) {
416
+ const result = { committed: false, skipped: true, hash: null, reason: 'skipped_gitignored' };
417
+ output(result, raw, 'skipped');
418
+ return;
419
+ }
420
+ // Ensure branching strategy branch exists before first commit (#1278).
421
+ // Pre-execution workflows (discuss, plan, research) commit artifacts but the branch
422
+ // was previously only created during execute-phase — too late.
423
+ const branchingStrategy = config['branching_strategy'];
424
+ if (branchingStrategy && branchingStrategy !== 'none') {
425
+ let branchName = null;
426
+ if (branchingStrategy === 'phase') {
427
+ // Determine which phase we're committing for from the file paths
428
+ const phaseMatch = (files || []).join(' ').match(/(\d+(?:\.\d+)*)-/);
429
+ if (phaseMatch) {
430
+ const phaseNum = phaseMatch[1];
431
+ const phaseInfo = findPhaseInternal(cwd, phaseNum);
432
+ if (phaseInfo) {
433
+ branchName = config['phase_branch_template']
434
+ .replace('{phase}', phaseInfo['phase_number'])
435
+ .replace('{slug}', phaseInfo['phase_slug'] || 'phase');
436
+ }
437
+ }
438
+ }
439
+ else if (branchingStrategy === 'milestone') {
440
+ const milestone = getMilestoneInfo(cwd);
441
+ if (milestone && milestone.version) {
442
+ branchName = config['milestone_branch_template']
443
+ .replace('{milestone}', milestone.version)
444
+ .replace('{slug}', generateSlugInternal(milestone.name) || 'milestone');
445
+ }
446
+ }
447
+ if (branchName) {
448
+ const currentBranch = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd });
449
+ if (currentBranch.exitCode === 0 && currentBranch.stdout.trim() !== branchName) {
450
+ // Create branch if it doesn't exist, or switch to it if it does
451
+ const create = (0, shell_command_projection_cjs_1.execGit)(['checkout', '-b', branchName], { cwd });
452
+ if (create.exitCode !== 0) {
453
+ (0, shell_command_projection_cjs_1.execGit)(['checkout', branchName], { cwd });
454
+ }
455
+ }
456
+ }
457
+ }
458
+ // Stage files
459
+ const explicitFiles = files && files.length > 0;
460
+ const filesToStage = explicitFiles ? files : ['.planning/'];
461
+ for (const file of filesToStage) {
462
+ const fullPath = node_path_1.default.join(cwd, file);
463
+ if (!node_fs_1.default.existsSync(fullPath)) {
464
+ if (explicitFiles) {
465
+ // Caller passed an explicit --files list: missing files are skipped.
466
+ // Staging a deletion here would silently remove tracked planning files
467
+ // (e.g. STATE.md, ROADMAP.md) when they are temporarily absent (#2014).
468
+ continue;
469
+ }
470
+ // Default mode (staging all of .planning/): stage the deletion so
471
+ // removed planning files are not left dangling in the index.
472
+ (0, shell_command_projection_cjs_1.execGit)(['rm', '--cached', '--ignore-unmatch', file], { cwd });
473
+ }
474
+ else {
475
+ (0, shell_command_projection_cjs_1.execGit)(['add', file], { cwd });
476
+ }
477
+ }
478
+ // Commit (--no-verify skips pre-commit hooks, used by parallel executor agents)
479
+ const commitArgs = amend ? ['commit', '--amend', '--no-edit'] : ['commit', '-m', sanitizedMessage];
480
+ if (noVerify)
481
+ commitArgs.push('--no-verify');
482
+ const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd });
483
+ if (commitResult.exitCode !== 0) {
484
+ if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
485
+ const result = { committed: false, hash: null, reason: 'nothing_to_commit' };
486
+ output(result, raw, 'nothing');
487
+ return;
488
+ }
489
+ const result = {
490
+ committed: false,
491
+ hash: null,
492
+ reason: 'commit_failed',
493
+ error: commitResult.stderr || commitResult.stdout,
494
+ };
495
+ output(result, raw, 'failed');
496
+ return;
497
+ }
498
+ // Get short hash
499
+ const hashResult = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--short', 'HEAD'], { cwd });
500
+ const hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
501
+ const result = { committed: true, hash, reason: 'committed' };
502
+ output(result, raw, hash || 'committed');
503
+ }
504
+ /**
505
+ * Route a list of changed files to their sub-repo prefixes.
506
+ *
507
+ * Bucket sub-repos by their first path segment. Any file that matches a
508
+ * sub-repo prefix must share that sub-repo's first segment, so we only scan
509
+ * the (small) bucket for the file's first segment instead of all sub-repos
510
+ * — O(F + R) expected vs the prior O(F*R) find-in-loop. Candidates stay in
511
+ * sub-repo array order, preserving the original first-match semantics
512
+ * (incl. multi-segment sub-repos like "vendor/pkg", which resolve via the
513
+ * inner startsWith). (#311)
514
+ *
515
+ * @param files - changed file paths (relative to project root)
516
+ * @param subRepos - sub-repo path prefixes from config.sub_repos
517
+ */
518
+ function groupFilesBySubrepo(files, subRepos) {
519
+ const reposByFirstSeg = new Map();
520
+ for (const repo of subRepos) {
521
+ const firstSeg = String(repo).split('/')[0];
522
+ let bucket = reposByFirstSeg.get(firstSeg);
523
+ if (!bucket) {
524
+ bucket = [];
525
+ reposByFirstSeg.set(firstSeg, bucket);
526
+ }
527
+ bucket.push(repo);
528
+ }
529
+ const grouped = {};
530
+ const unmatched = [];
531
+ for (const file of files) {
532
+ const candidates = reposByFirstSeg.get(file.split('/')[0]);
533
+ const match = candidates ? candidates.find(repo => file.startsWith(repo + '/')) : undefined;
534
+ if (match) {
535
+ (grouped[match] ||= []).push(file);
536
+ }
537
+ else {
538
+ unmatched.push(file);
539
+ }
540
+ }
541
+ return { grouped, unmatched };
542
+ }
543
+ function cmdCommitToSubrepo(cwd, message, files, raw) {
544
+ if (!message) {
545
+ error('commit message required');
546
+ }
547
+ const config = loadConfig(cwd);
548
+ const subRepos = config['sub_repos'];
549
+ if (!subRepos || subRepos.length === 0) {
550
+ error('no sub_repos configured in .planning/config.json');
551
+ }
552
+ if (!files || files.length === 0) {
553
+ error('--files required for commit-to-subrepo');
554
+ }
555
+ // Group files by sub-repo prefix
556
+ const { grouped, unmatched } = groupFilesBySubrepo(files, subRepos);
557
+ if (unmatched.length > 0) {
558
+ process.stderr.write(`Warning: ${unmatched.length} file(s) did not match any sub-repo prefix: ${unmatched.join(', ')}\n`);
559
+ }
560
+ const repos = {};
561
+ for (const [repo, repoFiles] of Object.entries(grouped)) {
562
+ const repoCwd = node_path_1.default.join(cwd, repo);
563
+ // Stage files (strip sub-repo prefix for paths relative to that repo)
564
+ for (const file of repoFiles) {
565
+ const relativePath = file.slice(repo.length + 1);
566
+ (0, shell_command_projection_cjs_1.execGit)(['add', relativePath], { cwd: repoCwd });
567
+ }
568
+ // Commit
569
+ const commitResult = (0, shell_command_projection_cjs_1.execGit)(['commit', '-m', message], { cwd: repoCwd });
570
+ if (commitResult.exitCode !== 0) {
571
+ if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
572
+ repos[repo] = { committed: false, hash: null, files: repoFiles, reason: 'nothing_to_commit' };
573
+ continue;
574
+ }
575
+ repos[repo] = { committed: false, hash: null, files: repoFiles, reason: 'error', error: commitResult.stderr };
576
+ continue;
577
+ }
578
+ // Get hash
579
+ const hashResult = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--short', 'HEAD'], { cwd: repoCwd });
580
+ const hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
581
+ repos[repo] = { committed: true, hash, files: repoFiles };
582
+ }
583
+ const result = {
584
+ committed: Object.values(repos).some(r => r.committed),
585
+ repos,
586
+ unmatched: unmatched.length > 0 ? unmatched : undefined,
587
+ };
588
+ output(result, raw, Object.entries(repos).map(([r, v]) => `${r}:${v.hash || 'skip'}`).join(' '));
589
+ }
590
+ function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
591
+ if (!summaryPath) {
592
+ error('summary-path required for summary-extract');
593
+ }
594
+ const fullPath = node_path_1.default.join(cwd, summaryPath);
595
+ if (!node_fs_1.default.existsSync(fullPath)) {
596
+ output({ error: 'File not found', path: summaryPath }, raw, undefined);
597
+ return;
598
+ }
599
+ const content = node_fs_1.default.readFileSync(fullPath, 'utf-8');
600
+ const fm = extractFrontmatter(content);
601
+ // Parse key-decisions into structured format
602
+ const parseDecisions = (decisionsList) => {
603
+ if (!decisionsList || !Array.isArray(decisionsList))
604
+ return [];
605
+ return decisionsList.map(d => {
606
+ const colonIdx = d.indexOf(':');
607
+ if (colonIdx > 0) {
608
+ return {
609
+ summary: d.substring(0, colonIdx).trim(),
610
+ rationale: d.substring(colonIdx + 1).trim(),
611
+ };
612
+ }
613
+ return { summary: d, rationale: null };
614
+ });
615
+ };
616
+ const techStack = fm['tech-stack'];
617
+ // Build full result
618
+ const fullResult = {
619
+ path: summaryPath,
620
+ one_liner: fm['one-liner'] || extractOneLinerFromBody(content) || null,
621
+ key_files: fm['key-files'] || [],
622
+ tech_added: (techStack && techStack['added']) || [],
623
+ patterns: fm['patterns-established'] || [],
624
+ decisions: parseDecisions(fm['key-decisions']),
625
+ // Tolerate both key forms: the template/reader use kebab `requirements-completed`,
626
+ // but the tool's own JSON output and the milestone audit `--pick` use snake
627
+ // `requirements_completed`. Reading both prevents a snake-keyed SUMMARY (the form the
628
+ // tool emits) from being silently dropped to []. See #628.
629
+ requirements_completed: fm['requirements-completed'] ?? fm['requirements_completed'] ?? [],
630
+ };
631
+ // If fields specified, filter to only those fields
632
+ if (fields && fields.length > 0) {
633
+ const filtered = { path: summaryPath };
634
+ for (const field of fields) {
635
+ if (fullResult[field] !== undefined) {
636
+ filtered[field] = fullResult[field];
637
+ }
638
+ }
639
+ output(filtered, raw, undefined);
640
+ return;
641
+ }
642
+ output(fullResult, raw, undefined);
643
+ }
644
+ function _wsSleep(ms) {
645
+ return new Promise(resolve => setTimeout(resolve, ms));
646
+ }
647
+ function _wsParseRetryAfter(header) {
648
+ if (!header)
649
+ return null;
650
+ const trimmed = header.trim();
651
+ if (/^\d+$/.test(trimmed)) {
652
+ return Math.min(Math.max(parseInt(trimmed, 10) * 1000, 0), 60000);
653
+ }
654
+ const asDate = Date.parse(trimmed);
655
+ if (!isNaN(asDate)) {
656
+ return Math.min(Math.max(asDate - Date.now(), 0), 60000);
657
+ }
658
+ return null;
659
+ }
660
+ function _wsRetryDelayMs(attempt) {
661
+ const base = 250;
662
+ const cap = 2000;
663
+ const exp = Math.min(base * Math.pow(2, attempt), cap);
664
+ return exp + Math.floor(Math.random() * 100);
665
+ }
666
+ async function cmdWebsearch(query, options, raw) {
667
+ const apiKey = process.env['BRAVE_API_KEY'];
668
+ if (!apiKey) {
669
+ // No key = silent skip, agent falls back to built-in WebSearch
670
+ output({ available: false, reason: 'BRAVE_API_KEY not set' }, raw, '');
671
+ return;
672
+ }
673
+ if (!query) {
674
+ output({ available: false, error: 'Query required' }, raw, '');
675
+ return;
676
+ }
677
+ const params = new URLSearchParams({
678
+ q: query,
679
+ count: String(options.limit || 10),
680
+ country: 'us',
681
+ search_lang: 'en',
682
+ text_decorations: 'false'
683
+ });
684
+ if (options.freshness) {
685
+ params.set('freshness', options.freshness);
686
+ }
687
+ const rawTimeout = parseInt(process.env['GSD_WEBSEARCH_TIMEOUT_MS'], 10);
688
+ const timeoutMs = (Number.isInteger(rawTimeout) && rawTimeout > 0) ? rawTimeout : 10000;
689
+ const MAX_RETRIES = 2;
690
+ let attempt = 0;
691
+ while (true) {
692
+ try {
693
+ const ac = new AbortController();
694
+ const timer = setTimeout(() => ac.abort(new Error('timeout')), timeoutMs);
695
+ let response;
696
+ try {
697
+ response = await fetch(
698
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
699
+ `https://api.search.brave.com/res/v1/web/search?${params}`, {
700
+ headers: {
701
+ 'Accept': 'application/json',
702
+ 'X-Subscription-Token': apiKey
703
+ },
704
+ signal: ac.signal
705
+ });
706
+ }
707
+ finally {
708
+ clearTimeout(timer);
709
+ }
710
+ if (response.ok) {
711
+ const data = await response.json();
712
+ const results = (data.web?.results || []).map(r => ({
713
+ title: r.title,
714
+ url: r.url,
715
+ description: r.description,
716
+ age: r.age || null
717
+ }));
718
+ output({
719
+ available: true,
720
+ query,
721
+ count: results.length,
722
+ results
723
+ }, raw, results.map(r => `${r.title}\n${r.url}\n${r.description}`).join('\n\n'));
724
+ return;
725
+ }
726
+ const status = response.status;
727
+ const isRetryable = status === 429 || status >= 500;
728
+ if (!isRetryable) {
729
+ // Non-retryable 4xx — fail immediately, no attempts field
730
+ output({ available: false, error: `API error: ${status}` }, raw, '');
731
+ return;
732
+ }
733
+ // Retryable HTTP error
734
+ attempt++;
735
+ if (attempt > MAX_RETRIES) {
736
+ output({ available: false, error: `API error: ${status}`, attempts: attempt }, raw, '');
737
+ return;
738
+ }
739
+ let delay;
740
+ if (status === 429) {
741
+ const retryAfter = _wsParseRetryAfter(response.headers.get('retry-after'));
742
+ delay = retryAfter !== null ? retryAfter : _wsRetryDelayMs(attempt - 1);
743
+ }
744
+ else {
745
+ delay = _wsRetryDelayMs(attempt - 1);
746
+ }
747
+ await _wsSleep(delay);
748
+ }
749
+ catch (err) {
750
+ attempt++;
751
+ if (attempt > MAX_RETRIES) {
752
+ output({ available: false, error: err.message, attempts: attempt }, raw, '');
753
+ return;
754
+ }
755
+ await _wsSleep(_wsRetryDelayMs(attempt - 1));
756
+ }
757
+ }
758
+ }
759
+ function cmdProgressRender(cwd, format, raw) {
760
+ const phasesDir = planningPaths(cwd).phases;
761
+ const milestone = getMilestoneInfo(cwd);
762
+ const phases = [];
763
+ let totalPlans = 0;
764
+ let totalSummaries = 0;
765
+ try {
766
+ const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
767
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
768
+ for (const dir of dirs) {
769
+ const dm = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
770
+ const phaseNum = dm ? dm[1] : dir;
771
+ const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
772
+ const phaseFiles = node_fs_1.default.readdirSync(node_path_1.default.join(phasesDir, dir));
773
+ const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
774
+ const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
775
+ totalPlans += plans;
776
+ totalSummaries += summaries;
777
+ const status = determinePhaseStatus(plans, summaries, node_path_1.default.join(phasesDir, dir), 'Pending');
778
+ phases.push({ number: phaseNum, name: phaseName, plans, summaries, status });
779
+ }
780
+ }
781
+ catch { /* intentionally empty */ }
782
+ const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
783
+ if (format === 'table') {
784
+ // Render markdown table
785
+ const barWidth = 10;
786
+ const filled = Math.round((percent / 100) * barWidth);
787
+ const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
788
+ let out = `# ${milestone.version} ${milestone.name}\n\n`;
789
+ out += `**Progress:** [${bar}] ${totalSummaries}/${totalPlans} plans (${percent}%)\n\n`;
790
+ out += `| Phase | Name | Plans | Status |\n`;
791
+ out += `|-------|------|-------|--------|\n`;
792
+ for (const p of phases) {
793
+ out += `| ${p.number} | ${p.name} | ${p.summaries}/${p.plans} | ${p.status} |\n`;
794
+ }
795
+ output({ rendered: out }, raw, out);
796
+ }
797
+ else if (format === 'bar') {
798
+ const barWidth = 20;
799
+ const filled = Math.round((percent / 100) * barWidth);
800
+ const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
801
+ const text = `[${bar}] ${totalSummaries}/${totalPlans} plans (${percent}%)`;
802
+ output({ bar: text, percent, completed: totalSummaries, total: totalPlans }, raw, text);
803
+ }
804
+ else {
805
+ // JSON format
806
+ output({
807
+ milestone_version: milestone.version,
808
+ milestone_name: milestone.name,
809
+ phases,
810
+ total_plans: totalPlans,
811
+ total_summaries: totalSummaries,
812
+ percent,
813
+ }, raw, undefined);
814
+ }
815
+ }
816
+ /**
817
+ * Match pending todos against a phase's goal/name/requirements.
818
+ * Returns todos with relevance scores based on keyword, area, and file overlap.
819
+ * Used by discuss-phase to surface relevant todos before scope-setting.
820
+ */
821
+ function cmdTodoMatchPhase(cwd, phase, raw) {
822
+ if (!phase) {
823
+ error('phase required for todo match-phase');
824
+ }
825
+ const pendingDir = node_path_1.default.join(planningDir(cwd), 'todos', 'pending');
826
+ const todos = [];
827
+ // Load pending todos
828
+ try {
829
+ const files = node_fs_1.default.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
830
+ for (const file of files) {
831
+ const content = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(pendingDir, file));
832
+ if (content === null)
833
+ continue;
834
+ const titleMatch = content.match(/^title:\s*(.+)$/m);
835
+ const areaMatch = content.match(/^area:\s*(.+)$/m);
836
+ const filesMatch = content.match(/^files:\s*(.+)$/m);
837
+ const body = content.replace(/^(title|area|files|created|priority):.*$/gm, '').trim();
838
+ todos.push({
839
+ file,
840
+ title: titleMatch ? titleMatch[1].trim() : 'Untitled',
841
+ area: areaMatch ? areaMatch[1].trim() : 'general',
842
+ files: filesMatch ? filesMatch[1].trim().split(/[,\s]+/).filter(Boolean) : [],
843
+ body: body.slice(0, 200), // first 200 chars for context
844
+ });
845
+ }
846
+ }
847
+ catch { /* intentionally empty */ }
848
+ if (todos.length === 0) {
849
+ output({ phase, matches: [], todo_count: 0 }, raw, undefined);
850
+ return;
851
+ }
852
+ // Load phase goal/name from ROADMAP
853
+ const phaseInfo = getRoadmapPhaseInternal(cwd, phase);
854
+ const phaseName = phaseInfo ? (phaseInfo['phase_name'] || '') : '';
855
+ const phaseGoal = phaseInfo ? (phaseInfo['goal'] || '') : '';
856
+ const phaseSection = phaseInfo ? (phaseInfo['section'] || '') : '';
857
+ // Build keyword set from phase name + goal + section text
858
+ const phaseText = `${phaseName} ${phaseGoal} ${phaseSection}`.toLowerCase();
859
+ const stopWords = new Set(['the', 'and', 'for', 'with', 'from', 'that', 'this', 'will', 'are', 'was', 'has', 'have', 'been', 'not', 'but', 'all', 'can', 'into', 'each', 'when', 'any', 'use', 'new']);
860
+ const phaseKeywords = new Set(phaseText.split(/[\s\-_/.,;:()\[\]{}|]+/)
861
+ .map(w => w.replace(/[^a-z0-9]/g, ''))
862
+ .filter(w => w.length > 2 && !stopWords.has(w)));
863
+ // Find phase directory to get expected file paths
864
+ const phaseInfoDisk = findPhaseInternal(cwd, phase);
865
+ const phasePlans = [];
866
+ if (phaseInfoDisk && phaseInfoDisk['found']) {
867
+ try {
868
+ const phaseDir = node_path_1.default.join(cwd, phaseInfoDisk['directory']);
869
+ const planFiles = node_fs_1.default.readdirSync(phaseDir).filter(f => f.endsWith('-PLAN.md'));
870
+ for (const pf of planFiles) {
871
+ const planContent = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(phaseDir, pf));
872
+ if (planContent === null)
873
+ continue;
874
+ const fmFiles = planContent.match(/files_modified:\s*\[([^\]]*)\]/);
875
+ if (fmFiles) {
876
+ phasePlans.push(...fmFiles[1].split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean));
877
+ }
878
+ }
879
+ }
880
+ catch { /* intentionally empty */ }
881
+ }
882
+ // Score each todo for relevance
883
+ const matches = [];
884
+ for (const todo of todos) {
885
+ let score = 0;
886
+ const reasons = [];
887
+ // Keyword match: todo title/body terms in phase text
888
+ const todoWords = `${todo.title} ${todo.body}`.toLowerCase()
889
+ .split(/[\s\-_/.,;:()\[\]{}|]+/)
890
+ .map(w => w.replace(/[^a-z0-9]/g, ''))
891
+ .filter(w => w.length > 2 && !stopWords.has(w));
892
+ const matchedKeywords = todoWords.filter(w => phaseKeywords.has(w));
893
+ if (matchedKeywords.length > 0) {
894
+ score += Math.min(matchedKeywords.length * 0.2, 0.6);
895
+ reasons.push(`keywords: ${[...new Set(matchedKeywords)].slice(0, 5).join(', ')}`);
896
+ }
897
+ // Area match: todo area appears in phase text
898
+ if (todo.area !== 'general' && phaseText.includes(todo.area.toLowerCase())) {
899
+ score += 0.3;
900
+ reasons.push(`area: ${todo.area}`);
901
+ }
902
+ // File match: todo files overlap with phase plan files
903
+ if (todo.files.length > 0 && phasePlans.length > 0) {
904
+ const fileOverlap = todo.files.filter(f => phasePlans.some(pf => pf.includes(f) || f.includes(pf)));
905
+ if (fileOverlap.length > 0) {
906
+ score += 0.4;
907
+ reasons.push(`files: ${fileOverlap.slice(0, 3).join(', ')}`);
908
+ }
909
+ }
910
+ if (score > 0) {
911
+ matches.push({
912
+ file: todo.file,
913
+ title: todo.title,
914
+ area: todo.area,
915
+ score: Math.round(score * 100) / 100,
916
+ reasons,
917
+ });
918
+ }
919
+ }
920
+ // Sort by score descending
921
+ matches.sort((a, b) => b.score - a.score);
922
+ output({ phase, matches, todo_count: todos.length }, raw, undefined);
923
+ }
924
+ function cmdTodoComplete(cwd, filename, raw) {
925
+ if (!filename) {
926
+ error('filename required for todo complete');
927
+ }
928
+ const pendingDir = node_path_1.default.join(planningDir(cwd), 'todos', 'pending');
929
+ const completedDir = node_path_1.default.join(planningDir(cwd), 'todos', 'completed');
930
+ const sourcePath = node_path_1.default.join(pendingDir, filename);
931
+ if (!node_fs_1.default.existsSync(sourcePath)) {
932
+ error(`Todo not found: ${filename}`);
933
+ }
934
+ // Ensure completed directory exists
935
+ (0, shell_command_projection_cjs_1.platformEnsureDir)(completedDir);
936
+ // Read, add completion timestamp, move
937
+ let content = node_fs_1.default.readFileSync(sourcePath, 'utf-8');
938
+ const today = new Date().toISOString().split('T')[0];
939
+ content = `completed: ${today}\n` + content;
940
+ (0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(completedDir, filename), content);
941
+ node_fs_1.default.unlinkSync(sourcePath);
942
+ output({ completed: true, file: filename, date: today }, raw, 'completed');
943
+ }
944
+ function cmdScaffold(cwd, type, options, raw) {
945
+ const { phase, name } = options;
946
+ const padded = phase ? normalizePhaseName(phase) : '00';
947
+ const today = new Date().toISOString().split('T')[0];
948
+ // Find phase directory
949
+ const phaseInfo = phase ? findPhaseInternal(cwd, phase) : null;
950
+ const phaseDir = phaseInfo ? node_path_1.default.join(cwd, phaseInfo['directory']) : null;
951
+ if (phase && !phaseDir && type !== 'phase-dir') {
952
+ error(`Phase ${phase} directory not found`);
953
+ }
954
+ let filePath, content;
955
+ switch (type) {
956
+ case 'context': {
957
+ filePath = node_path_1.default.join(phaseDir, `${padded}-CONTEXT.md`);
958
+ content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.['phase_name'] || 'Unnamed'}"\ncreated: ${today}\n---\n\n# Phase ${phase}: ${name || phaseInfo?.['phase_name'] || 'Unnamed'} — Context\n\n## Decisions\n\n_Decisions will be captured during ${String((0, runtime_slash_cjs_1.formatGsdSlash)('discuss-phase', (0, runtime_slash_cjs_1.resolveRuntime)(cwd)))} ${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`;
959
+ break;
960
+ }
961
+ case 'uat': {
962
+ filePath = node_path_1.default.join(phaseDir, `${padded}-UAT.md`);
963
+ content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.['phase_name'] || 'Unnamed'}"\ncreated: ${today}\nstatus: pending\n---\n\n# Phase ${phase}: ${name || phaseInfo?.['phase_name'] || 'Unnamed'} — User Acceptance Testing\n\n## Test Results\n\n| # | Test | Status | Notes |\n|---|------|--------|-------|\n\n## Summary\n\n_Pending UAT_\n`;
964
+ break;
965
+ }
966
+ case 'verification': {
967
+ filePath = node_path_1.default.join(phaseDir, `${padded}-VERIFICATION.md`);
968
+ content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.['phase_name'] || 'Unnamed'}"\ncreated: ${today}\nstatus: pending\n---\n\n# Phase ${phase}: ${name || phaseInfo?.['phase_name'] || 'Unnamed'} — 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`;
969
+ break;
970
+ }
971
+ case 'phase-dir': {
972
+ if (!phase || !name) {
973
+ error('phase and name required for phase-dir scaffold');
974
+ }
975
+ const slug = generateSlugInternal(name);
976
+ // #3287: apply project_code prefix to stay consistent with phase.add/phase.insert
977
+ const scaffoldConfig = loadConfig(cwd);
978
+ const scaffoldProjectCode = scaffoldConfig['project_code'] || '';
979
+ const scaffoldPrefix = scaffoldProjectCode ? `${scaffoldProjectCode}-` : '';
980
+ const dirName = `${scaffoldPrefix}${padded}-${slug}`;
981
+ const phasesParent = planningPaths(cwd).phases;
982
+ (0, shell_command_projection_cjs_1.platformEnsureDir)(phasesParent);
983
+ const dirPath = node_path_1.default.join(phasesParent, dirName);
984
+ (0, shell_command_projection_cjs_1.platformEnsureDir)(dirPath);
985
+ output({ created: true, directory: toPosixPath(node_path_1.default.relative(cwd, dirPath)), path: dirPath }, raw, dirPath);
986
+ return;
987
+ }
988
+ default:
989
+ error(`Unknown scaffold type: ${type}. Available: context, uat, verification, phase-dir`);
990
+ // unreachable — error() calls process.exit
991
+ return;
992
+ }
993
+ if (node_fs_1.default.existsSync(filePath)) {
994
+ output({ created: false, reason: 'already_exists', path: filePath }, raw, 'exists');
995
+ return;
996
+ }
997
+ (0, shell_command_projection_cjs_1.platformWriteSync)(filePath, content);
998
+ const relPath = toPosixPath(node_path_1.default.relative(cwd, filePath));
999
+ output({ created: true, path: relPath }, raw, relPath);
1000
+ }
1001
+ function cmdStats(cwd, format, raw) {
1002
+ const phasesDir = planningPaths(cwd).phases;
1003
+ const roadmapPath = planningPaths(cwd).roadmap;
1004
+ const reqPath = planningPaths(cwd).requirements;
1005
+ const statePath = planningPaths(cwd).state;
1006
+ const milestone = getMilestoneInfo(cwd);
1007
+ const isDirInMilestone = getMilestonePhaseFilter(cwd);
1008
+ // Phase & plan stats (reuse progress pattern)
1009
+ const phasesByNumber = new Map();
1010
+ let totalPlans = 0;
1011
+ let totalSummaries = 0;
1012
+ try {
1013
+ const roadmapRaw = (0, shell_command_projection_cjs_1.platformReadSync)(roadmapPath);
1014
+ if (roadmapRaw === null)
1015
+ throw new Error('roadmap missing');
1016
+ const roadmapContent = extractCurrentMilestone(roadmapRaw, cwd);
1017
+ // Matches both plain numeric (Phase 1:) and milestone-prefixed (Phase 2-01:) headings.
1018
+ // Also tolerates optional [bracket-token] scope prefix on phase headings.
1019
+ const headingPattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*(?:-[\w.-]+)*)\s*:\s*([^\n]+)/gi;
1020
+ let match;
1021
+ while ((match = headingPattern.exec(roadmapContent)) !== null) {
1022
+ const key = normalizePhaseName(match[1]);
1023
+ phasesByNumber.set(key, {
1024
+ number: key,
1025
+ name: match[2].replace(/\(INSERTED\)/i, '').trim(),
1026
+ plans: 0,
1027
+ summaries: 0,
1028
+ status: 'Not Started',
1029
+ });
1030
+ }
1031
+ }
1032
+ catch { /* intentionally empty */ }
1033
+ try {
1034
+ const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
1035
+ const dirs = entries
1036
+ .filter(e => e.isDirectory())
1037
+ .map(e => e.name)
1038
+ .filter(isDirInMilestone)
1039
+ .sort((a, b) => comparePhaseNum(a, b));
1040
+ for (const dir of dirs) {
1041
+ // Use extractPhaseToken to correctly parse M-NN-style and code-prefixed dir names.
1042
+ const phaseToken = extractPhaseToken(dir);
1043
+ const phaseNum = phaseToken || dir;
1044
+ // phaseName is everything after the token (strip leading '-')
1045
+ const afterToken = dir.slice(phaseToken ? phaseToken.length : 0).replace(/^-/, '');
1046
+ const phaseName = afterToken ? afterToken.replace(/-/g, ' ') : '';
1047
+ const phaseFiles = node_fs_1.default.readdirSync(node_path_1.default.join(phasesDir, dir));
1048
+ const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
1049
+ const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
1050
+ totalPlans += plans;
1051
+ totalSummaries += summaries;
1052
+ const status = determinePhaseStatus(plans, summaries, node_path_1.default.join(phasesDir, dir), 'Not Started');
1053
+ const normalizedNum = normalizePhaseName(phaseNum);
1054
+ const existing = phasesByNumber.get(normalizedNum);
1055
+ phasesByNumber.set(normalizedNum, {
1056
+ number: normalizedNum,
1057
+ name: existing?.name || phaseName,
1058
+ plans: (existing?.plans || 0) + plans,
1059
+ summaries: (existing?.summaries || 0) + summaries,
1060
+ status,
1061
+ });
1062
+ }
1063
+ }
1064
+ catch { /* intentionally empty */ }
1065
+ const phases = [...phasesByNumber.values()].sort((a, b) => comparePhaseNum(a.number, b.number));
1066
+ const completedPhases = phases.filter(p => p.status === 'Complete').length;
1067
+ const planPercent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
1068
+ const percent = phases.length > 0 ? Math.min(100, Math.round((completedPhases / phases.length) * 100)) : 0;
1069
+ // Requirements stats
1070
+ let requirementsTotal = 0;
1071
+ let requirementsComplete = 0;
1072
+ const reqContent = (0, shell_command_projection_cjs_1.platformReadSync)(reqPath);
1073
+ if (reqContent !== null) {
1074
+ const checked = reqContent.match(/^- \[x\] \*\*/gm);
1075
+ const unchecked = reqContent.match(/^- \[ \] \*\*/gm);
1076
+ requirementsComplete = checked ? checked.length : 0;
1077
+ requirementsTotal = requirementsComplete + (unchecked ? unchecked.length : 0);
1078
+ }
1079
+ // Last activity from STATE.md
1080
+ let lastActivity = null;
1081
+ const stateContent = (0, shell_command_projection_cjs_1.platformReadSync)(statePath);
1082
+ if (stateContent !== null) {
1083
+ const activityMatch = stateContent.match(/^last_activity:\s*(.+)$/im)
1084
+ || stateContent.match(/\*\*Last Activity:\*\*\s*(.+)/i)
1085
+ || stateContent.match(/^Last Activity:\s*(.+)$/im)
1086
+ || stateContent.match(/^Last activity:\s*(.+)$/im);
1087
+ if (activityMatch)
1088
+ lastActivity = activityMatch[1].trim();
1089
+ }
1090
+ // Git stats
1091
+ let gitCommits = 0;
1092
+ let gitFirstCommitDate = null;
1093
+ const commitCount = (0, shell_command_projection_cjs_1.execGit)(['rev-list', '--count', 'HEAD'], { cwd });
1094
+ if (commitCount.exitCode === 0) {
1095
+ gitCommits = parseInt(commitCount.stdout, 10) || 0;
1096
+ }
1097
+ const rootHash = (0, shell_command_projection_cjs_1.execGit)(['rev-list', '--max-parents=0', 'HEAD'], { cwd });
1098
+ if (rootHash.exitCode === 0 && rootHash.stdout) {
1099
+ const firstCommit = rootHash.stdout.split('\n')[0].trim();
1100
+ const firstDate = (0, shell_command_projection_cjs_1.execGit)(['show', '-s', '--format=%as', firstCommit], { cwd });
1101
+ if (firstDate.exitCode === 0) {
1102
+ gitFirstCommitDate = firstDate.stdout || null;
1103
+ }
1104
+ }
1105
+ const result = {
1106
+ milestone_version: milestone.version,
1107
+ milestone_name: milestone.name,
1108
+ phases,
1109
+ phases_completed: completedPhases,
1110
+ phases_total: phases.length,
1111
+ total_plans: totalPlans,
1112
+ total_summaries: totalSummaries,
1113
+ percent,
1114
+ plan_percent: planPercent,
1115
+ requirements_total: requirementsTotal,
1116
+ requirements_complete: requirementsComplete,
1117
+ git_commits: gitCommits,
1118
+ git_first_commit_date: gitFirstCommitDate,
1119
+ last_activity: lastActivity,
1120
+ };
1121
+ if (format === 'table') {
1122
+ const barWidth = 10;
1123
+ const filled = Math.round((percent / 100) * barWidth);
1124
+ const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
1125
+ let out = `# ${milestone.version} ${milestone.name} — Statistics\n\n`;
1126
+ out += `**Progress:** [${bar}] ${completedPhases}/${phases.length} phases (${percent}%)\n`;
1127
+ if (totalPlans > 0) {
1128
+ out += `**Plans:** ${totalSummaries}/${totalPlans} complete (${planPercent}%)\n`;
1129
+ }
1130
+ out += `**Phases:** ${completedPhases}/${phases.length} complete\n`;
1131
+ if (requirementsTotal > 0) {
1132
+ out += `**Requirements:** ${requirementsComplete}/${requirementsTotal} complete\n`;
1133
+ }
1134
+ out += '\n';
1135
+ out += `| Phase | Name | Plans | Completed | Status |\n`;
1136
+ out += `|-------|------|-------|-----------|--------|\n`;
1137
+ for (const p of phases) {
1138
+ out += `| ${p.number} | ${p.name} | ${p.plans} | ${p.summaries} | ${p.status} |\n`;
1139
+ }
1140
+ if (gitCommits > 0) {
1141
+ out += `\n**Git:** ${gitCommits} commits`;
1142
+ if (gitFirstCommitDate)
1143
+ out += ` (since ${gitFirstCommitDate})`;
1144
+ out += '\n';
1145
+ }
1146
+ if (lastActivity)
1147
+ out += `**Last activity:** ${lastActivity}\n`;
1148
+ output({ rendered: out }, raw, out);
1149
+ }
1150
+ else {
1151
+ output(result, raw, undefined);
1152
+ }
1153
+ }
1154
+ /**
1155
+ * Check whether a commit should be allowed based on commit_docs config.
1156
+ * When commit_docs is false, rejects commits that stage .planning/ files.
1157
+ * Intended for use as a pre-commit hook guard.
1158
+ */
1159
+ function cmdCheckCommit(cwd, raw) {
1160
+ const config = loadConfig(cwd);
1161
+ // If commit_docs is true (or not set), allow all commits
1162
+ if (config['commit_docs'] !== false) {
1163
+ output({ allowed: true, reason: 'commit_docs_enabled' }, raw, 'allowed');
1164
+ return;
1165
+ }
1166
+ // commit_docs is false — check if any .planning/ files are staged
1167
+ const stagedResult = (0, shell_command_projection_cjs_1.execGit)(['diff', '--cached', '--name-only'], { cwd });
1168
+ if (stagedResult.exitCode === 0) {
1169
+ const planningFiles = stagedResult.stdout.split('\n').filter(f => f.startsWith('.planning/') || f.startsWith('.planning\\'));
1170
+ if (planningFiles.length > 0) {
1171
+ error(`commit_docs is false but ${planningFiles.length} .planning/ file(s) are staged:\n` +
1172
+ planningFiles.map(f => ` ${f}`).join('\n') +
1173
+ `\n\nTo unstage: git reset HEAD ${planningFiles.join(' ')}`);
1174
+ }
1175
+ }
1176
+ // exitCode !== 0 → no staged files or not a git repo — allow
1177
+ output({ allowed: true, reason: 'no_planning_files_staged' }, raw, 'allowed');
1178
+ }
1179
+ module.exports = {
1180
+ groupFilesBySubrepo,
1181
+ determinePhaseStatus,
1182
+ cmdGenerateSlug,
1183
+ cmdCurrentTimestamp,
1184
+ cmdListTodos,
1185
+ cmdVerifyPathExists,
1186
+ cmdHistoryDigest,
1187
+ cmdResolveModel,
1188
+ cmdResolveGranularity,
1189
+ cmdResolveExecution,
1190
+ cmdEffortSync,
1191
+ cmdCommit,
1192
+ cmdCommitToSubrepo,
1193
+ cmdSummaryExtract,
1194
+ cmdWebsearch,
1195
+ cmdProgressRender,
1196
+ cmdTodoComplete,
1197
+ cmdTodoMatchPhase,
1198
+ cmdScaffold,
1199
+ cmdStats,
1200
+ cmdCheckCommit,
1201
+ _wsParseRetryAfter,
1202
+ };