@opengsd/gsd-core 1.15.0 → 1.16.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 (487) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +85 -5
  4. package/agents/gsd-code-fixer.compact.md +1 -1
  5. package/agents/gsd-code-fixer.md +1 -1
  6. package/agents/gsd-code-reviewer.compact.md +5 -3
  7. package/agents/gsd-code-reviewer.md +8 -6
  8. package/agents/gsd-debug-session-manager.compact.md +1 -1
  9. package/agents/gsd-debug-session-manager.md +1 -1
  10. package/agents/gsd-debugger.md +2 -2
  11. package/agents/gsd-eval-auditor.compact.md +1 -1
  12. package/agents/gsd-eval-auditor.md +1 -1
  13. package/agents/gsd-executor.md +4 -4
  14. package/agents/gsd-intel-updater.compact.md +1 -1
  15. package/agents/gsd-intel-updater.md +1 -1
  16. package/agents/gsd-mempalace-curator.md +2 -2
  17. package/agents/gsd-phase-researcher.md +1 -1
  18. package/agents/gsd-plan-checker.md +7 -2
  19. package/agents/gsd-planner.md +3 -3
  20. package/agents/gsd-project-researcher.compact.md +1 -1
  21. package/agents/gsd-project-researcher.md +1 -1
  22. package/agents/gsd-research-synthesizer.compact.md +1 -1
  23. package/agents/gsd-research-synthesizer.md +1 -1
  24. package/agents/gsd-ui-auditor.compact.md +21 -30
  25. package/agents/gsd-ui-auditor.md +42 -51
  26. package/agents/gsd-ui-researcher.compact.md +1 -1
  27. package/agents/gsd-ui-researcher.md +1 -1
  28. package/agents/gsd-verifier.md +31 -9
  29. package/bin/install.js +126 -196
  30. package/commands/gsd/add-tests.md +6 -1
  31. package/commands/gsd/ai-integration-phase.md +6 -1
  32. package/commands/gsd/audit-fix.md +5 -0
  33. package/commands/gsd/audit-milestone.md +6 -1
  34. package/commands/gsd/autonomous.md +5 -0
  35. package/commands/gsd/capture.md +8 -4
  36. package/commands/gsd/code-review.md +7 -2
  37. package/commands/gsd/complete-milestone.md +4 -0
  38. package/commands/gsd/config.md +7 -3
  39. package/commands/gsd/debug.md +11 -7
  40. package/commands/gsd/discuss-phase.md +7 -3
  41. package/commands/gsd/docs-update.md +12 -7
  42. package/commands/gsd/eval-review.md +6 -1
  43. package/commands/gsd/execute-phase.md +12 -7
  44. package/commands/gsd/extract-learnings.md +5 -0
  45. package/commands/gsd/fast.md +4 -0
  46. package/commands/gsd/forensics.md +5 -1
  47. package/commands/gsd/graphify.md +10 -6
  48. package/commands/gsd/health.md +5 -0
  49. package/commands/gsd/help.md +7 -2
  50. package/commands/gsd/import.md +7 -3
  51. package/commands/gsd/inbox.md +5 -0
  52. package/commands/gsd/ingest-docs.md +5 -1
  53. package/commands/gsd/manager.md +6 -1
  54. package/commands/gsd/map-codebase.md +7 -3
  55. package/commands/gsd/mempalace-capture.md +5 -1
  56. package/commands/gsd/mempalace-recall.md +5 -1
  57. package/commands/gsd/milestone-summary.md +5 -1
  58. package/commands/gsd/mvp-phase.md +8 -3
  59. package/commands/gsd/new-milestone.md +6 -1
  60. package/commands/gsd/new-project.md +5 -0
  61. package/commands/gsd/next.md +6 -1
  62. package/commands/gsd/ns-context.md +4 -0
  63. package/commands/gsd/ns-ideate.md +4 -0
  64. package/commands/gsd/ns-manage.md +4 -0
  65. package/commands/gsd/ns-project.md +4 -0
  66. package/commands/gsd/ns-review.md +4 -0
  67. package/commands/gsd/ns-workflow.md +4 -0
  68. package/commands/gsd/onboard.md +6 -1
  69. package/commands/gsd/pause-work.md +5 -1
  70. package/commands/gsd/phase.md +8 -4
  71. package/commands/gsd/plan-phase.md +6 -1
  72. package/commands/gsd/plan-review-convergence.md +5 -1
  73. package/commands/gsd/pr-branch.md +4 -0
  74. package/commands/gsd/profile-user.md +5 -1
  75. package/commands/gsd/progress.md +6 -1
  76. package/commands/gsd/quick-batch.md +20 -8
  77. package/commands/gsd/quick.md +12 -7
  78. package/commands/gsd/review.md +5 -1
  79. package/commands/gsd/secure-phase.md +6 -1
  80. package/commands/gsd/ship.md +5 -0
  81. package/commands/gsd/sketch.md +7 -2
  82. package/commands/gsd/spec-phase.md +5 -1
  83. package/commands/gsd/spike.md +8 -3
  84. package/commands/gsd/surface.md +5 -1
  85. package/commands/gsd/thread.md +4 -0
  86. package/commands/gsd/ui-phase.md +6 -1
  87. package/commands/gsd/ui-review.md +6 -1
  88. package/commands/gsd/ultraplan-phase.md +5 -1
  89. package/commands/gsd/undo.md +5 -1
  90. package/commands/gsd/update.md +6 -2
  91. package/commands/gsd/validate-phase.md +6 -1
  92. package/commands/gsd/verify-work.md +6 -1
  93. package/commands/gsd/workspace.md +7 -3
  94. package/gsd-core/bin/gsd-tools.cjs +142 -56
  95. package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
  96. package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
  97. package/gsd-core/bin/lib/audit.cjs +78 -44
  98. package/gsd-core/bin/lib/broken-windows.cjs +13 -13
  99. package/gsd-core/bin/lib/capability-activation.cjs +9 -4
  100. package/gsd-core/bin/lib/capability-registry.cjs +183 -103
  101. package/gsd-core/bin/lib/capability-validator.cjs +16 -0
  102. package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
  103. package/gsd-core/bin/lib/check-command-router.cjs +164 -1712
  104. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +13 -2
  105. package/gsd-core/bin/lib/cli-exit.cjs +12 -0
  106. package/gsd-core/bin/lib/codex-agent-toml.cjs +11 -8
  107. package/gsd-core/bin/lib/command-aliases.cjs +7 -0
  108. package/gsd-core/bin/lib/command-routing-hub.cjs +48 -1
  109. package/gsd-core/bin/lib/commands.cjs +173 -170
  110. package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
  111. package/gsd-core/bin/lib/config.cjs +43 -12
  112. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  113. package/gsd-core/bin/lib/coverage.cjs +4 -8
  114. package/gsd-core/bin/lib/decision-coverage-support.cjs +259 -0
  115. package/gsd-core/bin/lib/drift.cjs +177 -42
  116. package/gsd-core/bin/lib/frontmatter-fence.cjs +90 -0
  117. package/gsd-core/bin/lib/frontmatter-splice.cjs +494 -0
  118. package/gsd-core/bin/lib/frontmatter.cjs +413 -234
  119. package/gsd-core/bin/lib/gap-checker.cjs +72 -29
  120. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +381 -0
  121. package/gsd-core/bin/lib/gate-args.cjs +53 -0
  122. package/gsd-core/bin/lib/gate-codebase-drift.cjs +285 -0
  123. package/gsd-core/bin/lib/gate-config.cjs +46 -0
  124. package/gsd-core/bin/lib/gate-context-drift.cjs +141 -0
  125. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +169 -0
  126. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +126 -0
  127. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +555 -0
  128. package/gsd-core/bin/lib/gate-evidence.cjs +138 -0
  129. package/gsd-core/bin/lib/gate-exit.cjs +27 -0
  130. package/gsd-core/bin/lib/gate-gap-analysis-plan-post.cjs +61 -0
  131. package/gsd-core/bin/lib/gate-phase-context.cjs +170 -0
  132. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +1 -1
  133. package/gsd-core/bin/lib/gate-predicate.cjs +165 -0
  134. package/gsd-core/bin/lib/gate-prohibition-enforcement.cjs +94 -0
  135. package/gsd-core/bin/lib/gate-schema-drift.cjs +165 -0
  136. package/gsd-core/bin/lib/gate-tdd-red-evidence.cjs +100 -0
  137. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +182 -0
  138. package/gsd-core/bin/lib/gate-ui-plan.cjs +86 -0
  139. package/gsd-core/bin/lib/gate-ui-safety.cjs +80 -0
  140. package/gsd-core/bin/lib/gate-verdict.cjs +64 -0
  141. package/gsd-core/bin/lib/gate-verify-command-paths.cjs +78 -0
  142. package/gsd-core/bin/lib/gate-verify-failure-directions.cjs +41 -0
  143. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +43 -47
  144. package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
  145. package/gsd-core/bin/lib/init.cjs +162 -96
  146. package/gsd-core/bin/lib/install-engine.cjs +19 -35
  147. package/gsd-core/bin/lib/install-profiles.cjs +7 -4
  148. package/gsd-core/bin/lib/io.cjs +122 -3
  149. package/gsd-core/bin/lib/loop-resolver.cjs +95 -0
  150. package/gsd-core/bin/lib/markdown-sectionizer.cjs +75 -1
  151. package/gsd-core/bin/lib/milestone.cjs +19 -1
  152. package/gsd-core/bin/lib/model-resolver.cjs +18 -18
  153. package/gsd-core/bin/lib/observability/event.cjs +1 -1
  154. package/gsd-core/bin/lib/observability/logger.cjs +46 -1
  155. package/gsd-core/bin/lib/pattern.cjs +10 -0
  156. package/gsd-core/bin/lib/phase-command-router.cjs +11 -4
  157. package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
  158. package/gsd-core/bin/lib/phase-id.cjs +1 -1
  159. package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
  160. package/gsd-core/bin/lib/phase-status.cjs +360 -0
  161. package/gsd-core/bin/lib/phase.cjs +280 -80
  162. package/gsd-core/bin/lib/plan-document.cjs +93 -19
  163. package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
  164. package/gsd-core/bin/lib/planning-document.cjs +273 -40
  165. package/gsd-core/bin/lib/planning-inspect.cjs +42 -8
  166. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
  167. package/gsd-core/bin/lib/planning-workspace.cjs +76 -53
  168. package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
  169. package/gsd-core/bin/lib/profile-output.cjs +6 -3
  170. package/gsd-core/bin/lib/prohibition-enforcement.cjs +0 -55
  171. package/gsd-core/bin/lib/quick-batch-command-router.cjs +35 -9
  172. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +11 -8
  173. package/gsd-core/bin/lib/real-home-guard.cjs +9 -1
  174. package/gsd-core/bin/lib/report-parser.cjs +269 -0
  175. package/gsd-core/bin/lib/roadmap-command-router.cjs +13 -15
  176. package/gsd-core/bin/lib/roadmap-parser.cjs +82 -15
  177. package/gsd-core/bin/lib/roadmap-upgrade.cjs +127 -65
  178. package/gsd-core/bin/lib/roadmap.cjs +169 -72
  179. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +91 -157
  180. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -1
  181. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
  182. package/gsd-core/bin/lib/runtime-homes.cjs +13 -10
  183. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +307 -591
  184. package/gsd-core/bin/lib/runtime-name-policy.cjs +142 -27
  185. package/gsd-core/bin/lib/runtime-slash.cjs +47 -30
  186. package/gsd-core/bin/lib/shell-command-projection.cjs +37 -2
  187. package/gsd-core/bin/lib/smart-entry.cjs +19 -3
  188. package/gsd-core/bin/lib/stale-bake-guard.cjs +32 -48
  189. package/gsd-core/bin/lib/state-contract.cjs +15 -18
  190. package/gsd-core/bin/lib/state-document.cjs +100 -22
  191. package/gsd-core/bin/lib/state.cjs +214 -105
  192. package/gsd-core/bin/lib/surface.cjs +2 -1
  193. package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -152
  194. package/gsd-core/bin/lib/uat-predicate.cjs +313 -35
  195. package/gsd-core/bin/lib/uat.cjs +424 -7
  196. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +94 -58
  197. package/gsd-core/bin/lib/vendor/README.md +31 -9
  198. package/gsd-core/bin/lib/vendor/saxes.cjs +1934 -0
  199. package/gsd-core/bin/lib/vendor/saxes.cjs.LICENSE.txt +92 -0
  200. package/gsd-core/bin/lib/vendor/tap-parser.cjs +8927 -0
  201. package/gsd-core/bin/lib/vendor/tap-parser.cjs.LICENSE.txt +152 -0
  202. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  203. package/gsd-core/bin/lib/verification.cjs +1165 -314
  204. package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
  205. package/gsd-core/bin/lib/verify.cjs +286 -567
  206. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +14 -6
  207. package/gsd-core/bin/lib/workstream-inventory.cjs +31 -21
  208. package/gsd-core/bin/lib/workstream-name-policy.cjs +31 -1
  209. package/gsd-core/bin/lib/workstream.cjs +11 -2
  210. package/gsd-core/bin/shared/config-defaults.manifest.json +5 -0
  211. package/gsd-core/bin/shared/config-schema.manifest.json +4 -0
  212. package/gsd-core/references/autonomous-smart-discuss.md +2 -1
  213. package/gsd-core/references/autonomous-ui-design-contract.md +3 -3
  214. package/gsd-core/references/execute-mvp-tdd.md +5 -10
  215. package/gsd-core/references/execute-phase-between-wave-reset.md +3 -0
  216. package/gsd-core/references/execute-phase-response-language.md +1 -1
  217. package/gsd-core/references/gsd-run-resolver.md +1 -1
  218. package/gsd-core/references/loop-hook-dispatch.md +7 -1
  219. package/gsd-core/references/offer-next.md +1 -1
  220. package/gsd-core/references/planning-config.md +1 -1
  221. package/gsd-core/references/spidr-splitting.md +1 -1
  222. package/gsd-core/references/tdd.md +10 -4
  223. package/gsd-core/references/verifier-phase-gates.md +5 -2
  224. package/gsd-core/references/verify-mvp-mode.md +2 -2
  225. package/gsd-core/references/workstream-flag.md +33 -3
  226. package/gsd-core/templates/README.md +1 -1
  227. package/gsd-core/templates/UAT.md +17 -1
  228. package/gsd-core/templates/config.json +2 -11
  229. package/gsd-core/templates/verification-report.md +1 -1
  230. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  231. package/gsd-core/workflows/add-backlog.md +1 -1
  232. package/gsd-core/workflows/add-phase.md +8 -7
  233. package/gsd-core/workflows/add-tests.md +3 -2
  234. package/gsd-core/workflows/add-todo.md +4 -3
  235. package/gsd-core/workflows/ai-integration-phase.md +3 -2
  236. package/gsd-core/workflows/audit-fix.md +1 -1
  237. package/gsd-core/workflows/audit-milestone.md +4 -3
  238. package/gsd-core/workflows/audit-uat.md +1 -1
  239. package/gsd-core/workflows/autonomous.md +28 -18
  240. package/gsd-core/workflows/check-todos.md +6 -5
  241. package/gsd-core/workflows/cleanup.md +1 -1
  242. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
  243. package/gsd-core/workflows/code-review-fix.md +5 -4
  244. package/gsd-core/workflows/code-review.md +84 -90
  245. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
  246. package/gsd-core/workflows/complete-milestone.md +12 -7
  247. package/gsd-core/workflows/debug.md +30 -5
  248. package/gsd-core/workflows/diagnose-issues.md +3 -2
  249. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  250. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  251. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  252. package/gsd-core/workflows/discuss-phase.md +4 -3
  253. package/gsd-core/workflows/do.md +1 -1
  254. package/gsd-core/workflows/docs-update.md +4 -3
  255. package/gsd-core/workflows/edit-phase.md +4 -3
  256. package/gsd-core/workflows/eval-review.md +5 -3
  257. package/gsd-core/workflows/execute-phase/detail/elaboration.md +1 -1
  258. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +4 -2
  259. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +15 -4
  260. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +10 -7
  261. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +1 -1
  262. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +3 -1
  263. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +2 -2
  264. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  265. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  266. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
  267. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  268. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  269. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -0
  270. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  271. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +187 -0
  272. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +2 -3
  273. package/gsd-core/workflows/execute-phase.md +78 -170
  274. package/gsd-core/workflows/execute-plan.md +13 -19
  275. package/gsd-core/workflows/explore.md +2 -2
  276. package/gsd-core/workflows/extract-learnings.md +3 -2
  277. package/gsd-core/workflows/fast.md +1 -1
  278. package/gsd-core/workflows/forensics.md +1 -1
  279. package/gsd-core/workflows/graduation.md +1 -1
  280. package/gsd-core/workflows/health.md +2 -1
  281. package/gsd-core/workflows/import.md +3 -2
  282. package/gsd-core/workflows/inbox.md +1 -1
  283. package/gsd-core/workflows/ingest-docs.md +1 -1
  284. package/gsd-core/workflows/insert-phase.md +4 -3
  285. package/gsd-core/workflows/list-seeds.md +1 -1
  286. package/gsd-core/workflows/list-workspaces.md +1 -1
  287. package/gsd-core/workflows/manager.md +5 -3
  288. package/gsd-core/workflows/map-codebase.md +4 -3
  289. package/gsd-core/workflows/milestone-summary.md +3 -2
  290. package/gsd-core/workflows/mvp-phase.md +14 -14
  291. package/gsd-core/workflows/new-milestone.md +10 -10
  292. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  293. package/gsd-core/workflows/new-project.md +1 -1
  294. package/gsd-core/workflows/new-workspace.md +1 -1
  295. package/gsd-core/workflows/next.md +1 -1
  296. package/gsd-core/workflows/pause-work.md +2 -2
  297. package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
  298. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  299. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +1 -1
  300. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  301. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +1 -1
  302. package/gsd-core/workflows/plan-phase.md +27 -18
  303. package/gsd-core/workflows/plan-review-convergence.md +1 -1
  304. package/gsd-core/workflows/plant-seed.md +1 -1
  305. package/gsd-core/workflows/pr-branch.md +1 -1
  306. package/gsd-core/workflows/profile-user.md +1 -1
  307. package/gsd-core/workflows/progress.md +19 -49
  308. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -2
  309. package/gsd-core/workflows/quick/steps/quick-verification.md +4 -4
  310. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +30 -11
  311. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  312. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  313. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  314. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  315. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  316. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  317. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +9 -3
  318. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  319. package/gsd-core/workflows/quick-batch.md +15 -10
  320. package/gsd-core/workflows/quick.md +42 -31
  321. package/gsd-core/workflows/remove-phase.md +3 -2
  322. package/gsd-core/workflows/remove-workspace.md +1 -1
  323. package/gsd-core/workflows/resume-project.md +3 -2
  324. package/gsd-core/workflows/review.md +3 -2
  325. package/gsd-core/workflows/scan.md +3 -2
  326. package/gsd-core/workflows/secure-phase.md +11 -12
  327. package/gsd-core/workflows/settings-advanced.md +1 -1
  328. package/gsd-core/workflows/settings-integrations.md +1 -1
  329. package/gsd-core/workflows/settings.md +1 -1
  330. package/gsd-core/workflows/ship.md +12 -12
  331. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  332. package/gsd-core/workflows/sketch.md +1 -1
  333. package/gsd-core/workflows/smart-entry.md +1 -1
  334. package/gsd-core/workflows/spec-phase.md +1 -1
  335. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  336. package/gsd-core/workflows/spike.md +1 -1
  337. package/gsd-core/workflows/stats.md +1 -1
  338. package/gsd-core/workflows/sync-skills.md +1 -1
  339. package/gsd-core/workflows/thread.md +2 -2
  340. package/gsd-core/workflows/transition.md +13 -23
  341. package/gsd-core/workflows/ui-phase.md +5 -4
  342. package/gsd-core/workflows/ui-review.md +4 -3
  343. package/gsd-core/workflows/ultraplan-phase.md +3 -2
  344. package/gsd-core/workflows/undo.md +1 -1
  345. package/gsd-core/workflows/validate-phase.md +10 -12
  346. package/gsd-core/workflows/verify-work/detail/elaboration.md +43 -3
  347. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  348. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -3
  349. package/gsd-core/workflows/verify-work.md +68 -59
  350. package/hooks/dist/gsd-agent-isolation-guard.js +8 -0
  351. package/hooks/dist/gsd-check-update-worker.js +8 -0
  352. package/hooks/dist/gsd-check-update.js +8 -0
  353. package/hooks/dist/gsd-context-monitor.js +31 -8
  354. package/hooks/dist/gsd-cursor-subagent-start.js +8 -0
  355. package/hooks/dist/gsd-secret-read-guard.js +147 -16
  356. package/hooks/dist/gsd-statusline.js +15 -7
  357. package/hooks/dist/gsd-update-banner.js +8 -0
  358. package/hooks/dist/gsd-windsurf-pre-write.js +11 -2
  359. package/hooks/dist/gsd-workflow-guard.js +5 -4
  360. package/hooks/dist/gsd-worktree-path-guard.js +6 -2
  361. package/hooks/dist/lib/cli-exit.js +12 -0
  362. package/hooks/dist/lib/git-probe.js +17 -1
  363. package/hooks/dist/lib/isolation-sentinel.js +2 -2
  364. package/hooks/gsd-agent-isolation-guard.js +8 -0
  365. package/hooks/gsd-check-update-worker.js +8 -0
  366. package/hooks/gsd-check-update.js +8 -0
  367. package/hooks/gsd-context-monitor.js +31 -8
  368. package/hooks/gsd-cursor-subagent-start.js +8 -0
  369. package/hooks/gsd-secret-read-guard.js +147 -16
  370. package/hooks/gsd-statusline.js +15 -7
  371. package/hooks/gsd-update-banner.js +8 -0
  372. package/hooks/gsd-windsurf-pre-write.js +11 -2
  373. package/hooks/gsd-workflow-guard.js +5 -4
  374. package/hooks/gsd-worktree-path-guard.js +6 -2
  375. package/hooks/hooks.json +5 -5
  376. package/hooks/lib/cli-exit.js +12 -0
  377. package/hooks/lib/git-probe.js +17 -1
  378. package/hooks/lib/isolation-sentinel.js +2 -2
  379. package/package.json +21 -4
  380. package/scripts/changeset/parse.cjs +52 -4
  381. package/scripts/ci-timeout-report.cjs +770 -4
  382. package/scripts/command-contract-helpers.cjs +12 -8
  383. package/scripts/docs-guard-registry.cjs +6 -0
  384. package/scripts/gen-features.cjs +13 -8
  385. package/scripts/gen-hooks-cli-exit.cjs +12 -28
  386. package/scripts/gen-loop-host-contract.cjs +10 -1
  387. package/scripts/gen-platform-conformance-tier.cjs +187 -1
  388. package/scripts/gen-plugin-skills.cjs +87 -1
  389. package/scripts/gen-research-agents.cjs +24 -31
  390. package/scripts/gen-scripts-cli-exit.cjs +30 -3
  391. package/scripts/gen-test-timings.cjs +32 -7
  392. package/scripts/lib/cli-exit.cjs +12 -0
  393. package/scripts/lib/macos-conformance-tier.generated.cjs +20 -2
  394. package/scripts/lib/ndjson-reporter.cjs +28 -3
  395. package/scripts/lib/platform-conformance-tier.generated.cjs +34 -5
  396. package/scripts/lib/registration-ledger-preload.cjs +155 -0
  397. package/scripts/lib/vendor-bundle.cjs +59 -0
  398. package/scripts/lib/vendor-licenses/saxes-6.0.0.txt +64 -0
  399. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  400. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  401. package/scripts/lint-completion-predicate-drift.cjs +18 -19
  402. package/scripts/lint-descriptions.cjs +7 -3
  403. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +10 -0
  404. package/scripts/lint-eslint-glob-coverage.allowlist.json +20 -0
  405. package/scripts/lint-frontmatter-fence-drift.cjs +313 -0
  406. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +122 -16
  407. package/scripts/lint-phase-enumeration-drift.cjs +12 -8
  408. package/scripts/lint-phase-id-drift.cjs +29 -0
  409. package/scripts/lint-planning-document-positive-control.cjs +329 -0
  410. package/scripts/lint-response-language-coverage.cjs +3 -0
  411. package/scripts/lint-skill-deps.cjs +7 -3
  412. package/scripts/lint-test-file-count.allowlist.json +11 -1
  413. package/scripts/lint-test-file-count.cjs +34 -1
  414. package/scripts/lint-vendored-deps.cjs +41 -5
  415. package/scripts/lint-workflow-shellcheck-baseline.json +10 -10
  416. package/scripts/mutation-matrix.cjs +50 -5
  417. package/scripts/require-issue-link-policy.cjs +6 -2
  418. package/scripts/sync-runtime-launcher.cjs +184 -2
  419. package/scripts/verify-npm-publish.cjs +76 -20
  420. package/skills/gsd-add-tests/SKILL.md +6 -1
  421. package/skills/gsd-ai-integration-phase/SKILL.md +6 -1
  422. package/skills/gsd-audit-fix/SKILL.md +5 -0
  423. package/skills/gsd-audit-milestone/SKILL.md +6 -1
  424. package/skills/gsd-autonomous/SKILL.md +5 -0
  425. package/skills/gsd-capture/SKILL.md +8 -4
  426. package/skills/gsd-code-review/SKILL.md +7 -2
  427. package/skills/gsd-complete-milestone/SKILL.md +4 -0
  428. package/skills/gsd-config/SKILL.md +8 -4
  429. package/skills/gsd-debug/SKILL.md +11 -7
  430. package/skills/gsd-discuss-phase/SKILL.md +7 -3
  431. package/skills/gsd-docs-update/SKILL.md +12 -7
  432. package/skills/gsd-eval-review/SKILL.md +6 -1
  433. package/skills/gsd-execute-phase/SKILL.md +12 -7
  434. package/skills/gsd-extract-learnings/SKILL.md +5 -0
  435. package/skills/gsd-fast/SKILL.md +4 -0
  436. package/skills/gsd-forensics/SKILL.md +5 -1
  437. package/skills/gsd-graphify/SKILL.md +10 -6
  438. package/skills/gsd-health/SKILL.md +5 -0
  439. package/skills/gsd-help/SKILL.md +7 -2
  440. package/skills/gsd-import/SKILL.md +7 -3
  441. package/skills/gsd-inbox/SKILL.md +5 -0
  442. package/skills/gsd-ingest-docs/SKILL.md +5 -1
  443. package/skills/gsd-manager/SKILL.md +6 -1
  444. package/skills/gsd-map-codebase/SKILL.md +7 -3
  445. package/skills/gsd-mempalace-capture/SKILL.md +5 -1
  446. package/skills/gsd-mempalace-recall/SKILL.md +5 -1
  447. package/skills/gsd-milestone-summary/SKILL.md +5 -1
  448. package/skills/gsd-mvp-phase/SKILL.md +8 -3
  449. package/skills/gsd-new-milestone/SKILL.md +6 -1
  450. package/skills/gsd-new-project/SKILL.md +5 -0
  451. package/skills/gsd-next/SKILL.md +6 -1
  452. package/skills/gsd-ns-context/SKILL.md +4 -0
  453. package/skills/gsd-ns-ideate/SKILL.md +4 -0
  454. package/skills/gsd-ns-manage/SKILL.md +4 -0
  455. package/skills/gsd-ns-project/SKILL.md +4 -0
  456. package/skills/gsd-ns-review/SKILL.md +4 -0
  457. package/skills/gsd-ns-workflow/SKILL.md +4 -0
  458. package/skills/gsd-onboard/SKILL.md +6 -1
  459. package/skills/gsd-pause-work/SKILL.md +5 -1
  460. package/skills/gsd-phase/SKILL.md +8 -4
  461. package/skills/gsd-plan-phase/SKILL.md +6 -1
  462. package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
  463. package/skills/gsd-pr-branch/SKILL.md +4 -0
  464. package/skills/gsd-profile-user/SKILL.md +5 -1
  465. package/skills/gsd-progress/SKILL.md +6 -1
  466. package/skills/gsd-quick/SKILL.md +16 -10
  467. package/skills/gsd-quick-batch/SKILL.md +20 -8
  468. package/skills/gsd-review/SKILL.md +5 -1
  469. package/skills/gsd-review-backlog/SKILL.md +3 -2
  470. package/skills/gsd-secure-phase/SKILL.md +6 -1
  471. package/skills/gsd-ship/SKILL.md +5 -0
  472. package/skills/gsd-sketch/SKILL.md +7 -2
  473. package/skills/gsd-spec-phase/SKILL.md +5 -1
  474. package/skills/gsd-spike/SKILL.md +8 -3
  475. package/skills/gsd-surface/SKILL.md +5 -1
  476. package/skills/gsd-thread/SKILL.md +4 -0
  477. package/skills/gsd-ui-phase/SKILL.md +6 -1
  478. package/skills/gsd-ui-review/SKILL.md +6 -1
  479. package/skills/gsd-ultraplan-phase/SKILL.md +5 -1
  480. package/skills/gsd-undo/SKILL.md +5 -1
  481. package/skills/gsd-update/SKILL.md +6 -2
  482. package/skills/gsd-validate-phase/SKILL.md +6 -1
  483. package/skills/gsd-verify-work/SKILL.md +6 -1
  484. package/skills/gsd-workspace/SKILL.md +7 -3
  485. package/skills/gsd-workstreams/SKILL.md +6 -6
  486. package/vscode/package.json +1 -1
  487. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +0 -24
