@opengsd/gsd-core 1.11.0 → 1.12.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 (395) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-code-fixer.md +1 -1
  4. package/agents/gsd-debug-session-manager.md +1 -1
  5. package/agents/gsd-debugger.md +1 -1
  6. package/agents/gsd-dom-verifier.md +169 -0
  7. package/agents/gsd-eval-auditor.md +1 -1
  8. package/agents/gsd-executor.md +17 -9
  9. package/agents/gsd-framework-selector.md +1 -3
  10. package/agents/gsd-intel-updater.md +1 -1
  11. package/agents/gsd-mempalace-curator.md +0 -1
  12. package/agents/gsd-pattern-mapper.md +11 -0
  13. package/agents/gsd-phase-researcher.md +3 -1
  14. package/agents/gsd-plan-checker.md +15 -55
  15. package/agents/gsd-planner.md +6 -4
  16. package/agents/gsd-project-researcher.md +1 -1
  17. package/agents/gsd-research-synthesizer.md +2 -2
  18. package/agents/gsd-roadmapper.md +15 -11
  19. package/agents/gsd-ui-checker.md +63 -4
  20. package/agents/gsd-ui-researcher.md +41 -3
  21. package/agents/gsd-verifier.md +1 -1
  22. package/bin/install.js +609 -134
  23. package/commands/gsd/discuss-phase.md +1 -1
  24. package/commands/gsd/import.md +1 -1
  25. package/commands/gsd/quick.md +8 -4
  26. package/gsd-core/bin/gsd-tools.cjs +567 -51
  27. package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
  28. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  29. package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
  30. package/gsd-core/bin/lib/api-coverage.cjs +30 -9
  31. package/gsd-core/bin/lib/artifacts.cjs +2 -0
  32. package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
  33. package/gsd-core/bin/lib/audit.cjs +163 -41
  34. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  35. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  36. package/gsd-core/bin/lib/capability-registry.cjs +336 -95
  37. package/gsd-core/bin/lib/capability-state.cjs +18 -3
  38. package/gsd-core/bin/lib/capability-validator.cjs +205 -18
  39. package/gsd-core/bin/lib/check-command-router.cjs +145 -5
  40. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  41. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  42. package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
  43. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  44. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  45. package/gsd-core/bin/lib/commands.cjs +543 -44
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
  47. package/gsd-core/bin/lib/config-loader.cjs +118 -29
  48. package/gsd-core/bin/lib/config.cjs +92 -2
  49. package/gsd-core/bin/lib/configuration.cjs +129 -37
  50. package/gsd-core/bin/lib/core-utils.cjs +84 -7
  51. package/gsd-core/bin/lib/edge-probe.cjs +9 -1
  52. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  53. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  54. package/gsd-core/bin/lib/frontmatter.cjs +840 -305
  55. package/gsd-core/bin/lib/gap-checker.cjs +27 -3
  56. package/gsd-core/bin/lib/git-base-branch.cjs +174 -39
  57. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
  58. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +6 -3
  59. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
  60. package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
  61. package/gsd-core/bin/lib/host-integration.cjs +39 -6
  62. package/gsd-core/bin/lib/init-command-router.cjs +118 -21
  63. package/gsd-core/bin/lib/init.cjs +120 -41
  64. package/gsd-core/bin/lib/install-engine.cjs +68 -3
  65. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -1
  66. package/gsd-core/bin/lib/install-profiles.cjs +78 -4
  67. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  68. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  69. package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
  70. package/gsd-core/bin/lib/intel.cjs +101 -26
  71. package/gsd-core/bin/lib/io.cjs +160 -15
  72. package/gsd-core/bin/lib/learnings.cjs +85 -14
  73. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  74. package/gsd-core/bin/lib/markdown-table.cjs +52 -4
  75. package/gsd-core/bin/lib/milestone.cjs +90 -5
  76. package/gsd-core/bin/lib/model-catalog.cjs +177 -19
  77. package/gsd-core/bin/lib/model-resolver.cjs +10 -28
  78. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  79. package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
  80. package/gsd-core/bin/lib/phase-id.cjs +70 -4
  81. package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
  82. package/gsd-core/bin/lib/phase-locator.cjs +138 -17
  83. package/gsd-core/bin/lib/phase.cjs +405 -84
  84. package/gsd-core/bin/lib/plan-document.cjs +263 -0
  85. package/gsd-core/bin/lib/plan-scan.cjs +13 -2
  86. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  87. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  88. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -14
  89. package/gsd-core/bin/lib/planning-workspace.cjs +56 -0
  90. package/gsd-core/bin/lib/probe-core.cjs +4 -1
  91. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  92. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  93. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  94. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
  95. package/gsd-core/bin/lib/review-lane-descriptor.cjs +9 -9
  96. package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
  97. package/gsd-core/bin/lib/roadmap-parser.cjs +79 -16
  98. package/gsd-core/bin/lib/roadmap.cjs +74 -19
  99. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +96 -8
  100. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +34 -1
  101. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +287 -55
  102. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  103. package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
  104. package/gsd-core/bin/lib/shell-command-projection.cjs +71 -8
  105. package/gsd-core/bin/lib/smart-entry.cjs +12 -22
  106. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  107. package/gsd-core/bin/lib/state-command-router.cjs +47 -18
  108. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  109. package/gsd-core/bin/lib/state-document.cjs +186 -0
  110. package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
  111. package/gsd-core/bin/lib/state-transition.cjs +517 -101
  112. package/gsd-core/bin/lib/state.cjs +946 -163
  113. package/gsd-core/bin/lib/surface.cjs +10 -2
  114. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  115. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  116. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  117. package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
  118. package/gsd-core/bin/lib/uat.cjs +1376 -125
  119. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  120. package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
  121. package/gsd-core/bin/lib/unusable-input.cjs +13 -0
  122. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  123. package/gsd-core/bin/lib/vendor/README.md +43 -5
  124. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  125. package/gsd-core/bin/lib/verification.cjs +14 -1
  126. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  127. package/gsd-core/bin/lib/verify.cjs +95 -40
  128. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  129. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  130. package/gsd-core/bin/lib/worktree-safety.cjs +177 -21
  131. package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
  132. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  133. package/gsd-core/bin/shared/exit-codes.json +8 -0
  134. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  135. package/gsd-core/bin/shared/model-catalog.json +8 -1
  136. package/gsd-core/references/agent-contracts.md +3 -2
  137. package/gsd-core/references/api-coverage.md +24 -2
  138. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  139. package/gsd-core/references/checkpoints.md +37 -19
  140. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  141. package/gsd-core/references/edge-probe.md +8 -0
  142. package/gsd-core/references/execute-mvp-tdd.md +1 -3
  143. package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
  144. package/gsd-core/references/execute-phase-wave-guard.md +11 -9
  145. package/gsd-core/references/failing-direction.md +78 -0
  146. package/gsd-core/references/gate-prompts.md +1 -1
  147. package/gsd-core/references/git-integration.md +5 -5
  148. package/gsd-core/references/git-planning-commit.md +3 -3
  149. package/gsd-core/references/gsd-run-resolver.md +1 -1
  150. package/gsd-core/references/loop-hook-dispatch.md +22 -0
  151. package/gsd-core/references/model-profiles.md +1 -1
  152. package/gsd-core/references/nyquist-compliance.md +74 -0
  153. package/gsd-core/references/offer-next.md +3 -5
  154. package/gsd-core/references/phase-argument-parsing.md +3 -3
  155. package/gsd-core/references/planner-failing-direction.md +53 -0
  156. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  157. package/gsd-core/references/planner-revision.md +1 -1
  158. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  159. package/gsd-core/references/planning-config.md +37 -8
  160. package/gsd-core/references/reviewer-instances.md +31 -0
  161. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  162. package/gsd-core/references/tdd.md +1 -3
  163. package/gsd-core/references/ui-brand.md +65 -21
  164. package/gsd-core/references/ui-consideration-probe.md +1 -1
  165. package/gsd-core/references/universal-anti-patterns.md +2 -2
  166. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  167. package/gsd-core/references/verify-mvp-mode.md +1 -1
  168. package/gsd-core/references/workstream-flag.md +11 -11
  169. package/gsd-core/templates/README.md +1 -1
  170. package/gsd-core/templates/SECURITY.md +3 -3
  171. package/gsd-core/templates/UI-SPEC.md +25 -3
  172. package/gsd-core/templates/VALIDATION.md +3 -3
  173. package/gsd-core/templates/phase-prompt.md +3 -0
  174. package/gsd-core/templates/state.md +7 -0
  175. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  176. package/gsd-core/workflows/add-backlog.md +1 -1
  177. package/gsd-core/workflows/add-phase.md +3 -3
  178. package/gsd-core/workflows/add-tests.md +3 -8
  179. package/gsd-core/workflows/add-todo.md +1 -1
  180. package/gsd-core/workflows/ai-integration-phase.md +4 -9
  181. package/gsd-core/workflows/audit-fix.md +12 -3
  182. package/gsd-core/workflows/audit-milestone.md +9 -9
  183. package/gsd-core/workflows/audit-uat.md +17 -2
  184. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  185. package/gsd-core/workflows/autonomous.md +10 -26
  186. package/gsd-core/workflows/check-todos.md +1 -1
  187. package/gsd-core/workflows/cleanup.md +2 -2
  188. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
  189. package/gsd-core/workflows/code-review-fix.md +1 -1
  190. package/gsd-core/workflows/code-review.md +121 -40
  191. package/gsd-core/workflows/complete-milestone.md +15 -10
  192. package/gsd-core/workflows/debug.md +5 -3
  193. package/gsd-core/workflows/diagnose-issues.md +12 -6
  194. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  195. package/gsd-core/workflows/discuss-phase/modes/chain.md +3 -7
  196. package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
  197. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  198. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -2
  199. package/gsd-core/workflows/discuss-phase.md +1 -1
  200. package/gsd-core/workflows/do.md +3 -6
  201. package/gsd-core/workflows/docs-update.md +5 -4
  202. package/gsd-core/workflows/edit-phase.md +1 -1
  203. package/gsd-core/workflows/eval-review.md +4 -9
  204. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  205. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +113 -11
  206. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  207. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  208. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  209. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +22 -4
  210. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  211. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  212. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
  213. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  214. package/gsd-core/workflows/execute-phase.md +38 -54
  215. package/gsd-core/workflows/execute-plan.md +17 -12
  216. package/gsd-core/workflows/explore.md +1 -1
  217. package/gsd-core/workflows/extract-learnings.md +1 -1
  218. package/gsd-core/workflows/fast.md +2 -2
  219. package/gsd-core/workflows/forensics.md +1 -1
  220. package/gsd-core/workflows/graduation.md +5 -5
  221. package/gsd-core/workflows/health.md +3 -6
  222. package/gsd-core/workflows/import.md +14 -11
  223. package/gsd-core/workflows/inbox.md +4 -5
  224. package/gsd-core/workflows/ingest-docs.md +44 -11
  225. package/gsd-core/workflows/insert-phase.md +5 -5
  226. package/gsd-core/workflows/list-seeds.md +5 -3
  227. package/gsd-core/workflows/list-workspaces.md +1 -1
  228. package/gsd-core/workflows/manager.md +12 -23
  229. package/gsd-core/workflows/map-codebase.md +1 -1
  230. package/gsd-core/workflows/milestone-summary.md +1 -1
  231. package/gsd-core/workflows/mvp-phase.md +2 -2
  232. package/gsd-core/workflows/new-milestone.md +9 -21
  233. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  234. package/gsd-core/workflows/new-project.md +12 -26
  235. package/gsd-core/workflows/new-workspace.md +1 -1
  236. package/gsd-core/workflows/next.md +2 -2
  237. package/gsd-core/workflows/pause-work.md +1 -1
  238. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  239. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  240. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
  241. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
  242. package/gsd-core/workflows/plan-phase.md +121 -42
  243. package/gsd-core/workflows/plan-review-convergence.md +46 -9
  244. package/gsd-core/workflows/plant-seed.md +2 -2
  245. package/gsd-core/workflows/pr-branch.md +187 -51
  246. package/gsd-core/workflows/profile-user.md +16 -14
  247. package/gsd-core/workflows/progress.md +27 -12
  248. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  249. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -3
  250. package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
  251. package/gsd-core/workflows/quick/steps/research-phase.md +2 -4
  252. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  253. package/gsd-core/workflows/quick.md +20 -29
  254. package/gsd-core/workflows/remove-phase.md +4 -4
  255. package/gsd-core/workflows/remove-workspace.md +2 -2
  256. package/gsd-core/workflows/resume-project.md +8 -12
  257. package/gsd-core/workflows/review.md +193 -15
  258. package/gsd-core/workflows/scan.md +1 -1
  259. package/gsd-core/workflows/secure-phase.md +2 -2
  260. package/gsd-core/workflows/settings-advanced.md +7 -9
  261. package/gsd-core/workflows/settings-integrations.md +64 -31
  262. package/gsd-core/workflows/settings.md +3 -5
  263. package/gsd-core/workflows/ship.md +12 -6
  264. package/gsd-core/workflows/sketch-wrap-up.md +11 -17
  265. package/gsd-core/workflows/sketch.md +12 -18
  266. package/gsd-core/workflows/smart-entry.md +3 -5
  267. package/gsd-core/workflows/spec-phase.md +23 -1
  268. package/gsd-core/workflows/spike-wrap-up.md +7 -11
  269. package/gsd-core/workflows/spike.md +20 -31
  270. package/gsd-core/workflows/stats.md +2 -2
  271. package/gsd-core/workflows/sync-skills.md +1 -1
  272. package/gsd-core/workflows/thread.md +11 -7
  273. package/gsd-core/workflows/transition.md +5 -5
  274. package/gsd-core/workflows/ui-phase.md +10 -16
  275. package/gsd-core/workflows/ui-review.md +6 -10
  276. package/gsd-core/workflows/ultraplan-phase.md +5 -13
  277. package/gsd-core/workflows/undo.md +8 -16
  278. package/gsd-core/workflows/update.md +6 -10
  279. package/gsd-core/workflows/validate-phase.md +2 -2
  280. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  281. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  282. package/gsd-core/workflows/verify-work.md +57 -18
  283. package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
  284. package/hooks/dist/gsd-config-reload.js +18 -12
  285. package/hooks/dist/gsd-context-monitor.js +19 -10
  286. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  287. package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
  288. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  289. package/hooks/dist/gsd-cursor-stop.js +2 -1
  290. package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
  291. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
  292. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  293. package/hooks/dist/gsd-graphify-update.sh +22 -18
  294. package/hooks/dist/gsd-node-runner.sh +76 -0
  295. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  296. package/hooks/dist/gsd-prompt-guard.js +16 -7
  297. package/hooks/dist/gsd-read-guard.js +16 -7
  298. package/hooks/dist/gsd-read-injection-scanner.js +17 -8
  299. package/hooks/dist/gsd-session-state.sh +1 -0
  300. package/hooks/dist/gsd-statusline.js +215 -26
  301. package/hooks/dist/gsd-validate-commit.sh +80 -6
  302. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  303. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  304. package/hooks/dist/gsd-workflow-guard.js +34 -16
  305. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  306. package/hooks/dist/gsd-write-guard.js +35 -25
  307. package/hooks/dist/lib/cli-exit.js +560 -0
  308. package/hooks/dist/lib/exit-code-registry.js +98 -0
  309. package/hooks/dist/lib/git-probe.js +84 -0
  310. package/hooks/dist/lib/hook-exit.js +81 -0
  311. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  312. package/hooks/gsd-agent-isolation-guard.js +77 -38
  313. package/hooks/gsd-config-reload.js +18 -12
  314. package/hooks/gsd-context-monitor.js +19 -10
  315. package/hooks/gsd-cursor-post-tool.js +3 -1
  316. package/hooks/gsd-cursor-pre-tool.js +3 -1
  317. package/hooks/gsd-cursor-session-start.js +2 -1
  318. package/hooks/gsd-cursor-stop.js +2 -1
  319. package/hooks/gsd-cursor-subagent-start.js +28 -23
  320. package/hooks/gsd-cursor-subagent-stop.js +3 -1
  321. package/hooks/gsd-ensure-canonical-path.js +2 -1
  322. package/hooks/gsd-graphify-update.sh +22 -18
  323. package/hooks/gsd-node-runner.sh +76 -0
  324. package/hooks/gsd-phase-boundary.sh +1 -0
  325. package/hooks/gsd-prompt-guard.js +16 -7
  326. package/hooks/gsd-read-guard.js +16 -7
  327. package/hooks/gsd-read-injection-scanner.js +17 -8
  328. package/hooks/gsd-session-state.sh +1 -0
  329. package/hooks/gsd-statusline.js +215 -26
  330. package/hooks/gsd-validate-commit.sh +80 -6
  331. package/hooks/gsd-windsurf-pre-command.js +16 -11
  332. package/hooks/gsd-windsurf-pre-write.js +22 -13
  333. package/hooks/gsd-workflow-guard.js +34 -16
  334. package/hooks/gsd-worktree-path-guard.js +36 -21
  335. package/hooks/gsd-write-guard.js +35 -25
  336. package/hooks/lib/cli-exit.js +560 -0
  337. package/hooks/lib/exit-code-registry.js +98 -0
  338. package/hooks/lib/git-probe.js +84 -0
  339. package/hooks/lib/hook-exit.js +81 -0
  340. package/hooks/managed-hooks-registry.cjs +3 -0
  341. package/package.json +12 -7
  342. package/scripts/base64-scan.sh +74 -12
  343. package/scripts/build-hooks.js +5 -0
  344. package/scripts/check-glossary-refs.cjs +77 -15
  345. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  346. package/scripts/ci-check-job-near-cap.cjs +49 -0
  347. package/scripts/ci-pr-mergeability.cjs +262 -0
  348. package/scripts/ci-test-scope.cjs +45 -12
  349. package/scripts/ci-timeout-report.cjs +230 -0
  350. package/scripts/docs-guard-registry.cjs +396 -0
  351. package/scripts/gen-capability-registry.cjs +8 -6
  352. package/scripts/gen-exit-code-docs.cjs +318 -0
  353. package/scripts/gen-exit-code-registry.cjs +891 -0
  354. package/scripts/gen-features.cjs +836 -0
  355. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  356. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  357. package/scripts/gen-loop-host-contract.cjs +134 -1
  358. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  359. package/scripts/gen-state-md-docs.cjs +727 -0
  360. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  361. package/scripts/lib/ci-job-timing.cjs +72 -0
  362. package/scripts/lib/cli-exit.cjs +546 -44
  363. package/scripts/lib/drift-scan.cjs +32 -2
  364. package/scripts/lib/exit-code-registry.cjs +98 -0
  365. package/scripts/lib/ndjson-reporter.cjs +119 -0
  366. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  367. package/scripts/lint-docs-guard-registration.cjs +495 -0
  368. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
  369. package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
  370. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  371. package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
  372. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  373. package/scripts/lint-phase-enumeration-drift.cjs +21 -8
  374. package/scripts/lint-planning-prompt-drift.cjs +38 -1
  375. package/scripts/lint-removed-but-needed.cjs +184 -16
  376. package/scripts/lint-seam-enforcement.cjs +182 -0
  377. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  378. package/scripts/lint-source-test-name-collision.cjs +241 -0
  379. package/scripts/lint-state-write-path-drift.cjs +337 -432
  380. package/scripts/lint-test-file-count.allowlist.json +122 -4
  381. package/scripts/lint-test-file-count.cjs +25 -3
  382. package/scripts/lint-unreachable-guard-drift.cjs +51 -64
  383. package/scripts/lint-vendored-deps.cjs +208 -35
  384. package/scripts/mutation-matrix.cjs +599 -50
  385. package/scripts/prompt-injection-scan.sh +75 -14
  386. package/scripts/secret-scan.sh +75 -13
  387. package/scripts/select-docs-guards.cjs +56 -0
  388. package/scripts/sync-runtime-launcher.cjs +22 -3
  389. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  390. package/skills/gsd-import/SKILL.md +1 -1
  391. package/skills/gsd-quick/SKILL.md +8 -4
  392. package/vscode/package.json +1 -1
  393. package/bin/lib/ui-safety-gate.cjs +0 -109
  394. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  395. package/scripts/state-write-path-drift-baseline.json +0 -19
