@opengsd/gsd-core 1.11.0 → 1.13.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 (498) 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 +12 -0
  4. package/agents/gsd-code-fixer.md +1 -1
  5. package/agents/gsd-debug-session-manager.md +1 -1
  6. package/agents/gsd-debugger.md +1 -1
  7. package/agents/gsd-dom-verifier.md +169 -0
  8. package/agents/gsd-eval-auditor.md +1 -1
  9. package/agents/gsd-executor.md +78 -42
  10. package/agents/gsd-framework-selector.md +1 -3
  11. package/agents/gsd-intel-updater.md +1 -1
  12. package/agents/gsd-mempalace-curator.md +0 -1
  13. package/agents/gsd-pattern-mapper.md +11 -0
  14. package/agents/gsd-phase-researcher.md +3 -1
  15. package/agents/gsd-plan-checker.md +91 -112
  16. package/agents/gsd-planner.md +20 -4
  17. package/agents/gsd-project-researcher.md +1 -1
  18. package/agents/gsd-research-synthesizer.md +2 -2
  19. package/agents/gsd-roadmapper.md +15 -11
  20. package/agents/gsd-ui-checker.md +82 -7
  21. package/agents/gsd-ui-researcher.md +70 -3
  22. package/agents/gsd-verifier.md +24 -2
  23. package/bin/install.js +847 -200
  24. package/commands/gsd/discuss-phase.md +1 -1
  25. package/commands/gsd/execute-phase.md +1 -1
  26. package/commands/gsd/import.md +1 -1
  27. package/commands/gsd/ns-workflow.md +2 -1
  28. package/commands/gsd/phase.md +1 -1
  29. package/commands/gsd/quick-batch.md +105 -0
  30. package/commands/gsd/quick.md +8 -4
  31. package/commands/gsd/surface.md +18 -8
  32. package/gsd-core/bin/gsd-tools.cjs +761 -100
  33. package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
  34. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  35. package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
  36. package/gsd-core/bin/lib/api-coverage.cjs +30 -9
  37. package/gsd-core/bin/lib/artifacts.cjs +2 -0
  38. package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
  39. package/gsd-core/bin/lib/audit.cjs +163 -41
  40. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  41. package/gsd-core/bin/lib/capability-activation.cjs +27 -0
  42. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  43. package/gsd-core/bin/lib/capability-registry.cjs +785 -144
  44. package/gsd-core/bin/lib/capability-state.cjs +25 -4
  45. package/gsd-core/bin/lib/capability-validator.cjs +321 -18
  46. package/gsd-core/bin/lib/capability-writer.cjs +14 -4
  47. package/gsd-core/bin/lib/check-command-router.cjs +229 -6
  48. package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
  49. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  50. package/gsd-core/bin/lib/clusters.cjs +1 -0
  51. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  52. package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
  53. package/gsd-core/bin/lib/command-aliases.cjs +16 -0
  54. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  55. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  56. package/gsd-core/bin/lib/commands.cjs +877 -54
  57. package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
  58. package/gsd-core/bin/lib/config-loader.cjs +121 -29
  59. package/gsd-core/bin/lib/config.cjs +92 -2
  60. package/gsd-core/bin/lib/configuration.cjs +129 -37
  61. package/gsd-core/bin/lib/core-utils.cjs +118 -14
  62. package/gsd-core/bin/lib/decisions.cjs +213 -1
  63. package/gsd-core/bin/lib/edge-probe.cjs +23 -2
  64. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  65. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  66. package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
  67. package/gsd-core/bin/lib/frontmatter.cjs +975 -326
  68. package/gsd-core/bin/lib/gap-checker.cjs +41 -8
  69. package/gsd-core/bin/lib/git-base-branch.cjs +182 -39
  70. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
  71. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
  72. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +60 -14
  73. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
  74. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
  75. package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
  76. package/gsd-core/bin/lib/host-integration.cjs +96 -11
  77. package/gsd-core/bin/lib/init-command-router.cjs +132 -21
  78. package/gsd-core/bin/lib/init.cjs +252 -56
  79. package/gsd-core/bin/lib/install-engine.cjs +252 -15
  80. package/gsd-core/bin/lib/install-model-override-resolver.cjs +78 -1
  81. package/gsd-core/bin/lib/install-profiles.cjs +100 -18
  82. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  83. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  84. package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
  85. package/gsd-core/bin/lib/intel.cjs +101 -26
  86. package/gsd-core/bin/lib/io.cjs +195 -15
  87. package/gsd-core/bin/lib/learnings.cjs +85 -14
  88. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  89. package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
  90. package/gsd-core/bin/lib/markdown-table.cjs +175 -4
  91. package/gsd-core/bin/lib/milestone.cjs +112 -7
  92. package/gsd-core/bin/lib/model-catalog.cjs +177 -19
  93. package/gsd-core/bin/lib/model-resolver.cjs +10 -28
  94. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  95. package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
  96. package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
  97. package/gsd-core/bin/lib/phase-id.cjs +321 -13
  98. package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
  99. package/gsd-core/bin/lib/phase-locator.cjs +138 -17
  100. package/gsd-core/bin/lib/phase.cjs +1175 -115
  101. package/gsd-core/bin/lib/plan-document.cjs +273 -0
  102. package/gsd-core/bin/lib/plan-scan.cjs +13 -2
  103. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  104. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  105. package/gsd-core/bin/lib/planning-snapshot.cjs +165 -34
  106. package/gsd-core/bin/lib/planning-workspace.cjs +159 -28
  107. package/gsd-core/bin/lib/probe-core.cjs +4 -1
  108. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  109. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  110. package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
  111. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
  112. package/gsd-core/bin/lib/quick-batch.cjs +840 -0
  113. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  114. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
  115. package/gsd-core/bin/lib/review-lane-descriptor.cjs +62 -14
  116. package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
  117. package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
  118. package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
  119. package/gsd-core/bin/lib/roadmap-parser.cjs +577 -41
  120. package/gsd-core/bin/lib/roadmap.cjs +248 -64
  121. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +329 -41
  122. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
  123. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +320 -109
  124. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +487 -83
  125. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  126. package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
  127. package/gsd-core/bin/lib/shell-command-projection.cjs +75 -8
  128. package/gsd-core/bin/lib/smart-entry.cjs +19 -31
  129. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  130. package/gsd-core/bin/lib/state-command-router.cjs +47 -18
  131. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  132. package/gsd-core/bin/lib/state-document.cjs +216 -5
  133. package/gsd-core/bin/lib/state-md-schema.cjs +231 -0
  134. package/gsd-core/bin/lib/state-transition.cjs +850 -145
  135. package/gsd-core/bin/lib/state.cjs +1629 -287
  136. package/gsd-core/bin/lib/surface.cjs +33 -10
  137. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  138. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  139. package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
  140. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  141. package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
  142. package/gsd-core/bin/lib/uat.cjs +2542 -387
  143. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  144. package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
  145. package/gsd-core/bin/lib/unusable-input.cjs +13 -0
  146. package/gsd-core/bin/lib/update-context.cjs +6 -2
  147. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  148. package/gsd-core/bin/lib/validate.cjs +230 -12
  149. package/gsd-core/bin/lib/vendor/README.md +43 -5
  150. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  151. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  152. package/gsd-core/bin/lib/verification.cjs +287 -13
  153. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  154. package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
  155. package/gsd-core/bin/lib/verify.cjs +441 -56
  156. package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
  157. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  158. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  159. package/gsd-core/bin/lib/worktree-safety.cjs +185 -21
  160. package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
  161. package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
  162. package/gsd-core/bin/shared/exit-codes.json +8 -0
  163. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  164. package/gsd-core/bin/shared/model-catalog.json +8 -1
  165. package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
  166. package/gsd-core/references/agent-contracts.md +6 -5
  167. package/gsd-core/references/api-coverage.md +24 -2
  168. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  169. package/gsd-core/references/checkpoints.md +37 -19
  170. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  171. package/gsd-core/references/edge-probe.md +17 -5
  172. package/gsd-core/references/execute-mvp-tdd.md +18 -18
  173. package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
  174. package/gsd-core/references/execute-phase-response-language.md +6 -0
  175. package/gsd-core/references/execute-phase-wave-guard.md +11 -9
  176. package/gsd-core/references/executor-examples.md +42 -0
  177. package/gsd-core/references/failing-direction.md +78 -0
  178. package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
  179. package/gsd-core/references/gate-prompts.md +1 -1
  180. package/gsd-core/references/git-integration.md +5 -5
  181. package/gsd-core/references/git-planning-commit.md +3 -3
  182. package/gsd-core/references/gsd-run-resolver.md +1 -1
  183. package/gsd-core/references/loop-hook-dispatch.md +22 -0
  184. package/gsd-core/references/model-profiles.md +1 -1
  185. package/gsd-core/references/mvp-concepts.md +2 -2
  186. package/gsd-core/references/nyquist-compliance.md +74 -0
  187. package/gsd-core/references/offer-next.md +3 -5
  188. package/gsd-core/references/phase-argument-parsing.md +3 -3
  189. package/gsd-core/references/plan-checker-examples.md +41 -0
  190. package/gsd-core/references/planner-antipatterns.md +25 -0
  191. package/gsd-core/references/planner-chunked.md +5 -1
  192. package/gsd-core/references/planner-coupling.md +42 -0
  193. package/gsd-core/references/planner-failing-direction.md +53 -0
  194. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  195. package/gsd-core/references/planner-quick-batch.md +71 -0
  196. package/gsd-core/references/planner-reviews.md +47 -0
  197. package/gsd-core/references/planner-revision.md +76 -3
  198. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  199. package/gsd-core/references/planning-config.md +39 -9
  200. package/gsd-core/references/response-language-directive.md +9 -0
  201. package/gsd-core/references/reviewer-instances.md +31 -0
  202. package/gsd-core/references/revision-loop.md +118 -11
  203. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  204. package/gsd-core/references/tdd.md +15 -12
  205. package/gsd-core/references/ui-brand.md +65 -21
  206. package/gsd-core/references/ui-consideration-probe.md +1 -1
  207. package/gsd-core/references/universal-anti-patterns.md +2 -2
  208. package/gsd-core/references/verifier-evidence-gate.md +160 -0
  209. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  210. package/gsd-core/references/verify-mvp-mode.md +1 -1
  211. package/gsd-core/references/workstream-flag.md +11 -11
  212. package/gsd-core/templates/README.md +1 -1
  213. package/gsd-core/templates/SECURITY.md +3 -3
  214. package/gsd-core/templates/UI-SPEC.md +25 -3
  215. package/gsd-core/templates/VALIDATION.md +3 -3
  216. package/gsd-core/templates/phase-prompt.md +7 -0
  217. package/gsd-core/templates/state.md +7 -0
  218. package/gsd-core/templates/verification-report.md +5 -0
  219. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  220. package/gsd-core/workflows/add-backlog.md +3 -1
  221. package/gsd-core/workflows/add-phase.md +5 -3
  222. package/gsd-core/workflows/add-tests.md +4 -9
  223. package/gsd-core/workflows/add-todo.md +2 -2
  224. package/gsd-core/workflows/ai-integration-phase.md +5 -10
  225. package/gsd-core/workflows/analyze-dependencies.md +2 -0
  226. package/gsd-core/workflows/audit-fix.md +14 -3
  227. package/gsd-core/workflows/audit-milestone.md +11 -9
  228. package/gsd-core/workflows/audit-uat.md +19 -2
  229. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  230. package/gsd-core/workflows/autonomous.md +12 -26
  231. package/gsd-core/workflows/check-todos.md +2 -2
  232. package/gsd-core/workflows/cleanup.md +3 -3
  233. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +16 -14
  234. package/gsd-core/workflows/code-review-fix.md +3 -1
  235. package/gsd-core/workflows/code-review.md +192 -69
  236. package/gsd-core/workflows/complete-milestone.md +28 -14
  237. package/gsd-core/workflows/debug.md +6 -4
  238. package/gsd-core/workflows/diagnose-issues.md +17 -7
  239. package/gsd-core/workflows/discuss-phase/modes/advisor.md +3 -1
  240. package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
  241. package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
  242. package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
  243. package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
  244. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -7
  245. package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
  246. package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
  247. package/gsd-core/workflows/discuss-phase/modes/text.md +3 -1
  248. package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
  249. package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
  250. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  251. package/gsd-core/workflows/discuss-phase-assumptions.md +3 -3
  252. package/gsd-core/workflows/discuss-phase-power.md +2 -0
  253. package/gsd-core/workflows/discuss-phase.md +2 -2
  254. package/gsd-core/workflows/do.md +46 -19
  255. package/gsd-core/workflows/docs-update.md +6 -5
  256. package/gsd-core/workflows/edit-phase.md +3 -1
  257. package/gsd-core/workflows/eval-review.md +5 -10
  258. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -1
  259. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +129 -11
  260. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  261. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  262. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  263. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +29 -5
  264. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  265. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  266. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +4 -2
  267. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
  268. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  269. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
  270. package/gsd-core/workflows/execute-phase.md +68 -66
  271. package/gsd-core/workflows/execute-plan.md +25 -20
  272. package/gsd-core/workflows/explore.md +3 -1
  273. package/gsd-core/workflows/extract-learnings.md +3 -1
  274. package/gsd-core/workflows/fast.md +8 -2
  275. package/gsd-core/workflows/forensics.md +3 -1
  276. package/gsd-core/workflows/graduation.md +6 -6
  277. package/gsd-core/workflows/health.md +4 -7
  278. package/gsd-core/workflows/help/modes/brief.md +2 -0
  279. package/gsd-core/workflows/help/modes/default.md +2 -0
  280. package/gsd-core/workflows/help/modes/full.md +12 -0
  281. package/gsd-core/workflows/help/modes/topic.md +2 -0
  282. package/gsd-core/workflows/help.md +2 -0
  283. package/gsd-core/workflows/import.md +17 -14
  284. package/gsd-core/workflows/inbox.md +5 -6
  285. package/gsd-core/workflows/ingest-docs.md +45 -12
  286. package/gsd-core/workflows/insert-phase.md +7 -5
  287. package/gsd-core/workflows/list-phase-assumptions.md +2 -0
  288. package/gsd-core/workflows/list-seeds.md +7 -3
  289. package/gsd-core/workflows/list-workspaces.md +3 -1
  290. package/gsd-core/workflows/manager.md +15 -26
  291. package/gsd-core/workflows/map-codebase.md +3 -1
  292. package/gsd-core/workflows/milestone-summary.md +3 -1
  293. package/gsd-core/workflows/mvp-phase.md +3 -3
  294. package/gsd-core/workflows/new-milestone.md +10 -22
  295. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  296. package/gsd-core/workflows/new-project.md +17 -29
  297. package/gsd-core/workflows/new-workspace.md +2 -2
  298. package/gsd-core/workflows/next.md +4 -2
  299. package/gsd-core/workflows/node-repair.md +2 -0
  300. package/gsd-core/workflows/note.md +2 -0
  301. package/gsd-core/workflows/onboard.md +1 -1
  302. package/gsd-core/workflows/pause-work.md +20 -5
  303. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  304. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
  305. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +4 -4
  306. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +12 -3
  307. package/gsd-core/workflows/plan-phase.md +251 -54
  308. package/gsd-core/workflows/plan-review-convergence.md +148 -19
  309. package/gsd-core/workflows/plant-seed.md +3 -3
  310. package/gsd-core/workflows/pr-branch.md +195 -51
  311. package/gsd-core/workflows/profile-user.md +17 -15
  312. package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
  313. package/gsd-core/workflows/progress.md +52 -15
  314. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  315. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +38 -5
  316. package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
  317. package/gsd-core/workflows/quick/steps/research-phase.md +5 -7
  318. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  319. package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
  320. package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
  321. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
  322. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
  323. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
  324. package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
  325. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
  326. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
  327. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
  328. package/gsd-core/workflows/quick-batch.md +203 -0
  329. package/gsd-core/workflows/quick.md +33 -32
  330. package/gsd-core/workflows/reapply-patches.md +2 -0
  331. package/gsd-core/workflows/remove-phase.md +6 -4
  332. package/gsd-core/workflows/remove-workspace.md +3 -3
  333. package/gsd-core/workflows/resume-project.md +14 -14
  334. package/gsd-core/workflows/review.md +404 -21
  335. package/gsd-core/workflows/scan.md +3 -1
  336. package/gsd-core/workflows/section-manifest.json +12 -0
  337. package/gsd-core/workflows/secure-phase.md +3 -3
  338. package/gsd-core/workflows/session-report.md +2 -0
  339. package/gsd-core/workflows/settings-advanced.md +9 -9
  340. package/gsd-core/workflows/settings-integrations.md +66 -32
  341. package/gsd-core/workflows/settings.md +4 -6
  342. package/gsd-core/workflows/ship.md +22 -16
  343. package/gsd-core/workflows/sketch-wrap-up.md +13 -17
  344. package/gsd-core/workflows/sketch.md +13 -19
  345. package/gsd-core/workflows/smart-entry.md +4 -6
  346. package/gsd-core/workflows/spec-phase.md +31 -4
  347. package/gsd-core/workflows/spike-wrap-up.md +9 -11
  348. package/gsd-core/workflows/spike.md +21 -32
  349. package/gsd-core/workflows/stats.md +4 -2
  350. package/gsd-core/workflows/sync-skills.md +13 -5
  351. package/gsd-core/workflows/thread.md +13 -7
  352. package/gsd-core/workflows/transition.md +7 -5
  353. package/gsd-core/workflows/ui-phase.md +36 -21
  354. package/gsd-core/workflows/ui-review.md +7 -11
  355. package/gsd-core/workflows/ultraplan-phase.md +7 -13
  356. package/gsd-core/workflows/undo.md +9 -17
  357. package/gsd-core/workflows/update.md +47 -48
  358. package/gsd-core/workflows/validate-phase.md +3 -3
  359. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  360. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  361. package/gsd-core/workflows/verify-work.md +106 -21
  362. package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
  363. package/hooks/dist/gsd-check-update-worker.js +19 -2
  364. package/hooks/dist/gsd-config-reload.js +18 -12
  365. package/hooks/dist/gsd-context-monitor.js +302 -22
  366. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  367. package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
  368. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  369. package/hooks/dist/gsd-cursor-stop.js +2 -1
  370. package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
  371. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
  372. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  373. package/hooks/dist/gsd-graphify-update.sh +22 -18
  374. package/hooks/dist/gsd-node-runner.sh +77 -0
  375. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  376. package/hooks/dist/gsd-prompt-guard.js +46 -12
  377. package/hooks/dist/gsd-read-guard.js +18 -7
  378. package/hooks/dist/gsd-read-injection-scanner.js +22 -13
  379. package/hooks/dist/gsd-secret-read-guard.js +1079 -0
  380. package/hooks/dist/gsd-session-state.sh +1 -0
  381. package/hooks/dist/gsd-statusline.js +222 -29
  382. package/hooks/dist/gsd-validate-commit.sh +523 -12
  383. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  384. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  385. package/hooks/dist/gsd-workflow-guard.js +36 -17
  386. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  387. package/hooks/dist/gsd-write-guard.js +35 -25
  388. package/hooks/dist/lib/cli-exit.js +560 -0
  389. package/hooks/dist/lib/exit-code-registry.js +98 -0
  390. package/hooks/dist/lib/git-cmd.js +210 -1
  391. package/hooks/dist/lib/git-probe.js +84 -0
  392. package/hooks/dist/lib/hook-exit.js +81 -0
  393. package/hooks/dist/lib/injection-patterns.js +36 -6
  394. package/hooks/dist/managed-hooks-registry.cjs +4 -0
  395. package/hooks/gsd-agent-isolation-guard.js +77 -38
  396. package/hooks/gsd-check-update-worker.js +19 -2
  397. package/hooks/gsd-config-reload.js +18 -12
  398. package/hooks/gsd-context-monitor.js +302 -22
  399. package/hooks/gsd-cursor-post-tool.js +3 -1
  400. package/hooks/gsd-cursor-pre-tool.js +3 -1
  401. package/hooks/gsd-cursor-session-start.js +2 -1
  402. package/hooks/gsd-cursor-stop.js +2 -1
  403. package/hooks/gsd-cursor-subagent-start.js +28 -23
  404. package/hooks/gsd-cursor-subagent-stop.js +3 -1
  405. package/hooks/gsd-ensure-canonical-path.js +2 -1
  406. package/hooks/gsd-graphify-update.sh +22 -18
  407. package/hooks/gsd-node-runner.sh +77 -0
  408. package/hooks/gsd-phase-boundary.sh +1 -0
  409. package/hooks/gsd-prompt-guard.js +46 -12
  410. package/hooks/gsd-read-guard.js +18 -7
  411. package/hooks/gsd-read-injection-scanner.js +22 -13
  412. package/hooks/gsd-secret-read-guard.js +1079 -0
  413. package/hooks/gsd-session-state.sh +1 -0
  414. package/hooks/gsd-statusline.js +222 -29
  415. package/hooks/gsd-validate-commit.sh +523 -12
  416. package/hooks/gsd-windsurf-pre-command.js +16 -11
  417. package/hooks/gsd-windsurf-pre-write.js +22 -13
  418. package/hooks/gsd-workflow-guard.js +36 -17
  419. package/hooks/gsd-worktree-path-guard.js +36 -21
  420. package/hooks/gsd-write-guard.js +35 -25
  421. package/hooks/hooks.json +6 -0
  422. package/hooks/lib/cli-exit.js +560 -0
  423. package/hooks/lib/exit-code-registry.js +98 -0
  424. package/hooks/lib/git-cmd.js +210 -1
  425. package/hooks/lib/git-probe.js +84 -0
  426. package/hooks/lib/hook-exit.js +81 -0
  427. package/hooks/lib/injection-patterns.js +36 -6
  428. package/hooks/managed-hooks-registry.cjs +4 -0
  429. package/package.json +14 -9
  430. package/scripts/base64-scan.sh +74 -12
  431. package/scripts/build-hooks.js +12 -0
  432. package/scripts/check-glossary-refs.cjs +77 -15
  433. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  434. package/scripts/ci-check-job-near-cap.cjs +49 -0
  435. package/scripts/ci-pr-mergeability.cjs +262 -0
  436. package/scripts/ci-test-scope.cjs +52 -12
  437. package/scripts/ci-timeout-report.cjs +230 -0
  438. package/scripts/docs-guard-registry.cjs +406 -0
  439. package/scripts/gen-capability-registry.cjs +8 -6
  440. package/scripts/gen-exit-code-docs.cjs +318 -0
  441. package/scripts/gen-exit-code-registry.cjs +891 -0
  442. package/scripts/gen-features.cjs +836 -0
  443. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  444. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  445. package/scripts/gen-loop-host-contract.cjs +189 -4
  446. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  447. package/scripts/gen-state-md-docs.cjs +727 -0
  448. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  449. package/scripts/lib/ci-job-timing.cjs +72 -0
  450. package/scripts/lib/cli-exit.cjs +546 -44
  451. package/scripts/lib/drift-scan.cjs +32 -2
  452. package/scripts/lib/exit-code-registry.cjs +98 -0
  453. package/scripts/lib/ndjson-reporter.cjs +119 -0
  454. package/scripts/lib/shellcheck-fetch.cjs +247 -0
  455. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
  456. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  457. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  458. package/scripts/lint-docs-guard-registration.cjs +495 -0
  459. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +198 -0
  460. package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
  461. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  462. package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
  463. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  464. package/scripts/lint-phase-enumeration-drift.cjs +45 -14
  465. package/scripts/lint-phase-id-drift.cjs +133 -8
  466. package/scripts/lint-planning-prompt-drift.cjs +38 -1
  467. package/scripts/lint-portable-grep.cjs +176 -0
  468. package/scripts/lint-removed-but-needed.cjs +184 -16
  469. package/scripts/lint-response-language-coverage.cjs +524 -0
  470. package/scripts/lint-seam-enforcement.cjs +182 -0
  471. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  472. package/scripts/lint-source-test-name-collision.cjs +241 -0
  473. package/scripts/lint-state-write-path-drift.cjs +337 -432
  474. package/scripts/lint-test-file-count.allowlist.json +124 -4
  475. package/scripts/lint-test-file-count.cjs +25 -3
  476. package/scripts/lint-unreachable-guard-drift.cjs +51 -64
  477. package/scripts/lint-vendored-deps.cjs +208 -35
  478. package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
  479. package/scripts/lint-workflow-shellcheck.cjs +614 -0
  480. package/scripts/mutation-matrix.cjs +599 -50
  481. package/scripts/npm-audit-baseline.cjs +376 -0
  482. package/scripts/prompt-injection-scan.sh +83 -14
  483. package/scripts/require-issue-link-policy.cjs +16 -1
  484. package/scripts/secret-scan.sh +75 -13
  485. package/scripts/select-docs-guards.cjs +56 -0
  486. package/scripts/sync-runtime-launcher.cjs +22 -3
  487. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  488. package/skills/gsd-execute-phase/SKILL.md +1 -1
  489. package/skills/gsd-import/SKILL.md +1 -1
  490. package/skills/gsd-ns-workflow/SKILL.md +1 -0
  491. package/skills/gsd-phase/SKILL.md +1 -1
  492. package/skills/gsd-quick/SKILL.md +8 -4
  493. package/skills/gsd-quick-batch/SKILL.md +105 -0
  494. package/skills/gsd-surface/SKILL.md +18 -8
  495. package/vscode/package.json +1 -1
  496. package/bin/lib/ui-safety-gate.cjs +0 -109
  497. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  498. package/scripts/state-write-path-drift-baseline.json +0 -19