@@ -1,17 +1,17 @@
1
1
  "use strict";
2
2
  /**
3
- * Check subcommand router — auto-mode, decision-coverage-plan, decision-coverage-verify.
3
+ * Check subcommand router — parses argv, dispatches to one gate module per verb, and formats the
4
+ * gate's result (#5139, epic #5056, ADR-5057 §4 first bullet, design D2).
5
+ *
6
+ * The router decides nothing: every gate lives in a `src/gate-<verb>.cts` module (or, for
7
+ * `auto-mode`, `check-auto-mode.cts`) that returns a `GateResult` (`src/gate-verdict.cts`). The
8
+ * router's single output site, `emitGateResult`, prints a verdict's payload or turns a usage
9
+ * failure into `error()`. It imports no filesystem or subprocess primitive (eslint.config.mjs,
10
+ * `no-restricted-imports`; tests/check-router-gate-boundaries.test.cjs).
4
11
  *
5
12
  * ADR-457 build-at-publish: the hand-written bin/lib/check-command-router.cjs collapsed
6
- * to a TypeScript source of truth. Behaviour is preserved byte-for-behaviour
7
- * from the prior hand-written .cjs; only strict types are added.
13
+ * to a TypeScript source of truth.
8
14
  */
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_child_process_1 = require("node:child_process");
15
15
  // eslint-disable-next-line @typescript-eslint/no-require-imports
16
16
  const io = require("./io.cjs");
17
17
  const { output, ERROR_REASON } = io;
@@ -21,1637 +21,99 @@ const { output, ERROR_REASON } = io;
21
21
  // or an annotated const. Without the annotation every `error(...)` guard below would
22
22
  // need a dead `throw` after it to convince the checker that the value is non-null.
23
23
  const error = io.error;