@@ -21,6 +21,18 @@ const fs = require('fs');
21
21
  const path = require('path');
22
22
  const { spawnSync } = require('child_process');
23
23
  const { tokenize, skipToSubcommand } = require('./lib/git-cmd.js');
24
+ const { HOOK_ON_CRASH, allow, deny, crash } = require('./lib/hook-exit.js');
25
+ const { reportIfUndetermined } = require('./lib/git-probe.js');
26
+
27
+ // This guard is almost entirely advisory (fail open — a broken advisory must
28
+ // never wedge every tool call), with ONE hard block (#3504 force-add-on-
29
+ // agent-branch) that fails CLOSED on internal error instead. That split is
30
+ // re-derived dynamically inside the outer catch (failClosedBlockContext), not
31
+ // a fixed per-hook policy, so ON_CRASH names only the FINAL, unconditional
32
+ // fallback reached when the fail-closed context does not apply — i.e. the
33
+ // historical exit(0). Declared ONCE here so that fallback states its policy
34
+ // explicitly rather than inheriting a default (#3911).
35
+ const ON_CRASH = HOOK_ON_CRASH.ALLOW;
24
36
 
25
37
  function forceGitAddCwds(command, defaultCwd) {
26
38
  const tokens = tokenize(command || '');
@@ -82,6 +94,12 @@ function currentBranch(cwd) {
82
94
  timeout: 2000,
83
95
  killSignal: 'SIGTERM',
84
96
  });