@@ -7,11 +7,78 @@
7
7
  #
8
8
  # OPT-IN: This hook is a no-op unless config.json has hooks.community: true.
9
9
  # Enable with: "hooks": { "community": true } in .planning/config.json
10
+ set -euo pipefail
11
+
12
+ # Temp files created below for subprocess stderr capture (config read, command
13
+ # extraction, classifier). A single EXIT trap replaces three hand-rolled
14
+ # mktemp/rm-f pairs so an early or unexpected exit path can never leak one —
15
+ # and a future fourth check does not need its own copy (#3911 review).
16
+ # Idempotent and failure-proof by construction: unset vars expand to "" (a
17
+ # no-op rm -f target), and `|| true` guarantees the trap itself never changes
18
+ # the script's exit status.
19
+ ENABLED_ERR=""
20
+ CMD_ERR=""
21
+ CLASSIFY_ERR=""
22
+ cleanup_temp_files() {
23
+ rm -f "${ENABLED_ERR:-}" "${CMD_ERR:-}" "${CLASSIFY_ERR:-}" 2>/dev/null || true
24
+ }
25
+ trap cleanup_temp_files EXIT
26
+
27
+ # The 10 built-in Conventional Commits types — the SINGLE declaration (#3811
28
+ # review finding: this was previously hand-typed a second time inside the
29
+ # node -e script below, a generative-fix-divergence risk per CLAUDE.md's
30
+ # known-defect list). Threaded into node via an env var; reused directly by
31
+ # bash below when building COMMIT_TYPES.
32
+ BUILTIN_COMMIT_TYPES=(feat fix docs style refactor perf test build ci chore)
10
33
 
