@opengsd/gsd-core 1.14.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (551) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +85 -5
  4. package/README.ja-JP.md +3 -3
  5. package/README.ko-KR.md +3 -3
  6. package/README.pt-BR.md +3 -3
  7. package/README.zh-CN.md +3 -3
  8. package/agents/gsd-code-fixer.compact.md +7 -6
  9. package/agents/gsd-code-fixer.md +9 -8
  10. package/agents/gsd-code-reviewer.compact.md +5 -3
  11. package/agents/gsd-code-reviewer.md +8 -6
  12. package/agents/gsd-debug-session-manager.compact.md +17 -2
  13. package/agents/gsd-debug-session-manager.md +17 -2
  14. package/agents/gsd-debugger.md +3 -3
  15. package/agents/gsd-eval-auditor.compact.md +1 -1
  16. package/agents/gsd-eval-auditor.md +1 -1
  17. package/agents/gsd-executor.md +17 -12
  18. package/agents/gsd-intel-updater.compact.md +1 -1
  19. package/agents/gsd-intel-updater.md +1 -1
  20. package/agents/gsd-mempalace-curator.md +2 -2
  21. package/agents/gsd-phase-researcher.md +19 -11
  22. package/agents/gsd-plan-checker.md +15 -9
  23. package/agents/gsd-planner.md +15 -11
  24. package/agents/gsd-project-researcher.compact.md +1 -1
  25. package/agents/gsd-project-researcher.md +1 -1
  26. package/agents/gsd-research-synthesizer.compact.md +1 -1
  27. package/agents/gsd-research-synthesizer.md +1 -1
  28. package/agents/gsd-ui-auditor.compact.md +21 -30
  29. package/agents/gsd-ui-auditor.md +166 -37
  30. package/agents/gsd-ui-researcher.compact.md +1 -1
  31. package/agents/gsd-ui-researcher.md +1 -1
  32. package/agents/gsd-verifier.md +37 -14
  33. package/bin/install.js +764 -287
  34. package/commands/gsd/add-tests.md +6 -1
  35. package/commands/gsd/ai-integration-phase.md +6 -1
  36. package/commands/gsd/audit-fix.md +5 -0
  37. package/commands/gsd/audit-milestone.md +6 -1
  38. package/commands/gsd/autonomous.md +7 -2
  39. package/commands/gsd/capture.md +9 -5
  40. package/commands/gsd/code-review.md +7 -2
  41. package/commands/gsd/complete-milestone.md +4 -0
  42. package/commands/gsd/config.md +7 -3
  43. package/commands/gsd/debug.md +11 -7
  44. package/commands/gsd/discuss-phase.md +7 -3
  45. package/commands/gsd/docs-update.md +12 -7
  46. package/commands/gsd/eval-review.md +6 -1
  47. package/commands/gsd/execute-phase.md +12 -7
  48. package/commands/gsd/extract-learnings.md +5 -0
  49. package/commands/gsd/fast.md +4 -0
  50. package/commands/gsd/forensics.md +5 -1
  51. package/commands/gsd/graphify.md +10 -6
  52. package/commands/gsd/health.md +5 -0
  53. package/commands/gsd/help.md +7 -2
  54. package/commands/gsd/import.md +7 -3
  55. package/commands/gsd/inbox.md +5 -0
  56. package/commands/gsd/ingest-docs.md +5 -1
  57. package/commands/gsd/manager.md +6 -1
  58. package/commands/gsd/map-codebase.md +7 -3
  59. package/commands/gsd/mempalace-capture.md +12 -4
  60. package/commands/gsd/mempalace-recall.md +5 -1
  61. package/commands/gsd/milestone-summary.md +5 -1
  62. package/commands/gsd/mvp-phase.md +8 -3
  63. package/commands/gsd/new-milestone.md +6 -1
  64. package/commands/gsd/new-project.md +5 -0
  65. package/commands/gsd/next.md +6 -1
  66. package/commands/gsd/ns-context.md +4 -0
  67. package/commands/gsd/ns-ideate.md +4 -0
  68. package/commands/gsd/ns-manage.md +4 -0
  69. package/commands/gsd/ns-project.md +4 -0
  70. package/commands/gsd/ns-review.md +4 -0
  71. package/commands/gsd/ns-workflow.md +4 -0
  72. package/commands/gsd/onboard.md +6 -1
  73. package/commands/gsd/pause-work.md +5 -1
  74. package/commands/gsd/phase.md +8 -4
  75. package/commands/gsd/plan-phase.md +6 -1
  76. package/commands/gsd/plan-review-convergence.md +11 -7
  77. package/commands/gsd/pr-branch.md +4 -0
  78. package/commands/gsd/profile-user.md +5 -1
  79. package/commands/gsd/progress.md +7 -2
  80. package/commands/gsd/quick-batch.md +21 -9
  81. package/commands/gsd/quick.md +12 -7
  82. package/commands/gsd/review.md +7 -4
  83. package/commands/gsd/secure-phase.md +6 -1
  84. package/commands/gsd/ship.md +5 -0
  85. package/commands/gsd/sketch.md +7 -2
  86. package/commands/gsd/spec-phase.md +5 -1
  87. package/commands/gsd/spike.md +8 -3
  88. package/commands/gsd/surface.md +5 -1
  89. package/commands/gsd/thread.md +4 -0
  90. package/commands/gsd/ui-phase.md +6 -1
  91. package/commands/gsd/ui-review.md +6 -1
  92. package/commands/gsd/ultraplan-phase.md +5 -1
  93. package/commands/gsd/undo.md +5 -1
  94. package/commands/gsd/update.md +6 -2
  95. package/commands/gsd/validate-phase.md +6 -1
  96. package/commands/gsd/verify-work.md +6 -1
  97. package/commands/gsd/workspace.md +7 -3
  98. package/gsd-core/bin/gsd-tools.cjs +477 -78
  99. package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
  100. package/gsd-core/bin/lib/adr-parser.cjs +3 -1
  101. package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
  102. package/gsd-core/bin/lib/audit.cjs +144 -42
  103. package/gsd-core/bin/lib/broken-windows.cjs +13 -13
  104. package/gsd-core/bin/lib/capability-activation.cjs +9 -4
  105. package/gsd-core/bin/lib/capability-registry.cjs +197 -222
  106. package/gsd-core/bin/lib/capability-validator.cjs +16 -1
  107. package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
  108. package/gsd-core/bin/lib/check-command-router.cjs +164 -1625
  109. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +13 -2
  110. package/gsd-core/bin/lib/cli-exit.cjs +12 -0
  111. package/gsd-core/bin/lib/codex-agent-toml.cjs +32 -33
  112. package/gsd-core/bin/lib/command-aliases.cjs +7 -0
  113. package/gsd-core/bin/lib/command-routing-hub.cjs +48 -1
  114. package/gsd-core/bin/lib/commands.cjs +343 -207
  115. package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
  116. package/gsd-core/bin/lib/config-loader.cjs +65 -4
  117. package/gsd-core/bin/lib/config.cjs +76 -19
  118. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  119. package/gsd-core/bin/lib/coverage.cjs +4 -8
  120. package/gsd-core/bin/lib/decision-coverage-support.cjs +259 -0
  121. package/gsd-core/bin/lib/decisions.cjs +30 -14
  122. package/gsd-core/bin/lib/drift.cjs +177 -42
  123. package/gsd-core/bin/lib/frontmatter-fence.cjs +90 -0
  124. package/gsd-core/bin/lib/frontmatter-splice.cjs +494 -0
  125. package/gsd-core/bin/lib/frontmatter.cjs +426 -234
  126. package/gsd-core/bin/lib/gap-checker.cjs +72 -29
  127. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +381 -0
  128. package/gsd-core/bin/lib/gate-args.cjs +53 -0
  129. package/gsd-core/bin/lib/gate-codebase-drift.cjs +285 -0
  130. package/gsd-core/bin/lib/gate-config.cjs +46 -0
  131. package/gsd-core/bin/lib/gate-context-drift.cjs +141 -0
  132. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +169 -0
  133. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +126 -0
  134. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +555 -0
  135. package/gsd-core/bin/lib/gate-evidence.cjs +138 -0
  136. package/gsd-core/bin/lib/gate-exit.cjs +27 -0
  137. package/gsd-core/bin/lib/gate-gap-analysis-plan-post.cjs +61 -0
  138. package/gsd-core/bin/lib/gate-phase-context.cjs +170 -0
  139. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +1 -1
  140. package/gsd-core/bin/lib/gate-predicate.cjs +165 -0
  141. package/gsd-core/bin/lib/gate-prohibition-enforcement.cjs +94 -0
  142. package/gsd-core/bin/lib/gate-schema-drift.cjs +165 -0
  143. package/gsd-core/bin/lib/gate-tdd-red-evidence.cjs +100 -0
  144. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +182 -0
  145. package/gsd-core/bin/lib/gate-ui-plan.cjs +86 -0
  146. package/gsd-core/bin/lib/gate-ui-safety.cjs +80 -0
  147. package/gsd-core/bin/lib/gate-verdict.cjs +64 -0
  148. package/gsd-core/bin/lib/gate-verify-command-paths.cjs +78 -0
  149. package/gsd-core/bin/lib/gate-verify-failure-directions.cjs +41 -0
  150. package/gsd-core/bin/lib/graphify.cjs +10 -2
  151. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +43 -47
  152. package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
  153. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  154. package/gsd-core/bin/lib/init.cjs +364 -132
  155. package/gsd-core/bin/lib/install-engine.cjs +30 -33
  156. package/gsd-core/bin/lib/install-profiles.cjs +7 -4
  157. package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
  158. package/gsd-core/bin/lib/io.cjs +122 -3
  159. package/gsd-core/bin/lib/loop-resolver.cjs +95 -0
  160. package/gsd-core/bin/lib/markdown-sectionizer.cjs +75 -1
  161. package/gsd-core/bin/lib/milestone.cjs +37 -6
  162. package/gsd-core/bin/lib/model-resolver.cjs +171 -62
  163. package/gsd-core/bin/lib/observability/event.cjs +1 -1
  164. package/gsd-core/bin/lib/observability/logger.cjs +46 -1
  165. package/gsd-core/bin/lib/pattern.cjs +10 -0
  166. package/gsd-core/bin/lib/phase-command-router.cjs +20 -5
  167. package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
  168. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  169. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  170. package/gsd-core/bin/lib/phase-id.cjs +110 -8
  171. package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
  172. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  173. package/gsd-core/bin/lib/phase-status.cjs +360 -0
  174. package/gsd-core/bin/lib/phase.cjs +489 -88
  175. package/gsd-core/bin/lib/plan-document.cjs +142 -20
  176. package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
  177. package/gsd-core/bin/lib/planning-document.cjs +692 -0
  178. package/gsd-core/bin/lib/planning-inspect.cjs +60 -9
  179. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
  180. package/gsd-core/bin/lib/planning-workspace.cjs +83 -55
  181. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  182. package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
  183. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  184. package/gsd-core/bin/lib/profile-output.cjs +6 -3
  185. package/gsd-core/bin/lib/prohibition-enforcement.cjs +0 -55
  186. package/gsd-core/bin/lib/project-root.cjs +41 -2
  187. package/gsd-core/bin/lib/quick-batch-command-router.cjs +35 -9
  188. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +11 -8
  189. package/gsd-core/bin/lib/real-home-guard.cjs +9 -1
  190. package/gsd-core/bin/lib/report-parser.cjs +269 -0
  191. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  192. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  193. package/gsd-core/bin/lib/roadmap-command-router.cjs +25 -19
  194. package/gsd-core/bin/lib/roadmap-parser.cjs +242 -15
  195. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1653 -65
  196. package/gsd-core/bin/lib/roadmap.cjs +405 -88
  197. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +373 -187
  198. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +5 -2
  199. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
  200. package/gsd-core/bin/lib/runtime-homes.cjs +14 -7
  201. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +522 -624
  202. package/gsd-core/bin/lib/runtime-name-policy.cjs +246 -21
  203. package/gsd-core/bin/lib/runtime-slash.cjs +47 -30
  204. package/gsd-core/bin/lib/shell-command-projection.cjs +47 -8
  205. package/gsd-core/bin/lib/smart-entry.cjs +19 -3
  206. package/gsd-core/bin/lib/stale-bake-guard.cjs +32 -48
  207. package/gsd-core/bin/lib/state-contract.cjs +15 -18
  208. package/gsd-core/bin/lib/state-document.cjs +100 -22
  209. package/gsd-core/bin/lib/state-transition.cjs +39 -2
  210. package/gsd-core/bin/lib/state.cjs +256 -105
  211. package/gsd-core/bin/lib/surface.cjs +19 -2
  212. package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -79
  213. package/gsd-core/bin/lib/uat-predicate.cjs +359 -38
  214. package/gsd-core/bin/lib/uat.cjs +432 -7
  215. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  216. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +167 -40
  217. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  218. package/gsd-core/bin/lib/vendor/README.md +31 -9
  219. package/gsd-core/bin/lib/vendor/saxes.cjs +1934 -0
  220. package/gsd-core/bin/lib/vendor/saxes.cjs.LICENSE.txt +92 -0
  221. package/gsd-core/bin/lib/vendor/tap-parser.cjs +8927 -0
  222. package/gsd-core/bin/lib/vendor/tap-parser.cjs.LICENSE.txt +152 -0
  223. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  224. package/gsd-core/bin/lib/verification.cjs +1378 -274
  225. package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
  226. package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
  227. package/gsd-core/bin/lib/verify.cjs +357 -531
  228. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +14 -6
  229. package/gsd-core/bin/lib/workstream-inventory.cjs +31 -21
  230. package/gsd-core/bin/lib/workstream-name-policy.cjs +31 -1
  231. package/gsd-core/bin/lib/workstream.cjs +11 -2
  232. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  233. package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
  234. package/gsd-core/bin/shared/config-defaults.manifest.json +8 -0
  235. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  236. package/gsd-core/references/autonomous-smart-discuss.md +2 -1
  237. package/gsd-core/references/autonomous-ui-design-contract.md +3 -3
  238. package/gsd-core/references/checkpoints.md +5 -3
  239. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  240. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  241. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  242. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  243. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  244. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  245. package/gsd-core/references/edge-probe.md +195 -21
  246. package/gsd-core/references/execute-mvp-tdd.md +5 -10
  247. package/gsd-core/references/execute-phase-between-wave-reset.md +10 -6
  248. package/gsd-core/references/execute-phase-response-language.md +1 -1
  249. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  250. package/gsd-core/references/gsd-run-resolver.md +1 -1
  251. package/gsd-core/references/loop-hook-dispatch.md +7 -1
  252. package/gsd-core/references/model-profiles.md +1 -1
  253. package/gsd-core/references/offer-next.md +1 -1
  254. package/gsd-core/references/phase-argument-parsing.md +9 -7
  255. package/gsd-core/references/phase-id-convention.md +28 -0
  256. package/gsd-core/references/planner-gap-closure.md +2 -0
  257. package/gsd-core/references/planner-load-graph-context.md +24 -13
  258. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  259. package/gsd-core/references/planning-config.md +12 -3
  260. package/gsd-core/references/spidr-splitting.md +1 -1
  261. package/gsd-core/references/tdd.md +37 -8
  262. package/gsd-core/references/ui-consideration-probe.md +10 -5
  263. package/gsd-core/references/verifier-phase-gates.md +5 -2
  264. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  265. package/gsd-core/references/verify-mvp-mode.md +2 -2
  266. package/gsd-core/references/workstream-flag.md +33 -3
  267. package/gsd-core/references/worktree-path-safety.md +321 -0
  268. package/gsd-core/templates/README.md +1 -1
  269. package/gsd-core/templates/UAT.md +17 -1
  270. package/gsd-core/templates/config.json +2 -11
  271. package/gsd-core/templates/verification-report.md +1 -1
  272. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  273. package/gsd-core/workflows/add-backlog.md +1 -1
  274. package/gsd-core/workflows/add-phase.md +8 -7
  275. package/gsd-core/workflows/add-tests.md +4 -3
  276. package/gsd-core/workflows/add-todo.md +6 -5
  277. package/gsd-core/workflows/ai-integration-phase.md +13 -4
  278. package/gsd-core/workflows/audit-fix.md +1 -1
  279. package/gsd-core/workflows/audit-milestone.md +4 -3
  280. package/gsd-core/workflows/audit-uat.md +1 -1
  281. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  282. package/gsd-core/workflows/autonomous.md +43 -23
  283. package/gsd-core/workflows/check-todos.md +7 -6
  284. package/gsd-core/workflows/cleanup.md +2 -2
  285. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  286. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
  287. package/gsd-core/workflows/code-review-fix.md +112 -25
  288. package/gsd-core/workflows/code-review.md +146 -135
  289. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
  290. package/gsd-core/workflows/complete-milestone.md +13 -8
  291. package/gsd-core/workflows/debug.md +32 -7
  292. package/gsd-core/workflows/diagnose-issues.md +3 -2
  293. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  294. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  295. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  296. package/gsd-core/workflows/discuss-phase.md +4 -3
  297. package/gsd-core/workflows/do.md +2 -2
  298. package/gsd-core/workflows/docs-update.md +6 -5
  299. package/gsd-core/workflows/edit-phase.md +4 -3
  300. package/gsd-core/workflows/eval-review.md +14 -5
  301. package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
  302. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1019 -0
  303. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +15 -4
  304. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +10 -7
  305. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
  306. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +3 -1
  307. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +2 -2
  308. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  309. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  310. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  311. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  312. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  313. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  314. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -0
  315. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  316. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  317. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +187 -0
  318. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +2 -3
  319. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  320. package/gsd-core/workflows/execute-phase.md +96 -178
  321. package/gsd-core/workflows/execute-plan.md +18 -23
  322. package/gsd-core/workflows/explore.md +4 -4
  323. package/gsd-core/workflows/extract-learnings.md +4 -2
  324. package/gsd-core/workflows/fast.md +1 -1
  325. package/gsd-core/workflows/forensics.md +1 -1
  326. package/gsd-core/workflows/graduation.md +1 -1
  327. package/gsd-core/workflows/health.md +3 -2
  328. package/gsd-core/workflows/help/modes/full.compact.md +3 -3
  329. package/gsd-core/workflows/help/modes/full.md +5 -5
  330. package/gsd-core/workflows/help/modes/topic.md +15 -5
  331. package/gsd-core/workflows/import.md +4 -3
  332. package/gsd-core/workflows/inbox.md +2 -2
  333. package/gsd-core/workflows/ingest-docs.md +3 -3
  334. package/gsd-core/workflows/insert-phase.md +4 -3
  335. package/gsd-core/workflows/list-seeds.md +1 -1
  336. package/gsd-core/workflows/list-workspaces.md +1 -1
  337. package/gsd-core/workflows/manager.md +6 -4
  338. package/gsd-core/workflows/map-codebase.md +5 -4
  339. package/gsd-core/workflows/milestone-summary.md +3 -2
  340. package/gsd-core/workflows/mvp-phase.md +14 -14
  341. package/gsd-core/workflows/new-milestone.md +11 -11
  342. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  343. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  344. package/gsd-core/workflows/new-project.md +7 -7
  345. package/gsd-core/workflows/new-workspace.md +2 -2
  346. package/gsd-core/workflows/next.md +1 -1
  347. package/gsd-core/workflows/note.md +1 -1
  348. package/gsd-core/workflows/onboard.md +1 -1
  349. package/gsd-core/workflows/pause-work.md +2 -2
  350. package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
  351. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  352. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +1 -1
  353. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  354. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  355. package/gsd-core/workflows/plan-phase.md +50 -24
  356. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  357. package/gsd-core/workflows/plant-seed.md +62 -20
  358. package/gsd-core/workflows/pr-branch.md +113 -13
  359. package/gsd-core/workflows/profile-user.md +2 -2
  360. package/gsd-core/workflows/progress.md +19 -49
  361. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +27 -0
  362. package/gsd-core/workflows/quick/steps/quick-verification.md +4 -4
  363. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +30 -11
  364. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  365. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  366. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  367. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  368. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  369. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  370. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +9 -3
  371. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  372. package/gsd-core/workflows/quick-batch.md +15 -10
  373. package/gsd-core/workflows/quick.md +62 -39
  374. package/gsd-core/workflows/reapply-patches.md +9 -3
  375. package/gsd-core/workflows/remove-phase.md +3 -2
  376. package/gsd-core/workflows/remove-workspace.md +2 -2
  377. package/gsd-core/workflows/resume-project.md +3 -2
  378. package/gsd-core/workflows/review.md +33 -17
  379. package/gsd-core/workflows/scan.md +3 -2
  380. package/gsd-core/workflows/secure-phase.md +13 -13
  381. package/gsd-core/workflows/settings-advanced.md +30 -10
  382. package/gsd-core/workflows/settings-integrations.md +2 -3
  383. package/gsd-core/workflows/settings.md +4 -4
  384. package/gsd-core/workflows/ship.md +14 -13
  385. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  386. package/gsd-core/workflows/sketch.md +1 -1
  387. package/gsd-core/workflows/smart-entry.md +2 -2
  388. package/gsd-core/workflows/spec-phase.md +15 -5
  389. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  390. package/gsd-core/workflows/spike.md +1 -1
  391. package/gsd-core/workflows/stats.md +1 -1
  392. package/gsd-core/workflows/sync-skills.md +5 -5
  393. package/gsd-core/workflows/thread.md +2 -2
  394. package/gsd-core/workflows/transition.md +13 -23
  395. package/gsd-core/workflows/ui-phase.md +48 -11
  396. package/gsd-core/workflows/ui-review.md +21 -6
  397. package/gsd-core/workflows/ultraplan-phase.md +3 -2
  398. package/gsd-core/workflows/undo.md +339 -20
  399. package/gsd-core/workflows/update.md +7 -7
  400. package/gsd-core/workflows/validate-phase.md +12 -13
  401. package/gsd-core/workflows/verify-work/detail/elaboration.md +43 -3
  402. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  403. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -3
  404. package/gsd-core/workflows/verify-work.md +129 -55
  405. package/hooks/dist/gsd-agent-isolation-guard.js +32 -0
  406. package/hooks/dist/gsd-check-update-worker.js +8 -0
  407. package/hooks/dist/gsd-check-update.js +8 -0
  408. package/hooks/dist/gsd-context-monitor.js +31 -8
  409. package/hooks/dist/gsd-cursor-subagent-start.js +8 -0
  410. package/hooks/dist/gsd-secret-read-guard.js +161 -4
  411. package/hooks/dist/gsd-statusline.js +85 -20
  412. package/hooks/dist/gsd-update-banner.js +8 -0
  413. package/hooks/dist/gsd-validate-commit.sh +63 -4
  414. package/hooks/dist/gsd-windsurf-pre-write.js +11 -2
  415. package/hooks/dist/gsd-workflow-guard.js +5 -4
  416. package/hooks/dist/gsd-worktree-path-guard.js +6 -2
  417. package/hooks/dist/lib/cli-exit.js +12 -0
  418. package/hooks/dist/lib/git-probe.js +17 -1
  419. package/hooks/dist/lib/isolation-sentinel.js +2 -2
  420. package/hooks/gsd-agent-isolation-guard.js +32 -0
  421. package/hooks/gsd-check-update-worker.js +8 -0
  422. package/hooks/gsd-check-update.js +8 -0
  423. package/hooks/gsd-context-monitor.js +31 -8
  424. package/hooks/gsd-cursor-subagent-start.js +8 -0
  425. package/hooks/gsd-secret-read-guard.js +161 -4
  426. package/hooks/gsd-statusline.js +85 -20
  427. package/hooks/gsd-update-banner.js +8 -0
  428. package/hooks/gsd-validate-commit.sh +63 -4
  429. package/hooks/gsd-windsurf-pre-write.js +11 -2
  430. package/hooks/gsd-workflow-guard.js +5 -4
  431. package/hooks/gsd-worktree-path-guard.js +6 -2
  432. package/hooks/hooks.json +5 -5
  433. package/hooks/lib/cli-exit.js +12 -0
  434. package/hooks/lib/git-probe.js +17 -1
  435. package/hooks/lib/isolation-sentinel.js +2 -2
  436. package/package.json +22 -4
  437. package/scripts/build-hooks.js +15 -6
  438. package/scripts/changeset/parse.cjs +52 -4
  439. package/scripts/check-contract-drift.cjs +127 -11
  440. package/scripts/ci-timeout-report.cjs +770 -4
  441. package/scripts/command-contract-helpers.cjs +15 -8
  442. package/scripts/docs-guard-registry.cjs +34 -0
  443. package/scripts/gen-features.cjs +13 -8
  444. package/scripts/gen-hooks-cli-exit.cjs +12 -28
  445. package/scripts/gen-loop-host-contract.cjs +79 -1
  446. package/scripts/gen-platform-conformance-tier.cjs +187 -1
  447. package/scripts/gen-plugin-skills.cjs +87 -1
  448. package/scripts/gen-research-agents.cjs +24 -31
  449. package/scripts/gen-scripts-cli-exit.cjs +30 -3
  450. package/scripts/gen-test-timings.cjs +32 -7
  451. package/scripts/lib/cli-exit.cjs +12 -0
  452. package/scripts/lib/macos-conformance-tier.generated.cjs +34 -2
  453. package/scripts/lib/ndjson-reporter.cjs +31 -5
  454. package/scripts/lib/platform-conformance-tier.generated.cjs +45 -5
  455. package/scripts/lib/registration-ledger-preload.cjs +155 -0
  456. package/scripts/lib/vendor-bundle.cjs +59 -0
  457. package/scripts/lib/vendor-licenses/saxes-6.0.0.txt +64 -0
  458. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  459. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  460. package/scripts/lint-completion-predicate-drift.cjs +18 -19
  461. package/scripts/lint-descriptions.cjs +7 -3
  462. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +38 -1
  463. package/scripts/lint-eslint-glob-coverage.allowlist.json +20 -0
  464. package/scripts/lint-frontmatter-fence-drift.cjs +313 -0
  465. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +122 -16
  466. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  467. package/scripts/lint-phase-enumeration-drift.cjs +12 -8
  468. package/scripts/lint-phase-id-drift.cjs +319 -5
  469. package/scripts/lint-planning-document-positive-control.cjs +329 -0
  470. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  471. package/scripts/lint-response-language-coverage.cjs +3 -0
  472. package/scripts/lint-retired-runtime-name.cjs +619 -0
  473. package/scripts/lint-skill-deps.cjs +7 -3
  474. package/scripts/lint-state-write-path-drift.cjs +93 -0
  475. package/scripts/lint-test-file-count.allowlist.json +38 -9
  476. package/scripts/lint-test-file-count.cjs +34 -1
  477. package/scripts/lint-vendored-deps.cjs +41 -5
  478. package/scripts/lint-workflow-shellcheck-baseline.json +25 -10
  479. package/scripts/mutation-matrix.cjs +50 -5
  480. package/scripts/prompt-injection-scan.sh +4 -0
  481. package/scripts/release-tarball-smoke.cjs +194 -1
  482. package/scripts/require-issue-link-policy.cjs +6 -2
  483. package/scripts/sync-runtime-launcher.cjs +184 -2
  484. package/scripts/verify-npm-publish.cjs +76 -20
  485. package/skills/gsd-add-tests/SKILL.md +6 -1
  486. package/skills/gsd-ai-integration-phase/SKILL.md +6 -1
  487. package/skills/gsd-audit-fix/SKILL.md +5 -0
  488. package/skills/gsd-audit-milestone/SKILL.md +6 -1
  489. package/skills/gsd-autonomous/SKILL.md +7 -2
  490. package/skills/gsd-capture/SKILL.md +9 -5
  491. package/skills/gsd-code-review/SKILL.md +7 -2
  492. package/skills/gsd-complete-milestone/SKILL.md +4 -0
  493. package/skills/gsd-config/SKILL.md +8 -4
  494. package/skills/gsd-debug/SKILL.md +11 -7
  495. package/skills/gsd-discuss-phase/SKILL.md +7 -3
  496. package/skills/gsd-docs-update/SKILL.md +12 -7
  497. package/skills/gsd-eval-review/SKILL.md +6 -1
  498. package/skills/gsd-execute-phase/SKILL.md +12 -7
  499. package/skills/gsd-extract-learnings/SKILL.md +5 -0
  500. package/skills/gsd-fast/SKILL.md +4 -0
  501. package/skills/gsd-forensics/SKILL.md +5 -1
  502. package/skills/gsd-graphify/SKILL.md +10 -6
  503. package/skills/gsd-health/SKILL.md +5 -0
  504. package/skills/gsd-help/SKILL.md +7 -2
  505. package/skills/gsd-import/SKILL.md +7 -3
  506. package/skills/gsd-inbox/SKILL.md +5 -0
  507. package/skills/gsd-ingest-docs/SKILL.md +5 -1
  508. package/skills/gsd-manager/SKILL.md +6 -1
  509. package/skills/gsd-map-codebase/SKILL.md +7 -3
  510. package/skills/gsd-mempalace-capture/SKILL.md +12 -4
  511. package/skills/gsd-mempalace-recall/SKILL.md +5 -1
  512. package/skills/gsd-milestone-summary/SKILL.md +5 -1
  513. package/skills/gsd-mvp-phase/SKILL.md +8 -3
  514. package/skills/gsd-new-milestone/SKILL.md +6 -1
  515. package/skills/gsd-new-project/SKILL.md +5 -0
  516. package/skills/gsd-next/SKILL.md +6 -1
  517. package/skills/gsd-ns-context/SKILL.md +4 -0
  518. package/skills/gsd-ns-ideate/SKILL.md +4 -0
  519. package/skills/gsd-ns-manage/SKILL.md +4 -0
  520. package/skills/gsd-ns-project/SKILL.md +4 -0
  521. package/skills/gsd-ns-review/SKILL.md +4 -0
  522. package/skills/gsd-ns-workflow/SKILL.md +4 -0
  523. package/skills/gsd-onboard/SKILL.md +6 -1
  524. package/skills/gsd-pause-work/SKILL.md +5 -1
  525. package/skills/gsd-phase/SKILL.md +8 -4
  526. package/skills/gsd-plan-phase/SKILL.md +6 -1
  527. package/skills/gsd-plan-review-convergence/SKILL.md +10 -6
  528. package/skills/gsd-pr-branch/SKILL.md +4 -0
  529. package/skills/gsd-profile-user/SKILL.md +5 -1
  530. package/skills/gsd-progress/SKILL.md +7 -2
  531. package/skills/gsd-quick/SKILL.md +16 -10
  532. package/skills/gsd-quick-batch/SKILL.md +21 -9
  533. package/skills/gsd-review/SKILL.md +7 -4
  534. package/skills/gsd-review-backlog/SKILL.md +3 -2
  535. package/skills/gsd-secure-phase/SKILL.md +6 -1
  536. package/skills/gsd-ship/SKILL.md +5 -0
  537. package/skills/gsd-sketch/SKILL.md +7 -2
  538. package/skills/gsd-spec-phase/SKILL.md +5 -1
  539. package/skills/gsd-spike/SKILL.md +8 -3
  540. package/skills/gsd-surface/SKILL.md +5 -1
  541. package/skills/gsd-thread/SKILL.md +4 -0
  542. package/skills/gsd-ui-phase/SKILL.md +6 -1
  543. package/skills/gsd-ui-review/SKILL.md +6 -1
  544. package/skills/gsd-ultraplan-phase/SKILL.md +5 -1
  545. package/skills/gsd-undo/SKILL.md +5 -1
  546. package/skills/gsd-update/SKILL.md +6 -2
  547. package/skills/gsd-validate-phase/SKILL.md +6 -1
  548. package/skills/gsd-verify-work/SKILL.md +6 -1
  549. package/skills/gsd-workspace/SKILL.md +7 -3
  550. package/skills/gsd-workstreams/SKILL.md +6 -6
  551. package/vscode/package.json +1 -1
