@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
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+ // hooks/lib/git-probe.js — classify a bounded spawnSync(git, ...) result,
3
+ // distinguishing a GENUINE negative answer (git ran and said "no") from
4
+ // "could not determine" (timeout / spawn failure / signal kill) — #3911.
5
+ //
6
+ // Proven defect: hooks/gsd-worktree-path-guard.js, hooks/gsd-workflow-guard.js,
7
+ // and hooks/gsd-windsurf-pre-write.js each treat spawnSync(git, ..., {timeout})
8
+ // returning a non-zero/empty result as "not applicable" and allow silently.
9
+ // A macOS CI run showed three deny cases land at 2084ms/2112ms/2177ms — just
10
+ // past the 2000ms budget — each returning exit 0 with EMPTY stdout AND EMPTY
11
+ // stderr: a timeout is INDISTINGUISHABLE, at the call site, from git cleanly
12
+ // answering "not a repo" / "not this worktree" unless the spawnSync() result
13
+ // itself is inspected for `error`/`signal`, not just `status`/`stdout`.
14
+ //
15
+ // This module does not change any hook's decision or exit code — it only
16
+ // answers "was this probe's answer real?" so a caller can emit a diagnostic
17
+ // on the undetermined branch while keeping the existing fail-open behavior.
18
+ //
19
+ // Classification mirrors tests/helpers/process-seam.cjs's OUTCOME vocabulary
20
+ // (EXITED vs TIMED_OUT/SPAWN_FAILED/KILLED) — same evidence-over-classification
21
+ // ordering (a populated `status` outranks an attached `error`, since Node can
22
+ // report `error.code === 'ETIMEDOUT'` on a result that also carried a real
23
+ // exit that beat the timer) — so a probe here and a subprocess outcome in the
24
+ // test suite never disagree on the same raw spawnSync() shape.
25
+
26
+ /**
27
+ * @param {{error?: (Error & {code?: string})|null, status: number|null, signal: string|null}} result
28
+ * the raw return value of `spawnSync('git', ...)`.
29
+ * @returns {{determined: true} | {determined: false, reason: string}}
30
+ */
31
+ function classifyGitProbe(result) {
32
+ const error = result && result.error;
33
+ const status = result ? result.status : null;
34
+ const signal = result ? result.signal : null;
35
+
36
+ if (!error && signal === null) {
37
+ // A real exit — status 0 or non-zero is a genuine, trustworthy answer.
38
+ return { determined: true };
39
+ }
40
+ if (status !== null) {
41
+ // `status` is only ever populated by a real exit; it outranks an attached
42
+ // `error` even at the exact timeout boundary (process-seam.cjs's own
43
+ // "evidence over classification" rule).
44
+ return { determined: true };
45
+ }
46
+
47
+ const code = error ? (error.code ?? null) : null;
48
+ let reason;
49
+ if (code === 'ETIMEDOUT' || signal !== null) {
50
+ reason = signal ? `timed out (signal ${signal})` : 'timed out';
51
+ } else if (code) {
52
+ reason = `spawn failed (${code})`;
53
+ } else {
54
+ reason = 'could not run (unknown reason)';
55
+ }
56
+ return { determined: false, reason };
57
+ }
58
+
59
+ /**
60
+ * If `result` could not be determined (timeout / spawn failure / signal
61
+ * kill), write a stderr diagnostic naming the probe and why — never changes
62
+ * any exit code, never throws. A no-op when the probe genuinely ran and
63
+ * answered (status 0 or non-zero with no error/signal).
64
+ *
65
+ * @param {string} hookName - e.g. 'gsd-worktree-path-guard'
66
+ * @param {string} probeLabel - e.g. "git rev-parse --show-toplevel"
67
+ * @param {*} result - the raw spawnSync() return value
68
+ */
69
+ function reportIfUndetermined(hookName, probeLabel, result) {
70
+ const classification = classifyGitProbe(result);
71
+ if (classification.determined) return;
72
+ try {
73
+ process.stderr.write(
74
+ `${hookName}: git probe '${probeLabel}' ${classification.reason} — ` +
75
+ `allowing this call because the probe's answer is unknown, not because ` +
76
+ `git answered "no" (#3911).\n`
77
+ );
78
+ } catch {
79
+ // A stderr write failing (EPIPE, closed fd) must never itself change the
80
+ // hook's fail-open outcome.
81
+ }
82
+ }
83
+
84
+ module.exports = { classifyGitProbe, reportIfUndetermined };
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+ // hooks/lib/hook-exit.js — hand-written, NOT generated. A tiny hook-facing
3
+ // vocabulary layered over hooks/lib/cli-exit.js's terminateNow (ADR-3889 §3),
4
+ // for the 19 enforcement hooks P7/#3911 is migrating off raw process.exit().
5
+ //
6
+ // WHY onCrash is a REQUIRED argument, with no default (this is the entire
7
+ // point of this module): every hook audited for #3911 ended its outer
8
+ // try/catch in ONE of two ways — some `process.exit(0)` (fail open: a hook
9
+ // error must never block a legitimate tool call), others `process.exit(2)`
10
+ // (fail closed: a hook whose whole job is a security/safety denial must not
11
+ // let its own bug wave the denial through). Both are legitimate policies for
12
+ // DIFFERENT hooks — but neither is the "obviously correct" one, and a shared
13
+ // helper that silently defaulted to either would let a future hook inherit
14
+ // the wrong policy by omission, which is exactly the "nothing fails with
15
+ // success" defect ADR-3889 exists to close. Requiring the caller to name its
16
+ // policy at every call site — not just once at module load — makes "I forgot
17
+ // to decide" a load-time/call-time crash instead of a silent behavior. A
18
+ // caller that truly wants "allow on crash" must still write ALLOW; there is
19
+ // no path that reaches terminateNow without a declared choice.
20
+ //
21
+ // crash() itself is total: it does not throw on a bad `onCrash`, because
22
+ // throwing would unwind into the CALLER's own outer catch — the exact
23
+ // fail-open-by-accident hazard this module removes. Instead an unrecognized
24
+ // policy terminates via terminateNow('INTERNAL', ...) with a diagnostic
25
+ // payload naming the offending value, so a typo'd/misspelled policy is
26
+ // debuggable on stderr rather than silently reinterpreted as ALLOW or DENY.
27
+ const { terminateNow } = require('./cli-exit.js');
28
+
29
+ /** Frozen enum of the two declarable crash policies. No third option. */
30
+ const HOOK_ON_CRASH = Object.freeze({
31
+ ALLOW: 'allow',
32
+ DENY: 'deny',
33
+ });
34
+
35
+ /** Terminate with the hook-protocol PASS outcome (exit 0). */
36
+ function allow(payload) {
37
+ terminateNow('PASS', payload);
38
+ }
39
+
40
+ /**
41
+ * Terminate with the hook-protocol deny outcome (exit 2, stdout+stderr).
42
+ *
43
+ * @param {*} payload - JSON-serializable value written to fd 1 (and, when
44
+ * `stderrPayload` is omitted, fd 2 too).
45
+ * @param {*} [stderrPayload] - optional distinct value for fd 2, forwarded
46
+ * verbatim to terminateNow's own `stderrPayload` param — a string is
47
+ * written raw, anything else is JSON-stringified.
48
+ */
49
+ function deny(payload, stderrPayload) {
50
+ terminateNow('HOOK_DENY', payload, stderrPayload);
51
+ }
52
+
53
+ /**
54
+ * Dispatch to allow()/deny() per a DECLARED crash policy. `onCrash` is
55
+ * required — see the module header for why there is no default.
56
+ *
57
+ * @param {'allow'|'deny'} onCrash - one of HOOK_ON_CRASH's two values.
58
+ * @param {*} payload - JSON-serializable value forwarded to terminateNow.
59
+ */
60
+ function crash(onCrash, payload) {
61
+ if (onCrash === HOOK_ON_CRASH.ALLOW) {
62
+ allow(payload);
63
+ return;
64
+ }
65
+ if (onCrash === HOOK_ON_CRASH.DENY) {
66
+ deny(payload);
67
+ return;
68
+ }
69
+ // Never guess a policy and never throw (a throw would unwind into the
70
+ // caller's own outer catch — the fail-open-by-accident defect this module
71
+ // exists to remove). Terminate deterministically with INTERNAL, naming the
72
+ // offending value so a misspelled/omitted policy is diagnosable.
73
+ terminateNow('INTERNAL', {
74
+ error: 'hook-exit: crash() called with an unrecognized onCrash policy',
75
+ receivedOnCrash: onCrash,
76
+ expectedOneOf: [HOOK_ON_CRASH.ALLOW, HOOK_ON_CRASH.DENY],
77
+ originalPayload: payload,
78
+ });
79
+ }
80
+
81
+ module.exports = { HOOK_ON_CRASH, allow, deny, crash };
@@ -29,6 +29,9 @@ const MANAGED_HOOKS = [
29
29
  'gsd-cursor-subagent-stop.js',
30
30
  'gsd-ensure-canonical-path.js',
31
31
  'gsd-graphify-update.sh',
32
+ // #3662: portable node resolver (helper staged in hooks/; managed JS hook
33
+ // commands route through it under --portable-hooks).
34
+ 'gsd-node-runner.sh',
32
35
  'gsd-phase-boundary.sh',
33
36
  'gsd-prompt-guard.js',
34
37
  'gsd-read-guard.js',
@@ -65,6 +65,23 @@ const path = require('path');
65
65
  const os = require('os');
66
66
  const { readSentinel, VALID_ISOLATION, extractDispatchIdentifiers, sentinelAppliesToDispatch } = require('./lib/isolation-sentinel.js');
67
67
  const { REASON_CODE } = require('./lib/isolation-deny-reason.js');
68
+ const { HOOK_ON_CRASH, allow, deny, crash } = require('./lib/hook-exit.js');
69
+
70
+ // Required at module top, alongside the other ./lib requires — NOT behind
71
+ // ensureRuntimeBuild() below. Terminating on a parse/timeout failure must
72
+ // never depend on the gitignored build artifacts this hook self-heals for
73
+ // its own registry lookups (#3911).
74
+ //
75
+ // This guard's outer catch (main(), below) has always exited 0 (fail open):
76
+ // that outer catch only covers payload PARSING failing before applicability
77
+ // could even be determined (malformed stdin JSON, etc.) — the guard's real
78
+ // fail-closed logic (a GSD project whose dispatch-isolation configuration
79
+ // cannot be verified) is handled separately, inside evaluateDispatch/
80
+ // resolveIsolationState, and already returns a 'block' decision through the
81
+ // normal exit-2 path rather than through this catch. So an unparseable
82
+ // payload has nothing to enforce; allowing it preserves today's behavior
83
+ // exactly.
84
+ const ON_CRASH = HOOK_ON_CRASH.ALLOW;
68
85
  // #3582: gsd-core/bin/lib/*.cjs (runtime-name-policy.cjs, capability-registry.cjs
69
86
  // below) are tsc build artifacts (ADR-457), gitignored and absent on a raw
70
87
  // plugin-marketplace / git-clone install that never ran `npm run build:lib`.
@@ -95,40 +112,40 @@ function parseHarnessFlag(flag) {
95
112
  return { param: m[1], value: m[2] };
96
113
  }
97
114
 
98
- // ─── #3566: per-install runtime marker ────────────────────────────────────────
115
+ // ─── #3897 rung 2: per-install runtime marker, single canonical owner ────────
99
116
  // bin/install.js writes `<install>/gsd-core/.gsd-runtime` for EVERY runtime
100
117
  // install (#2297), co-located with VERSION. Unlike `~/.gsd/defaults.json` —
101
118
  // which is host-wide and names whichever runtime's install ran LAST, the exact
102
119
  // leakage #2840's config.cjs change exists to prevent — the marker describes
103
120
  // THIS install, which is the property runtime identity needs on a machine
104
- // with 2+ runtimes. Mirrors readInstallRuntimeMarker in src/model-resolver.cts
105
- // (same cache + test-seam shape); this hook cannot import that module without
106
- // dragging the whole model-resolution stack into a PreToolUse hot path, so the
107
- // 5-line read lives here against the same sibling-layout assumption the hook's
108
- // own require('../gsd-core/bin/lib/…') already makes. Epic #3473 B3 owns
109
- // consolidating every marker reader into one shared seam.
110
- let _installMarkerCache; // undefined = unread; null = known absent; string = value
111
-
121
+ // with 2+ runtimes. Previously this hook held its own private reader/cache
122
+ // (one of four #3897 found); it now delegates to the single canonical owner,
123
+ // `src/runtime-slash.cts` (compiled to gsd-core/bin/lib/runtime-slash.cjs),
124
+ // reached through `ensureRuntimeBuild()` like every other compiled-lib require
125
+ // in this file (`scripts/lint-hooks-runtime-build-seam.cjs`).
112
126
  function readInstallRuntimeMarker() {
113
- if (_installMarkerCache !== undefined) return _installMarkerCache;
114
127
  try {
115
- const markerPath = path.join(__dirname, '..', 'gsd-core', '.gsd-runtime');
116
- const raw = fs.readFileSync(markerPath, 'utf-8').trim();
117
- _installMarkerCache = raw || null;
128
+ ensureRuntimeBuild();
129
+ const runtimeSlash = require('../gsd-core/bin/lib/runtime-slash.cjs');
130
+ return runtimeSlash.readInstallRuntimeMarker();
118
131
  } catch {
119
- // No marker: dev/source tree, or an install predating #2297 — "no signal
120
- // from this rung", never a resolution failure. Falls through to the
121
- // defaults rung below.
122
- _installMarkerCache = null;
132
+ // Unbuilt runtime library, or any other failure reaching the canonical
133
+ // owner — "no signal from this rung" (N4), never a resolution failure.
134
+ return null;
123
135
  }
124
- return _installMarkerCache;
125
136
  }
126
137
 
127
- // Test seam for the marker rung (the dev/source tree has no marker file, so
128
- // the read always bottoms out at null there — same seam contract as
129
- // model-resolver.cts's _setInstallRuntimeMarkerForTests, #2297).
138
+ // Test seam for the marker rung — forwards to the canonical owner's seam so
139
+ // this hook and runtime-slash.cjs always share one cache (#3897 rung 2).
130
140
  function _setInstallRuntimeMarkerForTests(value) {
131
- _installMarkerCache = value;
141
+ try {
142
+ ensureRuntimeBuild();
143
+ const runtimeSlash = require('../gsd-core/bin/lib/runtime-slash.cjs');
144
+ runtimeSlash._setInstallRuntimeMarkerForTests(value);
145
+ } catch {
146
+ // Test-only seam; an unbuilt library here means the test itself will fail
147
+ // downstream, which is a louder and more actionable signal than throwing here.
148
+ }
132
149
  }
133
150
 
134
151
  /**
@@ -282,18 +299,40 @@ function resolveRegistryIsolation(cwd, configPath) {
282
299
 
283
300
  if (isolation === 'harness-worktree') {
284
301
  let useWorktrees = true;
302
+ // #3972: the opt-out read shares the ONE owner every other
303
+ // isolation-deciding surface uses — planning-workspace's
304
+ // worktreesOptedOut ladder (scoped own-key wins; root inherited under
305
+ // the GSD_WORKSTREAM gate; strict === false). A flat single-file read
306
+ // here made a workstream-LOCAL opt-out invisible, so the sentinel-absent
307
+ // fallback denied a sequential dispatch the config explicitly allowed.
308
+ // Reached through ensureRuntimeBuild() like every other compiled-lib
309
+ // require in this file (scripts/lint-hooks-runtime-build-seam.cjs); if
310
+ // the library is unreachable the flat-root read below remains as the
311
+ // degraded fallback — today's behavior, never worse.
312
+ let ladderAnswered = false;
285
313
  try {
286
- const raw = fs.readFileSync(configPath, 'utf-8');
287
- const parsedCfg = JSON.parse(raw);
288
- if (parsedCfg && typeof parsedCfg === 'object' && parsedCfg.workflow &&
289
- typeof parsedCfg.workflow === 'object' && parsedCfg.workflow.use_worktrees === false) {
290
- useWorktrees = false;
291
- }
314
+ ensureRuntimeBuild();
315
+ const { worktreesOptedOut } = require('../gsd-core/bin/lib/planning-workspace.cjs');
316
+ useWorktrees = !worktreesOptedOut(cwd);
317
+ ladderAnswered = true;
292
318
  } catch {
293
- // Unreadable config already propagated to the outer caller's catch
294
- // before this point in practice (resolveRuntimeIdentity reads it
295
- // first); tolerate defensively and keep the conservative (enforce)
296
- // default rather than silently disabling the guard.
319
+ // Unbuilt runtime library or a ladder failure — fall through to the
320
+ // legacy flat-root read (conservative: keep enforcing).
321
+ }
322
+ if (!ladderAnswered) {
323
+ try {
324
+ const raw = fs.readFileSync(configPath, 'utf-8');
325
+ const parsedCfg = JSON.parse(raw);
326
+ if (parsedCfg && typeof parsedCfg === 'object' && parsedCfg.workflow &&
327
+ typeof parsedCfg.workflow === 'object' && parsedCfg.workflow.use_worktrees === false) {
328
+ useWorktrees = false;
329
+ }
330
+ } catch {
331
+ // Unreadable config already propagated to the outer caller's catch
332
+ // before this point in practice (resolveRuntimeIdentity reads it
333
+ // first); tolerate defensively and keep the conservative (enforce)
334
+ // default rather than silently disabling the guard.
335
+ }
297
336
  }
298
337
  if (!useWorktrees) isolation = 'none';
299
338
  }
@@ -476,7 +515,7 @@ function evaluateDispatch(data, { clock = Date } = {}) {
476
515
  /* istanbul ignore next -- stdin adapter, exercised via spawnSync in tests */
477
516
  function main() {
478
517
  let input = '';
479
- const stdinTimeout = setTimeout(() => process.exit(0), 3000);
518
+ const stdinTimeout = setTimeout(() => allow(undefined), 3000);
480
519
  process.stdin.setEncoding('utf8');
481
520
  process.stdin.on('data', (chunk) => { input += chunk; });
482
521
  process.stdin.on('end', () => {
@@ -486,18 +525,18 @@ function main() {
486
525
  const decision = evaluateDispatch(data);
487
526
  if (decision.action === 'block') {
488
527
  const out = { decision: 'block', reason: decision.reason, reason_code: decision.reasonCode };
489
- process.stdout.write(JSON.stringify(out));
490
528
  // Kimi feeds stderr (not stdout) back to the model on exit 2.
491
- process.stderr.write(decision.reason);
492
- process.exit(2);
529
+ deny(out, decision.reason);
493
530
  }
494
- process.exit(0);
531
+ allow(undefined);
495
532
  } catch {
496
533
  // Silent fail — never block valid tool calls due to hook errors
497
534
  // (malformed payload, etc.). This is distinct from resolveIsolationState's
498
535
  // internal error handling, which DOES deny — this outer catch only
499
536
  // covers payload parsing before applicability could even be determined.
500
- process.exit(0);
537
+ // ON_CRASH is declared ALLOW at module top: this preserves today's
538
+ // exit(0) fail-open behavior exactly (#3911).
539
+ crash(ON_CRASH, undefined);
501
540
  }
502
541
  });
503
542
  }
@@ -20,11 +20,17 @@
20
20
 
21
21
  const fs = require('fs');
22
22
  const path = require('path');
23
+ const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
24
+
25
+ // This hook only injects an advisory config-reload summary; a crash mid-parse
26
+ // must not block the session or the FileChanged event that triggered it — the
27
+ // agent simply keeps using the config context it already had (#3911).
28
+ const ON_CRASH = HOOK_ON_CRASH.ALLOW;
23
29
 
24
30
  let input = '';
25
31
  // Timeout guard: if stdin does not close within 8s exit silently rather than
26
32
  // hanging until Claude Code kills the process and reports "hook error".
27
- const stdinTimeout = setTimeout(() => process.exit(0), 8000);
33
+ const stdinTimeout = setTimeout(() => allow(undefined), 8000);
28
34
  process.stdin.setEncoding('utf8');
29
35
  process.stdin.on('data', chunk => (input += chunk));
30
36
  process.stdin.on('end', () => {
@@ -42,24 +48,23 @@ process.stdin.on('end', () => {
42
48
  // inject spurious additionalContext.
43
49
  const basename = path.basename(filePath);
44
50
  if (basename !== 'config.json') {
45
- process.exit(0);
51
+ allow(undefined);
46
52
  }
47
53
  const expectedPath = path.resolve(cwd, '.planning', 'config.json');
48
54
  if (path.resolve(filePath) !== expectedPath) {
49
- process.exit(0);
55
+ allow(undefined);
50
56
  }
51
57
 
52
58
  // On unlink (deletion) emit a brief notice and exit
53
59
  if (event === 'unlink') {
54
- process.stdout.write(JSON.stringify({
60
+ allow({
55
61
  hookSpecificOutput: {
56
62
  hookEventName: 'FileChanged',
57
63
  additionalContext:
58
64
  'GSD config (.planning/config.json) was deleted. ' +
59
65
  'Falling back to built-in defaults for this session.',
60
66
  },
61
- }));
62
- process.exit(0);
67
+ });
63
68
  }
64
69
 
65
70
  // Read the updated config file
@@ -68,17 +73,16 @@ process.stdin.on('end', () => {
68
73
  const raw = fs.readFileSync(filePath, 'utf8');
69
74
  config = JSON.parse(raw);
70
75
  } catch (e) {
71
- if (e && e.code === 'ENOENT') process.exit(0);
76
+ if (e && e.code === 'ENOENT') allow(undefined);
72
77
  // Malformed JSON — inform the agent without crashing
73
- process.stdout.write(JSON.stringify({
78
+ allow({
74
79
  hookSpecificOutput: {
75
80
  hookEventName: 'FileChanged',
76
81
  additionalContext:
77
82
  'GSD config (.planning/config.json) was modified but could not be parsed. ' +
78
83
  'Check the file for JSON syntax errors.',
79
84
  },
80
- }));
81
- process.exit(0);
85
+ });
82
86
  }
83
87
 
84
88
  // Build a concise summary of key config fields the agent cares about
@@ -127,7 +131,9 @@ process.stdin.on('end', () => {
127
131
  },
128
132
  }));
129
133
  } catch (e) {
130
- // Silent fail — never block the session on a config reload error
131
- process.exit(0);
134
+ // Silent fail — never block the session on a config reload error.
135
+ // ON_CRASH is declared ALLOW at module top: this preserves today's
136
+ // exit(0) fail-open behavior exactly (#3911).
137
+ crash(ON_CRASH, undefined);
132
138
  }
133
139
  });
@@ -22,6 +22,13 @@ const fs = require('fs');
22
22
  const os = require('os');
23
23
  const path = require('path');
24
24
  const { spawn } = require('child_process');
25
+ const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
26
+
27
+ // This hook only injects an advisory context-usage warning; it never blocks
28
+ // the tool call it rides in on. A crash here (e.g. a malformed bridge file)
29
+ // must not turn a PostToolUse advisory into a blocked tool call — losing a
30
+ // context warning is far cheaper than stalling the agent's work (#3911).
31
+ const ON_CRASH = HOOK_ON_CRASH.ALLOW;
25
32
 
26
33
  const WARNING_THRESHOLD = 35; // remaining_percentage <= 35%
27
34
  const CRITICAL_THRESHOLD = 25; // remaining_percentage <= 25%
@@ -33,7 +40,7 @@ let input = '';
33
40
  // Windows/Git Bash, or slow Claude Code piping during large outputs),
34
41
  // exit silently instead of hanging until Claude Code kills the process
35
42
  // and reports "hook error". See #775, #1162.
36
- const stdinTimeout = setTimeout(() => process.exit(0), 10000);
43
+ const stdinTimeout = setTimeout(() => allow(undefined), 10000);
37
44
  process.stdin.setEncoding('utf8');
38
45
  process.stdin.on('data', chunk => input += chunk);
39
46
  process.stdin.on('end', () => {
@@ -43,14 +50,14 @@ process.stdin.on('end', () => {
43
50
  const sessionId = data.session_id;
44
51
 
45
52
  if (!sessionId) {
46
- process.exit(0);
53
+ allow(undefined);
47
54
  }
48
55
 
49
56
  // Reject session IDs that contain path traversal sequences or path separators.
50
57
  // session_id is used to construct file paths in /tmp — an unsanitized value
51
58
  // could escape the temp directory and read or write arbitrary files.
52
59
  if (/[/\\]|\.\./.test(sessionId)) {
53
- process.exit(0);
60
+ allow(undefined);
54
61
  }
55
62
 
56
63
  // Check if context warnings are disabled via config.
@@ -61,7 +68,7 @@ process.stdin.on('end', () => {
61
68
  const configPath = path.join(cwd, '.planning', 'config.json');
62
69
  const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
63
70
  if (config.hooks?.context_warnings === false) {
64
- process.exit(0);
71
+ allow(undefined);
65
72
  }
66
73
  } catch (e) {
67
74
  // Missing or unparseable config → proceed with defaults (context warnings enabled)
@@ -77,7 +84,7 @@ process.stdin.on('end', () => {
77
84
  try {
78
85
  metricsRaw = fs.readFileSync(metricsPath, 'utf8');
79
86
  } catch (e) {
80
- if (e && e.code === 'ENOENT') process.exit(0);
87
+ if (e && e.code === 'ENOENT') allow(undefined);
81
88
  throw e;
82
89
  }
83
90
  const metrics = JSON.parse(metricsRaw);
@@ -85,7 +92,7 @@ process.stdin.on('end', () => {
85
92
 
86
93
  // Ignore stale metrics
87
94
  if (metrics.timestamp && (now - metrics.timestamp) > STALE_SECONDS) {
88
- process.exit(0);
95
+ allow(undefined);
89
96
  }
90
97
 
91
98
  const remaining = metrics.remaining_percentage;
@@ -93,7 +100,7 @@ process.stdin.on('end', () => {
93
100
 
94
101
  // No warning needed
95
102
  if (remaining > WARNING_THRESHOLD) {
96
- process.exit(0);
103
+ allow(undefined);
97
104
  }
98
105
 
99
106
  // Debounce: check if we warned recently
@@ -121,7 +128,7 @@ process.stdin.on('end', () => {
121
128
  if (!firstWarn && warnData.callsSinceWarn < DEBOUNCE_CALLS && !severityEscalated) {
122
129
  // Update counter and exit without warning
123
130
  fs.writeFileSync(warnPath, JSON.stringify(warnData));
124
- process.exit(0);
131
+ allow(undefined);
125
132
  }
126
133
 
127
134
  // Reset debounce counter
@@ -208,7 +215,9 @@ process.stdin.on('end', () => {
208
215
  process.stdout.write(JSON.stringify(output));
209
216
  }
210
217
  } catch (e) {
211
- // Silent fail -- never block tool execution
212
- process.exit(0);
218
+ // Silent fail -- never block tool execution.
219
+ // ON_CRASH is declared ALLOW at module top: this preserves today's
220
+ // exit(0) fail-open behavior exactly (#3911).
221
+ crash(ON_CRASH, undefined);
213
222
  }
214
223
  });
@@ -22,6 +22,8 @@
22
22
 
23
23
  'use strict';
24
24
 
25
+ const { allow } = require('./lib/hook-exit.js');
26
+
25
27
  const WRITE_TOOL_RE = /write|edit|replace|create|delete|remove|append|apply|patch|insert|mkdir/i;
26
28
  const PATH_KEY_RE = /^(path|file|file_?path|filepath|target_?path|target|dir|directory|uri|filename)$/i;
27
29
  const PLANNING_PATH_RE = /(^|[\\/])\.planning([\\/]|$)/;
@@ -29,7 +31,7 @@ const PLANNING_PATH_RE = /(^|[\\/])\.planning([\\/]|$)/;
29
31
  let raw = '';
30
32
  const stdinTimeout = setTimeout(() => {
31
33
  // Timeout guard: exit silently rather than hanging.
32
- process.exit(0);
34
+ allow(undefined);
33
35
  }, 10000);
34
36
 
35
37
  process.stdin.setEncoding('utf8');
@@ -22,13 +22,15 @@
22
22
 
23
23
  'use strict';
24
24
 
25
+ const { allow } = require('./lib/hook-exit.js');
26
+
25
27
  const WRITE_TOOL_RE = /write|edit|replace|create|delete|remove|append|apply|patch|insert|mkdir/i;
26
28
  const PATH_KEY_RE = /^(path|file|file_?path|filepath|target_?path|target|dir|directory|uri|filename)$/i;
27
29
  const PLANNING_PATH_RE = /(^|[\\/])\.planning([\\/]|$)/;
28
30
 
29
31
  let raw = '';
30
32
  const stdinTimeout = setTimeout(() => {
31
- process.exit(0);
33
+ allow(undefined);
32
34
  }, 10000);
33
35
 
34
36
  process.stdin.setEncoding('utf8');
@@ -23,6 +23,7 @@
23
23
  'use strict';
24
24
 
25
25
  const fs = require('fs');
26
+ const { allow } = require('./lib/hook-exit.js');
26
27
 
27
28
  const MSG_PRESENT =
28
29
  'GSD: .planning/STATE.md is present — review the current phase and any blockers before acting.';
@@ -37,7 +38,7 @@ const { resolveStatePath } = require('./lib/cursor-workspace.js');
37
38
  let raw = '';
38
39
  const stdinTimeout = setTimeout(() => {
39
40
  // Timeout guard: exit silently rather than hanging.
40
- process.exit(0);
41
+ allow(undefined);
41
42
  }, 10000);
42
43
 
43
44
  process.stdin.setEncoding('utf8');
@@ -21,6 +21,7 @@
21
21
  'use strict';
22
22
 
23
23
  const fs = require('fs');
24
+ const { allow } = require('./lib/hook-exit.js');
24
25
 
25
26
  // Workspace resolution is shared across the Cursor hooks (#2587) — see
26
27
  // hooks/lib/cursor-workspace.js. Staged next to these scripts by
@@ -29,7 +30,7 @@ const { resolveStatePath } = require('./lib/cursor-workspace.js');
29
30
 
30
31
  let raw = '';
31
32
  const stdinTimeout = setTimeout(() => {
32
- process.exit(0);
33
+ allow(undefined);
33
34
  }, 10000);
34
35
 
35
36
  process.stdin.setEncoding('utf8');