24
- // eslint-disable-next-line @typescript-eslint/no-require-imports
25
- const planningWorkspaceMod = require("./planning-workspace.cjs");
26
- const { planningDir } = planningWorkspaceMod;
27
- // eslint-disable-next-line @typescript-eslint/no-require-imports
28
- const phaseLocatorMod = require("./phase-locator.cjs");
29
- const { findPhaseInternal } = phaseLocatorMod;
30
- const decisions_cjs_1 = require("./decisions.cjs");
31
- // eslint-disable-next-line @typescript-eslint/no-require-imports
32
- const frontmatterMod = require("./frontmatter.cjs");
33
- const { extractFrontmatter } = frontmatterMod;
34
- const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
35
- const security_cjs_1 = require("./security.cjs");
36
- const ui_safety_gate_cjs_1 = require("./ui-safety-gate.cjs");
37
- const ui_frontend_evidence_cjs_1 = require("./ui-frontend-evidence.cjs");
38
- // eslint-disable-next-line @typescript-eslint/no-require-imports
39
- const verifyModule = require("./verify.cjs");
40
- const { cmdVerifySchemaDrift, cmdVerifyCodebaseDrift, cmdVerifyContextDrift } = verifyModule;
41
- // eslint-disable-next-line @typescript-eslint/no-require-imports
42
- const roadmapModule = require("./roadmap.cjs");
43
- const { getRoadmapPhaseWithFallback } = roadmapModule;
44
- // eslint-disable-next-line @typescript-eslint/no-require-imports
45
- const gapCheckerModule = require("./gap-checker.cjs");
46
- const { runGapAnalysis } = gapCheckerModule;
47
- const prohibition_enforcement_cjs_1 = require("./prohibition-enforcement.cjs");
48
- const tdd_red_evidence_cjs_1 = require("./tdd-red-evidence.cjs");
49
- // eslint-disable-next-line @typescript-eslint/no-require-imports
50
- const gatePredicateEval = require("./gate-predicate-evaluator.cjs");
51
- const { evaluatePredicate } = gatePredicateEval;
52
- // eslint-disable-next-line @typescript-eslint/no-require-imports
53
- const apiCoverageMod = require("./api-coverage.cjs");
54
- const { detectApiIntegration, validateCoverageMatrix } = apiCoverageMod;
55
- const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
56
- // eslint-disable-next-line @typescript-eslint/no-require-imports
57
- const planScanMod = require("./plan-scan.cjs");
58
- const { scanPhasePlans } = planScanMod;
59
- // eslint-disable-next-line @typescript-eslint/no-require-imports
60
- const planningScopeMod = require("./planning-scope.cjs");
61
- const { SCOPE } = planningScopeMod;
62
- // eslint-disable-next-line @typescript-eslint/no-require-imports
63
- const verifyCommandGroundingMod = require("./verify-command-grounding.cjs");
64
- const { probePhaseVerifyCommands, probePhaseFailingDirections } = verifyCommandGroundingMod;
65
- // ─── Helpers ──────────────────────────────────────────────────────────────────
66
- function normalizePhrase(text) {
67
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
68
- return String(text || '')
69
- .toLowerCase()
70
- .replace(/[^a-z0-9\s]/g, ' ')
71
- .replace(/\s+/g, ' ')
72
- .trim();
73
- }
74
- const SOFT_PHRASE_MIN_WORDS = 6;
75
- function softPhrase(text) {
76
- const words = normalizePhrase(text).split(' ').filter(Boolean);
77
- if (words.length < SOFT_PHRASE_MIN_WORDS)
78
- return '';
79
- return words.slice(0, SOFT_PHRASE_MIN_WORDS).join(' ');
80
- }
81
- function decisionMentioned(haystack, decision) {
82
- if (!haystack)
83
- return false;
84
- if (new RegExp(`\\b${decision.id}\\b`).test(haystack))
85
- return true;
86
- const phrase = softPhrase(decision.text);
87
- return phrase ? normalizePhrase(haystack).includes(phrase) : false;
88
- }
89
- function readIfExists(filePath) {
90
- try {
91
- return node_fs_1.default.readFileSync(filePath, 'utf-8');
92
- }
93
- catch {
94
- return '';
95
- }
96
- }
97
- function resolvePath(inputPath, projectDir) {
98
- const candidate = node_path_1.default.isAbsolute(inputPath) ? inputPath : node_path_1.default.join(projectDir, inputPath);
99
- const contained = (0, security_cjs_1.tryWithinRoot)(candidate, projectDir, security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
100
- if (contained === null) {
101
- error(`path escapes its allowed directory: ${inputPath}`, ERROR_REASON.USAGE);
102
- }
103
- return contained;
104
- }
105
- function readWorkflowConfig(projectDir) {
106
- const configPath = node_path_1.default.join(projectDir, '.planning', 'config.json');
107
- try {
108
- const parsed = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
109
- const wf = parsed['workflow'] || {};
110
- return {
111
- ...wf,
112
- auto_advance: (wf['auto_advance'] ?? parsed['auto_advance']),
113
- _auto_chain_active: (wf['_auto_chain_active'] ?? parsed['_auto_chain_active']),
114
- context_coverage_gate: (wf['context_coverage_gate'] ?? parsed['context_coverage_gate']),
115
- };
116
- }
117
- catch {
118
- return {};
119
- }
120
- }
121
- function cmdAutoMode(projectDir, raw) {
122
- const workflow = readWorkflowConfig(projectDir);
123
- const autoAdvance = Boolean(workflow.auto_advance ?? false);
124
- const autoChainActive = Boolean(workflow._auto_chain_active ?? false);
125
- let source = 'none';
126
- if (autoChainActive && autoAdvance)
127
- source = 'both';
128
- else if (autoChainActive)
129
- source = 'auto_chain';
130
- else if (autoAdvance)
131
- source = 'auto_advance';
132
- output({
133
- active: autoChainActive || autoAdvance,
134
- source,
135
- auto_chain_active: autoChainActive,
136
- auto_advance: autoAdvance,
137
- }, raw, undefined);
138
- }
139
- function gateEnabled(projectDir) {
140
- const value = readWorkflowConfig(projectDir).context_coverage_gate;
141
- if (typeof value === 'boolean')
142
- return value;
143
- if (typeof value === 'string') {
144
- const lower = value.toLowerCase();
145
- if (lower === 'false' || lower === 'true')
146
- return lower !== 'false';
147
- }
148
- return true;
149
- }
150
- function loadPlanContents(phaseDir) {
151
- if (!node_fs_1.default.existsSync(phaseDir))
152
- return [];
153
- // #3183 (lint-plan-count-drift): source live plan files from the single
154
- // owner (scanPhasePlans) instead of a local `-PLAN.md` readdirSync filter
155
- // — picks up bare PLAN.md and nested plans/, and excludes plans marked
156
- // `status: superseded`, which the prior root-only exact-suffix filter did
157
- // neither for.
158
- return scanPhasePlans(phaseDir).planFiles
159
- .map((entry) => readIfExists(node_path_1.default.join(phaseDir, entry)));
160
- }
161
- const DESIGNATED_HEADINGS_RE = /^#{1,6}\s+(?:must[_ ]haves?|truths?|tasks?|objective)\b/i;
162
- // #2372: scanned-tag set must match the planner-canonical surfaces where a D-NN citation
163
- // is meaningful. `<objective>`/`<tasks>`/`<task>`/`<action>` are the historical core. The
164
- // planner is also explicitly told (plan-phase.md) to cite decisions in `<read_first>`,
165
- // `<behavior>`, `<verify>`, `<acceptance_criteria>`, and `<done>` — those are now scanned too,
166
- // so the gate no longer reports a false coverage gap when a decision is cited in any of them.
167
- //
168
- // Implementation: per-tag matching, NOT a single wide alternation. A single alternation
169
- // like `<(?:a|b|c)>...<\/(?:a|b|c)>` halts the outer tag's body capture at any inner tag
170
- // in the set, dropping any citation in the outer tag's prefix prose — e.g.
171
- // `<action>per D-05 <verify>npm test</verify></action>` would lose D-05 because `<verify>`
172
- // halts the `<action>` body before the citation. Per-tag matching avoids this: each tag's
173
- // body terminates only at its OWN closing tag, so `<verify>` inside `<action>` is absorbed
174
- // into `<action>`'s body (D-05 caught) AND `<verify>` is matched separately on its own pass.
175
- // Each per-tag regex keeps the ReDoS-safe negative-lookahead tempering (#2128).
176
- const XML_DECISION_TAG_NAMES = ['objective', 'tasks', 'task', 'action', 'read_first', 'behavior', 'verify', 'acceptance_criteria', 'done'];
177
- function buildXmlDecisionTagRegex(tagName) {
178
- // Per-tag: body tempering stops only at the SAME tag's reopening or closing — other
179
- // scanned tags pass through as text into this body. Non-greedy `*?` to first close.
180
- return new RegExp(`<${tagName}(?:\\s[^>]{0,1000})?>((?:(?!<${tagName}[\\s>])[\\s\\S])*?)<\\/${tagName}>`, 'gi');
181
- }
182
- function stripCommentsAndFences(text) {
183
- // HTML-comment stripping stays caller-side (the seam does not strip HTML comments).
184
- // Stop-at-next-open body (ReDoS-safe, #2128); an UNCLOSED `<!--` does not match,
185
- // so downstream tags are preserved (unlike a `(?:-->|$)` fallback, which would
186
- // wipe to EOF and fail-close the decision-coverage gate).
187
- const htmlStripped = text.replace(/<!--(?:(?!<!--)[\s\S])*?-->/g, ' ');
188
- // Fenced-code stripping: delegate to the canonical CommonMark-correct seam.
189
- // replaces the prior independent regex copy (```` ``` ``` ```` + `~~~ ~~~`).
190
- return (0, markdown_sectionizer_cjs_1.stripFencedCode)(htmlStripped).text;
191
- }
192
- function extractYamlBlock(frontmatter, key) {
193
- const match = frontmatter.match(new RegExp(`^${key}\\s*:(.*)$`, 'm'));
194
- if (!match)
195
- return '';
196
- const startIdx = (match.index || 0) + match[0].length;
197
- const rest = frontmatter.slice(startIdx + 1).split(/\r?\n/);
198
- const block = [match[1] || ''];
199
- for (const line of rest) {
200
- if (line === '' || /^\s/.test(line))
201
- block.push(line);
202
- else
203
- break;
204
- }
205
- return block.join('\n');
206
- }
207
- function extractXmlTagBodies(text) {
208
- const parts = [];
209
- for (const tagName of XML_DECISION_TAG_NAMES) {
210
- const re = buildXmlDecisionTagRegex(tagName);
211
- for (const match of text.matchAll(re)) {
212
- if (match[1])
213
- parts.push(match[1]);
214
- }
215
- }
216
- return parts.join('\n');
217
- }
218
- function extractPlanDesignatedSections(planContent) {
219
- if (!planContent)
220
- return '';
221
- const cleaned = stripCommentsAndFences(planContent);
222
- const fmMatch = cleaned.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
223
- const frontmatter = fmMatch ? fmMatch[1] : '';
224
- const body = fmMatch ? fmMatch[2] : cleaned;
225
- const parts = [];
226
- for (const key of ['must_haves', 'truths', 'objective']) {
227
- const block = extractYamlBlock(frontmatter, key);
228
- if (block)
229
- parts.push(block);
230
- }
231
- // Replace hand-rolled split(/\r?\n/) + heading walk with the seam's collectSections.
232
- // stopPredicate fires on EVERY heading (collectSections needs to start a section at
233
- // each heading), then we filter to designated ones — same semantics as the prior
234
- // inDesignated flag: emit the heading line + body only when DESIGNATED_HEADINGS_RE matches.
235
- const sections = (0, markdown_sectionizer_cjs_1.collectSections)(body, () => true);
236
- const bodyParts = [];
237
- for (const section of sections) {
238
- const headingLine = '#'.repeat(section.heading.level) + ' ' + section.heading.text;
239
- if (DESIGNATED_HEADINGS_RE.test(headingLine)) {
240
- bodyParts.push(headingLine);
241
- if (section.body)
242
- bodyParts.push(section.body);
243
- }
244
- }
245
- parts.push(bodyParts.join('\n'));
246
- parts.push(extractXmlTagBodies(cleaned));
247
- return parts.join('\n\n');
248
- }
249
- function buildPlanMessage(uncovered) {
250
- if (uncovered.length === 0)
251
- return 'All trackable CONTEXT.md decisions are covered by plans.';
252
- return [
253
- '## Decision Coverage Gap',
254
- '',
255
- `${uncovered.length} CONTEXT.md decision(s) are not covered by any plan:`,
256
- '',
257
- ...uncovered.map((item) => `- **${item.id}** (${item.category || 'uncategorized'}): ${item.text}`),
258
- '',
259
- 'Resolve by citing `D-NN:` in any of the scanned plan surfaces: front-matter',
260
- '`must_haves`/`truths`/`objective`, a `## must_haves`/`truths`/`tasks`/`objective`',
261
- 'heading, or an `<objective>`/`<tasks>`/`<task>`/`<action>`/`<read_first>`/`<behavior>`/`<verify>`/`<acceptance_criteria>`/`<done>`',
262
- 'tag body. Other locations (prose outside those headings, comments, other XML tags) are not scanned.',
263
- 'OR move the decision to `### Claude\'s Discretion` / tag it `[informational]` if it should not be tracked.',
264
- ].join('\n');
265
- }
266
- function buildVerifyMessage(notHonored) {
267
- if (notHonored.length === 0)
268
- return 'All trackable CONTEXT.md decisions are honored by shipped artifacts.';
269
- return [
270
- '### Decision Coverage (warning)',
271
- '',
272
- `${notHonored.length} decision(s) not found in shipped artifacts:`,
273
- '',
274
- ...notHonored.map((item) => `- **${item.id}** (${item.category || 'uncategorized'}): ${item.text}`),
275
- '',
276
- 'This is a soft warning - verification status is unchanged.',
277
- ].join('\n');
278
- }
279
- function loadDecisionExtraction(contextPath) {
280
- const extraction = (0, decisions_cjs_1.extractDecisions)(readIfExists(contextPath));
281
- return {
282
- trackable: extraction.decisions.filter((d) => d.trackable),
283
- outcome: extraction.outcome,
284
- unreadableIds: extraction.unreadableIds ?? [],
285
- };
24
+ const gate_verdict_cjs_1 = require("./gate-verdict.cjs");
25
+ const gate_exit_cjs_1 = require("./gate-exit.cjs");
26
+ const gate_args_cjs_1 = require("./gate-args.cjs");
27
+ const gate_decision_coverage_plan_cjs_1 = require("./gate-decision-coverage-plan.cjs");
28
+ const gate_decision_coverage_verify_cjs_1 = require("./gate-decision-coverage-verify.cjs");
29
+ const gate_evaluation_scope_cjs_1 = require("./gate-evaluation-scope.cjs");
30
+ const gate_ui_plan_cjs_1 = require("./gate-ui-plan.cjs");
31
+ const gate_ui_safety_cjs_1 = require("./gate-ui-safety.cjs");
32
+ const gate_tdd_review_checkpoint_cjs_1 = require("./gate-tdd-review-checkpoint.cjs");
33
+ const gate_tdd_red_evidence_cjs_1 = require("./gate-tdd-red-evidence.cjs");
34
+ const gate_verify_command_paths_cjs_1 = require("./gate-verify-command-paths.cjs");
35
+ const gate_verify_failure_directions_cjs_1 = require("./gate-verify-failure-directions.cjs");
36
+ const gate_gap_analysis_plan_post_cjs_1 = require("./gate-gap-analysis-plan-post.cjs");
37
+ const gate_predicate_cjs_1 = require("./gate-predicate.cjs");
38
+ const gate_api_coverage_verify_pre_cjs_1 = require("./gate-api-coverage-verify-pre.cjs");
39
+ const decision_coverage_support_cjs_1 = require("./decision-coverage-support.cjs");
40
+ const check_auto_mode_cjs_1 = require("./check-auto-mode.cjs");
41
+ const gate_schema_drift_cjs_1 = require("./gate-schema-drift.cjs");
42
+ const gate_codebase_drift_cjs_1 = require("./gate-codebase-drift.cjs");
43
+ const gate_context_drift_cjs_1 = require("./gate-context-drift.cjs");
44
+ const gate_prohibition_enforcement_cjs_1 = require("./gate-prohibition-enforcement.cjs");
45
+ /** A gate's usage failure as `error()`: the gate's own message, and its code when it is a known reason. */
46
+ function failGate(failed) {
47
+ const reason = Object.values(ERROR_REASON).find((value) => value === failed.failure.code);
48
+ return error(failed.failure.message, reason);
286
49
  }
287
50
  /**
288
- * `check decision-coverage-plan` — blocking plan-phase decision-coverage gate
289
- * (#2492, #1365 fail-loud, #2770 empty-arg fail-closed).
290
- *
291
- * Invocation (the context path may be supplied EITHER way; #4130 follow-up):
292
- * gsd_run check decision-coverage-plan <phase-dir> <context-path> (positional, the workflow caller's form)
293
- * gsd_run check decision-coverage-plan --context <path> [<phase-dir>]
294
- *
295
- * `--context <path>` follows the sibling flag convention (`check predicate`,
296
- * #2008): `--flag value` pairs parsed by the shared partitionPredicateArgs
297
- * pass, the flag WINNING over a same-purpose positional when both appear,
298
- * and a valueless `--context` counting as no context at all (it falls
299
- * through to the #2770 caller-error branch, not to the "CONTEXT.md missing"
300
- * green skip). The positional form keeps working unchanged — no sibling
301
- * check verb deprecates positionals and the plan-phase workflow passes them.
51
+ * The router's single output site for a gate: a verdict prints its payload; a usage failure
52
+ * fails through `error()` with the gate's own message and code (design D2).
302
53
  */
303
- function cmdDecisionCoveragePlan(projectDir, args, raw) {
304
- // args[0]='check', args[1]=subcommand — partition the REST so flag tokens
305
- // and their values never land in a positional slot.
306
- const { flags, positionals } = partitionPredicateArgs(args.slice(2));
307
- const phaseDir = positionals[0] ? resolvePath(positionals[0], projectDir) : '';
308
- // A VALUELESS `--context` stays a bare token in the positionals (sibling
309
- // parser semantics); it must not then be read as the context PATH — a
310
- // `--`-prefixed "path" is a caller mistake, and #2770's law says a missing
311
- // context argument fails CLOSED, never a silent "CONTEXT.md missing" green
312
- // skip. So only a non-flag positional may serve as the context.
313
- const positionalContext = positionals[1] && !positionals[1].startsWith('--') ? positionals[1] : '';
314
- const contextArg = flags['context'] ?? positionalContext ?? '';
315
- const contextPath = contextArg ? resolvePath(contextArg, projectDir) : '';
316
- if (!gateEnabled(projectDir)) {
317
- output({ passed: true, skipped: true, reason: 'workflow.context_coverage_gate is false', total: 0, covered: 0, uncovered: [], message: 'Decision coverage gate disabled by config.' }, raw, undefined);
318
- return;
319
- }
320
- // #2770: an EMPTY/MISSING contextPath argument is a CALLER ERROR (the workflow
321
- // forgot to pass the path — e.g. a shell variable lost between Bash blocks), not
322
- // evidence the phase has no CONTEXT.md. Fail closed (mirrors #1365 fail-loud) so a
323
- // blocking gate cannot silently certify success on a caller mistake.
324
- if (!contextArg || contextArg === '') {
325
- output({ passed: false, skipped: false, reason: 'missing context path argument', total: 0, covered: 0, uncovered: [], message: 'Decision coverage gate called without a context path argument — the caller (e.g. the plan-phase workflow) must pass the CONTEXT.md path. An empty argument is a caller error, not evidence there is nothing to check (#2770).' }, raw, undefined);
326
- return;
327
- }
328
- // A REAL path whose file genuinely does not exist is the LEGITIMATE green skip.
329
- if (!node_fs_1.default.existsSync(contextPath)) {
330
- output({ passed: true, skipped: true, reason: 'CONTEXT.md missing', total: 0, covered: 0, uncovered: [], message: 'No CONTEXT.md - nothing to check.' }, raw, undefined);
331
- return;
332
- }
333
- // #4794: a NON-FILE path (a directory — the adjacent same-looking positional
334
- // swapped, the issue's repro 2) is a caller error like #2770's empty argument:
335
- // fs.existsSync is true, the read yields nothing, and the gate used to
336
- // certify passed:true on a phase full of decisions. Fail closed, naming it.
337
- // The stat is wrapped: a path that vanishes between existsSync and statSync
338
- // (or any stat failure) must answer the SAME fail-closed JSON, never a throw.
339
- let contextIsFile = false;
340
- let contextKind = 'non-file entry';
341
- try {
342
- const st = node_fs_1.default.statSync(contextPath);
343
- contextIsFile = st.isFile();
344
- if (st.isDirectory())
345
- contextKind = 'directory';
54
+ function emitGateResult(result, raw) {
55
+ if ((0, gate_verdict_cjs_1.isGateUsageFailure)(result)) {
56
+ failGate(result);
346
57
  }
347
- catch {
348
- contextIsFile = false;
349
- contextKind = 'unreadable path';
350
- }
351
- if (!contextIsFile) {
352
- output({ passed: false, skipped: false, reason: 'context path is not a file', total: null, covered: null, message: `Decision coverage gate: the context path "${contextArg}" is not a readable file (${contextKind}). Swap the adjacent positionals or pass --context <path-to-CONTEXT.md>.` }, raw, undefined);
353
- return;
354
- }
355
- const { trackable: decisions, outcome, unreadableIds } = loadDecisionExtraction(contextPath);
356
- // #1365 fail-loud gate: any could-not-parse outcome must NOT silently pass —
357
- // even when some decisions were extracted (e.g. D-01 valid but D-02 malformed).
358
- // A parse-miss on ANY bullet means the gate cannot certify full coverage.
359
- // Fire independent of decisions.length so a partial-parse still blocks.
360
- if (outcome === 'could-not-parse') {
361
- // #4794: nothing was measured — the answer must not carry the fields of a
362
- // gate that did. total/covered are null (a type change is the point:
363
- // 0 reads as data, null does not), `uncovered` is OMITTED (the list was
364
- // never built), and the ids that failed to parse are carried so a caller
365
- // capturing stdout knows which decision to fix.
366
- const partialParse = decisions.length > 0;
367
- output({
368
- passed: false,
369
- skipped: false,
370
- reason: 'could-not-parse',
371
- total: null,
372
- covered: null,
373
- unreadable: unreadableIds,
374
- message: (partialParse
375
- ? 'Decision coverage gate: decisions could not be fully parsed — one or more ' +
376
- '`- **D-NN ...**` bullets appear malformed (missing `:` or ` — ` separator, or a phase ' +
377
- 'prefix that is not a digit run, e.g. `D4x-01`). Fix the bullet format so all decisions ' +
378
- 'can be read before re-running the gate.'
379
- : 'Decision coverage gate: could not parse decisions — possible format mismatch. ' +
380
- 'The CONTEXT.md appears to be decision-shaped (has a <decisions> block, a decisions heading, ' +
381
- 'or D- tokens) but no decision bullets could be extracted. Check the formatting of the decisions ' +
382
- 'block and ensure bullets follow the `- **D-NN:** text`, `- **D4-NN:** text` (phase-prefixed), ' +
383
- 'or `- **D-NN — title** body` form. An ID grammar the parser does not support (e.g. `DEC-01`) ' +
384
- 'also lands here.')
385
- + (unreadableIds.length > 0 ? ' Unreadable ids: ' + unreadableIds.join(', ') + '.' : ''),
386
- }, raw, undefined);
387
- return;
388
- }
389
- if (decisions.length === 0) {
390
- output({ passed: true, skipped: true, reason: 'no trackable decisions', total: 0, covered: 0, uncovered: [], message: 'No trackable decisions in CONTEXT.md.' }, raw, undefined);
391
- return;
392
- }
393
- const sections = loadPlanContents(phaseDir).map(extractPlanDesignatedSections);
394
- const uncovered = [];
395
- let covered = 0;
396
- for (const decision of decisions) {
397
- if (sections.some((section) => decisionMentioned(section, decision)))
398
- covered++;
399
- else
400
- uncovered.push({ id: decision.id, text: decision.text, category: decision.category });
401
- }
402
- output({
403
- passed: uncovered.length === 0,
404
- skipped: false,
405
- total: decisions.length,
406
- covered,
407
- uncovered,
408
- message: buildPlanMessage(uncovered),
409
- }, raw, undefined);
58
+ output(result.payload, raw, undefined);
59
+ // After output(): it rewrites the pending-outcome cell on every call. Payload mode — a delivered
60
+ // blocking verdict is still exit 0 (the dispatch contract); an unreadable one is UNAVAILABLE (#5170).
61
+ (0, gate_exit_cjs_1.declareGateExit)(result, 'payload');
410
62
  }
411
- function recentCommitMessages(projectDir) {
412
- try {
413
- return (0, node_child_process_1.execFileSync)('git', ['log', '-n', '200', '--pretty=%s%n%b'], {
414
- cwd: projectDir,
415
- encoding: 'utf-8',
416
- maxBuffer: 4 * 1024 * 1024,
417
- windowsHide: true,
418
- timeout: 15_000,
419
- });
420
- }
421
- catch {
422
- return '';
423
- }
63
+ // ─── Thin wrappers: argv[0]='check', argv[1]=verb — a gate takes the argv AFTER the verb ──────────
64
+ function cmdAutoMode(projectDir, raw) {
65
+ emitGateResult((0, gate_verdict_cjs_1.gateVerdict)('pass', false, { ...(0, check_auto_mode_cjs_1.readAutoModeState)(projectDir) }), raw);
424
66
  }
425
- function readModifiedFilesContent(projectDir, summaries) {
426
- const out = [];
427
- let total = 0;
428
- for (const summary of summaries) {
429
- if (!summary)
430
- continue;
431
- for (const blockMatch of summary.matchAll(/files_modified:\s*\n((?:[ \t]*-\s+.+\n?)+)/g)) {
432
- const files = [...(blockMatch[1] || '').matchAll(/-\s+(.+)/g)]
433
- .map((match) => match[1].trim().replace(/^["']|["']$/g, ''));
434
- for (const file of files) {
435
- if (total >= 50)
436
- break;
437
- if (!file)
438
- continue;
439
- // Migrated off the hand-rolled prefix check (ADR-4650): resolve+contain in one
440
- // step via the canonical realpath predicate — the eventual read below follows
441
- // symlinks, so containment must be decided on the resolved target, not a lexical
442
- // prefix. Read the value the predicate RETURNED; do not re-derive the path.
443
- const candidate = node_path_1.default.isAbsolute(file) ? file : node_path_1.default.join(projectDir, file);
444
- const contained = (0, security_cjs_1.tryWithinRoot)(candidate, projectDir, security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
445
- if (contained === null)
446
- continue;
447
- const raw = readIfExists(contained);
448
- out.push(raw.length > 256 * 1024 ? raw.slice(0, 256 * 1024) : raw);
449
- total++;
450
- }
451
- if (total >= 50)
452
- break;
453
- }
454
- if (total >= 50)
455
- break;
456
- }
457
- return out.join('\n\n');
67
+ function cmdDecisionCoveragePlan(projectDir, args, raw) {
68
+ emitGateResult((0, gate_decision_coverage_plan_cjs_1.evaluateDecisionCoveragePlan)({ projectDir, args: args.slice(2) }), raw);
458
69
  }
459
70
  function cmdDecisionCoverageVerify(projectDir, args, raw) {
460
- const phaseDir = args[2] ? resolvePath(args[2], projectDir) : '';
461
- const contextPath = args[3] ? resolvePath(args[3], projectDir) : '';
462
- if (!gateEnabled(projectDir)) {
463
- output({ skipped: true, blocking: false, reason: 'workflow.context_coverage_gate is false', total: 0, honored: 0, not_honored: [], message: 'Decision coverage gate disabled by config.' }, raw, undefined);
464
- return;
465
- }
466
- if (!contextPath || !node_fs_1.default.existsSync(contextPath)) {
467
- output({ skipped: true, blocking: false, reason: 'CONTEXT.md missing', total: 0, honored: 0, not_honored: [], message: 'No CONTEXT.md - nothing to check.' }, raw, undefined);
468
- return;
469
- }
470
- const { trackable: decisions, outcome: decisionOutcome } = loadDecisionExtraction(contextPath);
471
- // Mirror could-not-parse surface for verify (non-blocking advisory WARN).
472
- // Fire independent of decisions.length — a parse-miss on any bullet must surface,
473
- // even when some decisions were partially extracted (#1365 fix-parity with plan gate).
474
- if (decisionOutcome === 'could-not-parse') {
475
- const partialParse = decisions.length > 0;
476
- output({
477
- skipped: false,
478
- blocking: false,
479
- reason: 'could-not-parse',
480
- total: decisions.length,
481
- honored: 0,
482
- not_honored: [],
483
- message: partialParse
484
- ? 'Decision coverage verify (warning): decisions could not be fully parsed — one or more ' +
485
- '`- **D-NN ...**` bullets appear malformed (missing `:` or ` — ` separator, or a phase ' +
486
- 'prefix that is not a digit run). Fix the bullet format in the CONTEXT.md decisions block.'
487
- : 'Decision coverage verify (warning): could not parse decisions — possible format mismatch. ' +
488
- 'Check the formatting of the CONTEXT.md decisions block (accepted forms: `- **D-NN:** text`, ' +
489
- '`- **D4-NN:** text` (phase-prefixed), `- **D-NN — title** body`).',
490
- }, raw, undefined);
491
- return;
492
- }
493
- if (decisions.length === 0) {
494
- output({ skipped: true, blocking: false, reason: 'no trackable decisions', total: 0, honored: 0, not_honored: [], message: 'No trackable decisions in CONTEXT.md.' }, raw, undefined);
495
- return;
496
- }
497
- const planContents = loadPlanContents(phaseDir);
498
- // #3183 (lint-plan-count-drift): same single-owner sourcing as
499
- // loadPlanContents above — scanPhasePlans's summaryFiles instead of a
500
- // local `-SUMMARY.md` readdirSync filter.
501
- const summaryParts = node_fs_1.default.existsSync(phaseDir)
502
- ? scanPhasePlans(phaseDir).summaryFiles.map((entry) => readIfExists(node_path_1.default.join(phaseDir, entry)))
503
- : [];
504
- const haystack = [
505
- planContents.join('\n\n'),
506
- summaryParts.join('\n\n'),
507
- readModifiedFilesContent(projectDir, summaryParts),
508
- recentCommitMessages(projectDir),
509
- ].join('\n\n');
510
- const notHonored = [];
511
- let honored = 0;
512
- for (const decision of decisions) {
513
- if (decisionMentioned(haystack, decision))
514
- honored++;
515
- else
516
- notHonored.push({ id: decision.id, text: decision.text, category: decision.category });
517
- }
518
- output({
519
- skipped: false,
520
- blocking: false,
521
- total: decisions.length,
522
- honored,
523
- not_honored: notHonored,
524
- message: buildVerifyMessage(notHonored),
525
- }, raw, undefined);
526
- }
527
- // ─── ui-plan-gate ─────────────────────────────────────────────────────────────
528
- /**
529
- * ui-plan-gate: given a phase number, checks whether the phase has frontend
530
- * indicators and whether a *-UI-SPEC.md already exists in the phase directory.
531
- *
532
- * Returns JSON: { frontend, hasFrontendEvidence, hasUiSpec, block, uiSpecPath, matchedToken, matchedLine }
533
- * block = frontend && hasFrontendEvidence && !hasUiSpec (#3312: gate fires when
534
- * UI work is detected AND the repo has static frontend evidence but no spec exists)
535
- *
536
- * Invocable as: gsd_run check ui-plan-gate <phase>
537
- *
538
- * Uses checkUiPresence from ui-safety-gate.cjs — does NOT reimplement frontend detection.
539
- * Uses getRoadmapPhaseWithFallback + findPhaseInternal from leaf modules for phase data.
540
- */
541
- function findUiSpecInDir(phaseDir) {
542
- if (!phaseDir || !node_fs_1.default.existsSync(phaseDir))
543
- return '';
544
- try {
545
- const files = node_fs_1.default.readdirSync(phaseDir);
546
- const found = files.find((f) => /-UI-SPEC\.md$/.test(f));
547
- return found ? node_path_1.default.join(phaseDir, found) : '';
548
- }
549
- catch {
550
- return '';
551
- }
71
+ emitGateResult((0, gate_decision_coverage_verify_cjs_1.evaluateDecisionCoverageVerify)({ projectDir, args: args.slice(2) }), raw);
552
72
  }
553
- /**
554
- * Pure logic for ui-plan-gate — exposed for direct behavioral testing.
555
- *
556
- * Given a projectDir and phase number:
557
- * (a) Reads the phase section from ROADMAP.md via getRoadmapPhaseWithFallback —
558
- * same two-pass lookup (current milestone → full roadmap) as `roadmap.get-phase`
559
- * (cmdRoadmapGetPhase). Cross-milestone / older frontend phases resolve correctly.
560
- * If ROADMAP.md is missing, phaseSection is '' (ROADMAP.md not present = project
561
- * has no roadmap = cannot be frontend). If the phase truly can't be found after
562
- * both passes, phaseSection is '' and phaseLookupFailed is set so callers can
563
- * surface the miss — we do NOT silently degrade to frontend:false if the roadmap
564
- * exists but the phase header is absent.
565
- * (b) Runs checkUiPresence (frontend detection) — no reimplementation.
566
- * (c) Resolves the phase directory via findPhaseInternal (phase-locator.cjs); checks for *-UI-SPEC.md.
567
- *
568
- * Returns: { frontend, hasFrontendEvidence, hasUiSpec, block, uiSpecPath, matchedToken, matchedLine, phaseLookupFailed }
569
- * block = frontend && hasFrontendEvidence && !hasUiSpec (#3312)
570
- * phaseLookupFailed = ROADMAP.md present but phase header not found (surfaced for
571
- * onError:halt gates so a missing phase doesn't silently bypass)
572
- *
573
- * #3312 — structural corroboration: `frontend` is a vocabulary signal only. A
574
- * hyphen is a word boundary, so a phase naming the repo `dashboard-financeiro`
575
- * matches the token `dashboard` exactly like the real compound `micro-frontend`
576
- * (the boundary rule of #3718 is intentional and untouched). The gate therefore
577
- * blocks only when the token match is corroborated by static frontend evidence
578
- * in the repo tree (hasStaticFrontendEvidence: package.json UI-framework dep, a
579
- * component-framework file, or native UI evidence — a `.xaml` file or a
580
- * `.swift`/`.kt`/`.dart` file carrying its ecosystem's UI import marker,
581
- * #4658). This mirrors the sibling post-wave gate
582
- * computeUiSafetyGate, which requires `hasUiFiles` (git diff) before blocking.
583
- * matchedToken/matchedLine surface what tripped the sniffer so an operator can
584
- * judge the flag in one second instead of reaching for --skip-ui.
585
- */
586
- function computeUiPlanGate(projectDir, phase) {
587
- // (a) Read the phase section text using the same two-pass lookup as roadmap.get-phase.
588
- // getRoadmapPhaseWithFallback: current-milestone first, then stripShippedMilestones
589
- // fallback — mirrors cmdRoadmapGetPhase exactly.
590
- let phaseSection = '';
591
- let phaseLookupFailed;
592
- try {
593
- const section = getRoadmapPhaseWithFallback(projectDir, phase);
594
- if (section === null) {
595
- // Distinguish: ROADMAP.md missing (no-roadmap project) vs phase not found in ROADMAP.
596
- // planningDir(cwd) resolves the .planning/ root for workstream-aware paths.
597
- const planDir = planningDir(projectDir);
598
- const roadmapPath = node_path_1.default.join(planDir, 'ROADMAP.md');
599
- if (node_fs_1.default.existsSync(roadmapPath)) {
600
- // ROADMAP.md exists but phase was not found → surface the miss
601
- phaseLookupFailed = true;
602
- }
603
- // phaseSection stays ''
604
- }
605
- else {
606
- phaseSection = section;
607
- }
608
- }
609
- catch { /* roadmap read failure → treat as empty (non-frontend) */ }
610
- // (b) Run checkUiPresence (frontend detection) — reuse existing helper; no reimplementation
611
- const presenceResult = (0, ui_safety_gate_cjs_1.checkUiPresence)(phaseSection);
612
- const frontend = presenceResult.hasUI;
613
- // (b') #3312 — static structural corroboration. Only probed when the sniffer
614
- // matched (evidence is irrelevant otherwise); failures degrade to false.
615
- const hasFrontendEvidence = frontend ? (0, ui_frontend_evidence_cjs_1.hasStaticFrontendEvidence)(projectDir) : false;
616
- // (c) Resolve phase directory via findPhaseInternal and check for *-UI-SPEC.md
617
- let phaseDir = '';
618
- try {
619
- const result = findPhaseInternal(projectDir, phase);
620
- if (result && typeof result === 'object') {
621
- // findPhaseInternal returns { directory: '<relative-posix-path>', ... }
622
- // directory is relative to cwd — resolve it to absolute.
623
- const relDir = typeof result['directory'] === 'string' ? result['directory'] : '';
624
- if (relDir) {
625
- phaseDir = node_path_1.default.resolve(projectDir, relDir);
626
- }
627
- }
628
- else if (typeof result === 'string') {
629
- phaseDir = result;
630
- }
631
- }
632
- catch { /* phase dir lookup failure → hasUiSpec=false */ }
633
- const uiSpecPath = findUiSpecInDir(phaseDir);
634
- const hasUiSpec = uiSpecPath !== '';
635
- // block = frontend phase with structural frontend evidence and no UI-SPEC (#3312)
636
- const block = frontend && hasFrontendEvidence && !hasUiSpec;
637
- const result = {
638
- frontend, hasFrontendEvidence, hasUiSpec, block,
639
- uiSpecPath: hasUiSpec ? uiSpecPath : null,
640
- matchedToken: presenceResult.matchedToken,
641
- matchedLine: presenceResult.matchedLine,
642
- };
643
- if (phaseLookupFailed)
644
- result.phaseLookupFailed = true;
645
- return result;
73
+ function cmdEvaluationScope(projectDir, args, raw) {
74
+ emitGateResult((0, gate_evaluation_scope_cjs_1.evaluateEvaluationScope)({ projectDir, args: args.slice(2) }), raw);
646
75
  }
647
76
  function cmdUiPlanGate(projectDir, args, raw) {
648
- // args[0] = 'check', args[1] = 'ui-plan-gate', args[2] = phase
649
- const phase = args[2] || '';
650
- if (!phase) {
651
- error('ui-plan-gate requires a phase argument: check ui-plan-gate <phase>', ERROR_REASON.SDK_MISSING_ARG);
652
- return;
653
- }
654
- output(computeUiPlanGate(projectDir, phase), raw, undefined);
655
- }
656
- // ─── ui-safety-gate ───────────────────────────────────────────────────────────
657
- /**
658
- * ui-safety-gate: post-wave check that verifies UI-changed files conform to
659
- * the active UI-SPEC for the phase. Called after each wave by execute:wave:post.
660
- *
661
- * Returns JSON: { frontend: boolean, hasUiFiles: boolean, hasUiSpec: boolean, block: boolean, message?: string }
662
- * block = frontend && hasUiFiles && !hasUiSpec
663
- *
664
- * Args: check ui-safety-gate <phase>
665
- * Invocable as: gsd_run check ui-safety-gate <phase>
666
- * or gsd_run check ui.safety-gate <phase> (dots normalized to hyphens)
667
- *
668
- * Uses checkUiPresence from ui-safety-gate.cjs — does NOT reimplement frontend detection.
669
- * Checks whether any files changed in recent git history match frontend file patterns.
670
- * Also checks whether a *-UI-SPEC.md exists in the phase directory (same as ui-plan-gate).
671
- *
672
- * Limitation: uses git diff HEAD~1..HEAD which covers only the last commit; in a
673
- * multi-plan wave the wave-start commit would be more accurate but is not yet stored
674
- * in the wave manifest. This is tracked as a known limitation.
675
- */
676
- const UI_FILE_EXTENSIONS_RE = /\.(tsx|jsx|css|scss|sass|less|vue|svelte|html)$/i;
677
- const UI_PATH_PATTERNS_RE = /\/(components|pages|views|screens|layouts|ui|frontend)\//i;
678
- /**
679
- * Pure logic for ui-safety-gate — exposed for direct behavioral testing.
680
- *
681
- * Given a projectDir and phase number:
682
- * (a) Reads the phase section from ROADMAP.md via getRoadmapPhaseWithFallback —
683
- * same lookup as computeUiPlanGate — to determine if this is a frontend phase.
684
- * (b) Runs checkUiPresence (frontend detection) — no reimplementation.
685
- * (c) Checks git diff HEAD~1..HEAD for UI file changes in the current worktree.
686
- * (d) Resolves the phase directory via findPhaseInternal (phase-locator.cjs); checks for *-UI-SPEC.md.
687
- *
688
- * Returns: { frontend, hasUiFiles, hasUiSpec, block, message?, phaseLookupFailed? }
689
- * block = frontend && hasUiFiles && !hasUiSpec
690
- * phaseLookupFailed = ROADMAP.md present but phase header not found
691
- */
692
- function computeUiSafetyGate(projectDir, phase) {
693
- // (a) Read the phase section text (same two-pass lookup as computeUiPlanGate)
694
- let phaseSection = '';
695
- let phaseLookupFailed;
696
- try {
697
- const section = getRoadmapPhaseWithFallback(projectDir, phase);
698
- if (section === null) {
699
- const planDir = planningDir(projectDir);
700
- const roadmapPath = node_path_1.default.join(planDir, 'ROADMAP.md');
701
- if (node_fs_1.default.existsSync(roadmapPath)) {
702
- phaseLookupFailed = true;
703
- }
704
- }
705
- else {
706
- phaseSection = section;
707
- }
708
- }
709
- catch { /* roadmap read failure → treat as empty (non-frontend) */ }
710
- // (b) Run checkUiPresence (frontend detection) — reuse existing helper; no reimplementation
711
- const presenceResult = (0, ui_safety_gate_cjs_1.checkUiPresence)(phaseSection);
712
- const frontend = presenceResult.hasUI;
713
- // (c) Check whether any UI files were changed in recent git commits
714
- // Uses git diff HEAD~1..HEAD to detect frontend file changes since last commit.
715
- // Known limitation: multi-plan waves may need the wave-start commit for full coverage.
716
- let hasUiFiles = false;
717
- try {
718
- const changed = (0, node_child_process_1.execFileSync)('git', ['diff', '--name-only', 'HEAD~1', 'HEAD'], {
719
- cwd: projectDir,
720
- encoding: 'utf-8',
721
- maxBuffer: 2 * 1024 * 1024,
722
- windowsHide: true,
723
- timeout: 10_000,
724
- });
725
- hasUiFiles = changed.split('\n').some((f) => f.trim() && (UI_FILE_EXTENSIONS_RE.test(f) || UI_PATH_PATTERNS_RE.test(f)));
726
- }
727
- catch { /* git unavailable or no prior commit — treat as no UI files changed */ }
728
- // (d) Resolve phase directory and check for *-UI-SPEC.md (same as computeUiPlanGate)
729
- let phaseDir = '';
730
- try {
731
- const result = findPhaseInternal(projectDir, phase);
732
- if (result && typeof result === 'object') {
733
- const relDir = typeof result['directory'] === 'string' ? result['directory'] : '';
734
- if (relDir) {
735
- phaseDir = node_path_1.default.resolve(projectDir, relDir);
736
- }
737
- }
738
- else if (typeof result === 'string') {
739
- phaseDir = result;
740
- }
741
- }
742
- catch { /* phase dir lookup failure → hasUiSpec=false */ }
743
- const uiSpecPath = findUiSpecInDir(phaseDir);
744
- const hasUiSpec = uiSpecPath !== '';
745
- // block only when: this is a frontend phase AND UI files were changed AND no UI-SPEC exists
746
- const block = frontend && hasUiFiles && !hasUiSpec;
747
- const result = { frontend, hasUiFiles, hasUiSpec, block };
748
- if (block) {
749
- result.message = `UI files changed in this wave but no UI-SPEC.md exists for Phase ${phase}. ` +
750
- `Run /gsd:ui-phase ${phase} to generate the design contract before continuing.`;
751
- }
752
- if (phaseLookupFailed)
753
- result.phaseLookupFailed = true;
754
- return result;
77
+ emitGateResult((0, gate_ui_plan_cjs_1.evaluateUiPlanGate)({ projectDir, args: args.slice(2) }), raw);
755
78
  }
756
79
  function cmdUiSafetyGate(projectDir, args, raw) {
757
- // args[0] = 'check', args[1] = 'ui-safety-gate', args[2] = phase
758
- const phase = args[2] || '';
759
- if (!phase) {
760
- error('ui-safety-gate requires a phase argument: check ui-safety-gate <phase>', ERROR_REASON.SDK_MISSING_ARG);
761
- return;
762
- }
763
- output(computeUiSafetyGate(projectDir, phase), raw, undefined);
80
+ emitGateResult((0, gate_ui_safety_cjs_1.evaluateUiSafetyGate)({ projectDir, args: args.slice(2) }), raw);
764
81
  }
765
82
  function cmdTddReviewCheckpoint(projectDir, args, raw) {
766
- // args[0] = 'check', args[1] = 'tdd-review-checkpoint' (normalized), args[2] = phase
767
- const phase = args[2] || '';
768
- if (!phase) {
769
- error('tdd.review-checkpoint requires a phase argument: check tdd.review-checkpoint <phase>', ERROR_REASON.SDK_MISSING_ARG);
770
- return;
771
- }
772
- // Resolve phase directory
773
- let phaseDir = '';
774
- try {
775
- const result = findPhaseInternal(projectDir, phase);
776
- if (result && typeof result === 'object') {
777
- const relDir = typeof result['directory'] === 'string' ? result['directory'] : '';
778
- if (relDir)
779
- phaseDir = node_path_1.default.resolve(projectDir, relDir);
780
- }
781
- else if (typeof result === 'string') {
782
- phaseDir = result;
783
- }
784
- }
785
- catch { /* phase dir lookup failure */ }
786
- // Find all PLAN.md files with type: tdd in frontmatter
787
- const tddPlanFiles = [];
788
- if (phaseDir) {
789
- try {
790
- // #3183: canonical plan set (root+nested, superseded-excluded) from the
791
- // single owner, rather than a root-only hand-rolled readdirSync filter.
792
- const files = scanPhasePlans(phaseDir).planFiles;
793
- for (const file of files) {
794
- const planPath = node_path_1.default.join(phaseDir, file);
795
- const content = readIfExists(planPath);
796
- // Check frontmatter for type: tdd
797
- // CRLF-tolerant: a PLAN.md written with Windows line endings (---\r\n...---)
798
- // must still match. The same CRLF-tolerant form is already used at line 205
799
- // (extractPlanDesignatedSections); this is the same canonical pattern, applied
800
- // here for the tdd-classification path. Fixes #2449.
801
- const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
802
- if (frontmatterMatch) {
803
- const fm = frontmatterMatch[1];
804
- if (/^type:\s*tdd\s*$/m.test(fm)) {
805
- tddPlanFiles.push(planPath);
806
- }
807
- }
808
- }
809
- }
810
- catch { /* directory read failure */ }
811
- }
812
- if (tddPlanFiles.length === 0) {
813
- const result = {
814
- // Uniform gate contract: block = violations > 0 (advisory; never truly blocks).
815
- block: false,
816
- passed: true,
817
- tddPlans: 0,
818
- violations: 0,
819
- table: '',
820
- rows: [],
821
- message: `No type:tdd plans found in phase ${phase}. TDD review skipped.`,
822
- };
823
- // Pass undefined as rawValue so --raw emits JSON (not plain text).
824
- // The human-readable report is carried in `result.message` for the
825
- // dispatch's advisory branch to surface.
826
- output(result, raw, undefined);
827
- return;
828
- }
829
- // For each TDD plan, extract the plan ID (padded plan number) and check git log
830
- const rows = [];
831
- for (const planPath of tddPlanFiles) {
832
- // Extract plan ID from filename (e.g. "01-02-PLAN.md" → "01-02", or "03-PLAN.md" → "03")
833
- const basename = node_path_1.default.basename(planPath, '-PLAN.md');
834
- // planId for commit grep: phase-plan format, e.g. "01-02"
835
- const planId = basename;
836
- // Check for RED gate commit: test({planId}):
837
- let red = false;
838
- let green = false;
839
- let refactor = false;
840
- try {
841
- const redCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^test(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true, timeout: 10_000 });
842
- red = redCommit.trim().length > 0;
843
- }
844
- catch { /* git unavailable or no match */ }
845
- try {
846
- const greenCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^feat(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true, timeout: 10_000 });
847
- green = greenCommit.trim().length > 0;
848
- }
849
- catch { /* git unavailable or no match */ }
850
- try {
851
- const refactorCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^refactor(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true, timeout: 10_000 });
852
- refactor = refactorCommit.trim().length > 0;
853
- }
854
- catch { /* git unavailable or no match */ }
855
- const missing = [];
856
- if (!red)
857
- missing.push('RED');
858
- if (!green)
859
- missing.push('GREEN');
860
- const status = missing.length === 0 ? 'Pass' : 'FAIL';
861
- rows.push({ planId, red, green, refactor, status, missing });
862
- }
863
- const violations = rows.filter(r => r.status === 'FAIL').length;
864
- // Build review table
865
- const tableHeader = '| Plan | RED | GREEN | REFACTOR | Status |';
866
- const tableDivider = '|------|-----|-------|----------|--------|';
867
- const tableRows = rows.map(r => `| ${r.planId.padEnd(4)} | ${r.red ? ' ✓ ' : ' ✗ '} | ${r.green ? ' ✓ ' : ' ✗ '} | ${r.refactor ? ' ✓ ' : ' — '} | ${r.status.padEnd(6)} |`);
868
- let table = [
869
- `### TDD REVIEW — Phase ${phase}`,
870
- '',
871
- `TDD Plans: ${tddPlanFiles.length} | Gate violations: ${violations}`,
872
- '',
873
- tableHeader,
874
- tableDivider,
875
- ...tableRows,
876
- ].join('\n');
877
- if (violations > 0) {
878
- table += '\n\n⚠ Gate violations are advisory — review before advancing.';
879
- for (const r of rows.filter(row => row.status === 'FAIL')) {
880
- table += `\n Plan ${r.planId} missing: ${r.missing.join(', ')} gate commit(s).`;
881
- table += `\n Expected commit pattern: test(${r.planId}): ... → feat(${r.planId}): ...`;
882
- }
883
- }
884
- const result = {
885
- // Uniform gate contract: block = violations > 0.
886
- // This gate is advisory (blocking: false in capability.json) so block:true
887
- // only surfaces as a warning, never halts. Kept here so the host-loop
888
- // dispatch can read a single consistent `block` field.
889
- block: violations > 0,
890
- passed: true,
891
- tddPlans: tddPlanFiles.length,
892
- violations,
893
- table,
894
- rows,
895
- // Human-readable report in `message` so the dispatch's advisory branch
896
- // can surface it. --raw emits JSON (rawValue=undefined), not plain text.
897
- message: table,
898
- };
899
- // Pass undefined as rawValue so --raw emits JSON (not the raw table text).
900
- // The review table is carried in `result.message` and `result.table` so
901
- // the host-loop dispatch's advisory branch can surface it.
902
- output(result, raw, undefined);
83
+ emitGateResult((0, gate_tdd_review_checkpoint_cjs_1.evaluateTddReviewCheckpoint)({ projectDir, args: args.slice(2) }), raw);
903
84
  }
904
- // ─── tdd-red-evidence (#3770) ──────────────────────────────────────────────────
905
- /**
906
- * tdd-red-evidence: validates a persisted RED-phase test-run record for a
907
- * `type: tdd` plan (#3770). Only an INTENTIONAL failure of the target test
908
- * (verdict RED_EVIDENCE_OK) may authorize GREEN; zero-test discovery, fixture/
909
- * load crashes, nonzero exits without a failing test, unrelated failures, and
910
- * unexpected greens are INVALID_RED and block GREEN.
911
- *
912
- * The record is the JSON the executor persists after running the RED command:
913
- * { command, exitCode, output, targetTest, targetFile?, expected?, actual? }
914
- * Fail-closed: a missing/unreadable/unparseable record is INVALID_RED
915
- * (reason unreadable_record), never a pass.
916
- *
917
- * Args: check tdd-red-evidence <record.json>
918
- */
919
- function cmdTddRedEvidence(_projectDir, args, raw) {
920
- const recordPath = typeof args[2] === 'string' ? args[2] : '';
921
- if (!recordPath) {
922
- error('tdd-red-evidence requires a record path: check tdd-red-evidence <record.json>', ERROR_REASON.SDK_MISSING_ARG);
923
- return;
924
- }
925
- const resolved = node_path_1.default.resolve(recordPath);
926
- const text = readIfExists(resolved);
927
- const input = (() => {
928
- if (!text)
929
- return null;
930
- try {
931
- return (JSON.parse(text) ?? {});
932
- }
933
- catch {
934
- return null;
935
- }
936
- })();
937
- if (!input) {
938
- output({
939
- passed: false,
940
- block: true,
941
- verdict: 'INVALID_RED',
942
- reason: 'unreadable_record',
943
- record: resolved,
944
- readError: text ? `record is not valid JSON: ${resolved}` : `record not found or unreadable: ${resolved}`,
945
- }, raw, undefined);
946
- return;
947
- }
948
- const evidenceInput = {
949
- command: input['command'],
950
- exitCode: input['exitCode'],
951
- output: input['output'],
952
- targetTest: input['targetTest'],
953
- targetFile: input['targetFile'],
954
- expected: input['expected'],
955
- actual: input['actual'],
956
- };
957
- const result = (0, tdd_red_evidence_cjs_1.classifyRedEvidence)(evidenceInput);
958
- const record = (0, tdd_red_evidence_cjs_1.buildRedEvidenceRecord)(evidenceInput, result);
959
- output({
960
- // Uniform gate contract: block = !passed. INVALID_RED blocks GREEN.
961
- passed: result.verdict === 'RED_EVIDENCE_OK',
962
- block: result.verdict !== 'RED_EVIDENCE_OK',
963
- verdict: result.verdict,
964
- reason: result.reason,
965
- evidence: result.evidence,
966
- record,
967
- message: result.verdict === 'RED_EVIDENCE_OK'
968
- ? `RED evidence verified: target test "${result.evidence.target_test}" failed as expected (exit ${result.evidence.exit_code}). GREEN authorized.`
969
- : `INVALID_RED (${result.reason}): GREEN blocked. Fix the RED phase — only an intentional failure of target test "${result.evidence.target_test}" authorizes production edits.`,
970
- }, raw, undefined);
85
+ function cmdTddRedEvidence(projectDir, args, raw) {
86
+ emitGateResult((0, gate_tdd_red_evidence_cjs_1.evaluateTddRedEvidence)({ projectDir, args: args.slice(2) }), raw);
971
87
  }
972
- /**
973
- * Resolve a phase argument to an absolute phase directory, or '' when it
974
- * cannot be resolved. Shared by every `check` arm that probes a phase's
975
- * PLAN.md files, so the two never drift (DEFECT.GENERATIVE-FIX-DIVERGENCE).
976
- * Never throws — the callers emit a degraded JSON payload instead, because
977
- * a consumer must be able to tell "nothing to report" from "could not look".
978
- */
979
- function resolvePhaseDirOrEmpty(projectDir, phase) {
980
- try {
981
- const result = findPhaseInternal(projectDir, phase);
982
- if (result && typeof result === 'object') {
983
- // findPhaseInternal returns { directory: '<relative-posix-path>', ... }
984
- // directory is relative to cwd — resolve it to absolute.
985
- const relDir = typeof result['directory'] === 'string' ? result['directory'] : '';
986
- if (relDir) {
987
- return node_path_1.default.resolve(projectDir, relDir);
988
- }
989
- }
990
- else if (typeof result === 'string') {
991
- return result;
992
- }
993
- }
994
- catch { /* phase dir lookup failure → caller emits degraded payload */ }
995
- return '';
996
- }
997
- // ─── verify-command-paths (#2401) ──────────────────────────────────────────────
998
- /**
999
- * verify-command-paths: probes every `<automated>` verify command declared in a
1000
- * phase's `-PLAN.md` files against the filesystem WITHOUT executing anything —
1001
- * see verify-command-grounding.cjs for the recognizer contract.
1002
- *
1003
- * Args: check verify-command-paths <phase> | check verify-command-paths --dir <plan-dir>
1004
- * Invocable as: gsd_run check verify-command-paths <phase>
1005
- * gsd_run check verify-command-paths --dir <plan-dir>
1006
- *
1007
- * `--dir` (#4767) names a directory holding `-PLAN.md` files directly, for
1008
- * plans that live outside `.planning/phases/` — quick mode's
1009
- * `.planning/quick/<id>/` is the motivating caller, which until #4767 never ran
1010
- * this probe at all. The directory is resolved against the project root AND
1011
- * CONTAINED WITHIN IT — an absolute or climbing `--dir` that lands outside the
1012
- * root is `unresolvable`, never read — then probed exactly as a phase directory
1013
- * is; `projectRoot` stays the project root in both forms. `--dir <value>` is the
1014
- * only accepted spelling: `--dir=<value>` yields no `dir` flag and falls through to
1015
- * the no-argument arm, as does an empty value. Both are `partitionPredicateArgs`
1016
- * behaviour, inherited and unchanged. (How that parser resolves a REPEATED `--dir`
1017
- * is deliberately not characterised here — a malformed later occurrence does not
1018
- * displace an earlier valid one, so the obvious "last one wins" gloss is wrong.)
1019
- *
1020
- * When the phase cannot be resolved to a directory, this emits a non-throwing
1021
- * degraded JSON payload (status/commands/counts all zeroed, `readError`
1022
- * populated) rather than calling `error()` — the plan-checker parses this
1023
- * result and must be able to distinguish "nothing to report" from "could not
1024
- * look", which a non-zero exit / thrown error would collapse.
1025
- */
1026
88
  function cmdVerifyCommandPaths(projectDir, args, raw) {
1027
- // args[0] = 'check', args[1] = 'verify-command-paths', then either a phase
1028
- // positional or `--dir <plan-dir>` (#4767).
1029
- const { flags, positionals } = partitionPredicateArgs(args.slice(2));
1030
- const dirFlag = typeof flags['dir'] === 'string' ? flags['dir'] : '';
1031
- // First non-flag positional: `--raw` (valueless) lands in positionals too, and its position
1032
- // relative to the phase argument is the caller's choice.
1033
- const phase = positionals.find(p => !p.startsWith('--')) ?? '';
1034
- if (!phase && !dirFlag) {
1035
- output({
1036
- status: 'unresolvable',
1037
- commands: [],
1038
- counts: { blocker: 0, warning: 0, total: 0 },
1039
- readError: 'verify-command-paths requires a phase argument or --dir: check verify-command-paths <phase> | --dir <plan-dir>',
1040
- }, raw, undefined);
1041
- return;
1042
- }
1043
- // `--dir` is CALLER-SUPPLIED, so it is contained before it reaches the
1044
- // `readdirSync`/`readFileSync` calls in probePhaseVerifyCommands (#4785 review).
1045
- // Same predicate and policy as `resolvePath` above, and for the reason ADR-4650
1046
- // gives at the other read site: the reads below FOLLOW SYMLINKS, so containment
1047
- // must be decided on the resolved target, not a lexical prefix — a link inside
1048
- // the root pointing outside it passes `tryWithinRootLexical` and is then read.
1049
- // Read the value the predicate RETURNED; never re-derive the path. An escape
1050
- // degrades to the same non-throwing payload the unresolvable-phase arm emits,
1051
- // because a consumer must be able to tell "could not look" from "nothing to
1052
- // report" (and `error()` would collapse them).
1053
- //
1054
- // RESIDUAL, stated rather than left to be rediscovered: this is check-then-use, so
1055
- // a symlink planted at the resolved path BETWEEN this call and the reads inside
1056
- // probePhaseVerifyCommands would be followed. A link already in place when the
1057
- // command runs IS refused — the predicate resolves it and returns null (driven) —
1058
- // so the window is the in-process gap, not the ordinary case. It is a property of
1059
- // every `tryWithinRoot` call site in this repo, including `resolvePath` above and
1060
- // the artifact scan below, not of this arm; closing it needs O_NOFOLLOW/dirfd
1061
- // semantics inside the ADR-4650 predicate, which is a wider change than the bug
1062
- // this fixes.
1063
- let phaseDir;
1064
- if (dirFlag) {
1065
- const candidate = node_path_1.default.isAbsolute(dirFlag) ? dirFlag : node_path_1.default.join(projectDir, dirFlag);
1066
- const contained = (0, security_cjs_1.tryWithinRoot)(candidate, projectDir, security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
1067
- if (contained === null) {
1068
- output({
1069
- status: 'unresolvable',
1070
- commands: [],
1071
- counts: { blocker: 0, warning: 0, total: 0 },
1072
- readError: `--dir resolves outside the project root: ${dirFlag}`,
1073
- }, raw, undefined);
1074
- return;
1075
- }
1076
- phaseDir = contained;
1077
- }
1078
- else {
1079
- phaseDir = resolvePhaseDirOrEmpty(projectDir, phase);
1080
- }
1081
- if (!phaseDir) {
1082
- output({
1083
- status: 'unresolvable',
1084
- commands: [],
1085
- counts: { blocker: 0, warning: 0, total: 0 },
1086
- readError: `could not resolve phase directory for phase ${phase}`,
1087
- }, raw, undefined);
1088
- return;
1089
- }
1090
- const probed = probePhaseVerifyCommands({ phaseDir, projectRoot: projectDir });
1091
- output(probed, raw, undefined);
89
+ emitGateResult((0, gate_verify_command_paths_cjs_1.evaluateVerifyCommandPaths)({ projectDir, args: args.slice(2) }), raw);
1092
90
  }
1093
- // ─── verify-failure-directions (#3172) ─────────────────────────────────────────
1094
- /**
1095
- * verify-failure-directions: probes every `<automated>` verify command
1096
- * declared in a phase's `-PLAN.md` files for a stated `<fails_when>` failing
1097
- * direction — see verify-command-grounding.cjs for the recognizer contract.
1098
- *
1099
- * Args: check verify-failure-directions <phase>
1100
- * Invocable as: gsd_run check verify-failure-directions <phase>
1101
- *
1102
- * When the phase cannot be resolved to a directory, this emits a non-throwing
1103
- * degraded JSON payload (status/commands/counts all zeroed, `readError`
1104
- * populated) rather than calling `error()` — the plan-checker parses this
1105
- * result and must be able to distinguish "nothing to report" from "could not
1106
- * look", which a non-zero exit / thrown error would collapse.
1107
- */
1108
91
  function cmdVerifyFailureDirections(projectDir, args, raw) {
1109
- // args[0] = 'check', args[1] = 'verify-failure-directions', args[2] = phase
1110
- const phase = args[2] || '';
1111
- if (!phase) {
1112
- output({
1113
- status: 'unresolvable',
1114
- commands: [],
1115
- counts: { blocker: 0, warning: 0, total: 0 },
1116
- readError: 'verify-failure-directions requires a phase argument: check verify-failure-directions <phase>',
1117
- }, raw, undefined);
1118
- return;
1119
- }
1120
- const phaseDir = resolvePhaseDirOrEmpty(projectDir, phase);
1121
- if (!phaseDir) {
1122
- output({
1123
- status: 'unresolvable',
1124
- commands: [],
1125
- counts: { blocker: 0, warning: 0, total: 0 },
1126
- readError: `could not resolve phase directory for phase ${phase}`,
1127
- }, raw, undefined);
1128
- return;
1129
- }
1130
- const result = probePhaseFailingDirections({ phaseDir });
1131
- output(result, raw, undefined);
92
+ emitGateResult((0, gate_verify_failure_directions_cjs_1.evaluateVerifyFailureDirections)({ projectDir, args: args.slice(2) }), raw);
1132
93
  }
1133
- // ─── gap-analysis-plan-post ───────────────────────────────────────────────────
1134
- /**
1135
- * gap-analysis-plan-post: non-blocking advisory check that runs the post-planning
1136
- * gap analysis after all PLAN.md files are generated for a phase.
1137
- *
1138
- * Cross-references every REQ-ID and D-ID from REQUIREMENTS.md and CONTEXT.md
1139
- * against the concatenated text of all *-PLAN.md files, emitting a coverage table.
1140
- *
1141
- * This gate is always advisory (passed: true) — it never blocks phase advancement.
1142
- *
1143
- * Args: check gap-analysis.plan-post <phase-dir> [phase-req-ids]
1144
- * Invocable as: gsd_run check gap-analysis.plan-post <phase-dir> [phase-req-ids]
1145
- */
1146
94
  function cmdGapAnalysisPlanPost(projectDir, args, raw) {
1147
- // args[0] = 'check', args[1] = 'gap-analysis-plan-post' (normalized), args[2] = phaseDir, args[3] = phaseReqIds
1148
- const phaseDir = args[2] || '';
1149
- if (!phaseDir) {
1150
- error('gap-analysis.plan-post requires a phase-dir argument: check gap-analysis.plan-post <phase-dir> [phase-req-ids]', ERROR_REASON.SDK_MISSING_ARG);
1151
- return;
1152
- }
1153
- const resolvedPhaseDir = resolvePath(phaseDir, projectDir);
1154
- const phaseReqIds = args[3] ?? undefined;
1155
- const result = runGapAnalysis(projectDir, resolvedPhaseDir, { phaseReqIds });
1156
- // Uniform gate contract: block = false (gap-analysis is always advisory, never blocks).
1157
- // `message` carries the human-readable gap analysis report so the dispatch's
1158
- // advisory branch can surface it. --raw emits JSON (rawValue=undefined), not
1159
- // plain markdown text.
1160
- output({
1161
- block: false,
1162
- passed: true,
1163
- enabled: result.enabled,
1164
- table: result.table,
1165
- summary: result.summary,
1166
- counts: result.counts,
1167
- // Human-readable report in `message` for the host-loop advisory branch.
1168
- message: result.table || result.summary || '',
1169
- }, raw, undefined);
95
+ emitGateResult((0, gate_gap_analysis_plan_post_cjs_1.evaluateGapAnalysisPlanPost)({ projectDir, args: args.slice(2) }), raw);
1170
96
  }
1171
- // ─── predicate (generic gate-predicate evaluator, #2008) ──────────────────────
1172
- /**
1173
- * Production subprocess binding for the gate-predicate evaluator. Wraps the
1174
- * bounded `execTool` seam (shell-command-projection) as a `runBoundedShell`
1175
- * the pure evaluator consumes. `sh -c` runs the interpolated command; the
1176
- * subprocess inherits the process env and is killed (SIGTERM) on timeout.
1177
- *
1178
- * `timedOut` is derived from the kill signal: spawnSync sets `signal: 'SIGTERM'`
1179
- * when the `timeout` fires, distinct from a normal non-zero exit code. A command
1180
- * that self-terminates with SIGTERM is indistinguishable at this seam and is
1181
- * reported as a timeout — either way the gate blocks (non-zero), so the outcome
1182
- * is fail-closed and correct. See ADR-2008.
1183
- */
1184
- function buildPredicateDeps() {
1185
- return {
1186
- runBoundedShell(opts) {
1187
- const r = (0, shell_command_projection_cjs_1.execTool)('sh', ['-c', opts.command], { cwd: opts.cwd, timeout: opts.timeoutMs });
1188
- return {
1189
- exitCode: r.exitCode,
1190
- stdout: r.stdout,
1191
- stderr: r.stderr,
1192
- signal: r.signal,
1193
- timedOut: r.timedOut,
1194
- };
1195
- },
1196
- findPhaseArtifact(phaseDir, artifactSuffix) {
1197
- if (!node_fs_1.default.existsSync(phaseDir))
1198
- return null;
1199
- if (artifactSuffix === '.' ||
1200
- artifactSuffix === '..' ||
1201
- artifactSuffix.includes('\0') ||
1202
- node_path_1.default.basename(artifactSuffix) !== artifactSuffix ||
1203
- node_path_1.default.win32.basename(artifactSuffix) !== artifactSuffix) {
1204
- return null;
1205
- }
1206
- const directContained = (0, security_cjs_1.tryWithinRoot)(artifactSuffix, phaseDir);
1207
- if (directContained !== null && node_fs_1.default.existsSync(directContained) && node_fs_1.default.statSync(directContained).isFile()) {
1208
- return directContained;
1209
- }
1210
- const planningContained = (0, security_cjs_1.tryWithinRoot)(node_path_1.default.join('.planning', artifactSuffix), phaseDir);
1211
- if (planningContained !== null && node_fs_1.default.existsSync(planningContained) && node_fs_1.default.statSync(planningContained).isFile()) {
1212
- return planningContained;
1213
- }
1214
- try {
1215
- const files = node_fs_1.default.readdirSync(phaseDir);
1216
- for (const f of files) {
1217
- if (f.endsWith('-' + artifactSuffix) || f === artifactSuffix) {
1218
- const candidateContained = (0, security_cjs_1.tryWithinRoot)(f, phaseDir);
1219
- if (candidateContained !== null && node_fs_1.default.statSync(candidateContained).isFile())
1220
- return candidateContained;
1221
- }
1222
- }
1223
- }
1224
- catch { /* ignore */ }
1225
- return null;
1226
- },
1227
- readFrontmatter(filePath) {
1228
- const content = (0, shell_command_projection_cjs_1.platformReadSync)(filePath);
1229
- if (content === null)
1230
- throw new Error(`predicate artifact disappeared before it could be read: ${filePath}`);
1231
- const parsed = extractFrontmatter(content, filePath);
1232
- return parsed;
1233
- }
1234
- };
1235
- }
1236
- /**
1237
- * Split an args array into `--flag value` pairs and the leftover positional
1238
- * tokens, in ONE pass, with the semantics `check predicate` established
1239
- * (#2008): a `--flag` followed by a non-`--` token consumes it as the value
1240
- * (last write wins); a `--flag` with no value stays a bare token and moves to
1241
- * the positionals; everything else is positional. `parsePredicateFlags` is
1242
- * the flags half of this same pass — there is exactly one parser, so the
1243
- * flag-taking check verbs cannot drift apart (#4130 follow-up: `check
1244
- * decision-coverage-plan --context <path>` shares it).
1245
- */
1246
- function partitionPredicateArgs(args) {
1247
- const flags = {};
1248
- const positionals = [];
1249
- for (let i = 0; i < args.length; i++) {
1250
- const a = args[i];
1251
- if (typeof a !== 'string')
1252
- continue;
1253
- if (!a.startsWith('--')) {
1254
- positionals.push(a);
1255
- continue;
1256
- }
1257
- const key = a.slice(2);
1258
- const next = args[i + 1];
1259
- if (key.length > 0 && typeof next === 'string' && !next.startsWith('--')) {
1260
- flags[key] = next;
1261
- i++;
1262
- }
1263
- else {
1264
- positionals.push(a);
1265
- }
1266
- }
1267
- return { flags, positionals };
1268
- }
1269
- /** Parse `--flag value` pairs from an args array into a map (last write wins). */
1270
- function parsePredicateFlags(args) {
1271
- return partitionPredicateArgs(args).flags;
1272
- }
1273
- /**
1274
- * `check predicate` — generic evaluator for capability gate `check.predicate`
1275
- * blocks (#2008). The workflow gate-dispatch invokes this for any gate whose
1276
- * `check` carries a `predicate` (instead of a `query`); the predicate object is
1277
- * passed as `--predicate '<json>'`. Emits the standard `{ block, message,
1278
- * details? }` gate contract on success. A malformed predicate / unknown kind
1279
- * THROWS inside the evaluator and is mapped here to `error()` (non-zero exit),
1280
- * which the workflow's two-step gate contract treats as a step-1 command failure
1281
- * routed per the gate's `onError`.
1282
- *
1283
- * Invocation:
1284
- * gsd_run check predicate --predicate '<json>' \
1285
- * [--phase-dir <dir>] [--phase-number <n>] [--phase-req-ids <ids>] --raw
1286
- *
1287
- * The subprocess runs at the runtime project root (the `cwd` passed to this
1288
- * router), inheriting the process env. Interpolation placeholders
1289
- * ${PHASE_NUMBER}/${PHASE_DIR}/${PHASE_REQ_IDS} are substituted from the flags.
1290
- */
1291
97
  function cmdCheckPredicate(projectDir, args, raw) {
1292
- const flags = parsePredicateFlags(args);
1293
- const predicateJson = flags['predicate'];
1294
- if (!predicateJson) {
1295
- error('predicate requires --predicate <json> (the gate hook check.predicate object)', ERROR_REASON.SDK_MISSING_ARG);
1296
- return;
1297
- }
1298
- let predicate;
1299
- try {
1300
- predicate = JSON.parse(predicateJson);
1301
- }
1302
- catch {
1303
- error('predicate --predicate value must be valid JSON', ERROR_REASON.USAGE);
1304
- return;
1305
- }
1306
- const rawPhaseDir = flags['phase-dir'];
1307
- let resolvedPhaseDir = rawPhaseDir;
1308
- if (typeof rawPhaseDir === 'string' && rawPhaseDir !== '') {
1309
- resolvedPhaseDir = resolvePath(rawPhaseDir, projectDir);
1310
- }
1311
- const ctx = {
1312
- cwd: projectDir,
1313
- phaseNumber: flags['phase-number'],
1314
- phaseDir: resolvedPhaseDir,
1315
- phaseReqIds: flags['phase-req-ids'],
1316
- };
1317
- let result;
1318
- try {
1319
- result = evaluatePredicate(predicate, ctx, buildPredicateDeps());
1320
- }
1321
- catch (e) {
1322
- error(`gate predicate evaluation failed: ${e.message}`, ERROR_REASON.USAGE);
1323
- return;
1324
- }
1325
- output(result, raw, undefined);
98
+ emitGateResult((0, gate_predicate_cjs_1.evaluateCheckPredicate)({ projectDir, args: args.slice(2) }), raw);
1326
99
  }
1327
- // ─── api-coverage-verify-pre ──────────────────────────────────────────────────
1328
- /**
1329
- * api-coverage.verify-pre: BLOCKING seal-time gate for the ai-integration
1330
- * capability (#1562). Enforces "Full API Coverage by Default — Opt Out, Never
1331
- * Opt In." A phase that integrates an external API/SDK/service may not seal
1332
- * until a COVERAGE.md matrix enumerates the surface and every non-integrated
1333
- * capability is an explicit, reasoned opt-out.
1334
- *
1335
- * Contract (two touch points composed into one check):
1336
- * 1. If COVERAGE.md exists in the phase dir → validate it (acceptance #2).
1337
- * Block on any validation error (empty matrix, OPT-OUT without reason,
1338
- * duplicate/empty capability).
1339
- * 2. If COVERAGE.md is absent → run detectApiIntegration over the phase scope
1340
- * (PLAN.md body, then ROADMAP phase section as fallback). If a strong
1341
- * external-API-integration signal is detected → BLOCK ("integration
1342
- * detected without coverage matrix"). If no signal → PASS (treat as a
1343
- * non-API phase; acceptance #4 — low false positives).
1344
- *
1345
- * The detector is the FALLBACK for the "nobody decided / forgot the matrix"
1346
- * case; the primary path is the plan:pre contribution prompting COVERAGE.md.
1347
- *
1348
- * Args: check api-coverage.verify-pre <phase-dir>
1349
- * Emits the uniform gate contract: { block, passed, message, ...details }.
1350
- */
1351
100
  function cmdApiCoverageVerifyPre(projectDir, args, raw) {
1352
- const phaseArg = typeof args[2] === 'string' ? args[2] : '';
1353
- if (!phaseArg) {
1354
- error('api-coverage.verify-pre requires a phase argument: check api-coverage.verify-pre <phase-dir-or-token>', ERROR_REASON.SDK_MISSING_ARG);
1355
- return;
1356
- }
1357
- const pDir = planningDir(projectDir);
1358
- const phasesRoot = node_path_1.default.join(pDir, 'phases');
1359
- // SECURITY (path traversal): the phase argument is taken ONLY as a phase
1360
- // token — its basename — and resolved by findPhaseInternal strictly under
1361
- // .planning/phases/ (or a milestone archive). The raw arg is never used as a
1362
- // path, so `..`, absolute paths, and arbitrary directories cannot reach a
1363
- // file read. Mirrors cmdVerifySchemaDrift's token-match approach.
1364
- let token = (0, shell_command_projection_cjs_1.posixNormalize)(phaseArg).split('/').filter(Boolean).pop() || '';
1365
- // A token like ".." or "." carries no phase identity → unresolvable.
1366
- if (token === '.' || token === '..')
1367
- token = '';
1368
- // Not a GSD project (no phases tree at all) → fail-open: nothing to gate.
1369
- if (!node_fs_1.default.existsSync(phasesRoot)) {
1370
- output({
1371
- block: false,
1372
- passed: true,
1373
- coverage_present: false,
1374
- detected: false,
1375
- message: 'api-coverage: no .planning/phases directory; gate skipped (not a GSD project layout)',
1376
- }, raw, undefined);
1377
- return;
1378
- }
1379
- // Resolve the phase dir under the contained phases root.
1380
- let resolvedDir = null;
1381
- let phaseNumber = '';
1382
- if (token) {
1383
- const found = findPhaseInternal(projectDir, token);
1384
- if (found && found.directory) {
1385
- resolvedDir = found.directory;
1386
- phaseNumber = found.phase_number || '';
1387
- }
1388
- }
1389
- if (!resolvedDir) {
1390
- // The phases tree EXISTS but THIS phase could not be resolved. For a
1391
- // BLOCKING gate, fail-closed: a missing phase dir must not silently bypass
1392
- // the coverage requirement. (Distinguished from "no .planning at all"
1393
- // above, which is a genuine non-GSD-project → pass.)
1394
- output({
1395
- block: true,
1396
- passed: false,
1397
- coverage_present: false,
1398
- detected: false,
1399
- phase_lookup_failed: true,
1400
- message: `api-coverage: could not resolve phase "${phaseArg}" under .planning/phases/. ` +
1401
- 'Resolve the phase directory (or produce COVERAGE.md) before sealing.',
1402
- }, raw, undefined);
1403
- return;
1404
- }
1405
- // Defense-in-depth: the resolved dir must be inside the phases root (or a
1406
- // milestone archive under .planning/milestones).
1407
- const milestonesRoot = node_path_1.default.join(pDir, 'milestones');
1408
- // Lexical containment (ADR-4650): resolvedDir is a directory path, not read
1409
- // through here — mirrors the prior path.resolve(root, candidate)-based check
1410
- // without introducing a filesystem/realpath dependency this defense-in-depth
1411
- // recheck never had.
1412
- if ((0, security_cjs_1.tryWithinRootLexical)(resolvedDir, phasesRoot) === null &&
1413
- (0, security_cjs_1.tryWithinRootLexical)(resolvedDir, milestonesRoot) === null) {
1414
- output({
1415
- block: true,
1416
- passed: false,
1417
- coverage_present: false,
1418
- detected: false,
1419
- message: 'api-coverage: resolved phase dir escapes .planning/ — refusing to evaluate',
1420
- }, raw, undefined);
1421
- return;
1422
- }
1423
- // (1) locate COVERAGE.md — prefer the exact name, then a single *-COVERAGE.md.
1424
- let coverageFile = '';
1425
- let suffixed = [];
1426
- try {
1427
- const entries = node_fs_1.default.readdirSync(resolvedDir, { withFileTypes: true });
1428
- const files = entries.filter((e) => e.isFile()).map((e) => e.name);
1429
- const exact = files.find((f) => /^COVERAGE\.md$/i.test(f));
1430
- if (exact) {
1431
- coverageFile = exact;
1432
- }
1433
- else {
1434
- suffixed = files.filter((f) => /-COVERAGE\.md$/i.test(f)).sort();
1435
- if (suffixed.length === 1)
1436
- coverageFile = suffixed[0];
1437
- }
1438
- }
1439
- catch {
1440
- // readdir failure → treat as no matrix readable; fall through to detection.
1441
- }
1442
- if (coverageFile) {
1443
- let matrixText;
1444
- try {
1445
- matrixText = node_fs_1.default.readFileSync(node_path_1.default.join(resolvedDir, coverageFile), 'utf8');
1446
- }
1447
- catch {
1448
- // COVERAGE.md exists but is unreadable (EACCES/EIO/encoding). Fail-closed
1449
- // with a useful message rather than a raw throw.
1450
- output({
1451
- block: true,
1452
- passed: false,
1453
- coverage_present: true,
1454
- message: `api-coverage: COVERAGE.md exists but is unreadable — fix file permissions/encoding before sealing`,
1455
- }, raw, undefined);
1456
- return;
1457
- }
1458
- const v = validateCoverageMatrix(matrixText);
1459
- if (v.valid) {
1460
- if (v.none_declared) {
1461
- // The declaration is the human override for the detector — it PASSES
1462
- // even when detection fires (that is acceptance #5's point: the
1463
- // detector is fallible and the declaration is the reasoned overrule).
1464
- // But a contradiction must be VISIBLE, not silent: re-run detection
1465
- // over the phase scope and surface any signals it still finds
1466
- // (#2365 review S-1).
1467
- const declScope = readPhaseScope(projectDir, resolvedDir, phaseNumber);
1468
- const declDetection = detectApiIntegration(declScope.text);
1469
- const declSignals = declDetection.signals.map((s) => ({ verb: s.verb, noun: s.noun }));
1470
- // The declaration legitimately wins even over a read error (it is the
1471
- // human overrule), but if scope was incomplete we say so — the contract
1472
- // is that contradictions stay visible, not silent (#2365 review).
1473
- const baseMsg = declDetection.detected
1474
- ? `api-coverage: COVERAGE.md declares no external API integration, overriding ${declSignals.length} detected signal(s) — confirm the declaration is accurate`
1475
- : 'api-coverage: COVERAGE.md declares no external API integration — matrix not required';
1476
- output({
1477
- block: false,
1478
- passed: true,
1479
- coverage_present: true,
1480
- matrix: coverageFile,
1481
- counts: v.counts,
1482
- none_declared: true,
1483
- detected: declDetection.detected,
1484
- ...(declDetection.detected ? { signals: declSignals } : {}),
1485
- ...(declScope.readError ? { scope_read_error: declScope.readError } : {}),
1486
- message: declScope.readError
1487
- ? `${baseMsg} (note: phase scope was incompletely read — ${declScope.readError})`
1488
- : baseMsg,
1489
- }, raw, undefined);
1490
- return;
1491
- }
1492
- output({
1493
- block: false,
1494
- passed: true,
1495
- coverage_present: true,
1496
- matrix: coverageFile,
1497
- counts: v.counts,
1498
- message: `api-coverage: matrix present (${v.counts.surface} capabilities, ${v.counts.optout} opt-out)`,
1499
- }, raw, undefined);
1500
- return;
1501
- }
1502
- // Fixed-template message (no raw cell content echoed into the LLM-facing
1503
- // message). The structured `errors` array is safe (row-indexed, no cell
1504
- // values) and travels as data for tooling that wants detail.
1505
- output({
1506
- block: true,
1507
- passed: false,
1508
- coverage_present: true,
1509
- matrix: coverageFile,
1510
- error_count: v.errors.length,
1511
- errors: v.errors,
1512
- message: `api-coverage: COVERAGE.md has ${v.errors.length} problem(s) — fix the matrix (every capability INTEGRATE or OPT-OUT with a reason) before sealing`,
1513
- }, raw, undefined);
1514
- return;
1515
- }
1516
- if (suffixed.length > 1) {
1517
- output({
1518
- block: true,
1519
- passed: false,
1520
- coverage_present: false,
1521
- message: `api-coverage: multiple *-COVERAGE.md files found (${suffixed.length}) — consolidate into one COVERAGE.md before sealing`,
1522
- }, raw, undefined);
1523
- return;
1524
- }
1525
- // (2) no matrix — detect whether this phase integrates an external API.
1526
- const scope = readPhaseScope(projectDir, resolvedDir, phaseNumber);
1527
- if (scope.readError) {
1528
- // Fail-closed: an unreadable plan could be the one describing the
1529
- // integration, so we cannot certify "no integration" — block and surface it.
1530
- output({
1531
- block: true,
1532
- passed: false,
1533
- coverage_present: false,
1534
- detected: false,
1535
- message: `api-coverage: could not read the phase scope (${scope.readError}); ` +
1536
- 'refusing to certify no external-API integration from incomplete scope. ' +
1537
- 'Fix the unreadable plan file, or add a COVERAGE.md declaration.',
1538
- }, raw, undefined);
1539
- return;
1540
- }
1541
- // An EMPTY scope is not a negative verdict. This gate's neighbouring arms
1542
- // already fail closed (unresolvable phase → block; unreadable plan → block),
1543
- // but a phase with no plan body AND no roadmap section fell through to
1544
- // detection over zero bytes and CERTIFIED "no external-API integration" —
1545
- // clearing a blocking seal gate on a probe that examined nothing
1546
- // (ADR-3889 failure class (c), #3909). The discriminator is BYTES EXAMINED,
1547
- // never SIGNALS FOUND: a phase with real plans and no API vocabulary still
1548
- // reaches the pass below unchanged.
1549
- if (scope.text.trim() === '') {
1550
- output({
1551
- block: true,
1552
- passed: false,
1553
- coverage_present: false,
1554
- detected: false,
1555
- scope_unavailable: true,
1556
- message: 'api-coverage: the phase scope is empty — no plan body and no roadmap section were ' +
1557
- 'found, so nothing was examined. Refusing to certify no external-API integration ' +
1558
- 'from an unestablished scope. Add the phase plan, or add a COVERAGE.md declaration.',
1559
- }, raw, undefined);
1560
- return;
1561
- }
1562
- const detection = detectApiIntegration(scope.text);
1563
- if (detection.detected) {
1564
- // Surface only verb/noun (typed, bounded) — NOT raw prose snippets — so the
1565
- // gate output cannot relay injected PLAN.md instructions to the orchestrator.
1566
- const signals = detection.signals.map((s) => ({ verb: s.verb, noun: s.noun }));
1567
- output({
1568
- block: true,
1569
- passed: false,
1570
- coverage_present: false,
1571
- detected: true,
1572
- signals,
1573
- message: 'api-coverage: external-API integration detected without a coverage matrix. ' +
1574
- 'Produce COVERAGE.md enumerating the API surface (every capability INTEGRATE or ' +
1575
- 'OPT-OUT with a reason) before sealing. Full coverage is the default.',
1576
- }, raw, undefined);
1577
- return;
1578
- }
1579
- output({
1580
- block: false,
1581
- passed: true,
1582
- coverage_present: false,
1583
- detected: false,
1584
- message: 'api-coverage: no external-API integration detected; coverage matrix not required',
1585
- }, raw, undefined);
101
+ emitGateResult((0, gate_api_coverage_verify_pre_cjs_1.evaluateApiCoverageVerifyPre)({ projectDir, args: args.slice(2) }), raw);
1586
102
  }
1587
- /** A filesystem error that is NOT "does not exist" — i.e. a real read failure
1588
- * (EACCES/EIO/…) the gate must not swallow. `ENOENT` is a legitimate "not
1589
- * there yet" and is treated as absence, not error. */
1590
- function isRealReadFailure(err) {
1591
- const code = err?.code;
1592
- return err != null && code !== 'ENOENT';
103
+ /** `GSD_SKIP_SCHEMA_CHECK` is the schema-drift bypass: it arrives as `env`, read here, never by the gate. */
104
+ function cmdSchemaDriftGate(projectDir, args, raw, env) {
105
+ emitGateResult((0, gate_schema_drift_cjs_1.evaluateSchemaDriftGate)({ projectDir, args: args.slice(2), env }), raw);
1593
106
  }
1594
- function readPhaseScope(projectDir, phaseDir, phaseNumber) {
1595
- const chunks = [];
1596
- let readError = null;
1597
- // A MISSING phase directory is fine (no plans yet → fall through to the
1598
- // roadmap). Checked up front (rather than via a readdirSync catch) because
1599
- // #3183 (lint-plan-count-drift) now sources the plan-file list from the
1600
- // single owner (scanPhasePlans) instead of a local `-PLAN\.md$` readdirSync
1601
- // filter — picks up bare PLAN.md and nested plans/, and excludes
1602
- // superseded plans, none of which the prior root-only exact-suffix filter
1603
- // did.
1604
- if (node_fs_1.default.existsSync(phaseDir)) {
1605
- const scan = scanPhasePlans(phaseDir);
1606
- if (scan.scope === SCOPE.UNREADABLE) {
1607
- // Directory exists but scanPhasePlans's own readdirSync(phaseDir) call
1608
- // failed (EACCES/EIO race) — a real read failure the gate must not
1609
- // silently pass (#2365 review), mirroring the prior isRealReadFailure
1610
- // branch below for the readdirSync-throws case.
1611
- return {
1612
- text: '',
1613
- readError: 'could not read the phase directory: scanPhasePlans reported scope UNREADABLE',
1614
- };
1615
- }
1616
- const plans = [...scan.planFiles].sort();
1617
- for (const p of plans) {
1618
- try {
1619
- chunks.push(node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, p), 'utf8'));
1620
- }
1621
- catch (err) {
1622
- // A plan file that exists but cannot be read — record it and keep
1623
- // reading the rest so the message names the first failure.
1624
- if (!readError) {
1625
- readError = `could not read ${p}: ${err instanceof Error ? err.message : String(err)}`;
1626
- }
1627
- }
1628
- }
1629
- }
1630
- if (readError)
1631
- return { text: chunks.join('\n\n'), readError };
1632
- if (chunks.join('').trim().length > 0)
1633
- return { text: chunks.join('\n\n'), readError: null };
1634
- // Fallback: ONLY this phase's ROADMAP section (not the whole file, which
1635
- // would pollute detection with sibling-phase prose). A MISSING roadmap/section
1636
- // is non-fatal; a roadmap that exists but cannot be read is a real failure.
1637
- if (phaseNumber) {
1638
- try {
1639
- const section = getRoadmapPhaseWithFallback(projectDir, phaseNumber);
1640
- if (section)
1641
- return { text: section, readError: null };
1642
- }
1643
- catch (err) {
1644
- if (isRealReadFailure(err)) {
1645
- return {
1646
- text: '',
1647
- readError: `could not read the roadmap fallback: ${err instanceof Error ? err.message : String(err)}`,
1648
- };
1649
- }
1650
- }
1651
- }
1652
- return { text: '', readError: null };
107
+ function cmdCodebaseDriftGate(projectDir, args, raw) {
108
+ emitGateResult((0, gate_codebase_drift_cjs_1.evaluateCodebaseDriftGate)({ projectDir, args: args.slice(2) }), raw);
1653
109
  }
1654
- function routeCheckCommand({ args, cwd, raw }) {
110
+ function cmdContextDriftGate(projectDir, args, raw) {
111
+ emitGateResult((0, gate_context_drift_cjs_1.evaluateContextDriftGate)({ projectDir, args: args.slice(2) }), raw);
112
+ }
113
+ function cmdProhibitionEnforcement(projectDir, args, raw) {
114
+ emitGateResult((0, gate_prohibition_enforcement_cjs_1.evaluateProhibitionEnforcementGate)({ projectDir, args: args.slice(2) }), raw);
115
+ }
116
+ function routeCheckCommand({ args, cwd, raw, env = process.env }) {
1655
117
  // Normalize dots to hyphens in the subcommand so both forms are accepted.
1656
118
  // This makes `check.query = "ui.plan-gate"` (dotted form in capability.json gates)
1657
119
  // directly runnable as `gsd_run check ui.plan-gate` — the dot is normalized to
@@ -1660,117 +122,107 @@ function routeCheckCommand({ args, cwd, raw }) {
1660
122
  // so the declared query must be dispatchable exactly as declared.
1661
123
  const rawSubcommand = args[1];
1662
124
  const subcommand = typeof rawSubcommand === 'string' ? rawSubcommand.replace(/\./g, '-') : rawSubcommand;
1663
- if (subcommand === 'auto-mode') {
1664
- cmdAutoMode(cwd, raw);
1665
- return;
1666
- }
1667
- if (subcommand === 'decision-coverage-plan') {
1668
- cmdDecisionCoveragePlan(cwd, args, raw);
1669
- return;
1670
- }
1671
- if (subcommand === 'decision-coverage-verify') {
1672
- cmdDecisionCoverageVerify(cwd, args, raw);
1673
- return;
1674
- }
1675
- if (subcommand === 'ui-plan-gate') {
1676
- cmdUiPlanGate(cwd, args, raw);
1677
- return;
1678
- }
1679
- if (subcommand === 'gap-analysis-plan-post') {
1680
- cmdGapAnalysisPlanPost(cwd, args, raw);
1681
- return;
1682
- }
1683
- if (subcommand === 'verify-command-paths') {
1684
- // Deterministic filesystem probe for <automated> verify commands (#2401) —
1685
- // never executes anything; see verify-command-grounding.cjs.
1686
- cmdVerifyCommandPaths(cwd, args, raw);
1687
- return;
1688
- }
1689
- if (subcommand === 'verify-failure-directions') {
1690
- // Presence probe for a stated <fails_when> per <automated> command
1691
- // (#3172) — never executes anything; see verify-command-grounding.cjs.
1692
- cmdVerifyFailureDirections(cwd, args, raw);
1693
- return;
1694
- }
1695
- if (subcommand === 'api-coverage-verify-pre') {
1696
- // ai-integration capability blocking gate at verify:pre (#1562). Dot-to-
1697
- // hyphen normalization means query "api-coverage.verify-pre" routes here.
1698
- cmdApiCoverageVerifyPre(cwd, args, raw);
1699
- return;
1700
- }
1701
- if (subcommand === 'tdd-review-checkpoint') {
1702
- cmdTddReviewCheckpoint(cwd, args, raw);
1703
- return;
1704
- }
1705
- if (subcommand === 'tdd-red-evidence') {
1706
- // #3770: intentional-RED evidence gate — only a target-test failure may
1707
- // authorize GREEN. Validates the persisted record; never executes anything.
1708
- cmdTddRedEvidence(cwd, args, raw);
1709
- return;
1710
- }
1711
- if (subcommand === 'ui-safety-gate') {
1712
- cmdUiSafetyGate(cwd, args, raw);
1713
- return;
1714
- }
1715
- if (subcommand === 'verify-schema-drift') {
1716
- // Delegates to verify.schema-drift — drift capability gate at execute:wave:post (blocking).
1717
- // Dot-to-hyphen normalization means query "verify.schema-drift" routes here.
1718
- // Honor GSD_SKIP_SCHEMA_CHECK=true to bypass the gate (preserves the original inline gate behavior).
1719
- const phaseArg = typeof args[2] === 'string' ? args[2] : '';
1720
- const skipSchemaCheck = process.env['GSD_SKIP_SCHEMA_CHECK'] === 'true';
1721
- cmdVerifySchemaDrift(cwd, phaseArg, skipSchemaCheck, raw);
1722
- return;
1723
- }
1724
- if (subcommand === 'verify-codebase-drift') {
1725
- // Delegates to verify.codebase-drift — drift capability gate at execute:wave:post (non-blocking).
1726
- // Dot-to-hyphen normalization means query "verify.codebase-drift" routes here.
1727
- cmdVerifyCodebaseDrift(cwd, raw);
1728
- return;
1729
- }
1730
- if (subcommand === 'verify-context-drift') {
1731
- // Delegates to verify.context-drift — drift capability gate at plan:pre (non-blocking).
1732
- // Dot-to-hyphen normalization means query "verify.context-drift" routes here.
1733
- const phaseArg = typeof args[2] === 'string' ? args[2] : '';
1734
- cmdVerifyContextDrift(cwd, phaseArg, raw);
1735
- return;
1736
- }
1737
- if (subcommand === 'predicate') {
1738
- // Generic gate-predicate evaluator (#2008). The workflow gate-dispatch calls
1739
- // this for any gate whose `check` carries a `predicate` (instead of a `query`),
1740
- // passing the predicate object as --predicate '<json>'. NOTE: unlike the
1741
- // `check.query` subcommands above (which take positional phase args), this
1742
- // subcommand is flag-driven. `decision-coverage-plan` above now ALSO accepts
1743
- // `--context <path>` (its positionals still work) — both share
1744
- // partitionPredicateArgs, the one flag parser.
1745
- cmdCheckPredicate(cwd, args, raw);
1746
- return;
1747
- }
1748
- if (subcommand === 'prohibition-enforcement') {
1749
- // The deterministic test-tier prohibition PRODUCER/gate (#1259, ADR-550 D5d). Locates the
1750
- // wired mechanical check (node-test or lint-rule), confirms fail-first, runs it, builds
1751
- // enforcementEvidence, and emits the dispositionForProhibition verdict. Invocable as
1752
- // `gsd_run check prohibition-enforcement <request.json>`.
1753
- (0, prohibition_enforcement_cjs_1.routeProhibitionEnforcement)(args, raw);
1754
- return;
125
+ switch (subcommand) {
126
+ case 'auto-mode':
127
+ cmdAutoMode(cwd, raw);
128
+ return;
129
+ case 'decision-coverage-plan':
130
+ cmdDecisionCoveragePlan(cwd, args, raw);
131
+ return;
132
+ case 'decision-coverage-verify':
133
+ cmdDecisionCoverageVerify(cwd, args, raw);
134
+ return;
135
+ case 'evaluation-scope':
136
+ // ADR-5057 §4 / #5164: the one resolver for which commits and files a gate or
137
+ // workflow step evaluates (`--phase N`, `--plan P-N` or `--quick ID`).
138
+ cmdEvaluationScope(cwd, args, raw);
139
+ return;
140
+ case 'ui-plan-gate':
141
+ cmdUiPlanGate(cwd, args, raw);
142
+ return;
143
+ case 'gap-analysis-plan-post':
144
+ cmdGapAnalysisPlanPost(cwd, args, raw);
145
+ return;
146
+ case 'verify-command-paths':
147
+ // Deterministic filesystem probe for <automated> verify commands (#2401) —
148
+ // never executes anything; see verify-command-grounding.cjs.
149
+ cmdVerifyCommandPaths(cwd, args, raw);
150
+ return;
151
+ case 'verify-failure-directions':
152
+ // Presence probe for a stated <fails_when> per <automated> command
153
+ // (#3172) — never executes anything; see verify-command-grounding.cjs.
154
+ cmdVerifyFailureDirections(cwd, args, raw);
155
+ return;
156
+ case 'api-coverage-verify-pre':
157
+ // ai-integration capability blocking gate at verify:pre (#1562). Dot-to-
158
+ // hyphen normalization means query "api-coverage.verify-pre" routes here.
159
+ cmdApiCoverageVerifyPre(cwd, args, raw);
160
+ return;
161
+ case 'tdd-review-checkpoint':
162
+ cmdTddReviewCheckpoint(cwd, args, raw);
163
+ return;
164
+ case 'tdd-red-evidence':
165
+ // #3770: intentional-RED evidence gate — only a target-test failure may
166
+ // authorize GREEN. Validates the persisted record; never executes anything.
167
+ cmdTddRedEvidence(cwd, args, raw);
168
+ return;
169
+ case 'ui-safety-gate':
170
+ cmdUiSafetyGate(cwd, args, raw);
171
+ return;
172
+ case 'verify-schema-drift':
173
+ // The drift capability's gate at execute:wave:post (blocking; src/gate-schema-drift.cts).
174
+ // Dot-to-hyphen normalization means query "verify.schema-drift" routes here.
175
+ // GSD_SKIP_SCHEMA_CHECK=true bypasses the gate: read from `env` here, passed to the gate.
176
+ cmdSchemaDriftGate(cwd, args, raw, env);
177
+ return;
178
+ case 'verify-codebase-drift':
179
+ // The drift capability's gate at execute:wave:post (non-blocking; src/gate-codebase-drift.cts).
180
+ // Dot-to-hyphen normalization means query "verify.codebase-drift" routes here.
181
+ cmdCodebaseDriftGate(cwd, args, raw);
182
+ return;
183
+ case 'verify-context-drift':
184
+ // The drift capability's gate at plan:pre (non-blocking; src/gate-context-drift.cts).
185
+ // Dot-to-hyphen normalization means query "verify.context-drift" routes here.
186
+ cmdContextDriftGate(cwd, args, raw);
187
+ return;
188
+ case 'predicate':
189
+ // Generic gate-predicate evaluator (#2008). The workflow gate-dispatch calls
190
+ // this for any gate whose `check` carries a `predicate` (instead of a `query`),
191
+ // passing the predicate object as --predicate '<json>'. NOTE: unlike the
192
+ // `check.query` subcommands above (which take positional phase args), this
193
+ // subcommand is flag-driven. `decision-coverage-plan` above now ALSO accepts
194
+ // `--context <path>` (its positionals still work) — both share
195
+ // partitionPredicateArgs, the one flag parser.
196
+ cmdCheckPredicate(cwd, args, raw);
197
+ return;
198
+ case 'prohibition-enforcement':
199
+ // The deterministic test-tier prohibition PRODUCER/gate (#1259, ADR-550 D5d). Locates the
200
+ // wired mechanical check (node-test or lint-rule), confirms fail-first, runs it, builds
201
+ // enforcementEvidence, and emits the dispositionForProhibition verdict
202
+ // (src/gate-prohibition-enforcement.cts). Invocable as
203
+ // `gsd_run check prohibition-enforcement <request.json>`.
204
+ cmdProhibitionEnforcement(cwd, args, raw);
205
+ return;
206
+ default:
207
+ error('Unknown check subcommand. Available: api-coverage-verify-pre, auto-mode, decision-coverage-plan, decision-coverage-verify, evaluation-scope, gap-analysis-plan-post, predicate, prohibition-enforcement, tdd-red-evidence, tdd-review-checkpoint, ui-plan-gate, ui-safety-gate, verify-command-paths, verify-failure-directions, verify-schema-drift, verify-codebase-drift, verify-context-drift', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1755
208
  }
1756
- error('Unknown check subcommand. Available: api-coverage-verify-pre, auto-mode, decision-coverage-plan, decision-coverage-verify, gap-analysis-plan-post, predicate, prohibition-enforcement, tdd-red-evidence, tdd-review-checkpoint, ui-plan-gate, ui-safety-gate, verify-command-paths, verify-failure-directions, verify-schema-drift, verify-codebase-drift, verify-context-drift', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1757
209
  }
1758
210
  module.exports = {
1759
211
  routeCheckCommand,
1760
- decisionMentioned,
1761
- extractPlanDesignatedSections,
1762
- computeUiPlanGate,
1763
- computeUiSafetyGate,
212
+ decisionMentioned: decision_coverage_support_cjs_1.decisionMentioned,
213
+ extractPlanDesignatedSections: decision_coverage_support_cjs_1.extractPlanDesignatedSections,
214
+ computeUiPlanGate: gate_ui_plan_cjs_1.computeUiPlanGate,
215
+ computeUiSafetyGate: gate_ui_safety_cjs_1.computeUiSafetyGate,
1764
216
  cmdGapAnalysisPlanPost,
1765
217
  cmdVerifyCommandPaths,
1766
218
  cmdVerifyFailureDirections,
1767
219
  cmdTddReviewCheckpoint,
1768
220
  cmdTddRedEvidence,
1769
221
  cmdCheckPredicate,
1770
- buildPredicateDeps,
1771
- parsePredicateFlags,
1772
- partitionPredicateArgs,
222
+ buildPredicateDeps: gate_predicate_cjs_1.buildPredicateDeps,
223
+ parsePredicateFlags: gate_args_cjs_1.parsePredicateFlags,
224
+ partitionPredicateArgs: gate_args_cjs_1.partitionPredicateArgs,
1773
225
  // Fail-closed phase-scope reader for the api-coverage gate — exported for
1774
226
  // in-process failure-injection tests (#2365 review).
1775
- readPhaseScope,
227
+ readPhaseScope: gate_api_coverage_verify_pre_cjs_1.readPhaseScope,
1776
228
  };