@@ -38,7 +38,7 @@ const commonjs_marker_cjs_1 = require("./commonjs-marker.cjs");
38
38
  const imperative_hook_bus_cjs_1 = require("./host-integration-adapters/imperative-hook-bus.cjs");
39
39
  // eslint-disable-next-line @typescript-eslint/no-require-imports
40
40
  const shellCmdProjection = require("./shell-command-projection.cjs");
41
- const { isManagedHookBasename, isManagedHookCommand, projectLegacySettingsHookCommand, projectManagedHookCommand, projectPortableHookBaseDir, projectCodexHookTomlCommand, shellHookOmitsBashRunner, escapeTomlDoubleQuotedString, escapePosixDoubleQuoted, } = shellCmdProjection;
41
+ const { isManagedHookBasename, isManagedHookCommand, projectLegacySettingsHookCommand, projectManagedHookCommand, projectPortableHookBaseDir, projectCodexHookTomlCommand, shellHookOmitsBashRunner, escapeTomlDoubleQuotedString, escapePosixDoubleQuoted, resolveExecutableBinary, } = shellCmdProjection;
42
42
  // ---------------------------------------------------------------------------
43
43
  // Terminal color constants (mirrors install.js for console output parity)
44
44
  // ---------------------------------------------------------------------------
@@ -576,7 +576,15 @@ function buildBakedNodeToken(opts) {
576
576
  * guarantee), ahead of `command -v node` and the well-known fallback list.
