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