97
+ // #3911: a timeout/spawn-failure here previously degraded to '' exactly
98
+ // like a clean "not on a branch" answer — indistinguishable from the
99
+ // caller's point of view. reportIfUndetermined is a no-op on a genuine
100
+ // negative and only emits a diagnostic when the probe itself could not
101
+ // run; the '' fallback (and therefore this hook's exit code) is unchanged.
102
+ reportIfUndetermined('gsd-workflow-guard', 'git branch --show-current', result);
85
103
  if (result.status !== 0) return '';
86
104
  return result.stdout.trim();
87
105
  }
@@ -114,9 +132,10 @@ function emitForceAddBlock(origin) {
114
132
  origin: failClosed ? 'fail-closed' : 'force-add-detected',
115
133
  reason,
116
134
  };
117
- process.stdout.write(JSON.stringify(output));
118
- // Kimi CLI's exit-2 protocol feeds stderr back to the model (#2304)
119
- process.stderr.write(output.reason);
135
+ // Kimi CLI's exit-2 protocol feeds stderr back to the model (#2304) — deny's
136
+ // stderrPayload keeps fd 2 to the plain reason string, matching the
137
+ // pre-migration two-write byte-for-byte.
138
+ deny(output, output.reason);
120
139
  }
121
140
 
122
141
  // #3504 fail-closed context re-derivation. Runs INSIDE the outer catch, after