577
577
  */
578
578
  const NODE_RUNNER_RESOLVER_HOOK = 'gsd-node-runner.sh';
579
- function resolveBashRunner(opts) {
579
+ /**
580
+ * The one Git Bash policy: `bash` off win32; on win32 GSD_BASH_PATH, then the
581
+ * well-known Git for Windows install locations, never a PATH lookup (which
582
+ * can reach WSL's System32 launcher). null when win32 has no Git Bash.
583
+ * #5082: the test process seam (tests/helpers/process-seam.cjs) resolves
584
+ * `bash` through this too, so hooks and bash-driven tests run under the same
585
+ * bash.
586
+ */
587
+ function resolveBashExecutable(opts) {
580
588
  const platform = (opts && opts.platform) || process.platform;
581
589
  if (platform !== 'win32')
582
590
  return 'bash';
@@ -594,12 +602,19 @@ function resolveBashRunner(opts) {
594
602
  candidates.push(node_path_1.default.win32.join(env.SystemDrive, 'Program Files (x86)', 'Git', 'bin', 'bash.exe'));
595
603
  }
596
604
  for (const candidate of candidates) {
597
- if (candidate && exists(candidate)) {
598
- return JSON.stringify(shellCmdProjection.posixNormalize(candidate));
599
- }
605
+ if (candidate && exists(candidate))
606
+ return shellCmdProjection.posixNormalize(candidate);
600
607
  }
601
608
  return null;
602
609
  }
610
+ function resolveBashRunner(opts) {
611
+ const executable = resolveBashExecutable(opts);
612
+ if (executable === null)
613
+ return null;
614
+ return ((opts && opts.platform) || process.platform) === 'win32'
615
+ ? JSON.stringify(executable)
616
+ : executable;
617
+ }
603
618
  // #3662 — recognize the two runtime-resolving command shapes the installer
604
619
  // emits, so the rewriter never churns (or un-does) an entry that already
605
620
  // works in every environment sharing the config root.
@@ -961,16 +976,18 @@ function reconcileCodexHooksJsonEvent(targetDir, eventName, opts = {}) {
961
976
  function reconcileCodexHooksJsonSessionStart(targetDir, opts = {}) {
962
977
  return reconcileCodexHooksJsonEvent(targetDir, 'SessionStart', opts);
963
978
  }
964
- function buildCodexHookWindowsShimIR(scriptAbsPath, absoluteRunnerToken) {
965
- if (!absoluteRunnerToken)
966
- return null;
967
- let interpreter;
979
+ function parseAbsoluteRunnerToken(absoluteRunnerToken) {
968
980
  try {
969
- interpreter = JSON.parse(absoluteRunnerToken);
981
+ return JSON.parse(absoluteRunnerToken);
970
982
  }
971
983
  catch {
972
- interpreter = absoluteRunnerToken;
984
+ return absoluteRunnerToken;
973
985
  }
986
+ }
987
+ function buildCodexHookWindowsShimIR(scriptAbsPath, absoluteRunnerToken) {
988
+ if (!absoluteRunnerToken)
989
+ return null;
990
+ const interpreter = parseAbsoluteRunnerToken(absoluteRunnerToken);
974
991
  const targetAbs = shellCmdProjection.posixNormalize(scriptAbsPath);
975
992
  const scriptQuoted = JSON.stringify(targetAbs);
976
993
  const cmdPath = scriptAbsPath.replace(/\.js$/, '.cmd');
@@ -995,6 +1012,7 @@ function ensureCodexHooksJsonSessionStart(targetDir, opts = {}) {
995
1012
  return { changed: false, wrote: false, path: hooksJsonPath };
996
1013
  const scriptPath = shellCmdProjection.posixNormalize(node_path_1.default.resolve(targetDir, 'hooks', 'gsd-check-update.js'));
997
1014
  const cmdShimPath = scriptPath.replace(/\.js$/, '.cmd');
1015
+ const configuredEntrypoints = [];
998
1016
  let managedCommand;
999
1017
  if (platform === 'win32') {
1000
1018
  const shimIR = buildCodexHookWindowsShimIR(scriptPath, absoluteRunner);
@@ -1011,6 +1029,7 @@ function ensureCodexHooksJsonSessionStart(targetDir, opts = {}) {
1011
1029
  return { changed: false, wrote: false, path: hooksJsonPath };
1012
1030
  }
1013
1031
  managedCommand = shimIR.hookCommand;
1032
+ configuredEntrypoints.push({ runtime: 'codex', configPath: hooksJsonPath, scriptPath: shimIR.cmdPath, platform, selfExecutable: true }, { runtime: 'codex', configPath: hooksJsonPath, scriptPath, interpreterCandidates: [parseAbsoluteRunnerToken(absoluteRunner)], platform });
1014
1033
  }
1015
1034
  else {
1016
1035
  managedCommand = projectManagedHookCommand({
@@ -1019,13 +1038,23 @@ function ensureCodexHooksJsonSessionStart(targetDir, opts = {}) {
1019
1038
  runtime: 'codex',
1020
1039
  platform,
1021
1040
  }) ?? undefined;
1041
+ if (managedCommand) {
1042
+ configuredEntrypoints.push({
1043
+ runtime: 'codex',
1044
+ configPath: hooksJsonPath,
1045
+ scriptPath,
1046
+ interpreterCandidates: [parseAbsoluteRunnerToken(absoluteRunner)],
1047
+ platform,
1048
+ });
1049
+ }
1022
1050
  }
1023
1051
  if (!managedCommand)
1024
1052
  return { changed: false, wrote: false, path: hooksJsonPath };
1025
1053
  const commandWindows = platform === 'win32'
1026
1054
  ? JSON.stringify(shellCmdProjection.posixNormalize(cmdShimPath))
1027
1055
  : undefined;
1028
- return reconcileCodexHooksJsonSessionStart(targetDir, { managedCommand, commandWindows });
1056
+ const result = reconcileCodexHooksJsonSessionStart(targetDir, { managedCommand, commandWindows });
1057
+ return { ...result, configuredEntrypoints };
1029
1058
  }
1030
1059
  function ensureCodexHooksJsonEvent(targetDir, eventName, opts = {}) {
1031
1060
  const platform = opts.platform || process.platform;
@@ -1201,6 +1230,56 @@ function cleanupOrphanedCodexContextMonitorScript(targetDir) {
1201
1230
  }
1202
1231
  return result;
1203
1232
  }
1233
+ function configuredEntrypointsForHook(configDir, hookName, opts) {
1234
+ const platform = opts.platform || process.platform;
1235
+ const runtime = opts.runtime || 'generic';
1236
+ const configPath = opts.configPath || configDir;
1237
+ const target = {
1238
+ runtime,
1239
+ configPath,
1240
+ scriptPath: node_path_1.default.join(configDir, 'hooks', hookName),
1241
+ platform,
1242
+ };
1243
+ const isShellHook = hookName.endsWith('.sh');
1244
+ if (shellHookOmitsBashRunner({ platform, runtime, isShellHook }))
1245
+ return [{ ...target, selfExecutable: true }];
1246
+ const bash = resolveBashExecutable(opts);
1247
+ if (isShellHook) {
1248
+ // An unresolved bash must still surface as an interpreterCandidates entry
1249
+ // (the literal token, same as the portableHooks runner below) so
1250
+ // validateConfiguredEntrypoints reports 'unresolved-interpreter' instead
1251
+ // of silently skipping the check because the field is absent.
1252
+ return [{ ...target, interpreterCandidates: [bash === null ? 'bash' : bash] }];
1253
+ }
1254
+ // #4249: check the SAME stable alias buildNodeRunnerChainToken bakes as its
1255
+ // first candidate (normalizeNodePath rewrites a version-manager shim like
1256
+ // fnm/nvm/mise/volta into its persistent path), not the raw, currently-
1257
+ // running process.execPath — which always trivially resolves regardless of
1258
+ // whether the alias actually baked into the persisted command still does.
1259
+ const nodeCandidates = [
1260
+ normalizeNodePath(opts.execPath || process.execPath, opts),
1261
+ 'node',
1262
+ '/usr/local/bin/node',
1263
+ '/usr/bin/node',
1264
+ ].filter((candidate) => Boolean(candidate));
1265
+ if (!opts.portableHooks) {
1266
+ return [{ ...target, interpreterCandidates: nodeCandidates }];
1267
+ }
1268
+ const runner = {
1269
+ runtime,
1270
+ configPath,
1271
+ scriptPath: node_path_1.default.join(configDir, 'hooks', NODE_RUNNER_RESOLVER_HOOK),
1272
+ interpreterCandidates: bash === null ? ['bash'] : [bash],
1273
+ platform,
1274
+ };
1275
+ return [runner, { ...target, interpreterCandidates: nodeCandidates }];
1276
+ }
1277
+ function recordConfiguredHookCommand(command, configDir, hookName, opts) {
1278
+ if (command && opts.configuredEntrypoints) {
1279
+ opts.configuredEntrypoints.push(...configuredEntrypointsForHook(configDir, hookName, opts).map(entry => ({ ...entry, command })));
1280
+ }
1281
+ return command;
1282
+ }
1204
1283
  function buildHookCommand(configDir, hookName, opts) {
1205
1284
  if (!opts)
1206
1285
  opts = {};
@@ -1208,44 +1287,56 @@ function buildHookCommand(configDir, hookName, opts) {
1208
1287
  const runtime = opts.runtime || 'generic';
1209
1288
  const hookShell = opts.hookShell;
1210
1289
  const isShellHook = hookName.endsWith('.sh');
1290
+ const track = (command) => recordConfiguredHookCommand(command, configDir, hookName, opts);
1211
1291
  if (shellHookOmitsBashRunner({ platform, runtime, isShellHook })) {
1212
1292
  if (opts.portableHooks) {
1213
1293
  const portableBaseDir = projectPortableHookBaseDir({
1214
1294
  configDir,
1215
1295
  homeDir: node_os_1.default.homedir(),
1216
1296
  });
1217
- return JSON.stringify(`${portableBaseDir}/hooks/${hookName}`);
1297
+ return track(JSON.stringify(`${portableBaseDir}/hooks/${hookName}`));
1218
1298
  }
1219
- return JSON.stringify(shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName);
1299
+ return track(JSON.stringify(shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName));
1220
1300
  }
1221
1301
  // .sh hooks keep the pre-#3662 shape everywhere: the bash runner resolves
1222
1302
  // at install time like today, and `bash` itself is a PATH-stable binary
1223
1303
  // (the absolute Git-Bash discovery covers win32 — #580/#3393).
1224
1304
  if (isShellHook) {
1225
1305
  const runner = resolveBashRunner(opts);
1226
- if (runner === null)
1306
+ if (runner === null) {
1307
+ // #4249 (antigravity review): this early return skips `track()` below,
1308
+ // so an unresolved bash on win32 (no Git Bash found) previously left
1309
+ // this hook silently unregistered with nothing for
1310
+ // validateConfiguredEntrypoints to reject — configuredEntrypointsForHook's
1311
+ // own 'unresolved bash must still surface' comment describes intent this
1312
+ // return never reached. Push the entry directly (no `command`, since
1313
+ // none was ever built) so the gate actually sees it.
1314
+ if (opts.configuredEntrypoints) {
1315
+ opts.configuredEntrypoints.push(...configuredEntrypointsForHook(configDir, hookName, opts));
1316
+ }
1227
1317
  return null;
1318
+ }
1228
1319
  if (opts.portableHooks) {
1229
1320
  const portableBaseDir = projectPortableHookBaseDir({
1230
1321
  configDir,
1231
1322
  homeDir: node_os_1.default.homedir(),
1232
1323
  });
1233
- return projectManagedHookCommand({
1324
+ return track(projectManagedHookCommand({
1234
1325
  absoluteRunner: runner,
1235
1326
  scriptPath: `${portableBaseDir}/hooks/${hookName}`,
1236
1327
  runtime: opts.runtime || 'generic',
1237
1328
  platform,
1238
1329
  hookShell,
1239
- });
1330
+ }));
1240
1331
  }
1241
1332
  const hooksPath = shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName;
1242
- return projectManagedHookCommand({
1333
+ return track(projectManagedHookCommand({
1243
1334
  absoluteRunner: runner,
1244
1335
  scriptPath: hooksPath,
1245
1336
  runtime,
1246
1337
  platform,
1247
1338
  hookShell,
1248
- });
1339
+ }));
1249
1340
  }
1250
1341
  // JS hooks (#3662): the node runner is resolved at hook-fire time, never
1251
1342
  // baked as a bare absolute path — an install-environment absolute path is
@@ -1268,7 +1359,7 @@ function buildHookCommand(configDir, hookName, opts) {
1268
1359
  // Absolute Git-Bash discovery on win32 when available (#580); `bash` on
1269
1360
  // PATH otherwise — the same assumption .sh hooks already make.
1270
1361
  const resolverRunner = resolveBashRunner(opts) || 'bash';
1271
- return shellCmdProjection.projectShellCommandText({
1362
+ return track(shellCmdProjection.projectShellCommandText({
1272
1363
  runnerToken: resolverRunner,
1273
1364
  argTokens: [
1274
1365
  JSON.stringify(`${portableBaseDir}/hooks/${NODE_RUNNER_RESOLVER_HOOK}`),
@@ -1278,19 +1369,19 @@ function buildHookCommand(configDir, hookName, opts) {
1278
1369
  runtime,
1279
1370
  platform,
1280
1371
  hookShell,
1281
- });
1372
+ }));
1282
1373
  }
1283
1374
  const chainRunner = buildNodeRunnerChainToken(opts);
1284
1375
  if (chainRunner === null)
1285
1376
  return null;
1286
1377
  const hooksPath = shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName;
1287
- return shellCmdProjection.projectShellCommandText({
1378
+ return track(shellCmdProjection.projectShellCommandText({
1288
1379
  runnerToken: chainRunner,
1289
1380
  argTokens: [JSON.stringify(hooksPath)],
1290
1381
  runtime,
1291
1382
  platform,
1292
1383
  hookShell,
1293
- });
1384
+ }));
1294
1385
  }
1295
1386
  // ---------------------------------------------------------------------------
1296
1387
  // Cline helpers
@@ -1391,6 +1482,7 @@ function mergeGsdAgentsMd(filePath, gsdContent) {
1391
1482
  // ---------------------------------------------------------------------------
1392
1483
  function writeClineArtifacts(targetDir, isGlobalInstall) {
1393
1484
  const written = [];
1485
+ const configuredEntrypoints = [];
1394
1486
  const clinerulesDir = node_path_1.default.join(targetDir, '.clinerules');
1395
1487
  try {
1396
1488
  if (node_fs_1.default.existsSync(clinerulesDir)) {
@@ -1416,6 +1508,20 @@ function writeClineArtifacts(targetDir, isGlobalInstall) {
1416
1508
  catch { /* Windows: hooks unsupported anyway */ }
1417
1509
  written.push('.clinerules/hooks/PreToolUse');
1418
1510
  console.log(` ${green}✓${reset} Wrote .clinerules/hooks/PreToolUse`);
1511
+ // #4249 (CodeRabbit): Cline invokes this file directly via its own
1512
+ // `#!/usr/bin/env node` shebang — a hybrid case. The script itself still
1513
+ // needs the execute bit (selfExecutable), but unlike GSD's other JS hooks
1514
+ // (which bake an absolute, install-time-resolved node path specifically to
1515
+ // avoid this) its interpreter is looked up on PATH by `env` at hook-fire
1516
+ // time, so `node` must also resolve or the hook can never run.
1517
+ configuredEntrypoints.push({
1518
+ runtime: 'cline',
1519
+ configPath: hookPath,
1520
+ scriptPath: hookPath,
1521
+ interpreterCandidates: ['node'],
1522
+ selfExecutable: true,
1523
+ platform: process.platform,
1524
+ });
1419
1525
  if (isGlobalInstall) {
1420
1526
  try {
1421
1527
  const agentsPath = node_path_1.default.join(node_os_1.default.homedir(), '.agents', 'AGENTS.md');
@@ -1426,7 +1532,7 @@ function writeClineArtifacts(targetDir, isGlobalInstall) {
1426
1532
  console.warn(` ${yellow}⚠${reset} Could not write ~/.agents/AGENTS.md: ${err.message}`);
1427
1533
  }
1428
1534
  }
1429
- return written;
1535
+ return { written, configuredEntrypoints };
1430
1536
  }
1431
1537
  // ---------------------------------------------------------------------------
1432
1538
  // Cursor hook functions
@@ -1677,7 +1783,13 @@ function writeCursorHooksJson(targetDir, src, opts) {
1677
1783
  if (installedScripts.size > 0) {
1678
1784
  ensureCommonJsMarker(hooksDir);
1679
1785
  }
1680
- const hookOpts = { runtime: 'cursor', platform: opts.platform || process.platform };
1786
+ const configuredEntrypoints = [];
1787
+ const hookOpts = {
1788
+ runtime: 'cursor',
1789
+ platform: opts.platform || process.platform,
1790
+ configPath: node_path_1.default.join(targetDir, 'hooks.json'),
1791
+ configuredEntrypoints,
1792
+ };
1681
1793
  const commands = {};
1682
1794
  for (const ev of events) {
1683
1795
  const script = imperative_hook_bus_cjs_1.CURSOR_EVENT_SCRIPT_MAP[ev];
@@ -1691,7 +1803,7 @@ function writeCursorHooksJson(targetDir, src, opts) {
1691
1803
  const managedEntries = (0, imperative_hook_bus_cjs_1.buildHookBusEntries)(events, commands);
1692
1804
  const hooksJsonPath = node_path_1.default.join(targetDir, 'hooks.json');
1693
1805
  const result = reconcileCursorHooksJson(hooksJsonPath, managedEntries);
1694
- return { hooksJsonPath, changed: result.changed };
1806
+ return { hooksJsonPath, changed: result.changed, configuredEntrypoints };
1695
1807
  }
1696
1808
  function removeCursorHooksJson(targetDir) {
1697
1809
  const hooksJsonPath = node_path_1.default.join(targetDir, 'hooks.json');
@@ -1896,7 +2008,13 @@ function writeWindsurfHooksJson(targetDir, src, opts) {
1896
2008
  if (installedScripts.size > 0) {
1897
2009
  ensureCommonJsMarker(hooksDir);
1898
2010
  }
1899
- const hookOpts = { runtime: 'windsurf', platform: opts.platform || process.platform };
2011
+ const configuredEntrypoints = [];
2012
+ const hookOpts = {
2013
+ runtime: 'windsurf',
2014
+ platform: opts.platform || process.platform,
2015
+ configPath: node_path_1.default.join(targetDir, 'hooks.json'),
2016
+ configuredEntrypoints,
2017
+ };
1900
2018
  const commands = {};
1901
2019
  for (const ev of WINDSURF_HOOK_EVENTS) {
1902
2020
  const script = WINDSURF_EVENT_SCRIPT_MAP[ev];
@@ -1910,7 +2028,7 @@ function writeWindsurfHooksJson(targetDir, src, opts) {
1910
2028
  }
1911
2029
  const hooksJsonPath = node_path_1.default.join(targetDir, 'hooks.json');
1912
2030
  const result = reconcileWindsurfHooksJson(hooksJsonPath, managedEntries);
1913
- return { hooksJsonPath, changed: result.changed };
2031
+ return { hooksJsonPath, changed: result.changed, configuredEntrypoints };
1914
2032
  }
1915
2033
  /**
1916
2034
  * Remove all GSD-managed Cascade hook entries from hooks.json. User-owned
@@ -2008,608 +2126,268 @@ function writeCopilotHookConfig(targetDir) {
2008
2126
  node_fs_1.default.writeFileSync(hookPath, JSON.stringify(buildCopilotHookConfig(), null, 2) + '\n');
2009
2127
  return hookPath;
2010
2128
  }
2011
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2012
- function applySettingsJsonHooks(settings, opts) {
2013
- /* eslint-disable @typescript-eslint/no-unsafe-member-access,
2014
- @typescript-eslint/no-unsafe-call,
2015
- @typescript-eslint/no-unsafe-assignment */
2016
- const { runtime, isGlobal, targetDir, postToolEvent, hookEvents, extendedHookEvents, hooksSurface, updateCheckCommand, contextMonitorCommand, promptGuardCommand, readGuardCommand, readInjectionScannerCommand, configReloadCommand, hookOpts, localCmd, localShellCmd, } = opts;
2017
- // ADR-857 phase 5f-3: extended hook events are now driven by the registry
2129
+ const BASH_UNAVAILABLE_MESSAGE = 'Bash executable path unavailable (#3393)';
2130
+ // The tables are exported as a contract; freeze every row (and its nested
2131
+ // objects) so no consumer can mutate a matcher or timeout at runtime.
2132
+ function deepFreeze(value) {
2133
+ for (const child of Object.values(value)) {
2134
+ if (child !== null && typeof child === 'object')
2135
+ deepFreeze(child);
2136
+ }
2137
+ return Object.freeze(value);
2138
+ }
2139
+ const SETTINGS_JSON_HOOK_ROWS = deepFreeze([
2140
+ // Guard: only register if the hook file was actually installed (#1754, #1817).
2141
+ { file: 'gsd-check-update.js', event: 'SessionStart', command: { opts: 'updateCheckCommand' },
2142
+ configuredMessage: 'Configured update check hook', skipLabel: 'update check hook' },
2143
+ { file: 'gsd-context-monitor.js', event: 'post', matcher: 'Bash|Edit|Write|MultiEdit|Agent|Task', timeout: 10,
2144
+ command: { opts: 'contextMonitorCommand' },
2145
+ configuredMessage: 'Configured context window monitor hook', skipLabel: 'context monitor hook',
2146
+ onPresent: { kind: 'migrate-matcher-timeout', message: 'Updated context monitor hook (added matcher + timeout)' } },
2147
+ { file: 'gsd-prompt-guard.js', event: 'pre', matcher: 'Write|Edit', timeout: 'blocking',
2148
+ command: { opts: 'promptGuardCommand' },
2149
+ configuredMessage: 'Configured prompt injection guard hook', skipLabel: 'prompt guard hook' },
2150
+ // Read-before-edit guidance (#1628): advisory-only, does not block.
2151
+ { file: 'gsd-read-guard.js', event: 'pre', matcher: 'Write|Edit', timeout: 5,
2152
+ command: { opts: 'readGuardCommand' },
2153
+ configuredMessage: 'Configured read-before-edit guard hook', skipLabel: 'read guard hook' },
2154
+ // Read-time prompt injection scanning (#2201).
2155
+ { file: 'gsd-read-injection-scanner.js', event: 'post', matcher: 'Read', timeout: 5,
2156
+ command: { opts: 'readInjectionScannerCommand' },
2157
+ configuredMessage: 'Configured read injection scanner hook', skipLabel: 'read injection scanner hook' },
2158
+ // Community hooks — registered on install but opt-in at runtime via .planning/config.json.
2159
+ { file: 'gsd-workflow-guard.js', event: 'pre', matcher: 'Bash|Edit|Write|MultiEdit', timeout: 'blocking',
2160
+ command: { build: 'js' },
2161
+ configuredMessage: 'Configured workflow guard hook (opt-in via hooks.workflow_guard)', skipLabel: 'workflow guard hook',
2162
+ onPresent: { kind: 'repair-matcher', message: 'Updated workflow guard hook matcher' } },
2163
+ // Worktree absolute-path safety (#260).
2164
+ { file: 'gsd-worktree-path-guard.js', event: 'pre', matcher: 'Write|Edit|MultiEdit', timeout: 'blocking',
2165
+ command: { build: 'js' },
2166
+ configuredMessage: 'Configured worktree path guard hook', skipLabel: 'worktree path guard hook' },
2167
+ // Agent-dispatch isolation (#3045); matcher widened to Agent|Task (#3045 MAJOR 1).
2168
+ { file: 'gsd-agent-isolation-guard.js', event: 'pre', matcher: 'Agent|Task', timeout: 'blocking',
2169
+ command: { build: 'js' },
2170
+ configuredMessage: 'Configured agent isolation dispatch guard hook', skipLabel: 'agent isolation guard hook' },
2171
+ // Catastrophic-shrink protection (#2255, fix 3 of #973).
2172
+ { file: 'gsd-write-guard.js', event: 'pre', matcher: 'Write', timeout: 'blocking',
2173
+ command: { build: 'js' },
2174
+ configuredMessage: 'Configured write guard hook (catastrophic-shrink protection)', skipLabel: 'write guard hook' },
2175
+ // Secret-file read protection (#4221).
2176
+ { file: 'gsd-secret-read-guard.js', event: 'pre', matcher: 'Read|Grep|Bash', timeout: 'blocking',
2177
+ command: { build: 'js' },
2178
+ configuredMessage: 'Configured secret read guard hook (.env / .secrets read protection)', skipLabel: 'secret read guard hook' },
2179
+ // The four `.sh` rows: their built commands also feed the #3329 reconcile.
2180
+ { file: 'gsd-validate-commit.sh', event: 'pre', matcher: 'Bash', timeout: 'blocking',
2181
+ command: { build: 'sh' },
2182
+ configuredMessage: 'Configured commit validation hook (opt-in via config)', skipLabel: 'commit validation hook',
2183
+ noCommandMessage: BASH_UNAVAILABLE_MESSAGE },
2184
+ { file: 'gsd-graphify-update.sh', event: 'post', matcher: 'Bash', timeout: 5,
2185
+ command: { build: 'sh' },
2186
+ configuredMessage: 'Configured graphify auto-update hook (opt-in via graphify.auto_update)', skipLabel: 'graphify auto-update hook',
2187
+ noCommandMessage: BASH_UNAVAILABLE_MESSAGE },
2188
+ { file: 'gsd-session-state.sh', event: 'SessionStart',
2189
+ command: { build: 'sh' },
2190
+ configuredMessage: 'Configured session state orientation hook (opt-in via config)', skipLabel: 'session state hook',
2191
+ noCommandMessage: BASH_UNAVAILABLE_MESSAGE },
2192
+ { file: 'gsd-phase-boundary.sh', event: 'post', matcher: 'Write|Edit', timeout: 5,
2193
+ command: { build: 'sh' },
2194
+ configuredMessage: 'Configured phase boundary detection hook (opt-in via config)', skipLabel: 'phase boundary hook',
2195
+ noCommandMessage: BASH_UNAVAILABLE_MESSAGE },
2196
+ ]);
2197
+ // Extended lifecycle events are descriptor-gated (`extendedHookEvents`): a row
2198
+ // registers only for runtimes whose descriptor lists its event. SubagentStart
2199
+ // is qwen/codebuddy-only today; the Before/After Agent/Model rows are an inert
2200
+ // seam (no runtime declares them) that re-activates if one does (#776, #1928).
2201
+ // FileChanged hot-reloads the gsd config on .planning/config.json edits.
2202
+ function contextMonitorRow(event, runtimeLabelSuffix) {
2203
+ return {
2204
+ event,
2205
+ file: 'gsd-context-monitor.js',
2206
+ command: { opts: 'contextMonitorCommand' },
2207
+ timeout: 10,
2208
+ configuredMessage: `Configured ${event} context monitor hook`,
2209
+ ...(runtimeLabelSuffix ? { runtimeLabelSuffix } : {}),
2210
+ };
2211
+ }
2212
+ const SETTINGS_JSON_EXTENDED_ROWS = deepFreeze([
2213
+ contextMonitorRow('SubagentStop', true),
2214
+ contextMonitorRow('Stop', true),
2215
+ contextMonitorRow('PreCompact', true),
2216
+ contextMonitorRow('SubagentStart', true),
2217
+ contextMonitorRow('BeforeAgent', false),
2218
+ contextMonitorRow('AfterAgent', false),
2219
+ contextMonitorRow('BeforeModel', false),
2220
+ { event: 'FileChanged', file: 'gsd-config-reload.js', command: { opts: 'configReloadCommand' }, matcher: 'config.json', timeout: 8,
2221
+ configuredMessage: 'Configured FileChanged config-reload hook (Claude Code)',
2222
+ noCommandMessage: 'Node executable path unavailable' },
2223
+ ]);
2224
+ function hookStem(file) {
2225
+ return file.replace(/\.(?:js|sh)$/, '');
2226
+ }
2227
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access,
2228
+ @typescript-eslint/no-unsafe-call,
2229
+ @typescript-eslint/no-unsafe-assignment,
2230
+ @typescript-eslint/no-explicit-any */
2231
+ function groupReferencesHook(entry, stem) {
2232
+ return Boolean(entry.hooks && entry.hooks.some((h) => referencesHook(h, stem)));
2233
+ }
2234
+ function resolveRowTimeout(timeout) {
2235
+ return timeout === 'blocking' ? BLOCKING_GUARD_TIMEOUT_S : timeout;
2236
+ }
2237
+ function resolveRowCommand(row, opts) {
2238
+ if ('opts' in row.command)
2239
+ return opts[row.command.opts];
2240
+ if (opts.isGlobal)
2241
+ return buildHookCommand(opts.targetDir, row.file, opts.hookOpts);
2242
+ return row.command.build === 'sh' ? opts.localShellCmd(row.file) : opts.localCmd(row.file);
2243
+ }
2244
+ function reconcilePresentHook(row, entries, existing) {
2245
+ const present = row.onPresent;
2246
+ if (!present)
2247
+ return;
2248
+ if (present.kind === 'repair-matcher') {
2249
+ if (existing.matcher !== row.matcher) {
2250
+ existing.matcher = row.matcher;
2251
+ console.log(` ${green}✓${reset} ${present.message}`);
2252
+ }
2253
+ return;
2254
+ }
2255
+ // migrate-matcher-timeout: add the matcher and timeout an older installer omitted.
2256
+ const stem = hookStem(row.file);
2257
+ const timeout = resolveRowTimeout(row.timeout);
2258
+ for (const entry of entries) {
2259
+ if (!groupReferencesHook(entry, stem))
2260
+ continue;
2261
+ let migrated = false;
2262
+ if (!entry.matcher) {
2263
+ entry.matcher = row.matcher;
2264
+ migrated = true;
2265
+ }
2266
+ for (const h of entry.hooks) {
2267
+ if (referencesHook(h, stem) && !h.timeout) {
2268
+ h.timeout = timeout;
2269
+ migrated = true;
2270
+ }
2271
+ }
2272
+ if (migrated)
2273
+ console.log(` ${green}✓${reset} ${present.message}`);
2274
+ }
2275
+ }
2276
+ function pushHookGroup(entries, matcher, command, timeout) {
2277
+ const hook = { type: 'command', command };
2278
+ if (timeout !== undefined)
2279
+ hook['timeout'] = timeout;
2280
+ entries.push(matcher === undefined ? { hooks: [hook] } : { matcher, hooks: [hook] });
2281
+ }
2282
+ function applySettingsJsonHookTables(settings, opts, tables) {
2283
+ const { runtime, postToolEvent, hookEvents, extendedHookEvents, hooksSurface, targetDir, hookOpts } = opts;
2284
+ // ADR-857 phase 5f-3: extended hook events are driven by the registry
2018
2285
  // descriptor field rather than hardcoded runtime-name checks.
2019
2286
  const extendedEvents = Array.isArray(extendedHookEvents) ? extendedHookEvents : [];
2020
2287
  // ADR-857 phase 5g drive 3: hook-skip guard is driven by the hooksSurface
2021
2288
  // descriptor field. Only runtimes with hooksSurface === 'settings-json'
2022
- // register settings.json hooks; runtimes with hooksSurface === 'none'
2023
- // (opencode, kilo) are skipped. Equivalence: hooksSurface !== 'none' iff
2024
- // the old !isOpencode && !isKilo check.
2289
+ // register settings.json hooks; 'none' (opencode, kilo) is skipped.
2025
2290
  // #2095: kimi's hooksSurface is 'kimi-hooks-toml' — it registers hooks into
2026
- // its own native config.toml via writeKimiHooksToml, not settings.json (kimi
2027
- // never writes settings.json at all: writesSharedSettings stays false). This
2028
- // guard must also skip kimi's surface so applySettingsJsonHooks doesn't log
2029
- // misleading "Configured ..." console messages for a settings object that
2030
- // finishInstall() will never persist for kimi.
2031
- if (hooksSurface !== 'none' && hooksSurface !== 'kimi-hooks-toml') {
2032
- if (!settings.hooks) {
2033
- settings.hooks = {};
2034
- }
2035
- if (!settings.hooks.SessionStart) {
2036
- settings.hooks.SessionStart = [];
2037
- }
2038
- // #3981: Claude Code treats a timed-out hook as NON-blocking — the tool
2039
- // call continues through the normal permission flow. The blocking
2040
- // PreToolUse guards therefore need a budget a host stall cannot exceed,
2041
- // not one sized to the hook's own ~0.1 s runtime. Observed stalls reached
2042
- // 84.3 s; 120 s is the top of the issue's prescribed 60–120 range and
2043
- // returns every observed verdict. Registration below uses this constant,
2044
- // and the migration pass right here raises existing managed entries.
2045
- const BLOCKING_GUARD_TIMEOUT_S = 120;
2046
- const blockingGuardNames = [
2047
- 'gsd-prompt-guard',
2048
- 'gsd-workflow-guard',
2049
- 'gsd-worktree-path-guard',
2050
- 'gsd-agent-isolation-guard',
2051
- 'gsd-write-guard',
2052
- 'gsd-secret-read-guard',
2053
- 'gsd-validate-commit',
2054
- ];
2055
- for (const entries of Object.values(settings.hooks)) {
2056
- if (!Array.isArray(entries))
2291
+ // its own native config.toml via writeKimiHooksToml and never writes
2292
+ // settings.json, so this guard must skip it too, or the console would log
2293
+ // "Configured ..." for a settings object finishInstall() never persists.
2294
+ if (hooksSurface === 'none' || hooksSurface === 'kimi-hooks-toml')
2295
+ return;
2296
+ if (!settings.hooks) {
2297
+ settings.hooks = {};
2298
+ }
2299
+ if (!settings.hooks.SessionStart) {
2300
+ settings.hooks.SessionStart = [];
2301
+ }
2302
+ // #3981: raise existing managed blocking-guard entries still on the old
2303
+ // 5 s budget to BLOCKING_GUARD_TIMEOUT_S (module-level; rationale there).
2304
+ for (const entries of Object.values(settings.hooks)) {
2305
+ if (!Array.isArray(entries))
2306
+ continue;
2307
+ for (const entry of entries) {
2308
+ if (!entry || !Array.isArray(entry.hooks))
2057
2309
  continue;
2058
- for (const entry of entries) {
2059
- if (!entry || !Array.isArray(entry.hooks))
2060
- continue;
2061
- for (const h of entry.hooks) {
2062
- if (blockingGuardNames.some((name) => referencesHook(h, name)) &&
2063
- h.timeout === 5) {
2064
- h.timeout = BLOCKING_GUARD_TIMEOUT_S;
2065
- }
2310
+ for (const h of entry.hooks) {
2311
+ if (BLOCKING_GUARD_NAMES.some((name) => referencesHook(h, name)) &&
2312
+ h.timeout === 5) {
2313
+ h.timeout = BLOCKING_GUARD_TIMEOUT_S;
2066
2314
  }
2067
2315
  }
2068
2316
  }
2069
- const hasGsdUpdateHook = settings.hooks.SessionStart.some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-check-update')));
2070
- // Guard: only register if the hook file was actually installed (#1754).
2071
- // When hooks/dist/ is missing from the npm package (as in v1.32.0), the
2072
- // copy step produces no files but the registration step ran unconditionally,
2073
- // causing "hook error" on every tool invocation.
2074
- const checkUpdateFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-check-update.js');
2075
- if (!hasGsdUpdateHook && node_fs_1.default.existsSync(checkUpdateFile) && updateCheckCommand) {
2076
- settings.hooks.SessionStart.push({
2077
- hooks: [
2078
- {
2079
- type: 'command',
2080
- command: updateCheckCommand
2081
- }
2082
- ]
2083
- });
2084
- console.log(` ${green}✓${reset} Configured update check hook`);
2085
- }
2086
- else if (!hasGsdUpdateHook && !node_fs_1.default.existsSync(checkUpdateFile)) {
2087
- console.warn(` ${yellow}⚠${reset} Skipped update check hook — gsd-check-update.js not found at target`);
2088
- }
2089
- // Configure post-tool hook for context window monitoring
2090
- if (!settings.hooks[postToolEvent]) {
2091
- settings.hooks[postToolEvent] = [];
2092
- }
2093
- const hasContextMonitorHook = settings.hooks[postToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-context-monitor')));
2094
- const contextMonitorFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-context-monitor.js');
2095
- if (!hasContextMonitorHook && node_fs_1.default.existsSync(contextMonitorFile) && contextMonitorCommand) {
2096
- settings.hooks[postToolEvent].push({
2097
- matcher: 'Bash|Edit|Write|MultiEdit|Agent|Task',
2098
- hooks: [
2099
- {
2100
- type: 'command',
2101
- command: contextMonitorCommand,
2102
- timeout: 10
2103
- }
2104
- ]
2105
- });
2106
- console.log(` ${green}✓${reset} Configured context window monitor hook`);
2317
+ }
2318
+ // hookEvents='gemini' → BeforeTool/AfterTool; all others → PreToolUse/PostToolUse.
2319
+ const preToolEvent = hookEvents === 'gemini' ? 'BeforeTool' : 'PreToolUse';
2320
+ const eventKey = (event) => event === 'SessionStart' ? 'SessionStart' : event === 'post' ? postToolEvent : preToolEvent;
2321
+ // Built `.sh` commands, in row order, feed the #3329 reconcile below.
2322
+ const shellCommands = {};
2323
+ for (const row of tables.rows) {
2324
+ const key = eventKey(row.event);
2325
+ if (!settings.hooks[key]) {
2326
+ settings.hooks[key] = [];
2327
+ }
2328
+ const entries = settings.hooks[key];
2329
+ const command = resolveRowCommand(row, opts);
2330
+ if ('build' in row.command && row.command.build === 'sh')
2331
+ shellCommands[row.file] = command;
2332
+ const stem = hookStem(row.file);
2333
+ const existing = entries.find((entry) => groupReferencesHook(entry, stem));
2334
+ if (existing) {
2335
+ reconcilePresentHook(row, entries, existing);
2336
+ continue;
2107
2337
  }
2108
- else if (!hasContextMonitorHook && !node_fs_1.default.existsSync(contextMonitorFile)) {
2109
- console.warn(` ${yellow}⚠${reset} Skipped context monitor hook — gsd-context-monitor.js not found at target`);
2338
+ if (!node_fs_1.default.existsSync(node_path_1.default.join(targetDir, 'hooks', row.file))) {
2339
+ console.warn(` ${yellow}⚠${reset} Skipped ${row.skipLabel} — ${row.file} not found at target`);
2110
2340
  }
2111
- else {
2112
- // Migrate existing context monitor hooks: add matcher and timeout if missing
2113
- for (const entry of settings.hooks[postToolEvent]) {
2114
- if (entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-context-monitor'))) {
2115
- let migrated = false;
2116
- if (!entry.matcher) {
2117
- entry.matcher = 'Bash|Edit|Write|MultiEdit|Agent|Task';
2118
- migrated = true;
2119
- }
2120
- for (const h of entry.hooks) {
2121
- if (referencesHook(h, 'gsd-context-monitor') && !h.timeout) {
2122
- h.timeout = 10;
2123
- migrated = true;
2124
- }
2125
- }
2126
- if (migrated) {
2127
- console.log(` ${green}✓${reset} Updated context monitor hook (added matcher + timeout)`);
2128
- }
2129
- }
2130
- }
2341
+ else if (command) {
2342
+ pushHookGroup(entries, row.matcher, command, resolveRowTimeout(row.timeout));
2343
+ console.log(` ${green}✓${reset} ${row.configuredMessage}`);
2131
2344
  }
2132
- // Configure PreToolUse hook for prompt injection detection
2133
- // ADR-857 phase 5f-2: drive dialect from opts.hookEvents (registry descriptor).
2134
- // hookEvents='gemini' → BeforeTool; all others → PreToolUse.
2135
- // Equivalence: hookEvents='gemini' iff runtime===antigravity (same as old check).
2136
- const preToolEvent = hookEvents === 'gemini' ? 'BeforeTool' : 'PreToolUse';
2137
- if (!settings.hooks[preToolEvent]) {
2138
- settings.hooks[preToolEvent] = [];
2139
- }
2140
- const hasPromptGuardHook = settings.hooks[preToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-prompt-guard')));
2141
- const promptGuardFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-prompt-guard.js');
2142
- if (!hasPromptGuardHook && node_fs_1.default.existsSync(promptGuardFile) && promptGuardCommand) {
2143
- settings.hooks[preToolEvent].push({
2144
- matcher: 'Write|Edit',
2145
- hooks: [
2146
- {
2147
- type: 'command',
2148
- command: promptGuardCommand,
2149
- timeout: BLOCKING_GUARD_TIMEOUT_S
2150
- }
2151
- ]
2152
- });
2153
- console.log(` ${green}✓${reset} Configured prompt injection guard hook`);
2154
- }
2155
- else if (!hasPromptGuardHook && !node_fs_1.default.existsSync(promptGuardFile)) {
2156
- console.warn(` ${yellow}⚠${reset} Skipped prompt guard hook — gsd-prompt-guard.js not found at target`);
2157
- }
2158
- // Configure PreToolUse hook for read-before-edit guidance (#1628)
2159
- // Prevents infinite retry loops when non-Claude models attempt to edit
2160
- // files without reading them first. Advisory-only — does not block.
2161
- const hasReadGuardHook = settings.hooks[preToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-read-guard')));
2162
- const readGuardFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-read-guard.js');
2163
- if (!hasReadGuardHook && node_fs_1.default.existsSync(readGuardFile) && readGuardCommand) {
2164
- settings.hooks[preToolEvent].push({
2165
- matcher: 'Write|Edit',
2166
- hooks: [
2167
- {
2168
- type: 'command',
2169
- command: readGuardCommand,
2170
- timeout: 5
2171
- }
2172
- ]
2173
- });
2174
- console.log(` ${green}✓${reset} Configured read-before-edit guard hook`);
2175
- }
2176
- else if (!hasReadGuardHook && !node_fs_1.default.existsSync(readGuardFile)) {
2177
- console.warn(` ${yellow}⚠${reset} Skipped read guard hook — gsd-read-guard.js not found at target`);
2178
- }
2179
- // Configure PostToolUse hook for read-time prompt injection scanning (#2201)
2180
- // Scans content returned by the Read tool for injection patterns, including
2181
- // summarisation-specific patterns that survive context compression.
2182
- const hasReadInjectionScannerHook = settings.hooks[postToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-read-injection-scanner')));
2183
- const readInjectionScannerFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-read-injection-scanner.js');
2184
- if (!hasReadInjectionScannerHook && node_fs_1.default.existsSync(readInjectionScannerFile) && readInjectionScannerCommand) {
2185
- settings.hooks[postToolEvent].push({
2186
- matcher: 'Read',
2187
- hooks: [
2188
- {
2189
- type: 'command',
2190
- command: readInjectionScannerCommand,
2191
- timeout: 5
2192
- }
2193
- ]
2194
- });
2195
- console.log(` ${green}✓${reset} Configured read injection scanner hook`);
2196
- }
2197
- else if (!hasReadInjectionScannerHook && !node_fs_1.default.existsSync(readInjectionScannerFile)) {
2198
- console.warn(` ${yellow}⚠${reset} Skipped read injection scanner hook — gsd-read-injection-scanner.js not found at target`);
2199
- }
2200
- // Community hooks — registered on install but opt-in at runtime.
2201
- // Each hook checks .planning/config.json for hooks.community: true
2202
- // and exits silently (no-op) if not enabled. This lets users enable
2203
- // them per-project by adding: "hooks": { "community": true }
2204
- // Configure workflow guard hook (opt-in via hooks.workflow_guard: true)
2205
- // Detects file edits outside GSD workflow context and advises using
2206
- // /gsd-quick or /gsd-fast for state-tracked changes. Also hard-blocks
2207
- // unsafe Bash commands that violate worktree-agent isolation.
2208
- const workflowGuardCommand = isGlobal
2209
- ? buildHookCommand(targetDir, 'gsd-workflow-guard.js', hookOpts)
2210
- : localCmd('gsd-workflow-guard.js');
2211
- const workflowGuardMatcher = 'Bash|Edit|Write|MultiEdit';
2212
- const workflowGuardHookEntry = settings.hooks[preToolEvent].find((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-workflow-guard')));
2213
- const hasWorkflowGuardHook = Boolean(workflowGuardHookEntry);
2214
- const workflowGuardFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-workflow-guard.js');
2215
- if (hasWorkflowGuardHook && workflowGuardHookEntry.matcher !== workflowGuardMatcher) {
2216
- workflowGuardHookEntry.matcher = workflowGuardMatcher;
2217
- console.log(` ${green}✓${reset} Updated workflow guard hook matcher`);
2218
- }
2219
- else if (!hasWorkflowGuardHook && node_fs_1.default.existsSync(workflowGuardFile) && workflowGuardCommand) {
2220
- settings.hooks[preToolEvent].push({
2221
- matcher: workflowGuardMatcher,
2222
- hooks: [
2223
- {
2224
- type: 'command',
2225
- command: workflowGuardCommand,
2226
- timeout: BLOCKING_GUARD_TIMEOUT_S
2227
- }
2228
- ]
2229
- });
2230
- console.log(` ${green}✓${reset} Configured workflow guard hook (opt-in via hooks.workflow_guard)`);
2231
- }
2232
- else if (!hasWorkflowGuardHook && !node_fs_1.default.existsSync(workflowGuardFile)) {
2233
- console.warn(` ${yellow}⚠${reset} Skipped workflow guard hook — gsd-workflow-guard.js not found at target`);
2234
- }
2235
- // Configure PreToolUse hook for worktree absolute-path safety (#260)
2236
- // Hard-blocks Edit/Write/MultiEdit tool calls with absolute paths that resolve
2237
- // outside the current worktree root. Prevents executor agents from
2238
- // accidentally writing to the main checkout when running in isolation="worktree".
2239
- const worktreePathGuardCommand = isGlobal
2240
- ? buildHookCommand(targetDir, 'gsd-worktree-path-guard.js', hookOpts)
2241
- : localCmd('gsd-worktree-path-guard.js');
2242
- const hasWorktreePathGuardHook = settings.hooks[preToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-worktree-path-guard')));
2243
- const worktreePathGuardFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-worktree-path-guard.js');
2244
- if (!hasWorktreePathGuardHook && node_fs_1.default.existsSync(worktreePathGuardFile) && worktreePathGuardCommand) {
2245
- settings.hooks[preToolEvent].push({
2246
- matcher: 'Write|Edit|MultiEdit',
2247
- hooks: [
2248
- {
2249
- type: 'command',
2250
- command: worktreePathGuardCommand,
2251
- timeout: BLOCKING_GUARD_TIMEOUT_S
2252
- }
2253
- ]
2254
- });
2255
- console.log(` ${green}✓${reset} Configured worktree path guard hook`);
2256
- }
2257
- else if (!hasWorktreePathGuardHook && !node_fs_1.default.existsSync(worktreePathGuardFile)) {
2258
- console.warn(` ${yellow}⚠${reset} Skipped worktree path guard hook — gsd-worktree-path-guard.js not found at target`);
2259
- }
2260
- // Configure PreToolUse hook for Agent-dispatch isolation (#3045)
2261
- // Hard-blocks an executor Agent() dispatch (subagent_type="gsd-executor")
2262
- // missing its harness isolation parameter when this project's resolved
2263
- // dispatch isolation is harness-worktree. Prevents the executor from
2264
- // silently running and committing in the primary checkout when the
2265
- // model-authored dispatch omits isolation="worktree".
2266
- const agentIsolationGuardCommand = isGlobal
2267
- ? buildHookCommand(targetDir, 'gsd-agent-isolation-guard.js', hookOpts)
2268
- : localCmd('gsd-agent-isolation-guard.js');
2269
- const hasAgentIsolationGuardHook = settings.hooks[preToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-agent-isolation-guard')));
2270
- const agentIsolationGuardFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-agent-isolation-guard.js');
2271
- if (!hasAgentIsolationGuardHook && node_fs_1.default.existsSync(agentIsolationGuardFile) && agentIsolationGuardCommand) {
2272
- settings.hooks[preToolEvent].push({
2273
- // #3045 MAJOR 1: widened from "Agent"-only — hooks.json's own
2274
- // PostToolUse precedent (context-monitor) already hedges both names,
2275
- // and the hook itself now accepts tool_name "Task" too.
2276
- matcher: 'Agent|Task',
2277
- hooks: [
2278
- {
2279
- type: 'command',
2280
- command: agentIsolationGuardCommand,
2281
- timeout: BLOCKING_GUARD_TIMEOUT_S
2282
- }
2283
- ]
2284
- });
2285
- console.log(` ${green}✓${reset} Configured agent isolation dispatch guard hook`);
2286
- }
2287
- else if (!hasAgentIsolationGuardHook && !node_fs_1.default.existsSync(agentIsolationGuardFile)) {
2288
- console.warn(` ${yellow}⚠${reset} Skipped agent isolation guard hook — gsd-agent-isolation-guard.js not found at target`);
2289
- }
2290
- // Configure PreToolUse hook for catastrophic-shrink protection (#2255, fix 3 of #973)
2291
- // Hard-blocks a whole-file Write that collapses a curated .planning/ artifact
2292
- // (ROADMAP.md, milestone roadmaps, STATE.md) far below its on-disk size.
2293
- // Escape hatches (both named in the block message): the single-use
2294
- // sentinel .planning/.gsd-allow-shrink (workflow steps — a per-step env
2295
- // cannot reach a hook) and GSD_ALLOW_PLANNING_SHRINK=1 (interactive).
2296
- const writeGuardCommand = isGlobal
2297
- ? buildHookCommand(targetDir, 'gsd-write-guard.js', hookOpts)
2298
- : localCmd('gsd-write-guard.js');
2299
- const hasWriteGuardHook = settings.hooks[preToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-write-guard')));
2300
- const writeGuardFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-write-guard.js');
2301
- if (!hasWriteGuardHook && node_fs_1.default.existsSync(writeGuardFile) && writeGuardCommand) {
2302
- settings.hooks[preToolEvent].push({
2303
- matcher: 'Write',
2304
- hooks: [
2305
- {
2306
- type: 'command',
2307
- command: writeGuardCommand,
2308
- timeout: BLOCKING_GUARD_TIMEOUT_S
2309
- }
2310
- ]
2311
- });
2312
- console.log(` ${green}✓${reset} Configured write guard hook (catastrophic-shrink protection)`);
2313
- }
2314
- else if (!hasWriteGuardHook && !node_fs_1.default.existsSync(writeGuardFile)) {
2315
- console.warn(` ${yellow}⚠${reset} Skipped write guard hook — gsd-write-guard.js not found at target`);
2316
- }
2317
- // Configure PreToolUse hook for secret-file read protection (#4221).
2318
- // Hard-blocks Read/Grep/Bash reads of .env, .env.<suffix> and .secrets.
2319
- // Replaces the Read(.env*) permission deny rules the installer used to
2320
- // write (#768): on Claude Code >= 2.1.259 ANY Read() deny rule makes every
2321
- // `cd DIR && grep …` compound prompt for approval, even in auto mode; a
2322
- // hook denial is not a permission rule and never arms that check.
2323
- const secretReadGuardCommand = isGlobal
2324
- ? buildHookCommand(targetDir, 'gsd-secret-read-guard.js', hookOpts)
2325
- : localCmd('gsd-secret-read-guard.js');
2326
- const hasSecretReadGuardHook = settings.hooks[preToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-secret-read-guard')));
2327
- const secretReadGuardFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-secret-read-guard.js');
2328
- if (!hasSecretReadGuardHook && node_fs_1.default.existsSync(secretReadGuardFile) && secretReadGuardCommand) {
2329
- settings.hooks[preToolEvent].push({
2330
- matcher: 'Read|Grep|Bash',
2331
- hooks: [
2332
- {
2333
- type: 'command',
2334
- command: secretReadGuardCommand,
2335
- timeout: BLOCKING_GUARD_TIMEOUT_S
2336
- }
2337
- ]
2338
- });
2339
- console.log(` ${green}✓${reset} Configured secret read guard hook (.env / .secrets read protection)`);
2340
- }
2341
- else if (!hasSecretReadGuardHook && !node_fs_1.default.existsSync(secretReadGuardFile)) {
2342
- console.warn(` ${yellow}⚠${reset} Skipped secret read guard hook — gsd-secret-read-guard.js not found at target`);
2343
- }
2344
- // Configure commit validation hook (Conventional Commits enforcement, opt-in)
2345
- const validateCommitCommand = isGlobal
2346
- ? buildHookCommand(targetDir, 'gsd-validate-commit.sh', hookOpts)
2347
- : localShellCmd('gsd-validate-commit.sh');
2348
- const hasValidateCommitHook = settings.hooks[preToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-validate-commit')));
2349
- // Guard: only register if the .sh file was actually installed. If the npm package
2350
- // omitted the file (as happened in v1.32.0, bug #1817), registering a missing hook
2351
- // causes a hook error on every Bash tool invocation.
2352
- const validateCommitFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-validate-commit.sh');
2353
- if (!hasValidateCommitHook && node_fs_1.default.existsSync(validateCommitFile) && validateCommitCommand) {
2354
- settings.hooks[preToolEvent].push({
2355
- matcher: 'Bash',
2356
- hooks: [
2357
- {
2358
- type: 'command',
2359
- command: validateCommitCommand,
2360
- timeout: BLOCKING_GUARD_TIMEOUT_S
2361
- }
2362
- ]
2363
- });
2364
- console.log(` ${green}✓${reset} Configured commit validation hook (opt-in via config)`);
2365
- }
2366
- else if (!hasValidateCommitHook && !node_fs_1.default.existsSync(validateCommitFile)) {
2367
- console.warn(` ${yellow}⚠${reset} Skipped commit validation hook — gsd-validate-commit.sh not found at target`);
2368
- }
2369
- else if (!hasValidateCommitHook && !validateCommitCommand) {
2370
- console.warn(` ${yellow}⚠${reset} Skipped commit validation hook — Bash executable path unavailable (#3393)`);
2371
- }
2372
- // Configure graphify auto-update hook (opt-in via graphify.auto_update; default false, #3347).
2373
- // PostToolUse Bash matcher — fires after git commit/merge/pull/rebase --continue/cherry-pick
2374
- // on the default branch, dispatches `graphify update .` in a detached subprocess. No-op unless
2375
- // .planning/config.json has BOTH graphify.enabled=true AND graphify.auto_update=true.
2376
- const graphifyUpdateCommand = isGlobal
2377
- ? buildHookCommand(targetDir, 'gsd-graphify-update.sh', hookOpts)
2378
- : localShellCmd('gsd-graphify-update.sh');
2379
- const hasGraphifyUpdateHook = settings.hooks[postToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-graphify-update')));
2380
- const graphifyUpdateFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-graphify-update.sh');
2381
- if (!hasGraphifyUpdateHook && node_fs_1.default.existsSync(graphifyUpdateFile) && graphifyUpdateCommand) {
2382
- settings.hooks[postToolEvent].push({
2383
- matcher: 'Bash',
2384
- hooks: [
2385
- {
2386
- type: 'command',
2387
- command: graphifyUpdateCommand,
2388
- timeout: 5
2389
- }
2390
- ]
2391
- });
2392
- console.log(` ${green}✓${reset} Configured graphify auto-update hook (opt-in via graphify.auto_update)`);
2393
- }
2394
- else if (!hasGraphifyUpdateHook && !node_fs_1.default.existsSync(graphifyUpdateFile)) {
2395
- console.warn(` ${yellow}⚠${reset} Skipped graphify auto-update hook — gsd-graphify-update.sh not found at target`);
2396
- }
2397
- else if (!hasGraphifyUpdateHook && !graphifyUpdateCommand) {
2398
- console.warn(` ${yellow}⚠${reset} Skipped graphify auto-update hook — Bash executable path unavailable (#3393)`);
2399
- }
2400
- // Configure session state orientation hook (opt-in)
2401
- const sessionStateCommand = isGlobal
2402
- ? buildHookCommand(targetDir, 'gsd-session-state.sh', hookOpts)
2403
- : localShellCmd('gsd-session-state.sh');
2404
- const hasSessionStateHook = settings.hooks.SessionStart.some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-session-state')));
2405
- const sessionStateFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-session-state.sh');
2406
- if (!hasSessionStateHook && node_fs_1.default.existsSync(sessionStateFile) && sessionStateCommand) {
2407
- settings.hooks.SessionStart.push({
2408
- hooks: [
2409
- {
2410
- type: 'command',
2411
- command: sessionStateCommand
2412
- }
2413
- ]
2414
- });
2415
- console.log(` ${green}✓${reset} Configured session state orientation hook (opt-in via config)`);
2416
- }
2417
- else if (!hasSessionStateHook && !node_fs_1.default.existsSync(sessionStateFile)) {
2418
- console.warn(` ${yellow}⚠${reset} Skipped session state hook — gsd-session-state.sh not found at target`);
2419
- }
2420
- else if (!hasSessionStateHook && !sessionStateCommand) {
2421
- console.warn(` ${yellow}⚠${reset} Skipped session state hook — Bash executable path unavailable (#3393)`);
2422
- }
2423
- // Configure phase boundary detection hook (opt-in)
2424
- const phaseBoundaryCommand = isGlobal
2425
- ? buildHookCommand(targetDir, 'gsd-phase-boundary.sh', hookOpts)
2426
- : localShellCmd('gsd-phase-boundary.sh');
2427
- const hasPhaseBoundaryHook = settings.hooks[postToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-phase-boundary')));
2428
- const phaseBoundaryFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-phase-boundary.sh');
2429
- if (!hasPhaseBoundaryHook && node_fs_1.default.existsSync(phaseBoundaryFile) && phaseBoundaryCommand) {
2430
- settings.hooks[postToolEvent].push({
2431
- matcher: 'Write|Edit',
2432
- hooks: [
2433
- {
2434
- type: 'command',
2435
- command: phaseBoundaryCommand,
2436
- timeout: 5
2437
- }
2438
- ]
2439
- });
2440
- console.log(` ${green}✓${reset} Configured phase boundary detection hook (opt-in via config)`);
2441
- }
2442
- else if (!hasPhaseBoundaryHook && !node_fs_1.default.existsSync(phaseBoundaryFile)) {
2443
- console.warn(` ${yellow}⚠${reset} Skipped phase boundary hook — gsd-phase-boundary.sh not found at target`);
2444
- }
2445
- else if (!hasPhaseBoundaryHook && !phaseBoundaryCommand) {
2446
- console.warn(` ${yellow}⚠${reset} Skipped phase boundary hook — Bash executable path unavailable (#3393)`);
2447
- }
2448
- // #3329: the four `.sh` sites above register only-if-absent, so an entry
2449
- // registered by an older installer keeps its old command forever —
2450
- // /gsd-update (which re-invokes the installer) never re-derived it. On
2451
- // Claude/win32 that left the pre-#580/#3393 bash-runner-prefixed commands
2452
- // in settings.json indefinitely. Reconcile existing managed `.sh` entries
2453
- // to the command this install would generate today. Inert wherever the
2454
- // bash runner is still the correct shape; scoped to exact managed
2455
- // basenames so user-authored hooks are never touched.
2456
- if (reconcileManagedShellHookCommands(settings, {
2457
- 'gsd-validate-commit.sh': validateCommitCommand,
2458
- 'gsd-graphify-update.sh': graphifyUpdateCommand,
2459
- 'gsd-session-state.sh': sessionStateCommand,
2460
- 'gsd-phase-boundary.sh': phaseBoundaryCommand,
2461
- }, { platform: hookOpts.platform, runtime })) {
2462
- console.log(` ${green}✓${reset} Reconciled managed .sh hook commands to current format (#3329)`);
2463
- }
2464
- // ── Extended hook events: SubagentStop / Stop / PreCompact / SubagentStart
2465
- // (#788 + #770 + #2092) ────────────────────────────────────────────────
2466
- // Claude Code (since #770) and Qwen Code (since #788) both support the
2467
- // SubagentStop / Stop / PreCompact lifecycle events. Qwen Code additionally
2468
- // supports SubagentStart (#2092 Phase B, Upgrade 2). Wire gsd-context-
2469
- // monitor so agents get context-headroom warnings at subagent start,
2470
- // subagent completion, model stop, and pre-compaction (the most critical
2471
- // moment to surface headroom info).
2472
- //
2473
- // SubagentStart — subagent lifecycle start (context headroom tracking;
2474
- // qwen-only today — no other runtime declares it in
2475
- // extendedHookEvents)
2476
- // SubagentStop — subagent lifecycle completion (context headroom tracking)
2477
- // Stop — model stop / final-response moment (context headroom)
2478
- // PreCompact — fires before conversation compaction (most critical
2479
- // moment to surface context headroom warnings)
2480
- //
2481
- // Note: UserPromptSubmit is NOT wired here. That event carries the raw
2482
- // user prompt text, not a tool invocation, so gsd-prompt-guard (which
2483
- // exits unless tool_name is Write/Edit) would be a silent no-op. A
2484
- // dedicated handler for UserPromptSubmit is deferred to a follow-on issue.
2485
- // SubagentStart, SubagentStop, Stop, PreCompact — route through the context monitor.
2486
- // Guard is descriptor-driven: only events present in extendedEvents are wired,
2487
- // so this loop is a no-op for every runtime that doesn't list SubagentStart.
2488
- {
2489
- // Descriptor-driven (ADR-1239 / #2092): folded from a hardcoded
2490
- // `runtime === 'qwen' ? ... : ...` ternary into a capability-title
2491
- // lookup (see _capabilityTitle above).
2492
- const runtimeLabel = _capabilityTitle(runtime);
2493
- for (const event of ['SubagentStop', 'Stop', 'PreCompact', 'SubagentStart']) {
2494
- if (!extendedEvents.includes(event))
2495
- continue;
2496
- if (!settings.hooks[event]) {
2497
- settings.hooks[event] = [];
2498
- }
2499
- const alreadyHasContextMonitor = settings.hooks[event].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-context-monitor')));
2500
- if (!alreadyHasContextMonitor && node_fs_1.default.existsSync(contextMonitorFile) && contextMonitorCommand) {
2501
- settings.hooks[event].push({
2502
- hooks: [
2503
- {
2504
- type: 'command',
2505
- command: contextMonitorCommand,
2506
- timeout: 10
2507
- }
2508
- ]
2509
- });
2510
- console.log(` ${green}✓${reset} Configured ${event} context monitor hook (${runtimeLabel})`);
2511
- }
2512
- else if (!alreadyHasContextMonitor && !node_fs_1.default.existsSync(contextMonitorFile)) {
2513
- console.warn(` ${yellow}⚠${reset} Skipped ${event} hook — gsd-context-monitor.js not found at target`);
2514
- }
2515
- }
2345
+ else if (row.noCommandMessage) {
2346
+ console.warn(` ${yellow}⚠${reset} Skipped ${row.skipLabel} — ${row.noCommandMessage}`);
2516
2347
  }
2517
- // ── end SubagentStop / Stop / PreCompact / SubagentStart events ────────────
2518
- // ── Extended hook events (#776; Gemini runtime removed #1928) ──────────────
2519
- // The Gemini-3-backend dialect exposes several hook events beyond
2520
- // BeforeTool/AfterTool. These were added for the now-removed Gemini CLI
2521
- // runtime (#776). No currently supported runtime declares them —
2522
- // Antigravity's descriptor carries `extendedHookEvents: []` — so this loop
2523
- // is an inert, descriptor-driven seam: it no-ops for every present runtime
2524
- // and re-activates automatically if a future runtime declares any of them.
2525
- // Three high-value events would be wired here:
2526
- //
2527
- // BeforeAgent — fires after user submits a prompt, before the agent
2528
- // plans. Wire gsd-context-monitor for context headroom
2529
- // awareness at prompt time.
2530
- // AfterAgent — fires once per turn after the model generates its final
2531
- // response. Wire gsd-context-monitor to track headroom
2532
- // after each agent turn completes.
2533
- // BeforeModel — fires before each LLM call (per-turn, not per-session).
2534
- // Wire gsd-context-monitor for per-turn context injection
2535
- // — more precise than session-start-only injection.
2536
- //
2537
- // All three reuse gsd-context-monitor.js — no new hook files needed.
2538
- // The `decision:"deny"` retry capability of AfterAgent is intentionally
2539
- // left to the hook script to implement when triggered (gsd-context-monitor
2540
- // exits 0 / advisory-only today; an active quality gate is a follow-on).
2541
- //
2542
- // Note: BeforeToolSelection is NOT wired. That event does not map to a
2543
- // gsd hook use case at this time; deferred to a follow-on issue.
2544
- //
2545
- // Guard is now descriptor-driven: only events present in extendedEvents are wired.
2546
- for (const extendedEvent of ['BeforeAgent', 'AfterAgent', 'BeforeModel']) {
2547
- if (!extendedEvents.includes(extendedEvent))
2548
- continue;
2549
- if (!Array.isArray(settings.hooks[extendedEvent])) {
2550
- settings.hooks[extendedEvent] = [];
2551
- }
2552
- const alreadyHasContextMonitor = settings.hooks[extendedEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-context-monitor')));
2553
- if (!alreadyHasContextMonitor && node_fs_1.default.existsSync(contextMonitorFile) && contextMonitorCommand) {
2554
- settings.hooks[extendedEvent].push({
2555
- hooks: [
2556
- {
2557
- type: 'command',
2558
- command: contextMonitorCommand,
2559
- timeout: 10
2560
- }
2561
- ]
2562
- });
2563
- console.log(` ${green}✓${reset} Configured ${extendedEvent} context monitor hook`);
2564
- }
2565
- else if (!alreadyHasContextMonitor && !node_fs_1.default.existsSync(contextMonitorFile)) {
2566
- console.warn(` ${yellow}⚠${reset} Skipped ${extendedEvent} hook — gsd-context-monitor.js not found at target`);
2567
- }
2348
+ }
2349
+ // #3329: `.sh` rows register only-if-absent, so an entry registered by an
2350
+ // older installer keeps its old command forever — /gsd-update (which
2351
+ // re-invokes the installer) never re-derived it. On Claude/win32 that left
2352
+ // the pre-#580/#3393 bash-runner-prefixed commands in settings.json
2353
+ // indefinitely. Reconcile existing managed `.sh` entries to the command this
2354
+ // install would generate today. Inert wherever the bash runner is still the
2355
+ // correct shape; scoped to exact managed basenames so user-authored hooks
2356
+ // are never touched.
2357
+ if (reconcileManagedShellHookCommands(settings, shellCommands, { platform: hookOpts.platform, runtime })) {
2358
+ console.log(` ${green}✓${reset} Reconciled managed .sh hook commands to current format (#3329)`);
2359
+ }
2360
+ // Descriptor-driven (ADR-1239 / #2092): the runtime's capability title.
2361
+ const runtimeLabel = _capabilityTitle(runtime);
2362
+ for (const row of tables.extendedRows) {
2363
+ if (!extendedEvents.includes(row.event))
2364
+ continue;
2365
+ if (!Array.isArray(settings.hooks[row.event])) {
2366
+ settings.hooks[row.event] = [];
2568
2367
  }
2569
- // ── end Antigravity-only extended hook events ──────────────────────────────
2570
- // ── FileChanged hook: hot-reload gsd config on .planning/config.json edits ─
2571
- // Claude Code fires FileChanged when a watched file changes on disk. Wire
2572
- // gsd-config-reload.js to reload the gsd config context whenever the user
2573
- // edits .planning/config.json mid-session, eliminating the need to restart.
2574
- //
2575
- // The matcher "config.json" watches for changes to any file named config.json
2576
- // (Claude Code matches by filename, not full path). The hook exits silently
2577
- // when the changed file is not the gsd config.
2578
- //
2579
- // Scoped to Claude Code only: Qwen Code's FileChanged support is not yet
2580
- // verified; extend in a follow-on if empirically confirmed.
2581
- if (extendedEvents.includes('FileChanged')) {
2582
- if (!settings.hooks.FileChanged) {
2583
- settings.hooks.FileChanged = [];
2584
- }
2585
- const configReloadFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-config-reload.js');
2586
- const alreadyHasConfigReload = settings.hooks.FileChanged.some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-config-reload')));
2587
- if (!alreadyHasConfigReload && node_fs_1.default.existsSync(configReloadFile) && configReloadCommand) {
2588
- settings.hooks.FileChanged.push({
2589
- matcher: 'config.json',
2590
- hooks: [
2591
- {
2592
- type: 'command',
2593
- command: configReloadCommand,
2594
- timeout: 8
2595
- }
2596
- ]
2597
- });
2598
- console.log(` ${green}✓${reset} Configured FileChanged config-reload hook (Claude Code)`);
2599
- }
2600
- else if (!alreadyHasConfigReload && !node_fs_1.default.existsSync(configReloadFile)) {
2601
- console.warn(` ${yellow}⚠${reset} Skipped FileChanged hook — gsd-config-reload.js not found at target`);
2602
- }
2603
- else if (!alreadyHasConfigReload && !configReloadCommand) {
2604
- console.warn(` ${yellow}⚠${reset} Skipped FileChanged hook — Node executable path unavailable`);
2605
- }
2368
+ const entries = settings.hooks[row.event];
2369
+ if (entries.some((entry) => groupReferencesHook(entry, hookStem(row.file))))
2370
+ continue;
2371
+ const command = opts[row.command.opts];
2372
+ if (!node_fs_1.default.existsSync(node_path_1.default.join(targetDir, 'hooks', row.file))) {
2373
+ console.warn(` ${yellow}⚠${reset} Skipped ${row.event} hook — ${row.file} not found at target`);
2374
+ }
2375
+ else if (command) {
2376
+ pushHookGroup(entries, row.matcher, command, row.timeout);
2377
+ console.log(` ${green}✓${reset} ${row.configuredMessage}${row.runtimeLabelSuffix ? ` (${runtimeLabel})` : ''}`);
2378
+ }
2379
+ else if (row.noCommandMessage) {
2380
+ console.warn(` ${yellow}⚠${reset} Skipped ${row.event} hook — ${row.noCommandMessage}`);
2606
2381
  }
2607
- // ── end FileChanged hook ────────────────────────────────────────────────────
2608
2382
  }
2609
- /* eslint-enable @typescript-eslint/no-unsafe-member-access,
2610
- @typescript-eslint/no-unsafe-call,
2611
- @typescript-eslint/no-unsafe-assignment */
2612
2383
  }
2384
+ function applySettingsJsonHooks(settings, opts) {
2385
+ applySettingsJsonHookTables(settings, opts, { rows: SETTINGS_JSON_HOOK_ROWS, extendedRows: SETTINGS_JSON_EXTENDED_ROWS });
2386
+ }
2387
+ /* eslint-enable @typescript-eslint/no-unsafe-member-access,
2388
+ @typescript-eslint/no-unsafe-call,
2389
+ @typescript-eslint/no-unsafe-assignment,
2390
+ @typescript-eslint/no-explicit-any */
2613
2391
  // ---------------------------------------------------------------------------
2614
2392
  // Kimi hooks.toml (#2095 EoS/kimi Upgrade 1 — native hook bus)
2615
2393
  //
@@ -2682,13 +2460,13 @@ function buildKimiHooksTomlBlock(targetDir, opts) {
2682
2460
  { event: 'SessionStart', command: cmd('gsd-check-update.js') },
2683
2461
  { event: 'SessionStart', command: cmd('gsd-session-state.sh') },
2684
2462
  // PreToolUse
2685
- { event: 'PreToolUse', command: cmd('gsd-prompt-guard.js'), matcher: 'WriteFile|StrReplaceFile', timeout: 5 },
2463
+ { event: 'PreToolUse', command: cmd('gsd-prompt-guard.js'), matcher: 'WriteFile|StrReplaceFile', timeout: kimiBlockingGuardTimeout('gsd-prompt-guard.js') },
2686
2464
  { event: 'PreToolUse', command: cmd('gsd-read-guard.js'), matcher: 'WriteFile|StrReplaceFile', timeout: 5 },
2687
- { event: 'PreToolUse', command: cmd('gsd-worktree-path-guard.js'), matcher: 'WriteFile|StrReplaceFile', timeout: 5 },
2688
- { event: 'PreToolUse', command: cmd('gsd-write-guard.js'), matcher: 'WriteFile', timeout: 5 },
2689
- { event: 'PreToolUse', command: cmd('gsd-secret-read-guard.js'), matcher: 'ReadFile|Grep|Shell', timeout: 5 },
2690
- { event: 'PreToolUse', command: cmd('gsd-workflow-guard.js'), matcher: 'Shell|WriteFile|StrReplaceFile', timeout: 5 },
2691
- { event: 'PreToolUse', command: cmd('gsd-validate-commit.sh'), matcher: 'Shell', timeout: 5 },
2465
+ { event: 'PreToolUse', command: cmd('gsd-worktree-path-guard.js'), matcher: 'WriteFile|StrReplaceFile', timeout: kimiBlockingGuardTimeout('gsd-worktree-path-guard.js') },
2466
+ { event: 'PreToolUse', command: cmd('gsd-write-guard.js'), matcher: 'WriteFile', timeout: kimiBlockingGuardTimeout('gsd-write-guard.js') },
2467
+ { event: 'PreToolUse', command: cmd('gsd-secret-read-guard.js'), matcher: 'ReadFile|Grep|Shell', timeout: kimiBlockingGuardTimeout('gsd-secret-read-guard.js') },
2468
+ { event: 'PreToolUse', command: cmd('gsd-workflow-guard.js'), matcher: 'Shell|WriteFile|StrReplaceFile', timeout: kimiBlockingGuardTimeout('gsd-workflow-guard.js') },
2469
+ { event: 'PreToolUse', command: cmd('gsd-validate-commit.sh'), matcher: 'Shell', timeout: kimiBlockingGuardTimeout('gsd-validate-commit.sh') },
2692
2470
  // PostToolUse
2693
2471
  { event: 'PostToolUse', command: cmd('gsd-context-monitor.js'), timeout: 10 },
2694
2472
  { event: 'PostToolUse', command: cmd('gsd-phase-boundary.sh'), matcher: 'WriteFile|StrReplaceFile', timeout: 5 },
@@ -2761,13 +2539,21 @@ function stripKimiHooksTomlBlock(content) {
2761
2539
  * touch the file's mtime for no reason.
2762
2540
  */
2763
2541
  function writeKimiHooksToml(configPath, targetDir, opts) {
2542
+ const configuredEntrypoints = [];
2543
+ const trackedOpts = {
2544
+ hookOpts: {
2545
+ ...opts.hookOpts,
2546
+ configPath,
2547
+ configuredEntrypoints,
2548
+ },
2549
+ };
2764
2550
  const existing = node_fs_1.default.existsSync(configPath) ? node_fs_1.default.readFileSync(configPath, 'utf8') : '';
2765
2551
  const stripped = stripKimiHooksTomlBlock(existing) ?? '';
2766
- const block = buildKimiHooksTomlBlock(targetDir, opts);
2552
+ const block = buildKimiHooksTomlBlock(targetDir, trackedOpts);
2767
2553
  const entryCount = block ? (block.match(/\[\[hooks\]\]/g) || []).length : 0;
2768
2554
  if (!block) {
2769
2555
  if (stripped === existing)
2770
- return { changed: false, path: configPath, entryCount: 0 };
2556
+ return { changed: false, path: configPath, entryCount: 0, configuredEntrypoints };
2771
2557
  if (stripped.trim() === '') {
2772
2558
  if (node_fs_1.default.existsSync(configPath))
2773
2559
  node_fs_1.default.unlinkSync(configPath);
@@ -2776,15 +2562,15 @@ function writeKimiHooksToml(configPath, targetDir, opts) {
2776
2562
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(configPath), { recursive: true });
2777
2563
  atomicWriteFileSync(configPath, stripped, 'utf8');
2778
2564
  }
2779
- return { changed: true, path: configPath, entryCount: 0 };
2565
+ return { changed: true, path: configPath, entryCount: 0, configuredEntrypoints };
2780
2566
  }
2781
2567
  const separator = stripped.trim() === '' ? '' : (stripped.endsWith('\n') ? '\n' : '\n\n');
2782
2568
  const next = stripped.trim() === '' ? `${block}\n` : `${stripped}${separator}${block}\n`;
2783
2569
  if (next === existing)
2784
- return { changed: false, path: configPath, entryCount };
2570
+ return { changed: false, path: configPath, entryCount, configuredEntrypoints };
2785
2571
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(configPath), { recursive: true });
2786
2572
  atomicWriteFileSync(configPath, next, 'utf8');
2787
- return { changed: true, path: configPath, entryCount };
2573
+ return { changed: true, path: configPath, entryCount, configuredEntrypoints };
2788
2574
  }
2789
2575
  /**
2790
2576
  * Uninstall-time counterpart to writeKimiHooksToml: strips the GSD block and
@@ -2806,6 +2592,45 @@ function removeKimiHooksToml(configPath) {
2806
2592
  return { changed: true };
2807
2593
  }
2808
2594
  // ---------------------------------------------------------------------------
2595
+ // Blocking-guard host budget (#3981, #5180)
2596
+ //
2597
+ // Claude Code treats a timed-out hook as NON-blocking — the tool call
2598
+ // continues through the normal permission flow. The blocking PreToolUse
2599
+ // guards therefore need a budget a host stall cannot exceed, not one sized to
2600
+ // the hook's own ~0.1 s runtime. Observed stalls reached 84.3 s; 120 s is the
2601
+ // top of the issue's prescribed 60–120 range and returns every observed
2602
+ // verdict. ONE constant and ONE name list, shared by every registration
2603
+ // surface that carries a host timeout (settings.json install + migration,
2604
+ // Kimi config.toml) so a future change cannot diverge between them.
2605
+ // ---------------------------------------------------------------------------
2606
+ const BLOCKING_GUARD_TIMEOUT_S = 120;
2607
+ const BLOCKING_GUARD_NAMES = [
2608
+ 'gsd-prompt-guard',
2609
+ 'gsd-workflow-guard',
2610
+ 'gsd-worktree-path-guard',
2611
+ 'gsd-agent-isolation-guard',
2612
+ 'gsd-write-guard',
2613
+ 'gsd-secret-read-guard',
2614
+ 'gsd-validate-commit',
2615
+ ];
2616
+ // Kimi registers every blocking guard EXCEPT the ones named here: Claude's
2617
+ // Agent|Task (subagent-dispatch) matcher segment has no confirmed Kimi tool
2618
+ // name, so gsd-agent-isolation-guard has nothing to attach to there (see the
2619
+ // buildKimiHooksTomlBlock doc). The divergence from the installer's set is
2620
+ // intentional and explicit, not an accident of two hand-kept lists.
2621
+ const KIMI_UNREGISTERED_BLOCKING_GUARDS = ['gsd-agent-isolation-guard'];
2622
+ const KIMI_BLOCKING_GUARD_NAMES = BLOCKING_GUARD_NAMES.filter((name) => !KIMI_UNREGISTERED_BLOCKING_GUARDS.includes(name));
2623
+ // Host budget for a Kimi blocking-guard entry, by script file name. Throws for
2624
+ // a script outside KIMI_BLOCKING_GUARD_NAMES so a spec cannot claim the
2625
+ // blocking budget for a hook the shared list does not name.
2626
+ function kimiBlockingGuardTimeout(scriptFile) {
2627
+ const name = scriptFile.replace(/\.(?:js|sh)$/, '');
2628
+ if (!KIMI_BLOCKING_GUARD_NAMES.includes(name)) {
2629
+ throw new Error(`runtime-hooks-surface: ${scriptFile} is not a Kimi blocking guard (KIMI_BLOCKING_GUARD_NAMES)`);
2630
+ }
2631
+ return BLOCKING_GUARD_TIMEOUT_S;
2632
+ }
2633
+ // ---------------------------------------------------------------------------
2809
2634
  // referencesHook
2810
2635
  //
2811
2636
  // Pure predicate — checks whether a hook entry object references a managed
@@ -2830,6 +2655,68 @@ function referencesHook(h, hookName) {
2830
2655
  (Array.isArray(args) && args.some(a => typeof a === 'string' && a.includes(hookName))) ||
2831
2656
  (typeof url === 'string' && url.includes(hookName));
2832
2657
  }
2658
+ function validateConfiguredEntrypoints(entries, deps = {}) {
2659
+ const statSync = deps.statSync ?? node_fs_1.default.statSync;
2660
+ const accessSync = deps.accessSync ?? node_fs_1.default.accessSync;
2661
+ const resolve = deps.resolveExecutableBinary ?? resolveExecutableBinary;
2662
+ const invalid = [];
2663
+ for (const entry of entries) {
2664
+ let scriptOk = false;
2665
+ try {
2666
+ scriptOk = statSync(entry.scriptPath).isFile();
2667
+ if (!scriptOk) {
2668
+ invalid.push({ runtime: entry.runtime, configPath: entry.configPath, role: 'script', path: entry.scriptPath, reason: 'wrong-file-type' });
2669
+ }
2670
+ else {
2671
+ // #4249: statSync only needs search permission on the parent dirs, so
2672
+ // it succeeds even for a chmod-000 file — the EACCES catch below
2673
+ // never fires for that case. Read permission on the file itself must
2674
+ // be checked explicitly: an interpreter opens the script directly,
2675
+ // and even a self-executable shebang script is opened and read by
2676
+ // its kernel-invoked interpreter, not just exec'd — X_OK alone does
2677
+ // not prove it's readable.
2678
+ try {
2679
+ accessSync(entry.scriptPath, node_fs_1.default.constants.R_OK);
2680
+ }
2681
+ catch {
2682
+ scriptOk = false;
2683
+ invalid.push({ runtime: entry.runtime, configPath: entry.configPath, role: 'script', path: entry.scriptPath, reason: 'unreadable' });
2684
+ }
2685
+ }
2686
+ }
2687
+ catch (statErr) {
2688
+ // #4249 Nit: EACCES means a parent directory couldn't be searched —
2689
+ // a real (if rare) permission problem, distinct from ENOENT's "missing".
2690
+ // EPERM: Windows' equivalent permission-denied code for a directory a
2691
+ // parent path couldn't be traversed into.
2692
+ const code = statErr?.code;
2693
+ const reason = code === 'EACCES' || code === 'EPERM' ? 'unreadable' : 'missing';
2694
+ invalid.push({ runtime: entry.runtime, configPath: entry.configPath, role: 'script', path: entry.scriptPath, reason });
2695
+ }
2696
+ // #4249: selfExecutable is the sole source of truth for whether the OS
2697
+ // execs scriptPath directly via its own shebang (set explicitly by every
2698
+ // producer that needs it — a Windows-Claude .sh hook, Codex's Windows
2699
+ // .cmd shim, Cline's hybrid `env node` hook — rather than inferred from
2700
+ // the absence of interpreterCandidates, which Cline's hybrid case also
2701
+ // carries). Skip on win32 like resolveExecutableBinary's own X_OK
2702
+ // carve-out does: POSIX mode bits don't mean executable on Windows, and a
2703
+ // real accessSync(X_OK) there would fail a .cmd shim under a test that
2704
+ // simulates win32 on a POSIX runner (Node's own no-op only protects an
2705
+ // actual Windows machine). Cline is the only producer where this runs.
2706
+ if (scriptOk && entry.selfExecutable && (entry.platform ?? process.platform) !== 'win32') {
2707
+ try {
2708
+ accessSync(entry.scriptPath, node_fs_1.default.constants.X_OK);
2709
+ }
2710
+ catch {
2711
+ invalid.push({ runtime: entry.runtime, configPath: entry.configPath, role: 'script', path: entry.scriptPath, reason: 'not-executable' });
2712
+ }
2713
+ }
2714
+ if (entry.interpreterCandidates && !entry.interpreterCandidates.some(candidate => resolve(candidate, { platform: entry.platform, requireExecutable: true }) !== null)) {
2715
+ invalid.push({ runtime: entry.runtime, configPath: entry.configPath, role: 'interpreter', path: entry.interpreterCandidates.join(' | '), reason: 'unresolved-interpreter' });
2716
+ }
2717
+ }
2718
+ return invalid.length === 0 ? { ok: true } : { ok: false, invalid };
2719
+ }
2833
2720
  module.exports = {
2834
2721
  // Cline
2835
2722
  buildClineRulesBody,
@@ -2891,16 +2778,27 @@ module.exports = {
2891
2778
  removeKimiHooksToml,
2892
2779
  KIMI_HOOKS_TOML_MARKER_BEGIN,
2893
2780
  KIMI_HOOKS_TOML_MARKER_END,
2781
+ // Blocking-guard host budget (#3981, #5180)
2782
+ BLOCKING_GUARD_TIMEOUT_S,
2783
+ BLOCKING_GUARD_NAMES,
2784
+ KIMI_UNREGISTERED_BLOCKING_GUARDS,
2785
+ KIMI_BLOCKING_GUARD_NAMES,
2894
2786
  // Shared
2895
2787
  stageTransitiveHookLibs,
2896
2788
  buildHookCommand,
2789
+ recordConfiguredHookCommand,
2897
2790
  applySettingsJsonHooks,
2791
+ applySettingsJsonHookTables,
2792
+ SETTINGS_JSON_HOOK_ROWS,
2793
+ SETTINGS_JSON_EXTENDED_ROWS,
2794
+ validateConfiguredEntrypoints,
2898
2795
  referencesHook,
2899
2796
  rewriteLegacyManagedNodeHookCommands,
2900
2797
  reconcileManagedShellHookCommands,
2901
2798
  normalizeNodePath,
2902
2799
  resolveNodeRunner,
2903
2800
  buildNodeRunnerChainToken,
2801
+ resolveBashExecutable,
2904
2802
  resolveBashRunner,
2905
2803
  NODE_RUNNER_RESOLVER_HOOK,
2906
2804
  // Atomic write seam (shared with bin/install.js so all writes participate