@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,1932 @@
1
+ "use strict";
2
+ /**
3
+ * Verify — Verification suite, consistency, and health validation
4
+ *
5
+ * ADR-457 build-at-publish: the hand-written bin/lib/verify.cjs collapsed to
6
+ * a TypeScript source of truth, compiled by tsc to a gitignored .cjs at the
7
+ * same require() path. Behaviour preserved byte-for-behaviour; only 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 node_os_1 = __importDefault(require("node:os"));
15
+ const validate_cjs_1 = require("./validate.cjs");
16
+ const validate_cjs_2 = require("./validate.cjs");
17
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
18
+ const planningWorkspace = require("./planning-workspace.cjs");
19
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
20
+ const frontmatterMod = require("./frontmatter.cjs");
21
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- state.cjs is an export= CommonJS module
22
+ const stateMod = require("./state.cjs");
23
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- model-profiles.cjs is an export= CommonJS module
24
+ const modelProfilesMod = require("./model-profiles.cjs");
25
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module
26
+ const planScanMod = require("./plan-scan.cjs");
27
+ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
28
+ const package_identity_cjs_1 = require("./package-identity.cjs");
29
+ const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
30
+ const schema_detect_cjs_1 = require("./schema-detect.cjs");
31
+ const artifacts_cjs_1 = require("./artifacts.cjs");
32
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- agent-install-check.cjs is an export= CommonJS module
33
+ const agentInstallCheck = require("./agent-install-check.cjs");
34
+ const { checkAgentsInstalled } = agentInstallCheck;
35
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
36
+ const ioMod = require("./io.cjs");
37
+ const { output, error } = ioMod;
38
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
39
+ const configLoaderMod = require("./config-loader.cjs");
40
+ const { loadConfig, CONFIG_DEFAULTS } = configLoaderMod;
41
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
42
+ const phaseIdMod = require("./phase-id.cjs");
43
+ const { normalizePhaseName, phaseTokenMatches, escapeRegex, getMilestoneFromPhaseId } = phaseIdMod;
44
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
45
+ const phaseLocatorMod = require("./phase-locator.cjs");
46
+ const { findPhaseInternal } = phaseLocatorMod;
47
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
48
+ const roadmapParserMod = require("./roadmap-parser.cjs");
49
+ const { getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone } = roadmapParserMod;
50
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
51
+ const worktreeSafetyMod = require("./worktree-safety.cjs");
52
+ const { inspectWorktreeHealth } = worktreeSafetyMod;
53
+ const { planningDir } = planningWorkspace;
54
+ const { extractFrontmatter, parseMustHavesBlock } = frontmatterMod;
55
+ const { writeStateMd } = stateMod;
56
+ const { MODEL_PROFILES } = modelProfilesMod;
57
+ // Unused but imported for structural parity
58
+ void stripShippedMilestones;
59
+ void schema_detect_cjs_1.detectSchemaFiles;
60
+ function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
61
+ if (!summaryPath) {
62
+ error('summary-path required');
63
+ }
64
+ const fullPath = node_path_1.default.join(cwd, summaryPath);
65
+ const checkCount = checkFileCount || 2;
66
+ if (!node_fs_1.default.existsSync(fullPath)) {
67
+ const result = {
68
+ passed: false,
69
+ checks: {
70
+ summary_exists: false,
71
+ files_created: { checked: 0, found: 0, missing: [] },
72
+ commits_exist: false,
73
+ self_check: 'not_found',
74
+ },
75
+ errors: ['SUMMARY.md not found'],
76
+ };
77
+ output(result, raw, 'failed');
78
+ return;
79
+ }
80
+ const content = node_fs_1.default.readFileSync(fullPath, 'utf-8');
81
+ const errors = [];
82
+ const mentionedFiles = new Set();
83
+ const patterns = [
84
+ /`([^`]+\.[a-zA-Z]+)`/g,
85
+ /(?:Created|Modified|Added|Updated|Edited):\s*`?([^\s`]+\.[a-zA-Z]+)`?/gi,
86
+ ];
87
+ for (const pattern of patterns) {
88
+ let m;
89
+ while ((m = pattern.exec(content)) !== null) {
90
+ const filePath = m[1];
91
+ if (filePath && !filePath.startsWith('http') && filePath.includes('/')) {
92
+ mentionedFiles.add(filePath);
93
+ }
94
+ }
95
+ }
96
+ const filesToCheck = Array.from(mentionedFiles).slice(0, checkCount);
97
+ const missing = [];
98
+ for (const file of filesToCheck) {
99
+ if (!node_fs_1.default.existsSync(node_path_1.default.join(cwd, file))) {
100
+ missing.push(file);
101
+ }
102
+ }
103
+ const commitHashPattern = /\b[0-9a-f]{7,40}\b/g;
104
+ const hashes = content.match(commitHashPattern) || [];
105
+ let commitsExist = false;
106
+ if (hashes.length > 0) {
107
+ for (const hash of hashes.slice(0, 3)) {
108
+ const result = (0, shell_command_projection_cjs_1.execGit)(['cat-file', '-t', hash], { cwd });
109
+ if (result.exitCode === 0 && result.stdout.trim() === 'commit') {
110
+ commitsExist = true;
111
+ break;
112
+ }
113
+ }
114
+ }
115
+ let selfCheck = 'not_found';
116
+ const selfCheckPattern = /##\s*(?:Self[- ]?Check|Verification|Quality Check)/i;
117
+ if (selfCheckPattern.test(content)) {
118
+ const passPattern = /(?:all\s+)?(?:pass|✓|✅|complete|succeeded)/i;
119
+ const failPattern = /(?:fail|✗|❌|incomplete|blocked)/i;
120
+ const checkSection = content.slice(content.search(selfCheckPattern));
121
+ if (failPattern.test(checkSection)) {
122
+ selfCheck = 'failed';
123
+ }
124
+ else if (passPattern.test(checkSection)) {
125
+ selfCheck = 'passed';
126
+ }
127
+ }
128
+ if (missing.length > 0)
129
+ errors.push('Missing files: ' + missing.join(', '));
130
+ if (!commitsExist && hashes.length > 0)
131
+ errors.push('Referenced commit hashes not found in git history');
132
+ if (selfCheck === 'failed')
133
+ errors.push('Self-check section indicates failure');
134
+ const checks = {
135
+ summary_exists: true,
136
+ files_created: { checked: filesToCheck.length, found: filesToCheck.length - missing.length, missing },
137
+ commits_exist: commitsExist,
138
+ self_check: selfCheck,
139
+ };
140
+ const passed = missing.length === 0 && selfCheck !== 'failed';
141
+ const result = { passed, checks, errors };
142
+ output(result, raw, passed ? 'passed' : 'failed');
143
+ }
144
+ /**
145
+ * Issue #429 — negative-grep comment-text echo gate.
146
+ * A literal that an acceptance criterion negative-greps for (grep -c 'LIT' file == 0)
147
+ * must not also appear verbatim inside an <action> body, or the executor's commit-time
148
+ * verify gate fails on the comment echo rather than a real regression. Conservative:
149
+ * errors only on a confidently-extracted QUOTED literal; ambiguous (bareword) → warning.
150
+ */
151
+ function scanNegativeGrepCommentEcho(content) {
152
+ const errors = [];
153
+ const warnings = [];
154
+ // Normalize newlines; join backslash line-continuations so a verify command wrapped
155
+ // across lines (grep ... \ <newline> == 0) is still seen as one segment.
156
+ const text = (content || '')
157
+ .replace(/\r\n/g, '\n')
158
+ .replace(/\r/g, '\n')
159
+ .replace(/\\\n/g, ' ');
160
+ // 1. Allowlisted literals: <!-- planner-discipline-allow: LIT -->
161
+ const allow = new Set();
162
+ const allowRe = /<!--\s*planner-discipline-allow:\s*(.+?)\s*-->/g;
163
+ let am;
164
+ while ((am = allowRe.exec(text)) !== null)
165
+ allow.add(am[1]);
166
+ // Zero-equality comparison (the negative grep). The required leading whitespace
167
+ // before the operator distinguishes a shell comparison (`[ $c == 0 ]`, `... == 0`,
168
+ // always spaced) from an assignment (`VAR=0`, never spaced) and naturally excludes
169
+ // `>= 0`, `<= 0`, `!= 0`, `!== 0`, `=== 0`.
170
+ const zeroCmp = (s) => /\s==?\s*0\b/.test(s) || /-eq\s+0\b/.test(s) || /\bequals\s+0\b/.test(s);
171
+ // A grep invocation using a count flag (-c / -cF / -Fc / --count), capturing the
172
+ // search pattern (first quoted token, else first bareword) after a run of options.
173
+ // The options run lets `grep -c -F 'LIT'`, `grep -F -c 'LIT'`, `grep -c -e 'LIT'`
174
+ // and `grep --count 'LIT'` all resolve to the LIT pattern.
175
+ const countGrepRe = /grep((?:\s+-{1,2}[A-Za-z][A-Za-z-]*)+)\s+(?:'([^']*)'|"([^"]*)"|([^\s'"|>&;]+))/g;
176
+ const optsHaveCount = (opts) => /(?:^|\s)-[A-Za-z]*c[A-Za-z]*(?=\s|$)/.test(opts) || /--count\b/.test(opts);
177
+ // `grep -cv 'pat' == 0` counts NON-matching lines, so == 0 there asserts "all lines
178
+ // match" — a POSITIVE gate, not our negative gate. Skip inverted greps.
179
+ const optsHaveInvert = (opts) => /(?:^|\s)-[A-Za-z]*v[A-Za-z]*(?=\s|$)/.test(opts) || /--invert-match\b/.test(opts);
180
+ // Bareword sanity: a real grep target, not a stray operator/number/flag.
181
+ const plausibleBare = (s) => /[A-Za-z0-9_]/.test(s) && !/^[-=!<>0-9]+$/.test(s);
182
+ // 2. <action> text to scan, with negative-grep COMMAND SPANS removed (only the
183
+ // command, not the whole line) so a pasted verify command does not self-flag
184
+ // while a prose echo on the same line is still caught.
185
+ const cmdSpanRe = /grep(?:\s+-{1,2}[A-Za-z][A-Za-z-]*)+\s+(?:'[^']*'|"[^"]*"|[^\s'"|>&;]+)[^\n]*?(?:==|-eq|=)\s*0\b/g;
186
+ const actionZones = [];
187
+ const actionRe = /<action>([\s\S]*?)<\/action>/g;
188
+ let acm;
189
+ while ((acm = actionRe.exec(text)) !== null)
190
+ actionZones.push(acm[1]);
191
+ const scannableActionText = actionZones.map((zone) => zone.replace(cmdSpanRe, ' ')).join('\n');
192
+ // 3. Per shell SEGMENT (split lines on && / ||) extract count-grep literals and
193
+ // check echoes. Per-segment splitting keeps a positive gate (`== 1`) from
194
+ // poisoning a negative gate (`== 0`) sharing the same physical line.
195
+ const seenErr = new Set();
196
+ const seenWarn = new Set();
197
+ const segments = text.split('\n').flatMap((line) => line.split(/\s*(?:&&|\|\|)\s*/));
198
+ for (const seg of segments) {
199
+ if (!/grep(?:\s+-{1,2}[A-Za-z])/.test(seg) || !zeroCmp(seg))
200
+ continue;
201
+ countGrepRe.lastIndex = 0;
202
+ const quotedLits = [];
203
+ const bareLits = [];
204
+ let m;
205
+ while ((m = countGrepRe.exec(seg)) !== null) {
206
+ if (!optsHaveCount(m[1]) || optsHaveInvert(m[1]))
207
+ continue; // need count, not invert (-cv is positive)
208
+ if (m[2] !== undefined)
209
+ quotedLits.push(m[2]);
210
+ else if (m[3] !== undefined)
211
+ quotedLits.push(m[3]);
212
+ else if (m[4] !== undefined && plausibleBare(m[4]))
213
+ bareLits.push(m[4]);
214
+ }
215
+ for (const quoted of quotedLits) {
216
+ if (!quoted || allow.has(quoted) || seenErr.has(quoted))
217
+ continue;
218
+ if (scannableActionText.includes(quoted)) {
219
+ seenErr.add(quoted);
220
+ errors.push(`Plan body contains forbidden literal "${quoted}" in an <action> block, but an acceptance criterion negative-greps for it (grep -c ... == 0). Rephrase the literal by concept, remove it from the plan body, or add <!-- planner-discipline-allow: ${quoted} --> if it must legitimately appear.`);
221
+ }
222
+ }
223
+ if (quotedLits.length === 0) {
224
+ for (const bare of bareLits) {
225
+ if (allow.has(bare) || seenWarn.has(bare))
226
+ continue;
227
+ if (scannableActionText.includes(bare)) {
228
+ seenWarn.add(bare);
229
+ warnings.push(`Possible comment-text echo (#429): negative-grep target "${bare}" is unquoted so its literal could not be extracted unambiguously, but it appears in an <action> block. Quote the grep literal and add an allowlist marker if the echo is intended, or rephrase by concept.`);
230
+ }
231
+ }
232
+ }
233
+ }
234
+ return { errors, warnings };
235
+ }
236
+ /**
237
+ * Issue #968 — file-wide negative-grep sibling conflict detector.
238
+ * A file-wide negative grep gate (! grep -Eq 'PAT' FILE or grep -c 'PAT' FILE == 0)
239
+ * bans a construct across the WHOLE file. When a sibling task in the same plan
240
+ * legitimately requires the same construct in the same file, the two gates are
241
+ * mutually unsatisfiable. This is a WARN-only check (never changes valid:false).
242
+ */
243
+ function scanFileWideNegativeGateConflict(content) {
244
+ const warnings = [];
245
+ // Normalize newlines; join backslash line-continuations (same as #429).
246
+ const text = (content || '')
247
+ .replace(/\r\n/g, '\n')
248
+ .replace(/\r/g, '\n')
249
+ .replace(/\\\n/g, ' ');
250
+ // Allowlisted patterns: <!-- planner-region-allow: PAT -->
251
+ const allow = new Set();
252
+ const allowRe = /<!--\s*planner-region-allow:\s*(.+?)\s*-->/g;
253
+ let am;
254
+ while ((am = allowRe.exec(text)) !== null)
255
+ allow.add(am[1]);
256
+ // Helper predicates (reused from #429 style).
257
+ // Zero-equality comparison: spaced == 0 or -eq 0.
258
+ const zeroCmp = (s) => /\s==?\s*0\b/.test(s) || /-eq\s+0\b/.test(s) || /\bequals\s+0\b/.test(s);
259
+ // grep options include -c / --count
260
+ const optsHaveCount = (opts) => /(?:^|\s)-[A-Za-z]*c[A-Za-z]*(?=\s|$)/.test(opts) || /--count\b/.test(opts);
261
+ // grep options include -v / --invert-match (inverted count is NOT a negative gate)
262
+ const optsHaveInvert = (opts) => /(?:^|\s)-[A-Za-z]*v[A-Za-z]*(?=\s|$)/.test(opts) || /--invert-match\b/.test(opts);
263
+ // A bareword that is a plausible grep pattern (not a stray flag/number).
264
+ const plausibleBare = (s) => /[A-Za-z0-9_]/.test(s) && !/^[-=!<>0-9]+$/.test(s);
265
+ // Regex to extract grep arguments: opts run then PAT (quoted or bare).
266
+ const grepArgRe = /grep((?:\s+-{1,2}[A-Za-z][A-Za-z-]*)+)\s+(?:'([^']*)'|"([^"]*)"|([^\s'"|>&;$()\[\]]+))/g;
267
+ // FIX 1 (ReDoS): Linear-time "does reqText satisfy the grep pattern" — no RegExp execution.
268
+ // Never calls new RegExp, so no catastrophic backtracking is possible.
269
+ //
270
+ // Handles literal patterns and `.`/`.*/`.+`/`\s`-style wildcard gaps and `^`/`$` anchors.
271
+ // Patterns using character classes (`[…]`), alternation (`a|b`), or other regex constructs
272
+ // fall back to a conservative literal-substring check, so the detector may NOT warn on those
273
+ // (false-negative is the safe direction for a warn-only advisory).
274
+ const patternRequiredIn = (pat, reqText) => {
275
+ const hay = (reqText || '').slice(0, 8000); // bound the haystack
276
+ if (!pat)
277
+ return false;
278
+ // Strip ERE anchors — position constraints don't change whether the construct is required.
279
+ pat = pat.replace(/^\^/, '').replace(/\$$/, '');
280
+ if (!pat)
281
+ return false;
282
+ // Pure literal (no regex metacharacters): direct substring.
283
+ if (!/[.*+?^${}()|[\]\\]/.test(pat))
284
+ return hay.includes(pat);
285
+ const SENT = ' ';
286
+ // Replace simple wildcard gaps (\s* \w+ .* .+ .? bare .) with a sentinel.
287
+ let work = pat
288
+ .replace(/\\[sSwWdD][*+?]?/g, SENT)
289
+ .replace(/\.[*+?]/g, SENT)
290
+ .replace(/\./g, SENT);
291
+ work = work.replace(/\\(.)/g, '$1'); // de-escape \( \. etc → literal char
292
+ const joined = work.split(SENT).join('');
293
+ // Unhandled regex constructs remain → safe literal-substring fallback on the raw pattern.
294
+ if (/[*+?^${}()|[\]]/.test(joined))
295
+ return hay.includes(pat);
296
+ const frags = work.split(SENT).filter(Boolean);
297
+ if (!frags.length)
298
+ return false; // all-wildcard pattern → no meaningful requirement
299
+ let pos = 0;
300
+ for (const f of frags) {
301
+ const idx = hay.indexOf(f, pos);
302
+ if (idx === -1)
303
+ return false;
304
+ pos = idx + f.length;
305
+ }
306
+ return true;
307
+ };
308
+ // FIX 2 (file basename over-match): exact normalized match; basename fallback ONLY for
309
+ // unqualified gate files (no path separator).
310
+ const normPath = (p) => p.replace(/^\.\//, '').trim();
311
+ // File-wide discriminator: a token AFTER PAT that looks like a path.
312
+ // Paths have /, a file extension, or match a known task <files> entry.
313
+ // Globs (containing *) are excluded (unresolvable — no warn).
314
+ const looksLikePath = (token) => !token.includes('*') &&
315
+ (token.includes('/') || /\.[a-zA-Z]{1,6}$/.test(token));
316
+ // FIX 5 (hasLeadingNot): collapse to one command-boundary-anchored regex.
317
+ // Negation at a command boundary: start of segment, or after ; & | ( newline / then / do.
318
+ // FIX 4 (isRegionScoped tightened): return true ONLY when grep is downstream of a
319
+ // sed line-range or awk range producer. Other pipe sources (cat, tac, etc.) are file-wide.
320
+ const isRegionScoped = (seg) => {
321
+ if (!seg.includes('|'))
322
+ return false;
323
+ const before = seg.slice(0, seg.lastIndexOf('|'));
324
+ // sed -n line/range extraction, e.g. sed -n '12,40p' FILE or sed -n '/a/,/b/p' FILE
325
+ if (/\bsed\s+-n\b/.test(before))
326
+ return true;
327
+ // awk range pattern, e.g. awk '/start/,/end/' FILE
328
+ if (/\bawk\b[^|]*\/[^/]*\/\s*,\s*\/[^/]*\//.test(before))
329
+ return true;
330
+ return false;
331
+ };
332
+ const taskRe = /<task[^>]*>([\s\S]*?)<\/task>/g;
333
+ const tasks = [];
334
+ let tm;
335
+ while ((tm = taskRe.exec(text)) !== null) {
336
+ const tc = tm[1];
337
+ // Extract task name.
338
+ const namem = tc.match(/<name>([\s\S]*?)<\/name>/);
339
+ const name = namem ? namem[1].trim() : 'unnamed';
340
+ // Extract <files> entries.
341
+ const filesm = tc.match(/<files>([\s\S]*?)<\/files>/);
342
+ const filesText = filesm ? filesm[1] : '';
343
+ const files = filesText.split(/[,\s]+/).map(s => s.trim()).filter(Boolean);
344
+ // Gate text: <verify>/<automated>/<acceptance_criteria>.
345
+ const gateFragments = [];
346
+ for (const tag of ['verify', 'automated', 'acceptance_criteria']) {
347
+ const re = new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`, 'g');
348
+ let mm;
349
+ while ((mm = re.exec(tc)) !== null)
350
+ gateFragments.push(mm[1]);
351
+ }
352
+ // Requirement text: <action>/<acceptance_criteria>.
353
+ const reqFragments = [];
354
+ for (const tag of ['action', 'acceptance_criteria']) {
355
+ const re = new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`, 'g');
356
+ let mm;
357
+ while ((mm = re.exec(tc)) !== null)
358
+ reqFragments.push(mm[1]);
359
+ }
360
+ // Strip XML tags from gate text so segments containing embedded
361
+ // XML closing tags (e.g. <automated>cmd</automated> nested inside <verify>)
362
+ // don't bleed into the file-path token extraction.
363
+ const rawGateText = gateFragments.join('\n');
364
+ const gateText = rawGateText.replace(/<[^>]+>/g, ' ');
365
+ tasks.push({
366
+ name,
367
+ files,
368
+ gateText,
369
+ reqText: reqFragments.join('\n'),
370
+ });
371
+ }
372
+ if (tasks.length < 2)
373
+ return { warnings, valid: true };
374
+ // FIX 3 (extensionless known files): build a normalized set of ALL tasks' <files> entries
375
+ // so that extensionless filenames like Dockerfile are also recognized as valid file tokens.
376
+ const knownFiles = new Set();
377
+ for (const t of tasks) {
378
+ for (const f of t.files)
379
+ knownFiles.add(normPath(f));
380
+ }
381
+ // Extended looksLikePath: accepts known <files> entries even without an extension.
382
+ const isFileLike = (token) => {
383
+ if (token.includes('*'))
384
+ return false; // exclude globs
385
+ if (looksLikePath(token))
386
+ return true;
387
+ return knownFiles.has(normPath(token));
388
+ };
389
+ // Dedup key: (taskAIdx, taskBIdx, pat, file)
390
+ const seen = new Set();
391
+ // For each task A, scan gate text for file-wide negative grep bans.
392
+ for (let ai = 0; ai < tasks.length; ai++) {
393
+ const taskA = tasks[ai];
394
+ // Split gate text into shell segments (split on && / || within lines).
395
+ const segments = taskA.gateText.split('\n').flatMap(line => line.split(/\s*(?:&&|\|\|)\s*/));
396
+ for (const seg of segments) {
397
+ if (!/grep/.test(seg))
398
+ continue;
399
+ // FIX 5: Negation at a command boundary: start of segment, or after ; & | ( newline / then / do.
400
+ // Also handles ! negating an entire pipeline (e.g. ! cat FILE | grep ...).
401
+ const hasLeadingNot =
402
+ // Direct ! grep: negation immediately before grep keyword
403
+ /(?:^|[\n;&|(]|\bthen\b|\bdo\b)\s*!\s*grep/.test(seg) ||
404
+ // Pipeline negation: ! at command boundary, grep appears in pipeline after |
405
+ (/(?:^|[\n;&|(]|\bthen\b|\bdo\b)\s*!\s*\w/.test(seg) && /\|\s*grep\b/.test(seg));
406
+ const hasCountZero = zeroCmp(seg);
407
+ // Extract grep invocation and check for count.
408
+ grepArgRe.lastIndex = 0;
409
+ let pat = null;
410
+ let file = null;
411
+ let isBan = false;
412
+ // FIX 4 helper: given a segment and the grep match end position, find the
413
+ // file argument. First try the token immediately after PAT; if none qualifies,
414
+ // try a cat/tac producer or < FILE redirect from the full segment.
415
+ const resolveFileArg = (segment, afterPatStr) => {
416
+ // Primary: token immediately after PAT in the grep command
417
+ const fileM = afterPatStr.match(/^\s+([^\s'"|>&;$()\[\]]+)/);
418
+ const rawFile = fileM ? fileM[1] : null;
419
+ if (rawFile && isFileLike(rawFile))
420
+ return rawFile;
421
+ // FIX 4: For NON-region segments, also look for cat/tac producer or < FILE redirect
422
+ const catM = segment.match(/\b(?:cat|tac)\s+([^\s'"|>&;()]+)/);
423
+ if (catM && isFileLike(catM[1]))
424
+ return catM[1];
425
+ const redirM = segment.match(/<\s*([^\s'"|>&;()]+)/);
426
+ if (redirM && isFileLike(redirM[1]))
427
+ return redirM[1];
428
+ return null;
429
+ };
430
+ // If leading !, it might be a count or a direct !grep
431
+ if (hasLeadingNot && !hasCountZero) {
432
+ // Direct ! grep PAT FILE form: grep opts PAT FILE
433
+ // Extract PAT and FILE from the grep invocation
434
+ grepArgRe.lastIndex = 0;
435
+ let gm;
436
+ while ((gm = grepArgRe.exec(seg)) !== null) {
437
+ const opts = gm[1];
438
+ if (optsHaveInvert(opts))
439
+ continue; // -v form: not a ban
440
+ // PAT
441
+ const rawPat = gm[2] !== undefined ? gm[2] :
442
+ gm[3] !== undefined ? gm[3] :
443
+ gm[4] !== undefined && plausibleBare(gm[4]) ? gm[4] : null;
444
+ if (!rawPat)
445
+ continue;
446
+ // FILE: next non-option token after PAT (or cat/tac/redirect in segment)
447
+ const afterPat = seg.slice((gm.index || 0) + gm[0].length);
448
+ const rawFile = resolveFileArg(seg, afterPat);
449
+ if (rawFile) {
450
+ pat = rawPat;
451
+ file = rawFile;
452
+ isBan = true;
453
+ }
454
+ }
455
+ }
456
+ if (!isBan && hasCountZero) {
457
+ // count grep form: grep -c PAT FILE == 0 or [ $(grep -c PAT FILE) -eq 0 ]
458
+ grepArgRe.lastIndex = 0;
459
+ let gm;
460
+ while ((gm = grepArgRe.exec(seg)) !== null) {
461
+ const opts = gm[1];
462
+ if (!optsHaveCount(opts) || optsHaveInvert(opts))
463
+ continue;
464
+ const rawPat = gm[2] !== undefined ? gm[2] :
465
+ gm[3] !== undefined ? gm[3] :
466
+ gm[4] !== undefined && plausibleBare(gm[4]) ? gm[4] : null;
467
+ if (!rawPat)
468
+ continue;
469
+ const afterPat = seg.slice((gm.index || 0) + gm[0].length);
470
+ const rawFile = resolveFileArg(seg, afterPat);
471
+ if (rawFile) {
472
+ pat = rawPat;
473
+ file = rawFile;
474
+ isBan = true;
475
+ }
476
+ }
477
+ }
478
+ if (!isBan || !pat || !file)
479
+ continue;
480
+ if (allow.has(pat))
481
+ continue;
482
+ // Skip if region-scoped (grep downstream of a sed/awk pipe — region extracted)
483
+ if (isRegionScoped(seg))
484
+ continue;
485
+ // For each other task B: check if B's <files> includes FILE AND B's reqText contains PAT
486
+ for (let bi = 0; bi < tasks.length; bi++) {
487
+ if (bi === ai)
488
+ continue;
489
+ const taskB = tasks[bi];
490
+ // FIX 2: Exact normalized match; basename fallback ONLY for unqualified gate files.
491
+ const gateFile = normPath(file);
492
+ const bMatchesFile = taskB.files.some((bf) => {
493
+ const nbf = normPath(bf);
494
+ if (nbf === gateFile)
495
+ return true;
496
+ // basename fallback only when the gate file is an unqualified bare filename (no dir separator)
497
+ if (!gateFile.includes('/') && node_path_1.default.basename(nbf) === gateFile)
498
+ return true;
499
+ return false;
500
+ });
501
+ if (!bMatchesFile)
502
+ continue;
503
+ // FIX 1: Use linear-time patternRequiredIn instead of new RegExp (ReDoS-safe).
504
+ const bRequiresPat = patternRequiredIn(pat, taskB.reqText);
505
+ if (!bRequiresPat)
506
+ continue;
507
+ const dedupeKey = `${ai}:${bi}:${pat}:${file}`;
508
+ if (seen.has(dedupeKey))
509
+ continue;
510
+ seen.add(dedupeKey);
511
+ warnings.push(`Region-scope conflict (#968): task "${taskA.name}" negative-greps "${pat}" file-wide on ${file}, ` +
512
+ `but sibling task "${taskB.name}" requires it in the same file. ` +
513
+ `A file-wide ban is unsatisfiable when a sibling needs the construct elsewhere — ` +
514
+ `region-scope task "${taskA.name}"'s gate (sed -n/awk range then grep) or use an AST/test check. ` +
515
+ `See planner-antipatterns.md "Region-Scoped Negative Gates", or add ` +
516
+ `<!-- planner-region-allow: ${pat} --> if intentional.`);
517
+ }
518
+ }
519
+ }
520
+ // This detector is warn-only: it never sets valid=false.
521
+ return { warnings, valid: true };
522
+ }
523
+ function cmdVerifyPlanStructure(cwd, filePath, raw) {
524
+ if (!filePath) {
525
+ error('file path required');
526
+ }
527
+ const fullPath = node_path_1.default.isAbsolute(filePath) ? filePath : node_path_1.default.join(cwd, filePath);
528
+ const content = (0, shell_command_projection_cjs_1.platformReadSync)(fullPath);
529
+ if (!content) {
530
+ output({ error: 'File not found', path: filePath }, raw);
531
+ return;
532
+ }
533
+ const fm = extractFrontmatter(content);
534
+ const errors = [];
535
+ const warnings = [];
536
+ const required = ['phase', 'plan', 'type', 'wave', 'depends_on', 'files_modified', 'autonomous', 'must_haves'];
537
+ for (const field of required) {
538
+ if (fm[field] === undefined)
539
+ errors.push(`Missing required frontmatter field: ${field}`);
540
+ }
541
+ const taskPattern = /<task[^>]*>([\s\S]*?)<\/task>/g;
542
+ const tasks = [];
543
+ let taskMatch;
544
+ while ((taskMatch = taskPattern.exec(content)) !== null) {
545
+ const taskContent = taskMatch[1];
546
+ const nameMatch = taskContent.match(/<name>([\s\S]*?)<\/name>/);
547
+ const taskName = nameMatch ? nameMatch[1].trim() : 'unnamed';
548
+ const hasFiles = /<files>/.test(taskContent);
549
+ const hasAction = /<action>/.test(taskContent);
550
+ const hasVerify = /<verify>/.test(taskContent);
551
+ const hasDone = /<done>/.test(taskContent);
552
+ if (!nameMatch)
553
+ errors.push('Task missing <name> element');
554
+ if (!hasAction)
555
+ errors.push(`Task '${taskName}' missing <action>`);
556
+ if (!hasVerify)
557
+ warnings.push(`Task '${taskName}' missing <verify>`);
558
+ if (!hasDone)
559
+ warnings.push(`Task '${taskName}' missing <done>`);
560
+ if (!hasFiles)
561
+ warnings.push(`Task '${taskName}' missing <files>`);
562
+ tasks.push({ name: taskName, hasFiles, hasAction, hasVerify, hasDone });
563
+ }
564
+ if (tasks.length === 0)
565
+ warnings.push('No <task> elements found');
566
+ if (fm['wave'] &&
567
+ parseInt(fm['wave']) > 1 &&
568
+ (!fm['depends_on'] ||
569
+ (Array.isArray(fm['depends_on']) && fm['depends_on'].length === 0))) {
570
+ warnings.push('Wave > 1 but depends_on is empty');
571
+ }
572
+ const hasCheckpoints = /<task\s+type=["']?checkpoint/.test(content);
573
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string -- FrontmatterValue comparison
574
+ if (hasCheckpoints && fm['autonomous'] !== 'false' && String(fm['autonomous']) !== 'false') {
575
+ errors.push('Has checkpoint tasks but autonomous is not false');
576
+ }
577
+ const echoScan = scanNegativeGrepCommentEcho(content);
578
+ errors.push(...echoScan.errors);
579
+ warnings.push(...echoScan.warnings);
580
+ const conflictScan = scanFileWideNegativeGateConflict(content);
581
+ warnings.push(...conflictScan.warnings);
582
+ output({
583
+ valid: errors.length === 0,
584
+ errors,
585
+ warnings,
586
+ task_count: tasks.length,
587
+ tasks,
588
+ frontmatter_fields: Object.keys(fm),
589
+ }, raw, errors.length === 0 ? 'valid' : 'invalid');
590
+ }
591
+ function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
592
+ if (!phase) {
593
+ error('phase required');
594
+ }
595
+ const phaseInfoRaw = findPhaseInternal(cwd, phase);
596
+ if (!phaseInfoRaw || !phaseInfoRaw['found']) {
597
+ output({ error: 'Phase not found', phase }, raw);
598
+ return;
599
+ }
600
+ const phaseInfo = phaseInfoRaw;
601
+ const errors = [];
602
+ const warnings = [];
603
+ const phaseDir = node_path_1.default.join(cwd, phaseInfo['directory']);
604
+ let files;
605
+ try {
606
+ files = node_fs_1.default.readdirSync(phaseDir);
607
+ }
608
+ catch {
609
+ output({ error: 'Cannot read phase directory' }, raw);
610
+ return;
611
+ }
612
+ const plans = files.filter((f) => f.match(/-PLAN\.md$/i));
613
+ const summaries = files.filter((f) => f.match(/-SUMMARY\.md$/i));
614
+ const planIds = new Set(plans.map((p) => p.replace(/-PLAN\.md$/i, '')));
615
+ const summaryIds = new Set(summaries.map((s) => s.replace(/-SUMMARY\.md$/i, '')));
616
+ const incompletePlans = [...planIds].filter((id) => !summaryIds.has(id));
617
+ if (incompletePlans.length > 0) {
618
+ errors.push(`Plans without summaries: ${incompletePlans.join(', ')}`);
619
+ }
620
+ const orphanSummaries = [...summaryIds].filter((id) => !planIds.has(id));
621
+ if (orphanSummaries.length > 0) {
622
+ warnings.push(`Summaries without plans: ${orphanSummaries.join(', ')}`);
623
+ }
624
+ output({
625
+ complete: errors.length === 0,
626
+ phase: phaseInfo['phase_number'],
627
+ plan_count: plans.length,
628
+ summary_count: summaries.length,
629
+ incomplete_plans: incompletePlans,
630
+ orphan_summaries: orphanSummaries,
631
+ errors,
632
+ warnings,
633
+ }, raw, errors.length === 0 ? 'complete' : 'incomplete');
634
+ }
635
+ function cmdVerifyReferences(cwd, filePath, raw) {
636
+ if (!filePath) {
637
+ error('file path required');
638
+ }
639
+ const fullPath = node_path_1.default.isAbsolute(filePath) ? filePath : node_path_1.default.join(cwd, filePath);
640
+ const content = (0, shell_command_projection_cjs_1.platformReadSync)(fullPath);
641
+ if (!content) {
642
+ output({ error: 'File not found', path: filePath }, raw);
643
+ return;
644
+ }
645
+ const found = [];
646
+ const missing = [];
647
+ const atRefs = content.match(/@([^\s\n,)]+\/[^\s\n,)]+)/g) || [];
648
+ for (const ref of atRefs) {
649
+ const cleanRef = ref.slice(1);
650
+ const resolved = cleanRef.startsWith('~/')
651
+ ? node_path_1.default.join(process.env['HOME'] || '', cleanRef.slice(2))
652
+ : node_path_1.default.join(cwd, cleanRef);
653
+ if (node_fs_1.default.existsSync(resolved)) {
654
+ found.push(cleanRef);
655
+ }
656
+ else {
657
+ missing.push(cleanRef);
658
+ }
659
+ }
660
+ const backtickRefs = content.match(/`([^`]+\/[^`]+\.[a-zA-Z]{1,10})`/g) || [];
661
+ for (const ref of backtickRefs) {
662
+ const cleanRef = ref.slice(1, -1);
663
+ if (cleanRef.startsWith('http') || cleanRef.includes('${') || cleanRef.includes('{{'))
664
+ continue;
665
+ if (found.includes(cleanRef) || missing.includes(cleanRef))
666
+ continue;
667
+ const resolved = node_path_1.default.join(cwd, cleanRef);
668
+ if (node_fs_1.default.existsSync(resolved)) {
669
+ found.push(cleanRef);
670
+ }
671
+ else {
672
+ missing.push(cleanRef);
673
+ }
674
+ }
675
+ output({
676
+ valid: missing.length === 0,
677
+ found: found.length,
678
+ missing,
679
+ total: found.length + missing.length,
680
+ }, raw, missing.length === 0 ? 'valid' : 'invalid');
681
+ }
682
+ function cmdVerifyCommits(cwd, hashes, raw) {
683
+ if (!hashes || hashes.length === 0) {
684
+ error('At least one commit hash required');
685
+ }
686
+ const valid = [];
687
+ const invalid = [];
688
+ for (const hash of hashes) {
689
+ const result = (0, shell_command_projection_cjs_1.execGit)(['cat-file', '-t', hash], { cwd });
690
+ if (result.exitCode === 0 && result.stdout.trim() === 'commit') {
691
+ valid.push(hash);
692
+ }
693
+ else {
694
+ invalid.push(hash);
695
+ }
696
+ }
697
+ output({
698
+ all_valid: invalid.length === 0,
699
+ valid,
700
+ invalid,
701
+ total: hashes.length,
702
+ }, raw, invalid.length === 0 ? 'valid' : 'invalid');
703
+ }
704
+ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
705
+ if (!planFilePath) {
706
+ error('plan file path required');
707
+ }
708
+ const fullPath = node_path_1.default.isAbsolute(planFilePath) ? planFilePath : node_path_1.default.join(cwd, planFilePath);
709
+ const content = (0, shell_command_projection_cjs_1.platformReadSync)(fullPath);
710
+ if (!content) {
711
+ output({ error: 'File not found', path: planFilePath }, raw);
712
+ return;
713
+ }
714
+ const artifacts = parseMustHavesBlock(content, 'artifacts');
715
+ if (artifacts.length === 0) {
716
+ output({ error: 'No must_haves.artifacts found in frontmatter', path: planFilePath }, raw);
717
+ return;
718
+ }
719
+ const results = [];
720
+ for (const artifact of artifacts) {
721
+ if (typeof artifact === 'string')
722
+ continue;
723
+ const artPath = artifact['path'];
724
+ if (!artPath)
725
+ continue;
726
+ const artFullPath = node_path_1.default.join(cwd, artPath);
727
+ const exists = node_fs_1.default.existsSync(artFullPath);
728
+ const check = { path: artPath, exists, issues: [], passed: false };
729
+ if (exists) {
730
+ const fileContent = (0, shell_command_projection_cjs_1.platformReadSync)(artFullPath) || '';
731
+ const lineCount = fileContent.split('\n').length;
732
+ if (artifact['min_lines'] && lineCount < artifact['min_lines']) {
733
+ check['issues'].push(`Only ${lineCount} lines, need ${artifact['min_lines']}`);
734
+ }
735
+ if (artifact['contains'] && !fileContent.includes(artifact['contains'])) {
736
+ check['issues'].push(`Missing pattern: ${artifact['contains']}`);
737
+ }
738
+ if (artifact['exports']) {
739
+ const exports = Array.isArray(artifact['exports'])
740
+ ? artifact['exports']
741
+ : [artifact['exports']];
742
+ for (const exp of exports) {
743
+ if (!fileContent.includes(exp))
744
+ check['issues'].push(`Missing export: ${exp}`);
745
+ }
746
+ }
747
+ check['passed'] = check['issues'].length === 0;
748
+ }
749
+ else {
750
+ check['issues'].push('File not found');
751
+ }
752
+ results.push(check);
753
+ }
754
+ const passed = results.filter((r) => r['passed']).length;
755
+ output({
756
+ all_passed: passed === results.length,
757
+ passed,
758
+ total: results.length,
759
+ artifacts: results,
760
+ }, raw, passed === results.length ? 'valid' : 'invalid');
761
+ }
762
+ /**
763
+ * Returns a Set of file paths (relative to cwd) that are promised by plans in
764
+ * the same phase directory at a wave number >= minWave.
765
+ *
766
+ * Used by cmdVerifyKeyLinks to avoid hard-failing a missing `from:` file that
767
+ * is a planned future artifact (fix #1202).
768
+ */
769
+ function collectPromisedFilesAtOrAfterWave(phaseDir, minWave) {
770
+ const promised = new Set();
771
+ const { planFiles } = planScanMod.scanPhasePlans(phaseDir);
772
+ for (const planFile of planFiles) {
773
+ const planFullPath = node_path_1.default.join(phaseDir, planFile);
774
+ const planContent = (0, shell_command_projection_cjs_1.platformReadSync)(planFullPath);
775
+ if (!planContent)
776
+ continue;
777
+ const fm = extractFrontmatter(planContent);
778
+ const waveRaw = fm['wave'];
779
+ const wave = typeof waveRaw === 'string' ? parseInt(waveRaw, 10) : (typeof waveRaw === 'number' ? waveRaw : NaN);
780
+ if (isNaN(wave) || wave < minWave)
781
+ continue;
782
+ const filesModified = fm['files_modified'];
783
+ if (!filesModified)
784
+ continue;
785
+ const files = Array.isArray(filesModified)
786
+ ? filesModified
787
+ : (typeof filesModified === 'string' ? [filesModified] : []);
788
+ for (const f of files) {
789
+ if (typeof f === 'string' && f.trim())
790
+ promised.add(f.trim());
791
+ }
792
+ }
793
+ return promised;
794
+ }
795
+ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
796
+ if (!planFilePath) {
797
+ error('plan file path required');
798
+ }
799
+ const fullPath = node_path_1.default.isAbsolute(planFilePath) ? planFilePath : node_path_1.default.join(cwd, planFilePath);
800
+ const content = (0, shell_command_projection_cjs_1.platformReadSync)(fullPath);
801
+ if (!content) {
802
+ output({ error: 'File not found', path: planFilePath }, raw);
803
+ return;
804
+ }
805
+ const keyLinks = parseMustHavesBlock(content, 'key_links');
806
+ if (keyLinks.length === 0) {
807
+ output({ error: 'No must_haves.key_links found in frontmatter', path: planFilePath }, raw);
808
+ return;
809
+ }
810
+ // Derive the current plan's wave number and phase directory for wave-aware
811
+ // missing-file handling (fix #1202).
812
+ const currentFm = extractFrontmatter(content);
813
+ const currentWaveRaw = currentFm['wave'];
814
+ const currentWave = typeof currentWaveRaw === 'string'
815
+ ? parseInt(currentWaveRaw, 10)
816
+ : (typeof currentWaveRaw === 'number' ? currentWaveRaw : 1);
817
+ const phaseDir = node_path_1.default.dirname(fullPath);
818
+ // Collect files promised by plans at wave >= currentWave (lazy: computed once
819
+ // the first time a missing source is encountered).
820
+ let promisedFiles = null;
821
+ function getPromisedFiles() {
822
+ if (promisedFiles === null) {
823
+ promisedFiles = collectPromisedFilesAtOrAfterWave(phaseDir, isNaN(currentWave) ? 1 : currentWave);
824
+ }
825
+ return promisedFiles;
826
+ }
827
+ const results = [];
828
+ let pendingCount = 0;
829
+ for (const link of keyLinks) {
830
+ if (typeof link === 'string')
831
+ continue;
832
+ const check = {
833
+ from: link['from'],
834
+ to: link['to'],
835
+ via: link['via'] || '',
836
+ verified: false,
837
+ detail: '',
838
+ };
839
+ const fromPath = link['from'] || '';
840
+ const sourceContent = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(cwd, fromPath));
841
+ if (!sourceContent) {
842
+ // Check if the missing file is promised by a plan at the same or later wave.
843
+ const promised = getPromisedFiles();
844
+ const isPromised = fromPath.trim() !== '' && promised.has(fromPath.trim());
845
+ if (isPromised) {
846
+ check['pending'] = true;
847
+ check['detail'] = 'Source file not yet created — declared in files_modified of a same-or-later-wave plan';
848
+ pendingCount++;
849
+ }
850
+ else {
851
+ check['detail'] = 'Source file not found (from: must be a relative file path; describe components/endpoints in via:)';
852
+ }
853
+ }
854
+ else if (link['pattern']) {
855
+ try {
856
+ const regex = new RegExp(link['pattern']);
857
+ if (regex.test(sourceContent)) {
858
+ check['verified'] = true;
859
+ check['detail'] = 'Pattern found in source';
860
+ }
861
+ else {
862
+ const targetContent = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(cwd, link['to'] || ''));
863
+ if (targetContent && regex.test(targetContent)) {
864
+ check['verified'] = true;
865
+ check['detail'] = 'Pattern found in target';
866
+ }
867
+ else {
868
+ check['detail'] = `Pattern "${link['pattern']}" not found in source or target`;
869
+ }
870
+ }
871
+ }
872
+ catch {
873
+ check['detail'] = `Invalid regex pattern: ${link['pattern']}`;
874
+ }
875
+ }
876
+ else {
877
+ if (sourceContent.includes(link['to'] || '')) {
878
+ check['verified'] = true;
879
+ check['detail'] = 'Target referenced in source';
880
+ }
881
+ else {
882
+ check['detail'] = 'Target not referenced in source';
883
+ }
884
+ }
885
+ results.push(check);
886
+ }
887
+ const verified = results.filter((r) => r['verified']).length;
888
+ // A pending link (from: file promised by a same-or-later-wave plan) is not a
889
+ // hard failure — it should not count against the all_verified gate (#1202).
890
+ const hardFailed = results.filter((r) => !r['verified'] && !r['pending']).length;
891
+ const allVerified = hardFailed === 0;
892
+ output({
893
+ all_verified: allVerified,
894
+ verified,
895
+ pending: pendingCount,
896
+ total: results.length,
897
+ links: results,
898
+ }, raw, allVerified ? 'valid' : 'invalid');
899
+ }
900
+ function listMilestoneArchiveDirs(planBase) {
901
+ const milestonesDir = node_path_1.default.join(planBase, 'milestones');
902
+ try {
903
+ return node_fs_1.default
904
+ .readdirSync(milestonesDir, { withFileTypes: true })
905
+ .filter((e) => e.isDirectory() && validate_cjs_2.MILESTONE_ARCHIVE_DIR_RE.test(e.name))
906
+ .map((e) => node_path_1.default.join(milestonesDir, e.name))
907
+ .sort((a, b) => node_path_1.default.basename(a).localeCompare(node_path_1.default.basename(b), undefined, { numeric: true }));
908
+ }
909
+ catch {
910
+ return [];
911
+ }
912
+ }
913
+ function forEachArchivedPhaseToken(planBase, onPhase) {
914
+ for (const archiveDir of listMilestoneArchiveDirs(planBase)) {
915
+ try {
916
+ const entries = node_fs_1.default.readdirSync(archiveDir, { withFileTypes: true });
917
+ for (const e of entries) {
918
+ if (!e.isDirectory())
919
+ continue;
920
+ const m = e.name.match(validate_cjs_2.PHASE_TOKEN_FROM_DIR_RE);
921
+ if (m)
922
+ onPhase(m[1]);
923
+ }
924
+ }
925
+ catch {
926
+ /* archive dir absent/unreadable */
927
+ }
928
+ }
929
+ }
930
+ function getActiveMilestoneArchiveDir(planBase) {
931
+ const archiveDirs = listMilestoneArchiveDirs(planBase);
932
+ if (archiveDirs.length === 0)
933
+ return null;
934
+ try {
935
+ const statePath = node_path_1.default.join(planBase, 'STATE.md');
936
+ if (node_fs_1.default.existsSync(statePath)) {
937
+ const state = node_fs_1.default.readFileSync(statePath, 'utf-8');
938
+ const m = state.match(/^\s*(?:\*\*)?milestone(?:\*\*)?:\s*\*{0,2}\s*([^\s*\r\n#][^\s\r\n#]*)/mi);
939
+ if (m && m[1]) {
940
+ const milestone = m[1].trim();
941
+ const candidate = node_path_1.default.join(planBase, 'milestones', `${milestone}-phases`);
942
+ return archiveDirs.includes(candidate) ? candidate : null;
943
+ }
944
+ }
945
+ }
946
+ catch {
947
+ /* intentionally empty — fall through to version-sort below */
948
+ }
949
+ return archiveDirs[archiveDirs.length - 1];
950
+ }
951
+ function collectPhaseRoots(planBase) {
952
+ const roots = [];
953
+ const flatPhasesDir = node_path_1.default.join(planBase, 'phases');
954
+ if (node_fs_1.default.existsSync(flatPhasesDir))
955
+ roots.push(flatPhasesDir);
956
+ const activeArchive = getActiveMilestoneArchiveDir(planBase);
957
+ if (activeArchive)
958
+ roots.push(activeArchive);
959
+ return roots;
960
+ }
961
+ function collectDiskPhases(planBase) {
962
+ const diskPhases = new Set();
963
+ const phaseRoots = collectPhaseRoots(planBase);
964
+ const scanDir = (dir) => {
965
+ try {
966
+ const entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
967
+ for (const e of entries) {
968
+ if (e.isDirectory()) {
969
+ const m = e.name.match(validate_cjs_2.PHASE_TOKEN_FROM_DIR_RE);
970
+ if (m)
971
+ diskPhases.add(m[1]);
972
+ }
973
+ }
974
+ }
975
+ catch {
976
+ /* dir absent */
977
+ }
978
+ };
979
+ for (const root of phaseRoots)
980
+ scanDir(root);
981
+ return diskPhases;
982
+ }
983
+ function checkMilestonePrefixMismatches(roadmapContent, { getMilestoneFromPhaseId }) {
984
+ const mismatches = [];
985
+ const sections = [];
986
+ const sectionRx = /^#{1,3}\s+(?:\[[^\]]+\]\s*)?.*v(\d+\.\d+)/gim;
987
+ let m;
988
+ while ((m = sectionRx.exec(roadmapContent)) !== null) {
989
+ if (sections.length > 0)
990
+ sections[sections.length - 1].end = m.index;
991
+ sections.push({ version: `v${m[1]}`, start: m.index, end: roadmapContent.length });
992
+ }
993
+ for (const section of sections) {
994
+ const content = roadmapContent.slice(section.start, section.end);
995
+ const phaseRx = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*)\s*:/gi;
996
+ let pm;
997
+ while ((pm = phaseRx.exec(content)) !== null) {
998
+ const phaseId = pm[1];
999
+ const expectedMilestone = getMilestoneFromPhaseId(phaseId);
1000
+ if (expectedMilestone !== null && expectedMilestone !== section.version) {
1001
+ mismatches.push({
1002
+ phaseId,
1003
+ foundInMilestone: section.version,
1004
+ expectedMilestone,
1005
+ });
1006
+ }
1007
+ }
1008
+ }
1009
+ return mismatches;
1010
+ }
1011
+ function cmdValidateConsistency(cwd, raw) {
1012
+ const planBase = planningDir(cwd);
1013
+ const roadmapPath = node_path_1.default.join(planBase, 'ROADMAP.md');
1014
+ const errors = [];
1015
+ const warnings = [];
1016
+ if (!node_fs_1.default.existsSync(roadmapPath)) {
1017
+ errors.push('ROADMAP.md not found');
1018
+ output({ passed: false, errors, warnings }, raw, 'failed');
1019
+ return;
1020
+ }
1021
+ const roadmapContentRaw = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
1022
+ const roadmapContent = extractCurrentMilestone(roadmapContentRaw, cwd);
1023
+ const { roadmapPhases } = (0, validate_cjs_1.buildRoadmapPhaseVariants)(roadmapContent);
1024
+ const { roadmapPhaseVariants: fullRoadmapPhaseVariants } = (0, validate_cjs_1.buildRoadmapPhaseVariants)(roadmapContentRaw);
1025
+ const diskPhases = collectDiskPhases(planBase);
1026
+ for (const p of roadmapPhases) {
1027
+ if (!diskPhases.has(p) && !diskPhases.has(normalizePhaseName(p))) {
1028
+ warnings.push(`Phase ${p} in ROADMAP.md but no directory on disk`);
1029
+ }
1030
+ }
1031
+ for (const p of diskPhases) {
1032
+ const variants = (0, validate_cjs_1.phaseVariants)(p);
1033
+ if (![...variants].some((v) => fullRoadmapPhaseVariants.has(v))) {
1034
+ warnings.push(`Phase ${p} exists on disk but not in ROADMAP.md`);
1035
+ }
1036
+ }
1037
+ const config = loadConfig(cwd);
1038
+ if (config.phase_naming !== 'custom') {
1039
+ const integerPhases = [...diskPhases]
1040
+ .filter((p) => !p.includes('.'))
1041
+ .map((p) => parseInt(p, 10))
1042
+ .sort((a, b) => a - b);
1043
+ for (let i = 1; i < integerPhases.length; i++) {
1044
+ if (integerPhases[i] !== integerPhases[i - 1] + 1) {
1045
+ warnings.push(`Gap in phase numbering: ${integerPhases[i - 1]} → ${integerPhases[i]}`);
1046
+ }
1047
+ }
1048
+ }
1049
+ const phaseRoots = collectPhaseRoots(planBase);
1050
+ for (const phaseRoot of phaseRoots) {
1051
+ try {
1052
+ const entries = node_fs_1.default.readdirSync(phaseRoot, { withFileTypes: true });
1053
+ const dirs = entries
1054
+ .filter((e) => e.isDirectory())
1055
+ .map((e) => e.name)
1056
+ .sort();
1057
+ for (const dir of dirs) {
1058
+ const phasePath = node_path_1.default.join(phaseRoot, dir);
1059
+ const phaseLabel = node_path_1.default.relative(planBase, phasePath).replace(/\\/g, '/');
1060
+ const phaseFiles = node_fs_1.default.readdirSync(phasePath);
1061
+ const plans = phaseFiles.filter((f) => f.endsWith('-PLAN.md')).sort();
1062
+ const planNums = plans
1063
+ .map((p) => {
1064
+ const pm = p.match(/-(\d{2})-PLAN\.md$/);
1065
+ return pm ? parseInt(pm[1], 10) : null;
1066
+ })
1067
+ .filter((n) => n !== null);
1068
+ for (let i = 1; i < planNums.length; i++) {
1069
+ if (planNums[i] !== planNums[i - 1] + 1) {
1070
+ warnings.push(`Gap in plan numbering in ${phaseLabel}: plan ${planNums[i - 1]} → ${planNums[i]}`);
1071
+ }
1072
+ }
1073
+ const summaries = phaseFiles.filter((f) => f.endsWith('-SUMMARY.md'));
1074
+ const planIds = new Set(plans.map((p) => p.replace('-PLAN.md', '')));
1075
+ const summaryIds = new Set(summaries.map((s) => s.replace('-SUMMARY.md', '')));
1076
+ for (const sid of summaryIds) {
1077
+ if (!planIds.has(sid)) {
1078
+ warnings.push(`Summary ${sid}-SUMMARY.md in ${phaseLabel} has no matching PLAN.md`);
1079
+ }
1080
+ }
1081
+ for (const plan of plans) {
1082
+ const content = node_fs_1.default.readFileSync(node_path_1.default.join(phasePath, plan), 'utf-8');
1083
+ const fmData = extractFrontmatter(content);
1084
+ if (!fmData['wave']) {
1085
+ warnings.push(`${phaseLabel}/${plan}: missing 'wave' in frontmatter`);
1086
+ }
1087
+ }
1088
+ }
1089
+ }
1090
+ catch {
1091
+ /* intentionally empty */
1092
+ }
1093
+ }
1094
+ const passed = errors.length === 0;
1095
+ output({ passed, errors, warnings, warning_count: warnings.length }, raw, passed ? 'passed' : 'failed');
1096
+ }
1097
+ function cmdValidateHealth(cwd, options, raw) {
1098
+ const resolved = node_path_1.default.resolve(cwd);
1099
+ if (resolved === node_os_1.default.homedir()) {
1100
+ output({
1101
+ status: 'error',
1102
+ errors: [
1103
+ {
1104
+ code: 'E010',
1105
+ message: `CWD is home directory (${resolved}) — health check would read the wrong .planning/ directory. Run from your project root instead.`,
1106
+ fix: 'cd into your project directory and retry',
1107
+ },
1108
+ ],
1109
+ warnings: [],
1110
+ info: [{ code: 'I010', message: `Resolved CWD: ${resolved}` }],
1111
+ repairable_count: 0,
1112
+ }, raw);
1113
+ return;
1114
+ }
1115
+ const planBase = planningDir(cwd);
1116
+ const projectPath = node_path_1.default.join(planBase, 'PROJECT.md');
1117
+ const roadmapPath = node_path_1.default.join(planBase, 'ROADMAP.md');
1118
+ const statePath = node_path_1.default.join(planBase, 'STATE.md');
1119
+ const configPath = node_path_1.default.join(planBase, 'config.json');
1120
+ const phasesDir = node_path_1.default.join(planBase, 'phases');
1121
+ const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
1122
+ const slash = (name) => (0, runtime_slash_cjs_1.formatGsdSlash)(name, _slashRuntime);
1123
+ const errors = [];
1124
+ const warnings = [];
1125
+ const info = [];
1126
+ const repairs = [];
1127
+ const addIssue = (severity, code, message, fix, repairable = false) => {
1128
+ const issue = { code, message, fix, repairable };
1129
+ if (severity === 'error')
1130
+ errors.push(issue);
1131
+ else if (severity === 'warning')
1132
+ warnings.push(issue);
1133
+ else
1134
+ info.push(issue);
1135
+ };
1136
+ if (!node_fs_1.default.existsSync(planBase)) {
1137
+ addIssue('error', 'E001', '.planning/ directory not found', `Run ${slash('new-project')} to initialize`);
1138
+ output({ status: 'broken', errors, warnings, info, repairable_count: 0 }, raw);
1139
+ return;
1140
+ }
1141
+ if (!node_fs_1.default.existsSync(projectPath)) {
1142
+ addIssue('error', 'E002', 'PROJECT.md not found', `Run ${slash('new-project')} to create`);
1143
+ }
1144
+ else {
1145
+ const content = node_fs_1.default.readFileSync(projectPath, 'utf-8');
1146
+ const requiredSections = ['## What This Is', '## Core Value', '## Requirements'];
1147
+ for (const section of requiredSections) {
1148
+ if (!content.includes(section)) {
1149
+ addIssue('warning', 'W001', `PROJECT.md missing section: ${section}`, 'Add section manually');
1150
+ }
1151
+ }
1152
+ }
1153
+ if (!node_fs_1.default.existsSync(roadmapPath)) {
1154
+ addIssue('error', 'E003', 'ROADMAP.md not found', `Run ${slash('new-milestone')} to create roadmap`);
1155
+ }
1156
+ if (!node_fs_1.default.existsSync(statePath)) {
1157
+ addIssue('error', 'E004', 'STATE.md not found', `Run ${slash('health')} --repair to regenerate`, true);
1158
+ repairs.push('regenerateState');
1159
+ }
1160
+ else {
1161
+ const stateContent = node_fs_1.default.readFileSync(statePath, 'utf-8');
1162
+ const phaseRefs = [...stateContent.matchAll(/[Pp]hase\s+(\d+[A-Z]?(?:\.\d+)*)/g)].map((m) => m[1]);
1163
+ const validPhases = collectDiskPhases(planBase);
1164
+ try {
1165
+ if (node_fs_1.default.existsSync(roadmapPath)) {
1166
+ const roadmapRaw = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
1167
+ const all = [...roadmapRaw.matchAll(/#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)/gi)];
1168
+ for (const m of all)
1169
+ validPhases.add(m[1]);
1170
+ }
1171
+ }
1172
+ catch {
1173
+ /* intentionally empty */
1174
+ }
1175
+ forEachArchivedPhaseToken(planBase, (token) => validPhases.add(token));
1176
+ const normalizedValid = new Set();
1177
+ for (const p of validPhases) {
1178
+ normalizedValid.add(p);
1179
+ const dotIdx = p.indexOf('.');
1180
+ const head = dotIdx === -1 ? p : p.slice(0, dotIdx);
1181
+ const tail = dotIdx === -1 ? '' : p.slice(dotIdx);
1182
+ if (/^\d+$/.test(head)) {
1183
+ normalizedValid.add(head.padStart(2, '0') + tail);
1184
+ }
1185
+ }
1186
+ for (const ref of phaseRefs) {
1187
+ const dotIdx = ref.indexOf('.');
1188
+ const head = dotIdx === -1 ? ref : ref.slice(0, dotIdx);
1189
+ const tail = dotIdx === -1 ? '' : ref.slice(dotIdx);
1190
+ const padded = /^\d+$/.test(head) ? head.padStart(2, '0') + tail : ref;
1191
+ if (!normalizedValid.has(ref) && !normalizedValid.has(padded)) {
1192
+ if (normalizedValid.size > 0) {
1193
+ addIssue('warning', 'W002', `STATE.md references phase ${ref}, but only phases ${[...validPhases].sort((a, b) => a.localeCompare(b, undefined, { numeric: true })).join(', ')} are declared`, `Review STATE.md manually before changing it; ${slash('health')} --repair will not overwrite an existing STATE.md for phase mismatches`);
1194
+ }
1195
+ }
1196
+ }
1197
+ }
1198
+ if (!node_fs_1.default.existsSync(configPath)) {
1199
+ addIssue('warning', 'W003', 'config.json not found', `Run ${slash('health')} --repair to create with defaults`, true);
1200
+ repairs.push('createConfig');
1201
+ }
1202
+ else {
1203
+ try {
1204
+ const rawCfg = node_fs_1.default.readFileSync(configPath, 'utf-8');
1205
+ const parsed = JSON.parse(rawCfg);
1206
+ const validProfiles = ['quality', 'balanced', 'budget', 'inherit'];
1207
+ if (parsed['model_profile'] && !validProfiles.includes(parsed['model_profile'])) {
1208
+ addIssue('warning', 'W004', `config.json: invalid model_profile "${parsed['model_profile']}"`, `Valid values: ${validProfiles.join(', ')}`);
1209
+ }
1210
+ }
1211
+ catch (err) {
1212
+ addIssue('error', 'E005', `config.json: JSON parse error - ${err instanceof Error ? err.message : String(err)}`, `Run ${slash('health')} --repair to reset to defaults`, true);
1213
+ repairs.push('resetConfig');
1214
+ }
1215
+ }
1216
+ if (node_fs_1.default.existsSync(configPath)) {
1217
+ try {
1218
+ const configRaw = node_fs_1.default.readFileSync(configPath, 'utf-8');
1219
+ const configParsed = JSON.parse(configRaw);
1220
+ const workflow = configParsed['workflow'];
1221
+ if (workflow && workflow['nyquist_validation'] === undefined) {
1222
+ addIssue('warning', 'W008', 'config.json: workflow.nyquist_validation absent (defaults to enabled but agents may skip)', `Run ${slash('health')} --repair to add key`, true);
1223
+ if (!repairs.includes('addNyquistKey'))
1224
+ repairs.push('addNyquistKey');
1225
+ }
1226
+ if (workflow && workflow['ai_integration_phase'] === undefined) {
1227
+ addIssue('warning', 'W016', `config.json: workflow.ai_integration_phase absent (defaults to enabled — run ${slash('ai-integration-phase')} before planning AI system phases)`, `Run ${slash('health')} --repair to add key`, true);
1228
+ if (!repairs.includes('addAiIntegrationPhaseKey'))
1229
+ repairs.push('addAiIntegrationPhaseKey');
1230
+ }
1231
+ }
1232
+ catch {
1233
+ /* intentionally empty */
1234
+ }
1235
+ }
1236
+ let phaseDirEntries = [];
1237
+ const phaseDirFiles = new Map();
1238
+ try {
1239
+ phaseDirEntries = node_fs_1.default
1240
+ .readdirSync(phasesDir, { withFileTypes: true })
1241
+ .filter((e) => e.isDirectory());
1242
+ for (const e of phaseDirEntries) {
1243
+ try {
1244
+ phaseDirFiles.set(e.name, node_fs_1.default.readdirSync(node_path_1.default.join(phasesDir, e.name)));
1245
+ }
1246
+ catch {
1247
+ phaseDirFiles.set(e.name, []);
1248
+ }
1249
+ }
1250
+ }
1251
+ catch {
1252
+ /* intentionally empty */
1253
+ }
1254
+ for (const e of phaseDirEntries) {
1255
+ if (!e.name.match(validate_cjs_2.phaseDirNameRe)) {
1256
+ addIssue('warning', 'W005', `Phase directory "${e.name}" doesn't follow NN-name format`, 'Rename to match pattern (e.g., 01-setup)');
1257
+ }
1258
+ }
1259
+ for (const e of phaseDirEntries) {
1260
+ const phaseFiles = phaseDirFiles.get(e.name) || [];
1261
+ const plans = phaseFiles.filter((f) => f.endsWith('-PLAN.md') || f === 'PLAN.md');
1262
+ const summaries = phaseFiles.filter((f) => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
1263
+ const summaryBases = new Set();
1264
+ for (const s of summaries) {
1265
+ const summaryBase = s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
1266
+ summaryBases.add(summaryBase);
1267
+ summaryBases.add((0, validate_cjs_2.canonicalPlanStem)(summaryBase));
1268
+ }
1269
+ for (const plan of plans) {
1270
+ const planBase = plan.replace('-PLAN.md', '').replace('PLAN.md', '');
1271
+ const canonicalBase = (0, validate_cjs_2.canonicalPlanStem)(planBase);
1272
+ if (!summaryBases.has(planBase) && !summaryBases.has(canonicalBase)) {
1273
+ addIssue('info', 'I001', `${e.name}/${plan} has no SUMMARY.md`, 'May be in progress');
1274
+ }
1275
+ }
1276
+ }
1277
+ for (const e of phaseDirEntries) {
1278
+ const phaseFiles = phaseDirFiles.get(e.name) || [];
1279
+ const hasResearch = phaseFiles.some((f) => f.endsWith('-RESEARCH.md'));
1280
+ const hasValidation = phaseFiles.some((f) => f.endsWith('-VALIDATION.md'));
1281
+ if (hasResearch && !hasValidation) {
1282
+ const researchFile = phaseFiles.find((f) => f.endsWith('-RESEARCH.md'));
1283
+ try {
1284
+ const researchContent = node_fs_1.default.readFileSync(node_path_1.default.join(phasesDir, e.name, researchFile), 'utf-8');
1285
+ if (researchContent.includes('## Validation Architecture')) {
1286
+ addIssue('warning', 'W009', `Phase ${e.name}: has Validation Architecture in RESEARCH.md but no VALIDATION.md`, `Re-run ${slash('plan-phase')} with --research to regenerate`);
1287
+ }
1288
+ }
1289
+ catch {
1290
+ /* intentionally empty */
1291
+ }
1292
+ }
1293
+ }
1294
+ try {
1295
+ const agentStatus = checkAgentsInstalled();
1296
+ if (!agentStatus.agents_installed) {
1297
+ if ((agentStatus.installed_agents).length === 0) {
1298
+ addIssue('warning', 'W010', `No GSD agents found in ${agentStatus.agents_dir} — Task(subagent_type="gsd-*") will fall back to general-purpose`, `Run the GSD installer: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`);
1299
+ }
1300
+ else if ((agentStatus.incomplete_agents).length > 0 && (agentStatus.missing_agents).length === 0) {
1301
+ addIssue('warning', 'W010', `Incomplete agent installs (missing generated file): ${(agentStatus.incomplete_agents).join(', ')} — affected workflows may fall back to general-purpose`, `Re-run the GSD installer to complete the install: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`);
1302
+ }
1303
+ else if ((agentStatus.incomplete_agents).length > 0) {
1304
+ addIssue('warning', 'W010', `Missing ${(agentStatus.missing_agents).length} GSD agents: ${(agentStatus.missing_agents).join(', ')}; incomplete agent installs (missing generated file): ${(agentStatus.incomplete_agents).join(', ')} — affected workflows will fall back to general-purpose`, `Run the GSD installer: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`);
1305
+ }
1306
+ else {
1307
+ addIssue('warning', 'W010', `Missing ${(agentStatus.missing_agents).length} GSD agents: ${(agentStatus.missing_agents).join(', ')} — affected workflows will fall back to general-purpose`, `Run the GSD installer: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`);
1308
+ }
1309
+ }
1310
+ }
1311
+ catch {
1312
+ /* intentionally empty — agent check is non-blocking */
1313
+ }
1314
+ if (node_fs_1.default.existsSync(roadmapPath)) {
1315
+ const roadmapContentRaw = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
1316
+ const roadmapContent = extractCurrentMilestone(roadmapContentRaw, cwd);
1317
+ const { roadmapPhases } = (0, validate_cjs_1.buildRoadmapPhaseVariants)(roadmapContent);
1318
+ const { roadmapPhaseVariants: fullRoadmapPhaseVariants } = (0, validate_cjs_1.buildRoadmapPhaseVariants)(roadmapContentRaw);
1319
+ const diskPhases = collectDiskPhases(planBase);
1320
+ forEachArchivedPhaseToken(planBase, (token) => diskPhases.add(token));
1321
+ const activeDiskPhases = collectDiskPhases(planBase);
1322
+ const notStartedPhases = (0, validate_cjs_1.buildNotStartedPhaseVariants)(roadmapContent);
1323
+ for (const p of roadmapPhases) {
1324
+ const variants = (0, validate_cjs_1.phaseVariants)(p);
1325
+ const existsOnDisk = [...variants].some((v) => diskPhases.has(v));
1326
+ if (!existsOnDisk) {
1327
+ const isNotStarted = [...variants].some((v) => notStartedPhases.has(v));
1328
+ if (isNotStarted)
1329
+ continue;
1330
+ addIssue('warning', 'W006', `Phase ${p} in ROADMAP.md but no directory on disk`, 'Create phase directory or remove from roadmap');
1331
+ }
1332
+ }
1333
+ for (const p of activeDiskPhases) {
1334
+ const variants = (0, validate_cjs_1.phaseVariants)(p);
1335
+ if (![...variants].some((v) => fullRoadmapPhaseVariants.has(v))) {
1336
+ addIssue('warning', 'W007', `Phase ${p} exists on disk but not in ROADMAP.md`, 'Add to roadmap or remove directory');
1337
+ }
1338
+ }
1339
+ }
1340
+ if (node_fs_1.default.existsSync(statePath) && node_fs_1.default.existsSync(roadmapPath)) {
1341
+ try {
1342
+ const stateContent = node_fs_1.default.readFileSync(statePath, 'utf-8');
1343
+ const roadmapContentFull = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
1344
+ const currentPhaseMatch = stateContent.match(/\*\*Current Phase:\*\*\s*(\S+)/i) ||
1345
+ stateContent.match(/Current Phase:\s*(\S+)/i);
1346
+ if (currentPhaseMatch) {
1347
+ const statePhase = currentPhaseMatch[1].replace(/^0+/, '');
1348
+ const phaseCheckboxRe = new RegExp(`-\\s*\\[x\\].*Phase\\s+0*${escapeRegex(statePhase)}[:\\s]`, 'i');
1349
+ if (phaseCheckboxRe.test(roadmapContentFull)) {
1350
+ const stateStatus = stateContent.match(/\*\*Status:\*\*\s*(.+)/i);
1351
+ const statusVal = stateStatus ? stateStatus[1].trim().toLowerCase() : '';
1352
+ if (statusVal !== 'complete' && statusVal !== 'done') {
1353
+ addIssue('warning', 'W011', `STATE.md says current phase is ${statePhase} (status: ${statusVal || 'unknown'}) but ROADMAP.md shows it as [x] complete — state files may be out of sync`, `Run ${slash('progress')} to re-derive current position, or manually update STATE.md`);
1354
+ }
1355
+ }
1356
+ }
1357
+ }
1358
+ catch {
1359
+ /* intentionally empty — cross-validation is advisory */
1360
+ }
1361
+ }
1362
+ if (node_fs_1.default.existsSync(configPath)) {
1363
+ try {
1364
+ const configRaw = node_fs_1.default.readFileSync(configPath, 'utf-8');
1365
+ const configParsed = JSON.parse(configRaw);
1366
+ const validStrategies = ['none', 'phase', 'milestone'];
1367
+ if (configParsed['branching_strategy'] &&
1368
+ !validStrategies.includes(configParsed['branching_strategy'])) {
1369
+ addIssue('warning', 'W012', `config.json: invalid branching_strategy "${configParsed['branching_strategy']}"`, `Valid values: ${validStrategies.join(', ')}`);
1370
+ }
1371
+ if (configParsed['context_window'] !== undefined) {
1372
+ const cw = configParsed['context_window'];
1373
+ if (typeof cw !== 'number' || cw <= 0 || !Number.isInteger(cw)) {
1374
+ addIssue('warning', 'W013', `config.json: context_window should be a positive integer, got "${cw}"`, 'Set to 200000 (default) or 1000000 (for 1M models)');
1375
+ }
1376
+ }
1377
+ if (configParsed['phase_branch_template'] &&
1378
+ !configParsed['phase_branch_template'].includes('{phase}')) {
1379
+ addIssue('warning', 'W014', 'config.json: phase_branch_template missing {phase} placeholder', 'Template must include {phase} for phase number substitution');
1380
+ }
1381
+ if (configParsed['milestone_branch_template'] &&
1382
+ !configParsed['milestone_branch_template'].includes('{milestone}')) {
1383
+ addIssue('warning', 'W015', 'config.json: milestone_branch_template missing {milestone} placeholder', 'Template must include {milestone} for version substitution');
1384
+ }
1385
+ }
1386
+ catch {
1387
+ /* parse error already caught in Check 5 */
1388
+ }
1389
+ }
1390
+ try {
1391
+ const worktreeHealth = inspectWorktreeHealth(cwd, { staleAfterMs: 60 * 60 * 1000 }, { execGit: shell_command_projection_cjs_1.execGit, existsSync: node_fs_1.default.existsSync, statSync: node_fs_1.default.statSync });
1392
+ if (!worktreeHealth['ok']) {
1393
+ if (worktreeHealth['reason'] === 'git_timed_out') {
1394
+ addIssue('warning', 'W020', 'Worktree health check degraded: git worktree list timed out after 10s — orphan/stale worktrees could not be inspected', 'Run: git worktree list --porcelain to diagnose; check for .git/index.lock or a hung git process');
1395
+ }
1396
+ if (worktreeHealth['reason'] === 'git_list_failed') {
1397
+ addIssue('warning', 'W020', 'Worktree health check degraded: git worktree list failed — orphan/stale worktrees could not be inspected', 'Run: git worktree list --porcelain to diagnose; check git repository state and permissions');
1398
+ }
1399
+ }
1400
+ else {
1401
+ for (const finding of worktreeHealth['findings']) {
1402
+ if (finding['kind'] === 'orphan') {
1403
+ addIssue('warning', 'W017', `Orphan git worktree: ${finding['path']} (path no longer exists on disk)`, 'Run: git worktree prune');
1404
+ continue;
1405
+ }
1406
+ if (finding['kind'] === 'stale') {
1407
+ addIssue('warning', 'W017', `Stale git worktree: ${finding['path']} (last modified ${finding['ageMinutes']} minutes ago)`, `Run: git worktree remove ${finding['path']} --force`);
1408
+ }
1409
+ }
1410
+ }
1411
+ }
1412
+ catch {
1413
+ /* git worktree not available or not a git repo — skip silently */
1414
+ }
1415
+ try {
1416
+ const phaseConvention = (() => {
1417
+ if (!node_fs_1.default.existsSync(configPath))
1418
+ return null;
1419
+ try {
1420
+ const configRaw = node_fs_1.default.readFileSync(configPath, 'utf-8');
1421
+ const configParsed = JSON.parse(configRaw);
1422
+ return configParsed['phase_id_convention'] || null;
1423
+ }
1424
+ catch {
1425
+ return null;
1426
+ }
1427
+ })();
1428
+ if (phaseConvention === 'milestone-prefixed') {
1429
+ if (node_fs_1.default.existsSync(roadmapPath)) {
1430
+ const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
1431
+ const mismatches = checkMilestonePrefixMismatches(roadmapContent, {
1432
+ getMilestoneFromPhaseId: getMilestoneFromPhaseId,
1433
+ });
1434
+ for (const mm of mismatches) {
1435
+ addIssue('warning', 'W021', `Phase ${mm.phaseId}: integer prefix implies ${mm.expectedMilestone} but listed under ${mm.foundInMilestone}`, 'Run `gsd-tools roadmap upgrade --convention milestone-prefixed` to migrate (dry-run by default)');
1436
+ }
1437
+ }
1438
+ }
1439
+ }
1440
+ catch {
1441
+ /* W021 check is advisory — skip on error */
1442
+ }
1443
+ const milestonesPath = node_path_1.default.join(planBase, 'MILESTONES.md');
1444
+ const milestonesArchiveDir = node_path_1.default.join(planBase, 'milestones');
1445
+ const missingFromRegistry = [];
1446
+ try {
1447
+ if (node_fs_1.default.existsSync(milestonesArchiveDir)) {
1448
+ const archiveFiles = node_fs_1.default.readdirSync(milestonesArchiveDir);
1449
+ const archivedVersions = archiveFiles
1450
+ .map((f) => f.match(/^(v\d+\.\d+(?:\.\d+)?)-ROADMAP\.md$/))
1451
+ .filter(Boolean)
1452
+ .map((m) => m[1]);
1453
+ if (archivedVersions.length > 0) {
1454
+ const registryContent = node_fs_1.default.existsSync(milestonesPath)
1455
+ ? node_fs_1.default.readFileSync(milestonesPath, 'utf-8')
1456
+ : '';
1457
+ for (const ver of archivedVersions) {
1458
+ if (!registryContent.includes(`## ${ver}`)) {
1459
+ missingFromRegistry.push(ver);
1460
+ }
1461
+ }
1462
+ if (missingFromRegistry.length > 0) {
1463
+ addIssue('warning', 'W018', `MILESTONES.md missing ${missingFromRegistry.length} archived milestone(s): ${missingFromRegistry.join(', ')}`, `Run ${slash('health')} --backfill to synthesize missing entries from archive snapshots`, true);
1464
+ repairs.push('backfillMilestones');
1465
+ }
1466
+ }
1467
+ }
1468
+ }
1469
+ catch {
1470
+ /* intentionally empty — milestone sync check is advisory */
1471
+ }
1472
+ try {
1473
+ const entries = node_fs_1.default.readdirSync(planBase, { withFileTypes: true });
1474
+ for (const entry of entries) {
1475
+ if (!entry.isFile())
1476
+ continue;
1477
+ if (!entry.name.endsWith('.md'))
1478
+ continue;
1479
+ if (!(0, artifacts_cjs_1.isCanonicalPlanningFile)(entry.name)) {
1480
+ addIssue('warning', 'W019', `Unrecognized .planning/ file: ${entry.name} — not a canonical GSD artifact`, 'Move to .planning/milestones/ archive subdir or delete if stale. See templates/README.md for the canonical artifact list.', false);
1481
+ }
1482
+ }
1483
+ }
1484
+ catch {
1485
+ /* artifact check is advisory — skip on error */
1486
+ }
1487
+ try {
1488
+ if (node_fs_1.default.existsSync(statePath) && node_fs_1.default.existsSync(roadmapPath)) {
1489
+ const stateRaw = node_fs_1.default.readFileSync(statePath, 'utf-8');
1490
+ const statusMatch = stateRaw.match(/^status:\s*(.+)/im);
1491
+ const stateStatus = statusMatch ? statusMatch[1].trim().toLowerCase() : '';
1492
+ const isMarkedComplete = /milestone complete|archived/.test(stateStatus);
1493
+ if (isMarkedComplete) {
1494
+ const roadmapRaw = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
1495
+ const scopedContent = extractCurrentMilestone(roadmapRaw, cwd);
1496
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
1497
+ const unstarted = [];
1498
+ let pm;
1499
+ // Non-hoisted: load-order matters (circular dep guard)
1500
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
1501
+ const planningWorkspace2 = require('./planning-workspace.cjs');
1502
+ const phasesDir2 = planningWorkspace2.planningPaths(cwd).phases;
1503
+ const phaseDirNames2 = (() => {
1504
+ try {
1505
+ return node_fs_1.default
1506
+ .readdirSync(phasesDir2, { withFileTypes: true })
1507
+ .filter((e) => e.isDirectory())
1508
+ .map((e) => e.name);
1509
+ }
1510
+ catch {
1511
+ return [];
1512
+ }
1513
+ })();
1514
+ while ((pm = phasePattern.exec(scopedContent)) !== null) {
1515
+ const phaseNum = pm[1];
1516
+ const normalizedPh = normalizePhaseName(phaseNum);
1517
+ const hasDirectory = phaseDirNames2.some((d) => phaseTokenMatches(d, normalizedPh));
1518
+ if (!hasDirectory) {
1519
+ unstarted.push(phaseNum);
1520
+ }
1521
+ }
1522
+ if (unstarted.length > 0) {
1523
+ addIssue('warning', 'W021', `STATE says milestone complete but ROADMAP lists ${unstarted.length} unstarted phase(s) (e.g. Phase ${unstarted[0]})`, 'Run validate consistency or re-run complete-milestone after verifying all phases are done');
1524
+ }
1525
+ }
1526
+ }
1527
+ }
1528
+ catch {
1529
+ /* W021 check is advisory — skip on error */
1530
+ }
1531
+ // ─── Perform repairs if requested ─────────────────────────────────────────
1532
+ const repairActions = [];
1533
+ if (options['repair'] && repairs.length > 0) {
1534
+ for (const repair of repairs) {
1535
+ try {
1536
+ switch (repair) {
1537
+ case 'createConfig':
1538
+ case 'resetConfig': {
1539
+ const defaults = {
1540
+ model_profile: CONFIG_DEFAULTS.model_profile,
1541
+ commit_docs: CONFIG_DEFAULTS.commit_docs,
1542
+ search_gitignored: CONFIG_DEFAULTS.search_gitignored,
1543
+ branching_strategy: CONFIG_DEFAULTS.branching_strategy,
1544
+ phase_branch_template: CONFIG_DEFAULTS.phase_branch_template,
1545
+ milestone_branch_template: CONFIG_DEFAULTS.milestone_branch_template,
1546
+ quick_branch_template: CONFIG_DEFAULTS.quick_branch_template,
1547
+ workflow: {
1548
+ research: CONFIG_DEFAULTS.research,
1549
+ plan_check: CONFIG_DEFAULTS.plan_checker,
1550
+ verifier: CONFIG_DEFAULTS.verifier,
1551
+ nyquist_validation: CONFIG_DEFAULTS.nyquist_validation,
1552
+ },
1553
+ parallelization: CONFIG_DEFAULTS.parallelization,
1554
+ brave_search: CONFIG_DEFAULTS.brave_search,
1555
+ };
1556
+ (0, shell_command_projection_cjs_1.platformWriteSync)(configPath, JSON.stringify(defaults, null, 2));
1557
+ repairActions.push({ action: repair, success: true, path: 'config.json' });
1558
+ break;
1559
+ }
1560
+ case 'regenerateState': {
1561
+ if (node_fs_1.default.existsSync(statePath)) {
1562
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
1563
+ const backupPath = `${statePath}.bak-${timestamp}`;
1564
+ node_fs_1.default.copyFileSync(statePath, backupPath);
1565
+ repairActions.push({ action: 'backupState', success: true, path: backupPath });
1566
+ }
1567
+ const milestone = getMilestoneInfo(cwd);
1568
+ const projectRef = node_path_1.default
1569
+ .relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md'))
1570
+ .split(node_path_1.default.sep)
1571
+ .join('/');
1572
+ let stateContent = `# Session State\n\n`;
1573
+ stateContent += `## Project Reference\n\n`;
1574
+ stateContent += `See: ${projectRef}\n\n`;
1575
+ stateContent += `## Position\n\n`;
1576
+ stateContent += `**Milestone:** ${milestone.version} ${milestone.name}\n`;
1577
+ stateContent += `**Current phase:** (determining...)\n`;
1578
+ stateContent += `**Status:** Resuming\n\n`;
1579
+ stateContent += `## Session Log\n\n`;
1580
+ stateContent += `- ${new Date().toISOString().split('T')[0]}: STATE.md regenerated by ${slash('health')} --repair\n`;
1581
+ writeStateMd(statePath, stateContent, cwd);
1582
+ repairActions.push({ action: repair, success: true, path: 'STATE.md' });
1583
+ break;
1584
+ }
1585
+ case 'addNyquistKey': {
1586
+ if (node_fs_1.default.existsSync(configPath)) {
1587
+ try {
1588
+ const configRaw = node_fs_1.default.readFileSync(configPath, 'utf-8');
1589
+ const configParsed = JSON.parse(configRaw);
1590
+ if (!configParsed['workflow'])
1591
+ configParsed['workflow'] = {};
1592
+ const wf = configParsed['workflow'];
1593
+ if (wf['nyquist_validation'] === undefined) {
1594
+ wf['nyquist_validation'] = true;
1595
+ (0, shell_command_projection_cjs_1.platformWriteSync)(configPath, JSON.stringify(configParsed, null, 2));
1596
+ }
1597
+ repairActions.push({ action: repair, success: true, path: 'config.json' });
1598
+ }
1599
+ catch (err) {
1600
+ repairActions.push({
1601
+ action: repair,
1602
+ success: false,
1603
+ error: err instanceof Error ? err.message : String(err),
1604
+ });
1605
+ }
1606
+ }
1607
+ break;
1608
+ }
1609
+ case 'addAiIntegrationPhaseKey': {
1610
+ if (node_fs_1.default.existsSync(configPath)) {
1611
+ try {
1612
+ const configRaw = node_fs_1.default.readFileSync(configPath, 'utf-8');
1613
+ const configParsed = JSON.parse(configRaw);
1614
+ if (!configParsed['workflow'])
1615
+ configParsed['workflow'] = {};
1616
+ const wf = configParsed['workflow'];
1617
+ if (wf['ai_integration_phase'] === undefined) {
1618
+ wf['ai_integration_phase'] = true;
1619
+ (0, shell_command_projection_cjs_1.platformWriteSync)(configPath, JSON.stringify(configParsed, null, 2));
1620
+ }
1621
+ repairActions.push({ action: repair, success: true, path: 'config.json' });
1622
+ }
1623
+ catch (err) {
1624
+ repairActions.push({
1625
+ action: repair,
1626
+ success: false,
1627
+ error: err instanceof Error ? err.message : String(err),
1628
+ });
1629
+ }
1630
+ }
1631
+ break;
1632
+ }
1633
+ case 'backfillMilestones': {
1634
+ if (!options['backfill'] && !options['repair'])
1635
+ break;
1636
+ const today = new Date().toISOString().split('T')[0];
1637
+ let backfilled = 0;
1638
+ for (const ver of missingFromRegistry) {
1639
+ try {
1640
+ const snapshotPath = node_path_1.default.join(milestonesArchiveDir, `${ver}-ROADMAP.md`);
1641
+ const snapshot = (0, shell_command_projection_cjs_1.platformReadSync)(snapshotPath);
1642
+ const titleMatch = snapshot && snapshot.match(/^#\s+(.+)$/m);
1643
+ const milestoneName = titleMatch
1644
+ ? titleMatch[1].replace(/^Milestone\s+/i, '').replace(/^v[\d.]+\s*/, '').trim()
1645
+ : ver;
1646
+ const entry = `## ${ver}${milestoneName && milestoneName !== ver ? ` ${milestoneName}` : ''} (Backfilled: ${today})\n\n**Note:** Synthesized from archive snapshot by \`${slash('health')} --backfill\`. Original completion date unknown.\n\n---\n\n`;
1647
+ const milestonesContent = node_fs_1.default.existsSync(milestonesPath)
1648
+ ? node_fs_1.default.readFileSync(milestonesPath, 'utf-8')
1649
+ : '';
1650
+ if (!milestonesContent.trim()) {
1651
+ (0, shell_command_projection_cjs_1.platformWriteSync)(milestonesPath, `# Milestones\n\n${entry}`);
1652
+ }
1653
+ else {
1654
+ const headerMatch = milestonesContent.match(/^(#{1,3}\s+[^\n]*\n\n?)/);
1655
+ if (headerMatch) {
1656
+ const header = headerMatch[1];
1657
+ const rest = milestonesContent.slice(header.length);
1658
+ (0, shell_command_projection_cjs_1.platformWriteSync)(milestonesPath, header + entry + rest);
1659
+ }
1660
+ else {
1661
+ (0, shell_command_projection_cjs_1.platformWriteSync)(milestonesPath, entry + milestonesContent);
1662
+ }
1663
+ }
1664
+ backfilled++;
1665
+ }
1666
+ catch {
1667
+ /* intentionally empty — partial backfill is acceptable */
1668
+ }
1669
+ }
1670
+ repairActions.push({
1671
+ action: repair,
1672
+ success: true,
1673
+ detail: `Backfilled ${backfilled} milestone(s) into MILESTONES.md`,
1674
+ });
1675
+ break;
1676
+ }
1677
+ }
1678
+ }
1679
+ catch (err) {
1680
+ repairActions.push({
1681
+ action: repair,
1682
+ success: false,
1683
+ error: err instanceof Error ? err.message : String(err),
1684
+ });
1685
+ }
1686
+ }
1687
+ }
1688
+ let status;
1689
+ if (errors.length > 0) {
1690
+ status = 'broken';
1691
+ }
1692
+ else if (warnings.length > 0) {
1693
+ status = 'degraded';
1694
+ }
1695
+ else {
1696
+ status = 'healthy';
1697
+ }
1698
+ const repairableCount = errors.filter((e) => e.repairable).length + warnings.filter((w) => w.repairable).length;
1699
+ const result = {
1700
+ status,
1701
+ errors,
1702
+ warnings,
1703
+ info,
1704
+ repairable_count: repairableCount,
1705
+ repairs_performed: repairActions.length > 0 ? repairActions : undefined,
1706
+ };
1707
+ output(result, raw);
1708
+ return result;
1709
+ }
1710
+ function cmdValidateAgents(cwd, raw) {
1711
+ const agentStatus = checkAgentsInstalled();
1712
+ const expected = Object.keys(MODEL_PROFILES);
1713
+ output({
1714
+ agents_dir: agentStatus.agents_dir,
1715
+ agents_found: agentStatus.agents_installed,
1716
+ installed: agentStatus.installed_agents,
1717
+ missing: agentStatus.missing_agents,
1718
+ incomplete: agentStatus.incomplete_agents,
1719
+ expected,
1720
+ }, raw);
1721
+ }
1722
+ function cmdVerifySchemaDrift(cwd, phaseArg, skipFlag, raw) {
1723
+ if (!phaseArg) {
1724
+ error('Usage: verify schema-drift <phase> [--skip]');
1725
+ return;
1726
+ }
1727
+ const pDir = planningDir(cwd);
1728
+ const phasesDir = node_path_1.default.join(pDir, 'phases');
1729
+ if (!node_fs_1.default.existsSync(phasesDir)) {
1730
+ output({ block: false, drift_detected: false, blocking: false, message: 'No phases directory' }, raw);
1731
+ return;
1732
+ }
1733
+ let phaseDir = null;
1734
+ const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
1735
+ for (const entry of entries) {
1736
+ if (entry.isDirectory() && entry.name.includes(phaseArg)) {
1737
+ phaseDir = node_path_1.default.join(phasesDir, entry.name);
1738
+ break;
1739
+ }
1740
+ }
1741
+ if (!phaseDir) {
1742
+ const exact = node_path_1.default.join(phasesDir, phaseArg);
1743
+ if (node_fs_1.default.existsSync(exact))
1744
+ phaseDir = exact;
1745
+ }
1746
+ if (!phaseDir) {
1747
+ output({ block: false, drift_detected: false, blocking: false, message: `Phase directory not found: ${phaseArg}` }, raw);
1748
+ return;
1749
+ }
1750
+ const allFiles = [];
1751
+ const planFiles = node_fs_1.default.readdirSync(phaseDir).filter((f) => f.endsWith('-PLAN.md'));
1752
+ for (const pf of planFiles) {
1753
+ const content = node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, pf), 'utf-8');
1754
+ const fmMatch = content.match(/files_modified:\s*\[([^\]]*)\]/);
1755
+ if (fmMatch) {
1756
+ const files = fmMatch[1].split(',').map((f) => f.trim()).filter(Boolean);
1757
+ allFiles.push(...files);
1758
+ }
1759
+ }
1760
+ let executionLog = '';
1761
+ const summaryFiles = node_fs_1.default.readdirSync(phaseDir).filter((f) => f.endsWith('-SUMMARY.md'));
1762
+ for (const sf of summaryFiles) {
1763
+ executionLog += node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, sf), 'utf-8') + '\n';
1764
+ }
1765
+ const gitLog = (0, shell_command_projection_cjs_1.execGit)(['log', '--oneline', '--all', '-50'], { cwd });
1766
+ if (gitLog.exitCode === 0) {
1767
+ executionLog += '\n' + gitLog.stdout;
1768
+ }
1769
+ const result = (0, schema_detect_cjs_1.checkSchemaDrift)(allFiles, executionLog, { skipCheck: !!skipFlag });
1770
+ const isSkipped = !!result['skipped'];
1771
+ output({
1772
+ // Uniform gate contract: `block` = true means "this gate's bad condition is met".
1773
+ // When skipCheck is true (GSD_SKIP_SCHEMA_CHECK=true), the gate is bypassed —
1774
+ // block must be false regardless of whether drift was detected.
1775
+ // drift_detected and blocking are kept for compatibility.
1776
+ block: isSkipped ? false : !!result['driftDetected'],
1777
+ drift_detected: result['driftDetected'],
1778
+ blocking: result['blocking'],
1779
+ schema_files: result['schemaFiles'],
1780
+ orms: result['orms'],
1781
+ unpushed_orms: result['unpushedOrms'],
1782
+ message: result['message'],
1783
+ skipped: isSkipped,
1784
+ }, raw);
1785
+ }
1786
+ function cmdVerifyCodebaseDrift(cwd, raw) {
1787
+ // Non-hoisted: load-order matters for circular dep guard
1788
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- drift.cjs is an export= CommonJS module
1789
+ const drift = require('./drift.cjs');
1790
+ const emit = (payload) => output(payload, raw);
1791
+ try {
1792
+ const codebaseDir = node_path_1.default.join(planningDir(cwd), 'codebase');
1793
+ const structurePath = node_path_1.default.join(codebaseDir, 'STRUCTURE.md');
1794
+ if (!node_fs_1.default.existsSync(structurePath)) {
1795
+ emit({
1796
+ // Uniform gate contract: block = action_required (false when skipped).
1797
+ block: false,
1798
+ skipped: true,
1799
+ reason: 'no-structure-md',
1800
+ action_required: false,
1801
+ directive: 'none',
1802
+ elements: [],
1803
+ });
1804
+ return;
1805
+ }
1806
+ let structureMd;
1807
+ try {
1808
+ structureMd = node_fs_1.default.readFileSync(structurePath, 'utf-8');
1809
+ }
1810
+ catch (err) {
1811
+ emit({
1812
+ block: false,
1813
+ skipped: true,
1814
+ reason: 'cannot-read-structure-md: ' + (err instanceof Error ? err.message : String(err)),
1815
+ action_required: false,
1816
+ directive: 'none',
1817
+ elements: [],
1818
+ });
1819
+ return;
1820
+ }
1821
+ const lastMapped = drift['readMappedCommit'](structurePath);
1822
+ const revProbe = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', 'HEAD'], { cwd });
1823
+ if (revProbe.exitCode !== 0) {
1824
+ emit({
1825
+ block: false,
1826
+ skipped: true,
1827
+ reason: 'not-a-git-repo',
1828
+ action_required: false,
1829
+ directive: 'none',
1830
+ elements: [],
1831
+ });
1832
+ return;
1833
+ }
1834
+ const EMPTY_TREE = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
1835
+ let base = lastMapped;
1836
+ if (!base) {
1837
+ base = EMPTY_TREE;
1838
+ }
1839
+ else {
1840
+ const verify = (0, shell_command_projection_cjs_1.execGit)(['cat-file', '-t', base], { cwd });
1841
+ if (verify.exitCode !== 0)
1842
+ base = EMPTY_TREE;
1843
+ }
1844
+ const diff = (0, shell_command_projection_cjs_1.execGit)(['diff', '--name-status', base, 'HEAD'], { cwd });
1845
+ if (diff.exitCode !== 0) {
1846
+ emit({
1847
+ block: false,
1848
+ skipped: true,
1849
+ reason: 'git-diff-failed',
1850
+ action_required: false,
1851
+ directive: 'none',
1852
+ elements: [],
1853
+ });
1854
+ return;
1855
+ }
1856
+ const added = [];
1857
+ const modified = [];
1858
+ const deleted = [];
1859
+ for (const line of diff.stdout.split(/\r?\n/)) {
1860
+ if (!line.trim())
1861
+ continue;
1862
+ const m = line.match(/^([A-Z])\d*\t(.+?)(?:\t(.+))?$/);
1863
+ if (!m)
1864
+ continue;
1865
+ const status = m[1];
1866
+ const file = m[3] || m[2];
1867
+ if (status === 'A' || status === 'R' || status === 'C')
1868
+ added.push(file);
1869
+ else if (status === 'M')
1870
+ modified.push(file);
1871
+ else if (status === 'D')
1872
+ deleted.push(file);
1873
+ }
1874
+ const config = loadConfig(cwd);
1875
+ const wf = config?.workflow;
1876
+ const threshold = Number.isInteger(wf?.drift_threshold) && wf?.drift_threshold >= 1
1877
+ ? wf?.drift_threshold
1878
+ : 3;
1879
+ const action = wf?.drift_action === 'auto-remap' ? 'auto-remap' : 'warn';
1880
+ const driftResult = drift['detectDrift']({
1881
+ addedFiles: added,
1882
+ modifiedFiles: modified,
1883
+ deletedFiles: deleted,
1884
+ structureMd,
1885
+ threshold,
1886
+ action,
1887
+ runtime: (0, runtime_slash_cjs_1.resolveRuntime)(cwd),
1888
+ });
1889
+ const actionRequired = !!driftResult['actionRequired'];
1890
+ emit({
1891
+ // Uniform gate contract: block = action_required.
1892
+ block: actionRequired,
1893
+ skipped: !!driftResult['skipped'],
1894
+ reason: driftResult['reason'] || null,
1895
+ action_required: actionRequired,
1896
+ directive: driftResult['directive'],
1897
+ spawn_mapper: !!driftResult['spawnMapper'],
1898
+ affected_paths: driftResult['affectedPaths'] || [],
1899
+ elements: driftResult['elements'] || [],
1900
+ threshold,
1901
+ action,
1902
+ last_mapped_commit: lastMapped,
1903
+ message: driftResult['message'] || '',
1904
+ });
1905
+ }
1906
+ catch (err) {
1907
+ emit({
1908
+ block: false,
1909
+ skipped: true,
1910
+ reason: 'exception: ' + (err && err instanceof Error ? err.message : String(err)),
1911
+ action_required: false,
1912
+ directive: 'none',
1913
+ elements: [],
1914
+ });
1915
+ }
1916
+ }
1917
+ module.exports = {
1918
+ scanNegativeGrepCommentEcho,
1919
+ scanFileWideNegativeGateConflict,
1920
+ cmdVerifySummary,
1921
+ cmdVerifyPlanStructure,
1922
+ cmdVerifyPhaseCompleteness,
1923
+ cmdVerifyReferences,
1924
+ cmdVerifyCommits,
1925
+ cmdVerifyArtifacts,
1926
+ cmdVerifyKeyLinks,
1927
+ cmdValidateConsistency,
1928
+ cmdValidateHealth,
1929
+ cmdValidateAgents,
1930
+ cmdVerifySchemaDrift,
1931
+ cmdVerifyCodebaseDrift,
1932
+ };