11
34
  # Check opt-in config — exit silently if not enabled
12
35
  if [ -f .planning/config.json ]; then
13
- ENABLED=$(node -e "try{const c=require('./.planning/config.json');process.stdout.write(c.hooks?.community===true?'1':'0')}catch{process.stdout.write('0')}" 2>/dev/null)
36
+ ENABLED_ERR=$(mktemp)
37
+ # Single node invocation reads BOTH hooks.community (line 1: '1'/'0') and
38
+ # hooks.commit_types (remaining lines: one sanitized extra type per line) —
39
+ # see #3811. Sanitizing here, not in bash, keeps the safe-token check in one
40
+ # place and guarantees only [a-z][a-z0-9-]* strings ever reach the regex
41
+ # built below, so a configured value can never alter the compiled pattern's
42
+ # structure.
43
+ BUILTIN_COMMIT_TYPES_CSV=$(IFS=,; echo "${BUILTIN_COMMIT_TYPES[*]}")
44
+ CONFIG_OUT=$(GSD_BUILTIN_COMMIT_TYPES="$BUILTIN_COMMIT_TYPES_CSV" node -e "
45
+ try{
46
+ const c=require('./.planning/config.json');
47
+ process.stdout.write(c.hooks?.community===true?'1':'0');
48
+ process.stdout.write('\n');
49
+ const raw=c.hooks?.commit_types;
50
+ const list=Array.isArray(raw)?raw:[];
51
+ const seen=new Set((process.env.GSD_BUILTIN_COMMIT_TYPES||'').split(',').filter(Boolean));
52
+ for (const t of list){
53
+ if (typeof t!=='string') continue;
54
+ if (!/^[a-z][a-z0-9-]*\$/.test(t)) continue;
55
+ if (seen.has(t)) continue;
56
+ seen.add(t);
57
+ process.stdout.write(t+'\n');
58
+ }
59
+ }catch(e){
60
+ process.stderr.write('CONFIG_READ_FAILED: '+(e&&e.message?e.message:String(e)));
61
+ process.exit(3);
62
+ }
63
+ " 2>"$ENABLED_ERR") || CONFIG_STATUS=$?
64
+ CONFIG_STATUS=${CONFIG_STATUS:-0}
65
+ if [ "$CONFIG_STATUS" != "0" ]; then
66
+ # Could not determine the opt-in flag at all (node missing, JSON parse
67
+ # error other than absence, etc.) — distinct from ".planning/config.json
68
+ # exists and legitimately disables the hook". Say so and pass, per #3838.
69
+ echo "gsd-validate-commit.sh: could not read .planning/config.json (opt-in check) — validator disabled for this call. $(cat "$ENABLED_ERR")" >&2
70
+ exit 0
71
+ fi
72
+ ENABLED=$(printf '%s\n' "$CONFIG_OUT" | head -1)
14
73
  if [ "$ENABLED" != "1" ]; then exit 0; fi
74
+ # Remaining lines (if any) are the sanitized, deduped configured commit
75
+ # types beyond the 10 built-ins (#3811). Read into a bash-3.2-safe array —
76
+ # `mapfile`/`readarray` are bash 4+ only and this hook is tested against
77
+ # bash 3.2.57 (macOS default).
78
+ EXTRA_COMMIT_TYPES=()
79
+ while IFS= read -r _extra_type; do
80
+ [ -n "$_extra_type" ] && EXTRA_COMMIT_TYPES+=("$_extra_type")
81
+ done < <(printf '%s\n' "$CONFIG_OUT" | tail -n +2)
15
82
  else
16
83
  exit 0
17
84
  fi
@@ -19,32 +86,476 @@ fi
19
86
  INPUT=$(cat)
20
87
 
21
88
  # Extract command from JSON using Node (handles escaping correctly, no jq needed)
22
- CMD=$(echo "$INPUT" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{process.stdout.write(JSON.parse(d).tool_input?.command||'')}catch{}})" 2>/dev/null)
89
+ CMD_ERR=$(mktemp)
90
+ CMD=$(echo "$INPUT" | node -e "
91
+ let d='';
92
+ process.stdin.on('data',c=>d+=c);
93
+ process.stdin.on('end',()=>{
94
+ try{
95
+ process.stdout.write(JSON.parse(d).tool_input?.command||'');
96
+ }catch(e){
97
+ process.stderr.write('COMMAND_EXTRACTION_FAILED: '+(e&&e.message?e.message:String(e)));
98
+ process.exit(3);
99
+ }
100
+ });
101
+ " 2>"$CMD_ERR") || CMD_STATUS=$?
102
+ CMD_STATUS=${CMD_STATUS:-0}
103
+ if [ "$CMD_STATUS" != "0" ]; then
104
+ # Could not extract tool_input.command at all (node missing, malformed
105
+ # JSON, etc.) — distinct from "there is genuinely no command field". Say
106
+ # so and pass, per #3838.
107
+ echo "gsd-validate-commit.sh: could not extract tool_input.command from the hook payload — validator disabled for this call. $(cat "$CMD_ERR")" >&2
108
+ exit 0
109
+ fi
23
110
 
24
111
  # Only check git commit commands.
25
112
  # Delegates to hooks/lib/git-cmd.js isGitSubcommand() — the canonical token-walk
26
113
  # classifier that handles env-prefix, -C path, and full-path git invocations.
27
114
  # A naive `^git\s+commit` regex misses all three; this guard fixes that (#3129).
28
115
  HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
29
- if GIT_CMD_LIB="$HOOK_DIR/lib/git-cmd.js" node -e "
30
- const {isGitSubcommand}=require(process.env.GIT_CMD_LIB);
31
- process.exit(isGitSubcommand(process.argv[1],'commit')?0:1);
32
- " "$CMD" 2>/dev/null; then
33
- # Extract message from -m flag
116
+ CLASSIFY_ERR=$(mktemp)
117
+ GIT_CMD_LIB="$HOOK_DIR/lib/git-cmd.js" node -e "
118
+ try {
119
+ const {isGitSubcommand}=require(process.env.GIT_CMD_LIB);
120
+ process.exit(isGitSubcommand(process.argv[1],'commit')?0:1);
121
+ } catch(e) {
122
+ process.stderr.write('CLASSIFIER_THREW: '+(e&&e.message?e.message:String(e)));
123
+ process.exit(3);
124
+ }
125
+ " "$CMD" 2>"$CLASSIFY_ERR" || CLASSIFY_STATUS=$?
126
+ CLASSIFY_STATUS=${CLASSIFY_STATUS:-0}
127
+ if [ "$CLASSIFY_STATUS" != "0" ] && [ "$CLASSIFY_STATUS" != "1" ]; then
128
+ # 0 = is a git commit (validate below); 1 = genuinely not a git commit
129
+ # (real negative, pass silently) — the ONLY intentional non-zero exit the
130
+ # script above ever produces on success. Any other status — 127 node
131
+ # missing, or 3 from the try/catch above when the git-cmd.js require chain
132
+ # throws (e.g. its built dependency, gsd-core/bin/lib/token-scanner.cjs, is
133
+ # a gitignored build artifact and absent on a fresh checkout — run
134
+ # `npm run build:lib`) — means the classifier could not run at all. Say so
135
+ # on stderr and pass (#3838): PreToolUse stderr does not disturb the JSON
136
+ # protocol.
137
+ echo "gsd-validate-commit.sh: could not classify the command via hooks/lib/git-cmd.js (exit $CLASSIFY_STATUS) — validator disabled for this call. If this persists, run \`npm run build:lib\`. $(cat "$CLASSIFY_ERR")" >&2
138
+ exit 0
139
+ fi
140
+ if [ "$CLASSIFY_STATUS" = "0" ]; then
141
+ # Extract message from -m flag.
142
+ #
143
+ # MSG_QUOTE records WHICH arm matched. bash treats the two arms differently
144
+ # and the subject step below depends on that difference — see the resolver
145
+ # gate (review of #3816, round 4).
34
146
  MSG=""
147
+ MSG_QUOTE=""
148
+ MSG_MATCH=""
35
149
  if [[ "$CMD" =~ -m[[:space:]]+\"([^\"]+)\" ]]; then
36
150
  MSG="${BASH_REMATCH[1]}"
151
+ MSG_QUOTE=dq
152
+ MSG_MATCH="${BASH_REMATCH[0]}"
37
153
  elif [[ "$CMD" =~ -m[[:space:]]+\'([^\']+)\' ]]; then
38
154
  MSG="${BASH_REMATCH[1]}"
155
+ MSG_QUOTE=sq
156
+ MSG_MATCH="${BASH_REMATCH[0]}"
39
157
  fi
40
158
 
41
159
  if [ -n "$MSG" ]; then
42
- SUBJECT=$(echo "$MSG" | head -1)
160
+ # Subject = first line of the message, EXCEPT for the command-substituted
161
+ # heredoc form, where the first line is the opener rather than the message:
162
+ #
163
+ # git commit -m "$(cat <<'EOF'
164
+ # feat(auth): add login flow
165
+ # EOF
166
+ # )"
167
+ #
168
+ # The capture above spans it whole, because bash `[^"]` matches newlines, so
169
+ # `head -1` yielded the literal `$(cat <<'EOF'` and EVERY heredoc-form commit
170
+ # was blocked regardless of its message (#3802).
171
+ #
172
+ # Selection of WHICH argument is the message is unchanged above — only the
173
+ # subject-from-message step is delegated. Falls back to the previous `head -1`
174
+ # if node or the library is unavailable, so a broken extractor degrades to the
175
+ # old behavior instead of becoming a new silent-allow path.
176
+ #
177
+ # SINGLE-QUOTE GATE (review of #3816, round 4 — BLOCKER). The resolver may
178
+ # only run on the DOUBLE-quoted arm. Inside `-m '...'` bash performs NO
179
+ # command substitution, so `$(cat <<'EOF'` is literal text and git's real
180
+ # subject is that opener line — resolving the body there validates a
181
+ # message git never receives. Measured against the real hook, all four
182
+ # spellings (`<<'E'`, `<<"E"`, `<<\E`, `<<E`) went base=2 -> head=0: a
183
+ # net-new bypass reachable by the ordinary authoring slip of typing `'`
184
+ # for `"`. The sq arm therefore keeps the pre-fix `head -1`, which is exact
185
+ # base parity.
186
+ #
187
+ # ADJACENCY GUARD (review of #3816): text glued to the CLOSING quote —
188
+ # `-m "$(cat <<'EOF' ... )"suffix` — is concatenated by bash into the SAME
189
+ # argument, so the capture above holds only a PREFIX of the real message.
190
+ # Resolving a heredoc from a prefix hands the length gate a fraction of the
191
+ # real subject: a net-new bypass relative to base, which measured the
192
+ # opener line and blocked. When the quote is not followed by whitespace or
193
+ # the end of the command, skip the resolver and keep the pre-fix subject
194
+ # (first captured line): the heredoc form then fails the format gate
195
+ # exactly as it did on base, and the plain single-line form keeps base
196
+ # behavior unchanged. The guard is tested against the arm that MATCHED,
197
+ # not against both: testing both let a double-quoted heredoc whose BODY
198
+ # mentions a glued single-quoted token (`-m "... -m 'foo'bar ..."`) trip
199
+ # the sq arm and lose the fix for a message that never had a prefix
200
+ # problem (review of #3816, round 4, Minor 1).
201
+ # RESOLVER PRECONDITIONS. The resolver may run only where the captured text
202
+ # is provably the subject git receives. Each guard names an input where it
203
+ # is not; every refusal falls back to `head -1`, the pre-fix subject, which
204
+ # fails the format gate exactly as this whole form did before the fix.
205
+ RESOLVE=0
206
+ if [ "$MSG_QUOTE" = dq ]; then
207
+ RESOLVE=1
208
+ # Text before the message we matched. The heredoc BODY always sits after
209
+ # the match, so this window cannot be contaminated by message content —
210
+ # which is what lets the two guards below scan for tokens that would also
211
+ # be legal inside a commit message.
212
+ MSG_PREFIX="${CMD%%"$MSG_MATCH"*}"
213
+ # Text after it. Together, PREFIX and SUFFIX are the whole command MINUS
214
+ # the message — the window a guard must use when the token it scans for
215
+ # is also legal English inside a commit message, but may legally appear
216
+ # on EITHER side of the message on the command line.
217
+ MSG_SUFFIX="${CMD#*"$MSG_MATCH"}"
218
+ # LINE CONTINUATIONS ARE NOT SEPARATORS (review of #3816, rounds 8 and 9).
219
+ # `git commit \` newline ` -m "$(cat <<'EOF' …` is an ordinary way to
220
+ # spread an invocation over lines, and every guard below reads a newline in
221
+ # a window as a command separator, so the whole form was refused. That was
222
+ # disclosed as a fail-closed limit in round 8 because "is this newline a
223
+ # continuation" looked like the segmentation question this file has
224
+ # reverted twice. It is not: bash's rule is local and character-level. A
225
+ # newline preceded by an ODD run of backslashes is a continuation and bash
226
+ # removes both; an EVEN run (`\\` then newline) is a literal backslash
227
+ # followed by a real newline, which IS a separator. So the windows are
228
+ # joined the way bash joins them, in three bash-3.2-safe steps: every `\\`
229
+ # pair is parked on \x01, a byte no real command line carries, any
230
+ # backslash-newline that remains is a lone (odd) one and is removed, then
231
+ # the pairs are restored. Applied to BOTH windows, BEFORE the dequote
232
+ # copies are derived, so every scan sees the joined text.
233
+ #
234
+ # KNOWN OVER-BLOCK, fail-closed: a literal \x01 that IS present in the
235
+ # command is restored as `\\`, so an option-shaped token carrying one
236
+ # (`-\x01m`) reads as `-\\m`, dequotes to `-m`, and refuses where it did
237
+ # not before (independent review, round 9). Refusing is the recoverable
238
+ # direction; a control byte in an option name is not a spelling anyone
239
+ # types, and it is not a hole in the accept direction.
240
+ #
241
+ # Direction check: a continuation glued to the closing quote
242
+ # (`"$(…)"\` newline `suffix`) joins to `"$(…)"suffix`, which the glue
243
+ # guard refuses exactly as bash would have glued it; `\\` + newline keeps
244
+ # its newline and is still refused by the separator guard. Measured on
245
+ # bash 3.2.57 and 5.3.15 in tests/hooks-opt-in.test.cjs.
246
+ CONT_PARK=$'\x01'
247
+ MSG_PREFIX="${MSG_PREFIX//\\\\/$CONT_PARK}"
248
+ MSG_PREFIX="${MSG_PREFIX//\\$'\n'/}"
249
+ MSG_PREFIX="${MSG_PREFIX//$CONT_PARK/\\\\}"
250
+ MSG_SUFFIX="${MSG_SUFFIX//\\\\/$CONT_PARK}"
251
+ MSG_SUFFIX="${MSG_SUFFIX//\\$'\n'/}"
252
+ MSG_SUFFIX="${MSG_SUFFIX//$CONT_PARK/\\\\}"
253
+
254
+ # QUOTE-SPLICED SPELLINGS (independent review of #3816, round 6). Bash
255
+ # removes quotes before git ever sees an argument, so the same option has
256
+ # unboundedly many spellings on the command line: `--clean""up=verbatim`
257
+ # IS `--cleanup=verbatim` to git, and `-""m` IS `-m`. Both matched no
258
+ # literal and were measured ACCEPTING a 75-byte subject the length gate
259
+ # had recorded as 72. The guards below therefore scan a copy of their
260
+ # window with quote characters removed, which is what bash does to it.
261
+ # Only the two OPTION-NAME scans use it; the adjacency test deliberately
262
+ # does not, because it asks about a literal character position, and the
263
+ # message span itself is excluded from both windows either way.
264
+ MSG_PREFIX_DEQ="${MSG_PREFIX//[\"\']/}"
265
+ MSG_SUFFIX_DEQ="${MSG_SUFFIX//[\"\']/}"
266
+ # BACKSLASH-SPLICED SPELLINGS (independent review of #3816, round 7).
267
+ # Quote removal alone was not "the command as bash hands it to git": bash
268
+ # also removes syntactic backslashes, so `-\m WIP` IS `-m WIP` and
269
+ # `--clean\up=verbatim` IS `--cleanup=verbatim` to git, and both matched
270
+ # no literal. Measured: `-\m WIP -m <conforming heredoc>` accepted the
271
+ # heredoc while git recorded `WIP`, and a trailing `--clean\up=verbatim`
272
+ # accepted a 75-byte subject the length gate measured as 72. Stripped in a
273
+ # second pass so the class is unambiguous.
274
+ MSG_PREFIX_DEQ="${MSG_PREFIX_DEQ//\\/}"
275
+ MSG_SUFFIX_DEQ="${MSG_SUFFIX_DEQ//\\/}"
276
+ # DOLLAR-QUOTED SPELLINGS (independent review of #3816, round 8). The two
277
+ # passes above still were not "the command as bash hands it to git": bash
278
+ # has TWO more quoting forms whose introducer is a `$`, and removing the
279
+ # quote characters alone leaves that `$` stranded in the middle of the
280
+ # option name. `-$"m"` became `-$m` here while bash passes a real `-m` to
281
+ # git, and `--mes$'sage'=WIP` became `--mes$sage=WIP`; neither matched any
282
+ # literal, so the first-message guard below never fired. Measured on bash
283
+ # 3.2.57 and 5.3.15 against a real repository: the hook allowed
284
+ # `-$"m" WIP -m <conforming heredoc>` (exit 0) while `git cat-file -p`
285
+ # recorded the subject `WIP` — the same command spelled `-m WIP` is
286
+ # refused (exit 2). Stripping `$` closes both dollar-quote forms.
287
+ #
288
+ # RESIDUAL, and not fixable from a string: an option name assembled by an
289
+ # EXPANSION — `-${x}m`, `-$(printf m)` — is not knowable without running
290
+ # the command, the same limit this file already documents for expanded
291
+ # heredoc bodies. Stripping `$` makes those spellings collapse toward the
292
+ # literal too, which over-matches, and over-matching only refuses more.
293
+ MSG_PREFIX_DEQ="${MSG_PREFIX_DEQ//\$/}"
294
+ MSG_SUFFIX_DEQ="${MSG_SUFFIX_DEQ//\$/}"
295
+
296
+ # ADJACENCY GUARD (review of #3816): text glued to the CLOSING quote —
297
+ # `-m "$(cat <<'EOF' ... )"suffix` — is concatenated by bash into the SAME
298
+ # argument, so the capture holds only a PREFIX of the real message, and
299
+ # the length gate would measure a fraction of the real subject.
300
+ # SCOPE (review of #3816, round 6 — MAJOR). Glue is a property of the ONE
301
+ # character following the MATCHED span, so that character is the whole
302
+ # window. Scanning $CMD for the shape anywhere refused any conforming
303
+ # commit whose command merely CONTAINED a glued `-m` elsewhere —
304
+ # `git commit -m "<heredoc>" && echo -m "test"z` stayed blocked with
305
+ # CONVENTIONAL_COMMITS_VIOLATION. Base blocks it too, because base blocks
306
+ # EVERY heredoc form (that is #3802): this was the fix not reaching the
307
+ # shape, measured base=2 -> pre=2 -> post=0, not a regression.
308
+ # The separators and redirections are excluded because bash does NOT
309
+ # concatenate across them: in `-m "msg"&& echo hi` the argument ends at
310
+ # the quote, so there is no truncated capture to defend against.
311
+ # The class is held in a VARIABLE, not written inline. Inline, every
312
+ # member needs a backslash to get past the `[[ ]]` parser (`;`, `&` and
313
+ # `|` are metacharacters there) — and on bash 3.2, the system /bin/bash on
314
+ # macOS, those backslashes are passed THROUGH to the regex engine instead
315
+ # of being consumed by the shell, silently adding a literal `\` to the
316
+ # class. Unquoted expansion of a variable on the right of `=~` is the one
317
+ # spelling that is a plain regex on 3.2 and 5.x alike (review of #3816,
318
+ # round 8). Writing `[^[:space:];&|()<>]` inline is NOT the fix: it is a
319
+ # bash syntax error on both versions.
320
+ GLUE_CLASS='^[^[:space:];&|()<>]'
321
+ if [[ "$MSG_SUFFIX" =~ $GLUE_CLASS ]]; then RESOLVE=0; fi
322
+
323
+ # FIRST-MESSAGE GUARD (Codex review of #3816, round 4 — BLOCKER). The
324
+ # capture is a SEARCH over the whole command and the double-quoted arm is
325
+ # tried first, so it can select a `-m` that is not git's subject at all:
326
+ #
327
+ # git commit -m 'WIP first' -m "$(cat <<'EOF' -> git concatenates; the
328
+ # git commit -m WIP -m "$(cat <<'EOF' subject is `WIP first`
329
+ # git commit -m WIP -- -m "$(cat <<'EOF' -> after --, not a message
330
+ # git commit -m WIP && echo -m "$(cat <<'EOF' -> belongs to `echo`
331
+ #
332
+ # All four measured base=2 -> head=0, with git recording the FIRST message
333
+ # as the subject (verified against real commits, not the man page). The
334
+ # mis-selection is pre-existing; resolving it is what turned it into an
335
+ # enforcement bypass. Resolve only when nothing before the match could
336
+ # have been an earlier message, an end-of-options marker, or another
337
+ # command.
338
+ # BUNDLED SHORT OPTIONS (independent review of #3816, round 6). git splits
339
+ # `-am 'WIP first'` into `-a -m`, so the real subject is `WIP first` and
340
+ # the heredoc is git's SECOND message — measured accepting the heredoc's
341
+ # subject while git recorded `WIP first`. A standalone `-m` is therefore
342
+ # not the only spelling that claims the message; any short-option cluster
343
+ # ending in `m` does.
344
+ # ATTACHED VALUES AND --message ABBREVIATIONS (independent review of
345
+ # #3816, round 7). The scan required a space or `=` after the option name,
346
+ # so two spellings git accepts matched nothing: an ATTACHED short-option
347
+ # value (`-mWIP`, which git reads as `-m WIP`) and a long-option
348
+ # abbreviation (`--mes=WIP`), the same abbreviation behaviour this file
349
+ # already models for `--cleanup`. Both were measured accepting a later
350
+ # conforming heredoc while git recorded `WIP` as the subject — confirmed
351
+ # against the raw commit object, not `git log --pretty=%s`. The short arm
352
+ # therefore drops its trailing requirement entirely: a `-` followed by
353
+ # letters ending in `m` claims the message however it is spelled. Wider
354
+ # than git's own abbreviation set on purpose — over-matching only refuses
355
+ # more, which is the recoverable direction.
356
+ # Variable-held for the same bash-3.2 reason as GLUE_CLASS above.
357
+ # AN OPTION NAME BUILT BY A COMMAND SUBSTITUTION IS UNRESOLVABLE
358
+ # (independent review of #3816, round 8). Stripping `$` above collapses the
359
+ # two dollar-QUOTE forms onto their literals, but `--clean$(printf up)=`
360
+ # is a different thing: bash RUNS a program to finish the option name, so
361
+ # the argv git receives is not derivable from this string at all. Measured
362
+ # accepting a 75-byte subject the length gate had recorded as 72.
363
+ #
364
+ # SCOPED TO THE NAME, NOT THE VALUE. The class is a `-`-leading token whose
365
+ # characters up to the substitution contain no `=` — an option NAME being
366
+ # assembled. `--author="$(git config user.name)"` and `--author "$(…)"`
367
+ # both put the substitution in the VALUE, which this file never models and
368
+ # which stays allowed; only `-…$(` before any `=` refuses. Scanned on the
369
+ # RAW windows on purpose: the dequoted copies have had their `$` removed,
370
+ # so the shape is no longer visible there.
371
+ #
372
+ # This is a SHAPE, not a segmentation: it never tries to decide where
373
+ # git's own command ends. Two attempts at that were reverted for opening
374
+ # accept-direction holes, and the reasoning above still stands.
375
+ # WIDENED, and the strategy changed with it (independent review, round 9).
376
+ # The `$(`-only spelling above was the fourth patch in a row that tried to
377
+ # EMULATE what bash does to an argument before git sees it -- round 6
378
+ # removed quotes, round 7 backslashes, round 8 the `$` of a dollar-quote,
379
+ # and each time review found another transform that had been missed. Round
380
+ # 9 found four more, all measured accepting `WIP` as the real subject on
381
+ # bash 3.2.57 and 5.3.15 while the plain spelling of the same command is
382
+ # refused:
383
+ #
384
+ # -$'\155' WIP ANSI-C octal escape decodes to `m`
385
+ # -$'\x6d' WIP ANSI-C hex escape decodes to `m`
386
+ # -`printf m` WIP command substitution, backtick spelling
387
+ # x= … -${x}m WIP parameter expansion
388
+ # -? WIP pathname expansion, with a file named `-m`
389
+ #
390
+ # The last two settle the strategy: an option name finished by a PARAMETER
391
+ # expansion depends on a variable's runtime value, and one finished by a
392
+ # PATHNAME expansion depends on the contents of the working directory.
393
+ # Neither is derivable from the command string at any level of effort, so
394
+ # emulation cannot be completed -- not "has not been completed yet".
395
+ #
396
+ # So the rule is no longer "normalise it and match the literal". It is: an
397
+ # option NAME containing a shell expansion or quoting construct is
398
+ # UNRESOLVABLE, and unresolvable refuses. One rule covers every spelling
399
+ # above, and every spelling nobody has thought of yet, in the fail-closed
400
+ # direction. The dequoting passes above are kept: they still normalise the
401
+ # deterministic removals so the guards RECOGNISE `--clean""up=` and `-\m`
402
+ # rather than merely refusing them, which keeps the existing rows honest.
403
+ #
404
+ # SCOPED TO THE NAME, NOT THE VALUE, exactly as before: the class is a
405
+ # `-`-leading token whose characters up to the substitution contain no `=`.
406
+ # `--author="$(git config user.name)"` and `--author "$(…)"` put the
407
+ # construct in the VALUE and still resolve, pinned in both directions.
408
+ # Scanned on the RAW windows, because the dequoted copies have had `$` and
409
+ # the quote characters removed and the shape is no longer visible there.
410
+ #
411
+ # The class is bracket-only and holds no backslash, per round 8: a POSIX
412
+ # bracket expression has no escape mechanism, and a backslash written
413
+ # inside one becomes a literal member on bash 3.2.
414
+ SUBST_NAME_CLASS='(^|[[:space:]])-[^[:space:]=]*[$`?*[]'
415
+ if [[ "$MSG_PREFIX" =~ $SUBST_NAME_CLASS ]] \
416
+ || [[ "$MSG_SUFFIX" =~ $SUBST_NAME_CLASS ]]; then RESOLVE=0; fi
417
+ SEP_CLASS='[;&|]'
418
+ if [[ "$MSG_PREFIX_DEQ" =~ (^|[[:space:]])(-[a-zA-Z]*m|--m[a-z]*([=[:space:]]|$)) ]] \
419
+ || [[ "$MSG_PREFIX" =~ (^|[[:space:]])--([[:space:]]|$) ]] \
420
+ || [[ "$MSG_PREFIX" =~ $SEP_CLASS ]] \
421
+ || [[ "$MSG_PREFIX" == *$'\n'* ]]; then RESOLVE=0; fi
422
+ # NEWLINE IS A COMMAND SEPARATOR TOO (independent review of #3816, round
423
+ # 7) — the test above. The separator scan covered `;`, `&` and `|` but not
424
+ # a literal newline, so a LATER command's heredoc-shaped `-m` was taken
425
+ # for this commit's message:
426
+ #
427
+ # git commit --amend --no-edit
428
+ # echo -m "$(cat <<'EOF'
429
+ # fix: conforming text unrelated to the commit
430
+ # EOF
431
+ # )"
432
+ #
433
+ # The classifier recognises the leading commit, the capture reaches across
434
+ # the newline into `echo`'s argument, and a conforming string with no
435
+ # relationship to the commit was validated and allowed. Tested as a glob
436
+ # rather than folded into the bracket class, because a literal newline
437
+ # inside a bash regex bracket expression is not portably expressible.
438
+
439
+ # CLEANUP-MODE GUARD (Codex review of #3816, round 4 — BLOCKER). The
440
+ # resolver skips leading blank lines and strips trailing whitespace
441
+ # because git's DEFAULT cleanup=whitespace does. Under
442
+ # `--cleanup=verbatim` git does neither, so a 72-char subject plus three
443
+ # trailing spaces is committed as a 75-byte subject while the hook
444
+ # measured 72 — COMMIT_SUBJECT_TOO_LONG dodged (measured base=2 -> head=0;
445
+ # confirmed by reading the raw commit object, since `git log --pretty=%s`
446
+ # strips trailing whitespace in its own output and hides it).
447
+ # Any named mode other than `whitespace` refuses. A mode set persistently
448
+ # in git config is invisible here and stays a documented residual limit.
449
+ # SCOPE (review of #3816, round 5 — BLOCKER). This scan must exclude the
450
+ # message. `--cleanup=` and `commit.cleanup=` are ordinary English inside
451
+ # a commit message — this repository's own hooks and docs discuss them
452
+ # constantly — and the heredoc BODY sits verbatim inside $CMD, so
453
+ # scanning $CMD refused to resolve any conforming message that merely
454
+ # MENTIONED the token, blocking it with CONVENTIONAL_COMMITS_VIOLATION.
455
+ # Scanning $MSG_PREFIX alone (the fix as first prescribed) would reopen
456
+ # the bypass this guard exists for: git accepts the flag on either side
457
+ # of -m, and `git commit -m "<heredoc>" --cleanup=verbatim` is caught
458
+ # today only because the scan is command-wide. PREFIX + SUFFIX keeps both
459
+ # positions covered while excluding the one span that is message text.
460
+ # The two are joined with a space so a token cannot be forged across the
461
+ # seam out of a prefix tail and a suffix head.
462
+ # KNOWN LIMIT, deliberately fail-closed (#3816, round 6). This window is
463
+ # the whole command minus the message, so a `--cleanup=` that belongs to a
464
+ # DIFFERENT command — `git commit -m "<heredoc>" && echo --cleanup=verbatim`
465
+ # — also refuses, and a conforming commit git would accept stays blocked.
466
+ # Narrowing it to git's own segment was tried and reverted: deciding where
467
+ # git's command ends needs a shell parse, and a substring scan is not one.
468
+ # Trimming at the first `;&|` cut the window short whenever a separator sat
469
+ # inside an ordinary argument — `--author "a&b"`, and equally `--author
470
+ # a\&b` — which hid a REAL trailing `--cleanup=verbatim` and ACCEPTED a
471
+ # 75-byte subject the length gate had measured as 72. Two successive
472
+ # narrowings each reopened that hole on a shape the previous one missed, so
473
+ # the scan stays wide: refusing a commit git would take is recoverable,
474
+ # accepting an over-long subject is not.
475
+ # ABBREVIATIONS (independent review of #3816, round 6). git accepts any
476
+ # unambiguous prefix of a long option, so `--cle=verbatim` sets the mode
477
+ # while matching no literal `--cleanup` — measured accepting a 75-byte
478
+ # subject recorded as 72. The class is deliberately wider than git's own
479
+ # abbreviation set: over-matching only refuses more, which is the safe
480
+ # direction, and no other `--cl` option exists for git commit.
481
+ # LAST DIRECTIVE WINS, AND ONE MATCH CANNOT SEE IT (independent review of
482
+ # #3816, round 7). A bash regex yields ONE BASH_REMATCH, so only the
483
+ # FIRST cleanup directive was inspected — and git applies the LAST one.
484
+ # `--cleanup=whitespace -m <heredoc> --cleanup=verbatim` therefore read as
485
+ # mode=whitespace, resolution stayed enabled, and a 72-character subject
486
+ # plus trailing spaces was accepted while git recorded 75 bytes with the
487
+ # whitespace preserved (confirmed against the raw commit object). Deciding
488
+ # WHICH directive is last needs an argv order this substring scan does not
489
+ # have, so multiplicity itself refuses: more than one directive is
490
+ # unresolvable, not "probably fine". Single-directive behaviour is
491
+ # unchanged.
492
+ CLEANUP_WINDOW="$MSG_PREFIX_DEQ $MSG_SUFFIX_DEQ"
493
+ # `|| true` is load-bearing: this script runs under `set -euo pipefail`,
494
+ # and grep exits 1 when it matches NOTHING — which is the common case, a
495
+ # command with no cleanup directive at all. Without it the pipeline's
496
+ # non-zero status killed the hook outright (exit 1, no verdict) for every
497
+ # ordinary commit. Caught by running the real hook rather than the scan.
498
+ CLEANUP_HITS=$( { printf '%s' "$CLEANUP_WINDOW" | grep -oE '(--cl[a-z]*|commit\.cleanup)[=[:space:]]+[^[:space:]]+' || true; } | wc -l | tr -d ' ')
499
+ if [ "${CLEANUP_HITS:-0}" -gt 1 ]; then
500
+ RESOLVE=0
501
+ elif [[ "$CLEANUP_WINDOW" =~ (--cl[a-z]*|commit\.cleanup)[=[:space:]]+([^[:space:]]+) ]]; then
502
+ if [ "${BASH_REMATCH[2]}" != "whitespace" ]; then RESOLVE=0; fi
503
+ fi
504
+
505
+ # GIT-GENERATED SUBJECTS (independent review of #3816, round 7). With
506
+ # `--squash=<commit>` or `--fixup=<commit>` git composes the subject
507
+ # itself — measured recording `squash! base: something` while a conforming
508
+ # heredoc supplied via -m sailed through. The supplied message is not the
509
+ # subject in these modes at all, so there is nothing here worth measuring
510
+ # and resolution is refused outright. Abbreviations included for the same
511
+ # reason as --cleanup's. Deliberately NOT extended to the other
512
+ # message-SOURCE options (-C/--reuse-message, -c/--reedit-message,
513
+ # -F/--file, -t/--template): `-c` is also a git GLOBAL option that legally
514
+ # precedes the subcommand, so a scan for it would refuse ordinary
515
+ # `git -c k=v commit` invocations. Those remain a disclosed gap rather
516
+ # than a guessed guard.
517
+ if [[ "$MSG_PREFIX_DEQ $MSG_SUFFIX_DEQ" =~ (^|[[:space:]])--(squash|fixup|sq[a-z]*|fix[a-z]*)[=[:space:]] ]]; then RESOLVE=0; fi
518
+ fi
519
+
520
+ if [ "$RESOLVE" = 1 ]; then
521
+ SUBJECT=$(GIT_CMD_LIB="$HOOK_DIR/lib/git-cmd.js" MSG="$MSG" node -e "
522
+ const {resolveCommitSubject}=require(process.env.GIT_CMD_LIB);
523
+ process.stdout.write(resolveCommitSubject(process.env.MSG));
524
+ " 2>/dev/null) || SUBJECT=$(echo "$MSG" | head -1)
525
+ else
526
+ SUBJECT=$(echo "$MSG" | head -1)
527
+ fi
528
+ # Single source of truth for the accepted commit-type list (#3811): the
529
+ # 10 built-ins plus whatever passed the safe-token filter above. Both the
530
+ # regex alternation and the human-readable error text below are derived
531
+ # from this ONE array — no hand-synced second copy.
532
+ #
533
+ # The `"${EXTRA_COMMIT_TYPES[@]+"${EXTRA_COMMIT_TYPES[@]}"}"` form (not
534
+ # plain `"${EXTRA_COMMIT_TYPES[@]}"`) is required: on bash 3.2.57 (this
535
+ # repo's macOS test target), expanding `[@]` on an array that is declared
536
+ # but has zero elements throws "unbound variable" under `set -u` (which
537
+ # this script has via `set -euo pipefail`). Verified directly against
538
+ # /bin/bash 3.2.57 on macOS. The `${arr[@]+word}` form is the
539
+ # nounset-safe idiom for "expand if set, empty otherwise" on empty arrays.
540
+ COMMIT_TYPES=("${BUILTIN_COMMIT_TYPES[@]}" "${EXTRA_COMMIT_TYPES[@]+"${EXTRA_COMMIT_TYPES[@]}"}")
541
+ COMMIT_TYPE_ALT=$(IFS='|'; echo "${COMMIT_TYPES[*]}")
542
+ COMMIT_TYPE_LIST=$(printf '%s, ' "${COMMIT_TYPES[@]}")
543
+ COMMIT_TYPE_LIST="${COMMIT_TYPE_LIST%, }"
544
+ # Typed `valid_types` array (#3811 review finding): CONTRIBUTING.md bans
545
+ # substring/prose matching on `reason` in tests — a test needing to
546
+ # verify the accepted-type set must have a typed field, not grep prose.
547
+ # Safe to build with a bare printf (no JSON-escaping needed): every
548
+ # element of COMMIT_TYPES has already passed the `^[a-z][a-z0-9-]*$`
549
+ # safe-token filter (or is a literal built-in), so none can contain `"`
550
+ # or `\`.
551
+ COMMIT_TYPES_JSON=$(printf '"%s",' "${COMMIT_TYPES[@]}")
552
+ COMMIT_TYPES_JSON="[${COMMIT_TYPES_JSON%,}]"
43
553
  # Validate Conventional Commits format
44
- if ! [[ "$SUBJECT" =~ ^(feat|fix|docs|style|refactor|perf|test|build|ci|chore)(\(.+\))?:[[:space:]].+ ]]; then
45
- # Emit a typed `code` field alongside `reason` (#2974). Tests assert
46
- # on the stable code string; the reason is the human-readable copy.
47
- echo '{"decision": "block", "code": "CONVENTIONAL_COMMITS_VIOLATION", "reason": "Commit message must follow Conventional Commits: <type>(<scope>): <subject>. Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore. Subject must be <=72 chars, lowercase, imperative mood, no trailing period."}'
554
+ if ! [[ "$SUBJECT" =~ ^($COMMIT_TYPE_ALT)(\(.+\))?:[[:space:]].+ ]]; then
555
+ # Emit typed `code` and `valid_types` fields alongside `reason` (#2974,
556
+ # #3811). Tests assert on the stable code string and the typed array;
557
+ # the reason is the human-readable copy, never grepped by tests.
558
+ echo "{\"decision\": \"block\", \"code\": \"CONVENTIONAL_COMMITS_VIOLATION\", \"valid_types\": $COMMIT_TYPES_JSON, \"reason\": \"Commit message must follow Conventional Commits: <type>(<scope>): <subject>. Valid types: $COMMIT_TYPE_LIST. Subject must be <=72 chars, lowercase, imperative mood, no trailing period.\"}"
48
559
  exit 2
49
560
  fi
50
561
  if [ ${#SUBJECT} -gt 72 ]; then
@@ -47,6 +47,16 @@
47
47
 
48
48
  'use strict';
49
49
 
50
+ const { allow, deny } = require('./lib/hook-exit.js');
51
+
52
+ // #3911 (ADR-3889 Phase 7): the exit(2) call site (block(), below) is
53
+ // migrated to hook-exit.js's deny(undefined, reason) now that
54
+ // hooks/lib/cli-exit.js's terminateNow emits fd 1 and fd 2 (stderrPayload)
55
+ // in INDEPENDENT try/catch blocks — `payload=undefined` cleanly skips the
56
+ // fd 1 write (preserving "nothing written to stdout") instead of throwing
57
+ // into a shared catch that used to also swallow the fd 2 write, which is
58
+ // what silently dropped the deny reason before this fix.
59
+
50
60
  // No realistic destructive command comes anywhere close to this length.
51
61
  const MAX_COMMAND_LENGTH = 4096;
52
62
 
@@ -244,16 +254,11 @@ function destructiveReason(cmd) {
244
254
  }
245
255
 
246
256
  function block(reason) {
247
- process.stderr.write(`GSD windsurf pre_run_command guard: ${reason}\n`);
248
- process.exit(2);
249
- }
250
-
251
- function allow() {
252
- process.exit(0);
257
+ deny(undefined, `GSD windsurf pre_run_command guard: ${reason}\n`);
253
258
  }
254
259
 
255
260
  let input = '';
256
- const stdinTimeout = setTimeout(() => process.exit(0), 10000);
261
+ const stdinTimeout = setTimeout(() => allow(undefined), 10000);
257
262
  process.stdin.setEncoding('utf8');
258
263
  process.stdin.on('data', (chunk) => { input += chunk; });
259
264
  process.stdin.on('end', () => {
@@ -262,14 +267,14 @@ process.stdin.on('end', () => {
262
267
  const data = JSON.parse(input || '{}');
263
268
  const toolInfo = (data && typeof data.tool_info === 'object' && data.tool_info) || {};
264
269
  const commandLine = typeof toolInfo.command_line === 'string' ? toolInfo.command_line : '';
265
- if (!commandLine) { allow(); return; }
266
- if (commandLine.length > MAX_COMMAND_LENGTH) { allow(); return; }
270
+ if (!commandLine) { allow(undefined); return; }
271
+ if (commandLine.length > MAX_COMMAND_LENGTH) { allow(undefined); return; }
267
272
 
268
273
  const reason = destructiveReason(commandLine);
269
274
  if (reason) { block(reason); return; }
270
- allow();
275
+ allow(undefined);
271
276
  } catch {
272
277
  // Silent fail-open — never block a valid tool call due to a hook bug.
273
- allow();
278
+ allow(undefined);
274
279
  }
275
280
  });