@@ -260,7 +279,7 @@ function normalizeKimiPayload(data) {
260
279
  }
261
280
 
262
281
  let input = '';
263
- const stdinTimeout = setTimeout(() => process.exit(0), 3000);
282
+ const stdinTimeout = setTimeout(() => allow(undefined), 3000);
264
283
  process.stdin.setEncoding('utf8');
265
284
  process.stdin.on('data', chunk => input += chunk);
266
285
  process.stdin.on('end', () => {
@@ -282,29 +301,28 @@ process.stdin.on('end', () => {
282
301
 
283
302
  if (toolName === 'Bash') {
284
303
  if (!isWorkflowGuardEnabled) {
285
- process.exit(0);
304
+ allow(undefined);
286
305
  }
287
306
  const command = data.tool_input?.command || '';
288
307
  for (const gitCwd of forceGitAddCwds(command, cwd)) {
289
308
  const branch = currentBranch(gitCwd);
290
309
  if (/^(worktree-)?agent-/.test(branch)) {
291
310
  emitForceAddBlock();
292
- process.exit(2);
293
311
  }
294
312
  }
295
- process.exit(0);
313
+ allow(undefined);
296
314
  }
297
315
 
298
316
  // Only guard Write, Edit, and MultiEdit tool calls
299
317
  if (!['Write', 'Edit', 'MultiEdit'].includes(toolName)) {
300
- process.exit(0);
318
+ allow(undefined);
301
319
  }
302
320
 
303
321
  // Check if we're inside a GSD workflow (Task subagent or /gsd- skill)
304
322
  // Subagents have a session_id that differs from the parent
305
323
  // and typically have a description field set by the orchestrator
306
324
  if (data.tool_input?.is_subagent || data.session_type === 'task') {
307
- process.exit(0);
325
+ allow(undefined);
308
326
  }
309
327
 
310
328
  // Check the file being edited
@@ -319,7 +337,7 @@ process.stdin.on('end', () => {
319
337
 
320
338
  // Allow edits to .planning/ files (GSD state management)
321
339
  if (filePath.includes('.planning/') || filePath.includes('.planning\\')) {
322
- process.exit(0);
340
+ allow(undefined);
323
341
  }
324
342
 
325
343
  // Allow edits to common config/docs files that don't need GSD tracking
@@ -332,11 +350,11 @@ process.stdin.on('end', () => {
332
350
  /settings\.json$/,
333
351
  ];
334
352
  if (allowedPatterns.some(p => p.test(filePath))) {
335
- process.exit(0);
353
+ allow(undefined);
336
354
  }
337
355
 
338
356
  if (!isWorkflowGuardEnabled) {
339
- process.exit(0); // Guard disabled (default) or no GSD project
357
+ allow(undefined); // Guard disabled (default) or no GSD project
340
358
  }
341
359
 
342
360
  // If we get here: GSD project, guard enabled, file edit outside .planning/,
@@ -357,13 +375,13 @@ process.stdin.on('end', () => {
357
375
  // #3504: split posture on internal error. The ONE hard block in this hook
358
376
  // (force-add on agent branches) fails CLOSED — if the blocking context can
359
377
  // be re-derived from the payload (Bash tool + guard enabled + determinably
360
- // an agent branch), exit 2 rather than silently allowing. Everything else
378
+ // an agent branch), deny rather than silently allowing. Everything else
361
379
  // keeps the historical fail-open posture: a broken advisory guard must
362
- // never wedge the session's tool calls.
380
+ // never wedge the session's tool calls. ON_CRASH is declared ALLOW at
381
+ // module top for exactly that unconditional fallback (#3911).
363
382
  if (failClosedBlockContext(input)) {
364
383
  emitForceAddBlock('fail-closed');
365
- process.exit(2);
366
384
  }
367
- process.exit(0);
385
+ crash(ON_CRASH, undefined);
368
386
  }
369
387
  });
@@ -17,6 +17,16 @@
17
17
  const fs = require('fs');
18
18
  const path = require('path');
19
19
  const { spawnSync } = require('child_process');
20
+ const { HOOK_ON_CRASH, allow, deny, crash } = require('./lib/hook-exit.js');
21
+ const { reportIfUndetermined } = require('./lib/git-probe.js');
22
+
23
+ // This guard's outer catch has always exited 0 (fail open): a path guard that
24
+ // cannot resolve the worktree must not block the user's edit — its whole job
25
+ // is a targeted containment check, not a general-purpose file-write blocker,
26
+ // and an unresolved worktree root gives it nothing to check against. Declared
27
+ // ONCE here so the outer catch's crash() call states its policy explicitly
28
+ // rather than inheriting a default (#3911).
29
+ const ON_CRASH = HOOK_ON_CRASH.ALLOW;
20
30
 
21
31
  const SPAWNOPT = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000, windowsHide: true };
22
32
 
@@ -132,7 +142,7 @@ function normalizeKimiPayload(data) {
132
142
  }
133
143
 
134
144
  let input = '';
135
- const stdinTimeout = setTimeout(() => process.exit(0), 3000);
145
+ const stdinTimeout = setTimeout(() => allow(undefined), 3000);
136
146
  process.stdin.setEncoding('utf8');
137
147
  process.stdin.on('data', chunk => input += chunk);
138
148
  process.stdin.on('end', () => {
@@ -143,7 +153,7 @@ process.stdin.on('end', () => {
143
153
 
144
154
  // Only guard Edit, Write, and MultiEdit tool calls
145
155
  if (toolName !== 'Edit' && toolName !== 'Write' && toolName !== 'MultiEdit') {
146
- process.exit(0);
156
+ allow(undefined);
147
157
  }
148
158
 
149
159
  const cwd = data.cwd || process.cwd();
@@ -154,15 +164,20 @@ process.stdin.on('end', () => {
154
164
  // In the main repo or a submodule it returns .git (or a path without /worktrees/).
155
165
  // This approach works even when cwd is a subdirectory of the worktree.
156
166
  const gitDirResult = git(['rev-parse', '--git-dir'], cwd);
167
+ // #3911: a timeout/spawn-failure result is indistinguishable from a clean
168
+ // "not a git repo" answer by status/stdout alone — reportIfUndetermined
169
+ // is a no-op on a genuine negative and only fires the diagnostic when the
170
+ // probe itself could not run. The allow() below is UNCHANGED either way.
171
+ reportIfUndetermined('gsd-worktree-path-guard', 'git rev-parse --git-dir', gitDirResult);
157
172
  if (gitDirResult.status !== 0 || !gitDirResult.stdout) {
158
- process.exit(0); // not a git repo — pass through
173
+ allow(undefined); // not a git repo — pass through
159
174
  }
160
175
 
161
176
  const gitDir = gitDirResult.stdout.trim();
162
177
  // A linked worktree's --git-dir contains .git/worktrees/ as a path component
163
178
  const isLinkedWorktree = /[/\\]\.git[/\\]worktrees[/\\]/.test(gitDir);
164
179
  if (!isLinkedWorktree) {
165
- process.exit(0); // main repo, submodule, or separate-git-dir — no-op
180
+ allow(undefined); // main repo, submodule, or separate-git-dir — no-op
166
181
  }
167
182
 
168
183
  // #1342: Only enforce inside a GSD-managed isolated executor worktree. Those
@@ -172,18 +187,20 @@ process.stdin.on('end', () => {
172
187
  // on the user's own branch, so the guard must be a no-op there. Detached HEAD
173
188
  // / error → not GSD-managed → no-op.
174
189
  const branchResult = git(['symbolic-ref', '--short', 'HEAD'], cwd);
190
+ reportIfUndetermined('gsd-worktree-path-guard', 'git symbolic-ref --short HEAD', branchResult);
175
191
  const branch = branchResult.status === 0 && branchResult.stdout ? branchResult.stdout.trim() : '';
176
192
  // #3021: accept worktree-wf_<runid>-<n> branches (Workflow backend's naming).
177
193
  if (!/^((worktree-)?agent-|worktree-wf_)[A-Za-z0-9._/-]+$/.test(branch)) {
178
- process.exit(0); // not a GSD-managed executor worktree — no-op
194
+ allow(undefined); // not a GSD-managed executor worktree — no-op
179
195
  }
180
196
 
181
197
  // Get the raw --show-toplevel output for the worktree (cwd).
182
198
  // We keep it raw (not path.resolve'd) to compare directly with the
183
199
  // file's toplevel — same git binary, same format, no normalization needed.
184
200
  const wtTopResult = git(['rev-parse', '--show-toplevel'], cwd);
201
+ reportIfUndetermined('gsd-worktree-path-guard', 'git rev-parse --show-toplevel (worktree cwd)', wtTopResult);
185
202
  if (wtTopResult.status !== 0 || !wtTopResult.stdout) {
186
- process.exit(0); // can't determine root — fail open
203
+ allow(undefined); // can't determine root — fail open
187
204
  }
188
205
  const wtTopRaw = wtTopResult.stdout.trim();
189
206
 
@@ -201,7 +218,7 @@ process.stdin.on('end', () => {
201
218
  ? data.tool_input.file_path
202
219
  : '';
203
220
  if (!rawFilePath) {
204
- process.exit(0);
221
+ allow(undefined);
205
222
  }
206
223
 
207
224
  // Relative paths resolve against the tool's CWD, which is inside the worktree
@@ -219,7 +236,7 @@ process.stdin.on('end', () => {
219
236
  // `../`-laden path exits 0 at this line and escapes the worktree. Stating a
220
237
  // mechanism and an unverified premise — not asserting a live bypass.
221
238
  if (!path.isAbsolute(rawFilePath)) {
222
- process.exit(0);
239
+ allow(undefined);
223
240
  }
224
241
 
225
242
  // Normalise .. traversal so /worktree/src/../../../main/file
@@ -244,7 +261,7 @@ process.stdin.on('end', () => {
244
261
  // Walked to root without finding any directory — path is synthetic.
245
262
  // A path with no existing ancestor is not the #260 main-repo vector;
246
263
  // #260 is caught by the different-git-root branch below. Fail open. (#1342)
247
- process.exit(0);
264
+ allow(undefined);
248
265
  }
249
266
 
250
267
  // Ask git for the toplevel of the file's location.
@@ -254,6 +271,7 @@ process.stdin.on('end', () => {
254
271
  // back-slash inconsistencies) — both values come from the same git binary
255
272
  // in the same format by definition.
256
273
  const fileTopResult = git(['rev-parse', '--show-toplevel'], checkDir);
274
+ reportIfUndetermined('gsd-worktree-path-guard', 'git rev-parse --show-toplevel (file location)', fileTopResult);
257
275
 
258
276
  if (fileTopResult.status !== 0 || !fileTopResult.stdout) {
259
277
  // The target's location is not a git work tree. Two sub-cases:
@@ -263,6 +281,7 @@ process.stdin.on('end', () => {
263
281
  // - Truly outside all git repositories (e.g. ~/.claude/plans/) → not the
264
282
  // main-repo vector → fail open. (#1342)
265
283
  const insideGitDir = git(['rev-parse', '--is-inside-git-dir'], checkDir);
284
+ reportIfUndetermined('gsd-worktree-path-guard', 'git rev-parse --is-inside-git-dir', insideGitDir);
266
285
  if (insideGitDir.status === 0 && insideGitDir.stdout && insideGitDir.stdout.trim() === 'true') {
267
286
  const output = {
268
287
  decision: 'block',
@@ -271,20 +290,17 @@ process.stdin.on('end', () => {
271
290
  `not the active worktree at '${wtTopRaw}'. Writing to repository internals via an ` +
272
291
  `absolute path is not permitted from an isolated executor worktree. Use a relative path.`,
273
292
  };
274
- process.stdout.write(JSON.stringify(output));
275
- // Kimi feeds stderr (not stdout) back to the model on exit 2.
276
- process.stderr.write(output.reason);
277
- process.exit(2);
293
+ deny(output, output.reason);
278
294
  }
279
295
  // Outside all git repositories — fail open (#1342).
280
- process.exit(0);
296
+ allow(undefined);
281
297
  }
282
298
 
283
299
  const fileTopRaw = fileTopResult.stdout.trim();
284
300
 
285
301
  // Same git toplevel → file is inside the worktree → allow
286
302
  if (fileTopRaw === wtTopRaw) {
287
- process.exit(0);
303
+ allow(undefined);
288
304
  }
289
305
 
290
306
  // BLOCK: file resolves to a different git root than the active worktree
@@ -299,12 +315,11 @@ process.stdin.on('end', () => {
299
315
  `(hook cwd: '${cwd}').`,
300
316
  };
301
317
 
302
- process.stdout.write(JSON.stringify(output));
303
- // Kimi feeds stderr (not stdout) back to the model on exit 2.
304
- process.stderr.write(output.reason);
305
- process.exit(2);
318
+ deny(output, output.reason);
306
319
  } catch {
307
- // Silent fail — never block valid tool calls due to hook errors
308
- process.exit(0);
320
+ // Silent fail — never block valid tool calls due to hook errors.
321
+ // ON_CRASH is declared ALLOW at module top: this preserves today's
322
+ // exit(0) fail-open behavior exactly (#3911).
323
+ crash(ON_CRASH, undefined);
309
324
  }
310
325
  });
@@ -75,6 +75,21 @@
75
75
 
76
76
  const fs = require('fs');
77
77
  const path = require('path');
78
+ const { HOOK_ON_CRASH, allow, deny, crash } = require('./lib/hook-exit.js');
79
+
80
+ // This guard's outer catch has always exited 0 (fail open — a hook error
81
+ // must never block a legitimate tool call; see the emitBlock/consumeSentinelFor
82
+ // header comments). Declared ONCE here so the outer catch's crash() call
83
+ // states its policy explicitly rather than inheriting a default (#3911).
84
+ const ON_CRASH = HOOK_ON_CRASH.ALLOW;
85
+
86
+ // #3911 (ADR-3889 Phase 7) NOTE: the exit(2) call site (emitBlock, below) is
87
+ // migrated to hooks/lib/hook-exit.js's deny(), using its `stderrPayload`
88
+ // param (added for exactly this site): fd 1 still gets the full JSON
89
+ // `output`, fd 2 gets ONLY the plain-text `output.reason` string — because
90
+ // Kimi's native hook bus reads stderr verbatim back to the model, and a raw
91
+ // JSON-stringified object on stderr is not the same reason text a model
92
+ // should read. Byte-identical to the pre-migration emitBlock.
78
93
 
79
94
  // Block when the pending payload has fewer than this fraction of the on-disk
80
95
  // line count (0.4 → a Write shrinking a file below 40% of its current size).
@@ -158,20 +173,13 @@ function consumeSentinelFor(filePath, normalized) {
158
173
 
159
174
  // m2 (round 5): the block emission must itself be exception-safe. An EPIPE
160
175
  // from writeSync inside the outer try would land in the fail-OPEN catch —
161
- // the one outcome the fail-closed branches exist to prevent. The decision
162
- // stands regardless of whether the payload could be delivered.
176
+ // the one outcome the fail-closed branches exist to prevent. terminateNow
177
+ // (via deny()) already guarantees this: a failed write never changes the
178
+ // exit code and never throws out of the call. `output.reason` is passed as
179
+ // the distinct stderrPayload so fd 2 gets the plain reason string — not the
180
+ // full JSON `output` fd 1 gets — matching the pre-migration byte-for-byte.
163
181
  function emitBlock(output) {
164
- try {
165
- // writeSync: pipe writes via process.stdout/stderr are async on Windows
166
- // and process.exit() does not flush them — a truncated block payload is
167
- // a guard that silently half-fired.
168
- fs.writeSync(1, JSON.stringify(output));
169
- // Kimi feeds stderr (not stdout) back to the model on exit 2.
170
- fs.writeSync(2, output.reason);
171
- } catch {
172
- // Emission failed; the block still stands.
173
- }
174
- process.exit(2);
182
+ deny(output, output.reason);
175
183
  }
176
184
 
177
185
  // #2304: Kimi's native hook bus delivers Kimi's tool vocabulary in the payload
@@ -223,7 +231,7 @@ function normalizeKimiPayload(data) {
223
231
  }
224
232
 
225
233
  let input = '';
226
- const stdinTimeout = setTimeout(() => process.exit(0), 3000);
234
+ const stdinTimeout = setTimeout(() => allow(undefined), 3000);
227
235
  process.stdin.setEncoding('utf8');
228
236
  process.stdin.on('data', chunk => input += chunk);
229
237
  process.stdin.on('end', () => {
@@ -234,17 +242,17 @@ process.stdin.on('end', () => {
234
242
  // A null/primitive payload has nothing to guard — exit deliberately
235
243
  // rather than throwing into the fail-open catch below (#2595 class).
236
244
  if (data === null || typeof data !== 'object') {
237
- process.exit(0);
245
+ allow(undefined);
238
246
  }
239
247
 
240
248
  // Only whole-file Write is catastrophic-by-construction; Edit/MultiEdit
241
249
  // replace bounded spans and are out of scope by design (#2255).
242
250
  if (data.tool_name !== 'Write') {
243
- process.exit(0);
251
+ allow(undefined);
244
252
  }
245
253
 
246
254
  if (isOverrideSet()) {
247
- process.exit(0); // documented escape hatch — legitimate reset in progress
255
+ allow(undefined); // documented escape hatch — legitimate reset in progress
248
256
  }
249
257
 
250
258
  // Typed read (#2547 class): `[]`/`{}` are truthy, pass a `!value`
@@ -254,7 +262,7 @@ process.stdin.on('end', () => {
254
262
  const rawFilePath = typeof rawInput?.file_path === 'string' ? rawInput.file_path : '';
255
263
  const content = rawInput?.content;
256
264
  if (!rawFilePath || typeof content !== 'string') {
257
- process.exit(0);
265
+ allow(undefined);
258
266
  }
259
267
 
260
268
  // Resolve relative paths against the session cwd (the same base the
@@ -272,7 +280,7 @@ process.stdin.on('end', () => {
272
280
  const normalized = filePath.replace(/\\/g, '/');
273
281
 
274
282
  if (!CURATED_PATTERNS.some(re => re.test(normalized))) {
275
- process.exit(0); // not a curated planning artifact
283
+ allow(undefined); // not a curated planning artifact
276
284
  }
277
285
 
278
286
  // Only guard overwrites — creating a curated file fresh is fine.
@@ -285,7 +293,7 @@ process.stdin.on('end', () => {
285
293
  onDisk = fs.readFileSync(filePath, 'utf8');
286
294
  } catch (err) {
287
295
  if (err && err.code === 'ENOENT') {
288
- process.exit(0); // does not exist — new-file Write, nothing to clobber
296
+ allow(undefined); // does not exist — new-file Write, nothing to clobber
289
297
  }
290
298
  emitBlock({
291
299
  decision: 'block',
@@ -306,17 +314,17 @@ process.stdin.on('end', () => {
306
314
  const newLines = countLines(content);
307
315
 
308
316
  if (oldLines < FLOOR_LINES) {
309
- process.exit(0); // sub-floor stub — ratio checks are meaningless here
317
+ allow(undefined); // sub-floor stub — ratio checks are meaningless here
310
318
  }
311
319
 
312
320
  if (newLines >= oldLines * SHRINK_RATIO) {
313
- process.exit(0); // shrink (if any) is within tolerance
321
+ allow(undefined); // shrink (if any) is within tolerance
314
322
  }
315
323
 
316
324
  // The mechanical hatch for workflow steps (see header): consulted only
317
325
  // here, at the block point, so a within-tolerance write never burns it.
318
326
  if (consumeSentinelFor(filePath, normalized)) {
319
- process.exit(0); // armed for exactly this file, fresh, now consumed
327
+ allow(undefined); // armed for exactly this file, fresh, now consumed
320
328
  }
321
329
 
322
330
  const pct = Math.round((newLines / oldLines) * 100);
@@ -353,7 +361,9 @@ process.stdin.on('end', () => {
353
361
  `to bypass this guard once.`,
354
362
  });
355
363
  } catch {
356
- // Silent fail — never block valid tool calls due to hook errors
357
- process.exit(0);
364
+ // Silent fail — never block valid tool calls due to hook errors.
365
+ // ON_CRASH is declared ALLOW at module top: this preserves today's
366
+ // exit(0) fail-open behavior exactly (#3911).
367
+ crash(ON_CRASH, undefined);
358
368
  }
359
369
  });