@opengsd/gsd-core 1.11.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (498) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +12 -0
  4. package/agents/gsd-code-fixer.md +1 -1
  5. package/agents/gsd-debug-session-manager.md +1 -1
  6. package/agents/gsd-debugger.md +1 -1
  7. package/agents/gsd-dom-verifier.md +169 -0
  8. package/agents/gsd-eval-auditor.md +1 -1
  9. package/agents/gsd-executor.md +78 -42
  10. package/agents/gsd-framework-selector.md +1 -3
  11. package/agents/gsd-intel-updater.md +1 -1
  12. package/agents/gsd-mempalace-curator.md +0 -1
  13. package/agents/gsd-pattern-mapper.md +11 -0
  14. package/agents/gsd-phase-researcher.md +3 -1
  15. package/agents/gsd-plan-checker.md +91 -112
  16. package/agents/gsd-planner.md +20 -4
  17. package/agents/gsd-project-researcher.md +1 -1
  18. package/agents/gsd-research-synthesizer.md +2 -2
  19. package/agents/gsd-roadmapper.md +15 -11
  20. package/agents/gsd-ui-checker.md +82 -7
  21. package/agents/gsd-ui-researcher.md +70 -3
  22. package/agents/gsd-verifier.md +24 -2
  23. package/bin/install.js +847 -200
  24. package/commands/gsd/discuss-phase.md +1 -1
  25. package/commands/gsd/execute-phase.md +1 -1
  26. package/commands/gsd/import.md +1 -1
  27. package/commands/gsd/ns-workflow.md +2 -1
  28. package/commands/gsd/phase.md +1 -1
  29. package/commands/gsd/quick-batch.md +105 -0
  30. package/commands/gsd/quick.md +8 -4
  31. package/commands/gsd/surface.md +18 -8
  32. package/gsd-core/bin/gsd-tools.cjs +761 -100
  33. package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
  34. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  35. package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
  36. package/gsd-core/bin/lib/api-coverage.cjs +30 -9
  37. package/gsd-core/bin/lib/artifacts.cjs +2 -0
  38. package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
  39. package/gsd-core/bin/lib/audit.cjs +163 -41
  40. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  41. package/gsd-core/bin/lib/capability-activation.cjs +27 -0
  42. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  43. package/gsd-core/bin/lib/capability-registry.cjs +785 -144
  44. package/gsd-core/bin/lib/capability-state.cjs +25 -4
  45. package/gsd-core/bin/lib/capability-validator.cjs +321 -18
  46. package/gsd-core/bin/lib/capability-writer.cjs +14 -4
  47. package/gsd-core/bin/lib/check-command-router.cjs +229 -6
  48. package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
  49. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  50. package/gsd-core/bin/lib/clusters.cjs +1 -0
  51. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  52. package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
  53. package/gsd-core/bin/lib/command-aliases.cjs +16 -0
  54. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  55. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  56. package/gsd-core/bin/lib/commands.cjs +877 -54
  57. package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
  58. package/gsd-core/bin/lib/config-loader.cjs +121 -29
  59. package/gsd-core/bin/lib/config.cjs +92 -2
  60. package/gsd-core/bin/lib/configuration.cjs +129 -37
  61. package/gsd-core/bin/lib/core-utils.cjs +118 -14
  62. package/gsd-core/bin/lib/decisions.cjs +213 -1
  63. package/gsd-core/bin/lib/edge-probe.cjs +23 -2
  64. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  65. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  66. package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
  67. package/gsd-core/bin/lib/frontmatter.cjs +975 -326
  68. package/gsd-core/bin/lib/gap-checker.cjs +41 -8
  69. package/gsd-core/bin/lib/git-base-branch.cjs +182 -39
  70. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
  71. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
  72. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +60 -14
  73. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
  74. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
  75. package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
  76. package/gsd-core/bin/lib/host-integration.cjs +96 -11
  77. package/gsd-core/bin/lib/init-command-router.cjs +132 -21
  78. package/gsd-core/bin/lib/init.cjs +252 -56
  79. package/gsd-core/bin/lib/install-engine.cjs +252 -15
  80. package/gsd-core/bin/lib/install-model-override-resolver.cjs +78 -1
  81. package/gsd-core/bin/lib/install-profiles.cjs +100 -18
  82. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  83. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  84. package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
  85. package/gsd-core/bin/lib/intel.cjs +101 -26
  86. package/gsd-core/bin/lib/io.cjs +195 -15
  87. package/gsd-core/bin/lib/learnings.cjs +85 -14
  88. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  89. package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
  90. package/gsd-core/bin/lib/markdown-table.cjs +175 -4
  91. package/gsd-core/bin/lib/milestone.cjs +112 -7
  92. package/gsd-core/bin/lib/model-catalog.cjs +177 -19
  93. package/gsd-core/bin/lib/model-resolver.cjs +10 -28
  94. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  95. package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
  96. package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
  97. package/gsd-core/bin/lib/phase-id.cjs +321 -13
  98. package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
  99. package/gsd-core/bin/lib/phase-locator.cjs +138 -17
  100. package/gsd-core/bin/lib/phase.cjs +1175 -115
  101. package/gsd-core/bin/lib/plan-document.cjs +273 -0
  102. package/gsd-core/bin/lib/plan-scan.cjs +13 -2
  103. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  104. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  105. package/gsd-core/bin/lib/planning-snapshot.cjs +165 -34
  106. package/gsd-core/bin/lib/planning-workspace.cjs +159 -28
  107. package/gsd-core/bin/lib/probe-core.cjs +4 -1
  108. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  109. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  110. package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
  111. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
  112. package/gsd-core/bin/lib/quick-batch.cjs +840 -0
  113. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  114. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
  115. package/gsd-core/bin/lib/review-lane-descriptor.cjs +62 -14
  116. package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
  117. package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
  118. package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
  119. package/gsd-core/bin/lib/roadmap-parser.cjs +577 -41
  120. package/gsd-core/bin/lib/roadmap.cjs +248 -64
  121. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +329 -41
  122. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
  123. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +320 -109
  124. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +487 -83
  125. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  126. package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
  127. package/gsd-core/bin/lib/shell-command-projection.cjs +75 -8
  128. package/gsd-core/bin/lib/smart-entry.cjs +19 -31
  129. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  130. package/gsd-core/bin/lib/state-command-router.cjs +47 -18
  131. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  132. package/gsd-core/bin/lib/state-document.cjs +216 -5
  133. package/gsd-core/bin/lib/state-md-schema.cjs +231 -0
  134. package/gsd-core/bin/lib/state-transition.cjs +850 -145
  135. package/gsd-core/bin/lib/state.cjs +1629 -287
  136. package/gsd-core/bin/lib/surface.cjs +33 -10
  137. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  138. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  139. package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
  140. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  141. package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
  142. package/gsd-core/bin/lib/uat.cjs +2542 -387
  143. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  144. package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
  145. package/gsd-core/bin/lib/unusable-input.cjs +13 -0
  146. package/gsd-core/bin/lib/update-context.cjs +6 -2
  147. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  148. package/gsd-core/bin/lib/validate.cjs +230 -12
  149. package/gsd-core/bin/lib/vendor/README.md +43 -5
  150. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  151. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  152. package/gsd-core/bin/lib/verification.cjs +287 -13
  153. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  154. package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
  155. package/gsd-core/bin/lib/verify.cjs +441 -56
  156. package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
  157. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  158. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  159. package/gsd-core/bin/lib/worktree-safety.cjs +185 -21
  160. package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
  161. package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
  162. package/gsd-core/bin/shared/exit-codes.json +8 -0
  163. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  164. package/gsd-core/bin/shared/model-catalog.json +8 -1
  165. package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
  166. package/gsd-core/references/agent-contracts.md +6 -5
  167. package/gsd-core/references/api-coverage.md +24 -2
  168. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  169. package/gsd-core/references/checkpoints.md +37 -19
  170. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  171. package/gsd-core/references/edge-probe.md +17 -5
  172. package/gsd-core/references/execute-mvp-tdd.md +18 -18
  173. package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
  174. package/gsd-core/references/execute-phase-response-language.md +6 -0
  175. package/gsd-core/references/execute-phase-wave-guard.md +11 -9
  176. package/gsd-core/references/executor-examples.md +42 -0
  177. package/gsd-core/references/failing-direction.md +78 -0
  178. package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
  179. package/gsd-core/references/gate-prompts.md +1 -1
  180. package/gsd-core/references/git-integration.md +5 -5
  181. package/gsd-core/references/git-planning-commit.md +3 -3
  182. package/gsd-core/references/gsd-run-resolver.md +1 -1
  183. package/gsd-core/references/loop-hook-dispatch.md +22 -0
  184. package/gsd-core/references/model-profiles.md +1 -1
  185. package/gsd-core/references/mvp-concepts.md +2 -2
  186. package/gsd-core/references/nyquist-compliance.md +74 -0
  187. package/gsd-core/references/offer-next.md +3 -5
  188. package/gsd-core/references/phase-argument-parsing.md +3 -3
  189. package/gsd-core/references/plan-checker-examples.md +41 -0
  190. package/gsd-core/references/planner-antipatterns.md +25 -0
  191. package/gsd-core/references/planner-chunked.md +5 -1
  192. package/gsd-core/references/planner-coupling.md +42 -0
  193. package/gsd-core/references/planner-failing-direction.md +53 -0
  194. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  195. package/gsd-core/references/planner-quick-batch.md +71 -0
  196. package/gsd-core/references/planner-reviews.md +47 -0
  197. package/gsd-core/references/planner-revision.md +76 -3
  198. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  199. package/gsd-core/references/planning-config.md +39 -9
  200. package/gsd-core/references/response-language-directive.md +9 -0
  201. package/gsd-core/references/reviewer-instances.md +31 -0
  202. package/gsd-core/references/revision-loop.md +118 -11
  203. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  204. package/gsd-core/references/tdd.md +15 -12
  205. package/gsd-core/references/ui-brand.md +65 -21
  206. package/gsd-core/references/ui-consideration-probe.md +1 -1
  207. package/gsd-core/references/universal-anti-patterns.md +2 -2
  208. package/gsd-core/references/verifier-evidence-gate.md +160 -0
  209. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  210. package/gsd-core/references/verify-mvp-mode.md +1 -1
  211. package/gsd-core/references/workstream-flag.md +11 -11
  212. package/gsd-core/templates/README.md +1 -1
  213. package/gsd-core/templates/SECURITY.md +3 -3
  214. package/gsd-core/templates/UI-SPEC.md +25 -3
  215. package/gsd-core/templates/VALIDATION.md +3 -3
  216. package/gsd-core/templates/phase-prompt.md +7 -0
  217. package/gsd-core/templates/state.md +7 -0
  218. package/gsd-core/templates/verification-report.md +5 -0
  219. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  220. package/gsd-core/workflows/add-backlog.md +3 -1
  221. package/gsd-core/workflows/add-phase.md +5 -3
  222. package/gsd-core/workflows/add-tests.md +4 -9
  223. package/gsd-core/workflows/add-todo.md +2 -2
  224. package/gsd-core/workflows/ai-integration-phase.md +5 -10
  225. package/gsd-core/workflows/analyze-dependencies.md +2 -0
  226. package/gsd-core/workflows/audit-fix.md +14 -3
  227. package/gsd-core/workflows/audit-milestone.md +11 -9
  228. package/gsd-core/workflows/audit-uat.md +19 -2
  229. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  230. package/gsd-core/workflows/autonomous.md +12 -26
  231. package/gsd-core/workflows/check-todos.md +2 -2
  232. package/gsd-core/workflows/cleanup.md +3 -3
  233. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +16 -14
  234. package/gsd-core/workflows/code-review-fix.md +3 -1
  235. package/gsd-core/workflows/code-review.md +192 -69
  236. package/gsd-core/workflows/complete-milestone.md +28 -14
  237. package/gsd-core/workflows/debug.md +6 -4
  238. package/gsd-core/workflows/diagnose-issues.md +17 -7
  239. package/gsd-core/workflows/discuss-phase/modes/advisor.md +3 -1
  240. package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
  241. package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
  242. package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
  243. package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
  244. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -7
  245. package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
  246. package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
  247. package/gsd-core/workflows/discuss-phase/modes/text.md +3 -1
  248. package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
  249. package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
  250. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  251. package/gsd-core/workflows/discuss-phase-assumptions.md +3 -3
  252. package/gsd-core/workflows/discuss-phase-power.md +2 -0
  253. package/gsd-core/workflows/discuss-phase.md +2 -2
  254. package/gsd-core/workflows/do.md +46 -19
  255. package/gsd-core/workflows/docs-update.md +6 -5
  256. package/gsd-core/workflows/edit-phase.md +3 -1
  257. package/gsd-core/workflows/eval-review.md +5 -10
  258. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -1
  259. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +129 -11
  260. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  261. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  262. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  263. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +29 -5
  264. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  265. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  266. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +4 -2
  267. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
  268. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  269. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
  270. package/gsd-core/workflows/execute-phase.md +68 -66
  271. package/gsd-core/workflows/execute-plan.md +25 -20
  272. package/gsd-core/workflows/explore.md +3 -1
  273. package/gsd-core/workflows/extract-learnings.md +3 -1
  274. package/gsd-core/workflows/fast.md +8 -2
  275. package/gsd-core/workflows/forensics.md +3 -1
  276. package/gsd-core/workflows/graduation.md +6 -6
  277. package/gsd-core/workflows/health.md +4 -7
  278. package/gsd-core/workflows/help/modes/brief.md +2 -0
  279. package/gsd-core/workflows/help/modes/default.md +2 -0
  280. package/gsd-core/workflows/help/modes/full.md +12 -0
  281. package/gsd-core/workflows/help/modes/topic.md +2 -0
  282. package/gsd-core/workflows/help.md +2 -0
  283. package/gsd-core/workflows/import.md +17 -14
  284. package/gsd-core/workflows/inbox.md +5 -6
  285. package/gsd-core/workflows/ingest-docs.md +45 -12
  286. package/gsd-core/workflows/insert-phase.md +7 -5
  287. package/gsd-core/workflows/list-phase-assumptions.md +2 -0
  288. package/gsd-core/workflows/list-seeds.md +7 -3
  289. package/gsd-core/workflows/list-workspaces.md +3 -1
  290. package/gsd-core/workflows/manager.md +15 -26
  291. package/gsd-core/workflows/map-codebase.md +3 -1
  292. package/gsd-core/workflows/milestone-summary.md +3 -1
  293. package/gsd-core/workflows/mvp-phase.md +3 -3
  294. package/gsd-core/workflows/new-milestone.md +10 -22
  295. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  296. package/gsd-core/workflows/new-project.md +17 -29
  297. package/gsd-core/workflows/new-workspace.md +2 -2
  298. package/gsd-core/workflows/next.md +4 -2
  299. package/gsd-core/workflows/node-repair.md +2 -0
  300. package/gsd-core/workflows/note.md +2 -0
  301. package/gsd-core/workflows/onboard.md +1 -1
  302. package/gsd-core/workflows/pause-work.md +20 -5
  303. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  304. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
  305. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +4 -4
  306. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +12 -3
  307. package/gsd-core/workflows/plan-phase.md +251 -54
  308. package/gsd-core/workflows/plan-review-convergence.md +148 -19
  309. package/gsd-core/workflows/plant-seed.md +3 -3
  310. package/gsd-core/workflows/pr-branch.md +195 -51
  311. package/gsd-core/workflows/profile-user.md +17 -15
  312. package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
  313. package/gsd-core/workflows/progress.md +52 -15
  314. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  315. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +38 -5
  316. package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
  317. package/gsd-core/workflows/quick/steps/research-phase.md +5 -7
  318. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  319. package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
  320. package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
  321. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
  322. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
  323. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
  324. package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
  325. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
  326. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
  327. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
  328. package/gsd-core/workflows/quick-batch.md +203 -0
  329. package/gsd-core/workflows/quick.md +33 -32
  330. package/gsd-core/workflows/reapply-patches.md +2 -0
  331. package/gsd-core/workflows/remove-phase.md +6 -4
  332. package/gsd-core/workflows/remove-workspace.md +3 -3
  333. package/gsd-core/workflows/resume-project.md +14 -14
  334. package/gsd-core/workflows/review.md +404 -21
  335. package/gsd-core/workflows/scan.md +3 -1
  336. package/gsd-core/workflows/section-manifest.json +12 -0
  337. package/gsd-core/workflows/secure-phase.md +3 -3
  338. package/gsd-core/workflows/session-report.md +2 -0
  339. package/gsd-core/workflows/settings-advanced.md +9 -9
  340. package/gsd-core/workflows/settings-integrations.md +66 -32
  341. package/gsd-core/workflows/settings.md +4 -6
  342. package/gsd-core/workflows/ship.md +22 -16
  343. package/gsd-core/workflows/sketch-wrap-up.md +13 -17
  344. package/gsd-core/workflows/sketch.md +13 -19
  345. package/gsd-core/workflows/smart-entry.md +4 -6
  346. package/gsd-core/workflows/spec-phase.md +31 -4
  347. package/gsd-core/workflows/spike-wrap-up.md +9 -11
  348. package/gsd-core/workflows/spike.md +21 -32
  349. package/gsd-core/workflows/stats.md +4 -2
  350. package/gsd-core/workflows/sync-skills.md +13 -5
  351. package/gsd-core/workflows/thread.md +13 -7
  352. package/gsd-core/workflows/transition.md +7 -5
  353. package/gsd-core/workflows/ui-phase.md +36 -21
  354. package/gsd-core/workflows/ui-review.md +7 -11
  355. package/gsd-core/workflows/ultraplan-phase.md +7 -13
  356. package/gsd-core/workflows/undo.md +9 -17
  357. package/gsd-core/workflows/update.md +47 -48
  358. package/gsd-core/workflows/validate-phase.md +3 -3
  359. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  360. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  361. package/gsd-core/workflows/verify-work.md +106 -21
  362. package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
  363. package/hooks/dist/gsd-check-update-worker.js +19 -2
  364. package/hooks/dist/gsd-config-reload.js +18 -12
  365. package/hooks/dist/gsd-context-monitor.js +302 -22
  366. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  367. package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
  368. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  369. package/hooks/dist/gsd-cursor-stop.js +2 -1
  370. package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
  371. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
  372. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  373. package/hooks/dist/gsd-graphify-update.sh +22 -18
  374. package/hooks/dist/gsd-node-runner.sh +77 -0
  375. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  376. package/hooks/dist/gsd-prompt-guard.js +46 -12
  377. package/hooks/dist/gsd-read-guard.js +18 -7
  378. package/hooks/dist/gsd-read-injection-scanner.js +22 -13
  379. package/hooks/dist/gsd-secret-read-guard.js +1079 -0
  380. package/hooks/dist/gsd-session-state.sh +1 -0
  381. package/hooks/dist/gsd-statusline.js +222 -29
  382. package/hooks/dist/gsd-validate-commit.sh +523 -12
  383. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  384. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  385. package/hooks/dist/gsd-workflow-guard.js +36 -17
  386. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  387. package/hooks/dist/gsd-write-guard.js +35 -25
  388. package/hooks/dist/lib/cli-exit.js +560 -0
  389. package/hooks/dist/lib/exit-code-registry.js +98 -0
  390. package/hooks/dist/lib/git-cmd.js +210 -1
  391. package/hooks/dist/lib/git-probe.js +84 -0
  392. package/hooks/dist/lib/hook-exit.js +81 -0
  393. package/hooks/dist/lib/injection-patterns.js +36 -6
  394. package/hooks/dist/managed-hooks-registry.cjs +4 -0
  395. package/hooks/gsd-agent-isolation-guard.js +77 -38
  396. package/hooks/gsd-check-update-worker.js +19 -2
  397. package/hooks/gsd-config-reload.js +18 -12
  398. package/hooks/gsd-context-monitor.js +302 -22
  399. package/hooks/gsd-cursor-post-tool.js +3 -1
  400. package/hooks/gsd-cursor-pre-tool.js +3 -1
  401. package/hooks/gsd-cursor-session-start.js +2 -1
  402. package/hooks/gsd-cursor-stop.js +2 -1
  403. package/hooks/gsd-cursor-subagent-start.js +28 -23
  404. package/hooks/gsd-cursor-subagent-stop.js +3 -1
  405. package/hooks/gsd-ensure-canonical-path.js +2 -1
  406. package/hooks/gsd-graphify-update.sh +22 -18
  407. package/hooks/gsd-node-runner.sh +77 -0
  408. package/hooks/gsd-phase-boundary.sh +1 -0
  409. package/hooks/gsd-prompt-guard.js +46 -12
  410. package/hooks/gsd-read-guard.js +18 -7
  411. package/hooks/gsd-read-injection-scanner.js +22 -13
  412. package/hooks/gsd-secret-read-guard.js +1079 -0
  413. package/hooks/gsd-session-state.sh +1 -0
  414. package/hooks/gsd-statusline.js +222 -29
  415. package/hooks/gsd-validate-commit.sh +523 -12
  416. package/hooks/gsd-windsurf-pre-command.js +16 -11
  417. package/hooks/gsd-windsurf-pre-write.js +22 -13
  418. package/hooks/gsd-workflow-guard.js +36 -17
  419. package/hooks/gsd-worktree-path-guard.js +36 -21
  420. package/hooks/gsd-write-guard.js +35 -25
  421. package/hooks/hooks.json +6 -0
  422. package/hooks/lib/cli-exit.js +560 -0
  423. package/hooks/lib/exit-code-registry.js +98 -0
  424. package/hooks/lib/git-cmd.js +210 -1
  425. package/hooks/lib/git-probe.js +84 -0
  426. package/hooks/lib/hook-exit.js +81 -0
  427. package/hooks/lib/injection-patterns.js +36 -6
  428. package/hooks/managed-hooks-registry.cjs +4 -0
  429. package/package.json +14 -9
  430. package/scripts/base64-scan.sh +74 -12
  431. package/scripts/build-hooks.js +12 -0
  432. package/scripts/check-glossary-refs.cjs +77 -15
  433. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  434. package/scripts/ci-check-job-near-cap.cjs +49 -0
  435. package/scripts/ci-pr-mergeability.cjs +262 -0
  436. package/scripts/ci-test-scope.cjs +52 -12
  437. package/scripts/ci-timeout-report.cjs +230 -0
  438. package/scripts/docs-guard-registry.cjs +406 -0
  439. package/scripts/gen-capability-registry.cjs +8 -6
  440. package/scripts/gen-exit-code-docs.cjs +318 -0
  441. package/scripts/gen-exit-code-registry.cjs +891 -0
  442. package/scripts/gen-features.cjs +836 -0
  443. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  444. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  445. package/scripts/gen-loop-host-contract.cjs +189 -4
  446. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  447. package/scripts/gen-state-md-docs.cjs +727 -0
  448. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  449. package/scripts/lib/ci-job-timing.cjs +72 -0
  450. package/scripts/lib/cli-exit.cjs +546 -44
  451. package/scripts/lib/drift-scan.cjs +32 -2
  452. package/scripts/lib/exit-code-registry.cjs +98 -0
  453. package/scripts/lib/ndjson-reporter.cjs +119 -0
  454. package/scripts/lib/shellcheck-fetch.cjs +247 -0
  455. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
  456. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  457. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  458. package/scripts/lint-docs-guard-registration.cjs +495 -0
  459. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +198 -0
  460. package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
  461. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  462. package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
  463. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  464. package/scripts/lint-phase-enumeration-drift.cjs +45 -14
  465. package/scripts/lint-phase-id-drift.cjs +133 -8
  466. package/scripts/lint-planning-prompt-drift.cjs +38 -1
  467. package/scripts/lint-portable-grep.cjs +176 -0
  468. package/scripts/lint-removed-but-needed.cjs +184 -16
  469. package/scripts/lint-response-language-coverage.cjs +524 -0
  470. package/scripts/lint-seam-enforcement.cjs +182 -0
  471. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  472. package/scripts/lint-source-test-name-collision.cjs +241 -0
  473. package/scripts/lint-state-write-path-drift.cjs +337 -432
  474. package/scripts/lint-test-file-count.allowlist.json +124 -4
  475. package/scripts/lint-test-file-count.cjs +25 -3
  476. package/scripts/lint-unreachable-guard-drift.cjs +51 -64
  477. package/scripts/lint-vendored-deps.cjs +208 -35
  478. package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
  479. package/scripts/lint-workflow-shellcheck.cjs +614 -0
  480. package/scripts/mutation-matrix.cjs +599 -50
  481. package/scripts/npm-audit-baseline.cjs +376 -0
  482. package/scripts/prompt-injection-scan.sh +83 -14
  483. package/scripts/require-issue-link-policy.cjs +16 -1
  484. package/scripts/secret-scan.sh +75 -13
  485. package/scripts/select-docs-guards.cjs +56 -0
  486. package/scripts/sync-runtime-launcher.cjs +22 -3
  487. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  488. package/skills/gsd-execute-phase/SKILL.md +1 -1
  489. package/skills/gsd-import/SKILL.md +1 -1
  490. package/skills/gsd-ns-workflow/SKILL.md +1 -0
  491. package/skills/gsd-phase/SKILL.md +1 -1
  492. package/skills/gsd-quick/SKILL.md +8 -4
  493. package/skills/gsd-quick-batch/SKILL.md +105 -0
  494. package/skills/gsd-surface/SKILL.md +18 -8
  495. package/vscode/package.json +1 -1
  496. package/bin/lib/ui-safety-gate.cjs +0 -109
  497. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  498. package/scripts/state-write-path-drift-baseline.json +0 -19
@@ -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, } = shellCmdProjection;
41
+ const { isManagedHookBasename, isManagedHookCommand, projectLegacySettingsHookCommand, projectManagedHookCommand, projectPortableHookBaseDir, projectCodexHookTomlCommand, shellHookOmitsBashRunner, escapeTomlDoubleQuotedString, escapePosixDoubleQuoted, } = shellCmdProjection;
42
42
  // ---------------------------------------------------------------------------
43
43
  // Terminal color constants (mirrors install.js for console output parity)
44
44
  // ---------------------------------------------------------------------------
@@ -136,23 +136,64 @@ function _capabilityTitle(runtime) {
136
136
  let __atomicWriteCounter = 0;
137
137
  // Set<string> — absolute paths of .tmp-<pid>-<n> files this process created.
138
138
  const __atomicWrittenTmps = new Set();
139
+ // Retry budget for the EEXIST (squatted temp path) branch in atomicWriteFileSync.
140
+ const MAX_TEMP_FILE_ATTEMPTS = 4;
139
141
  function atomicWriteFileSync(target, data, options) {
140
- __atomicWriteCounter += 1;
141
- const tmp = `${target}.tmp-${process.pid}-${__atomicWriteCounter}`;
142
- __atomicWrittenTmps.add(tmp);
142
+ // A pre-existing target's permission bits must survive the rewrite:
143
+ // rename() swaps the temp file's inode into place, so without an explicit
144
+ // carry a user-hardened chmod (e.g. 600 on a secrets-bearing settings.json)
145
+ // would silently reset to the umask default.
146
+ let priorMode;
143
147
  try {
144
- node_fs_1.default.writeFileSync(tmp, data, options);
145
- shellCmdProjection.retryRenameSync(tmp, target);
146
- // Successful rename: the tmp path no longer exists, but leave it in the
147
- // Set so _cleanTmpFiles can recognise it as installer-owned if it somehow
148
- // lingers (e.g. a rename succeeded but left a stale entry on some FS).
149
- }
150
- catch (e) {
148
+ const st = node_fs_1.default.statSync(target);
149
+ if (st.isFile())
150
+ priorMode = st.mode & 0o7777;
151
+ }
152
+ catch { /* no pre-existing target: default creation mode applies */ }
153
+ // 'wx' (O_EXCL) refuses to follow a symlink pre-planted at the predictable
154
+ // temp path and refuses to reuse a foreign file already sitting there; on
155
+ // EEXIST the write retries under a fresh counter value.
156
+ const exclusiveOptions = typeof options === 'string' || options == null
157
+ ? { encoding: options ?? null, flag: 'wx' }
158
+ : { ...options, flag: 'wx' };
159
+ for (let attempt = 0;; attempt++) {
160
+ __atomicWriteCounter += 1;
161
+ const tmp = `${target}.tmp-${process.pid}-${__atomicWriteCounter}`;
162
+ __atomicWrittenTmps.add(tmp);
151
163
  try {
152
- node_fs_1.default.rmSync(tmp, { force: true });
164
+ node_fs_1.default.writeFileSync(tmp, data, exclusiveOptions);
153
165
  }
154
- catch { /* ignore */ }
155
- throw e;
166
+ catch (e) {
167
+ if (e.code === 'EEXIST') {
168
+ // The file at tmp is not ours — never rmSync it.
169
+ if (attempt < MAX_TEMP_FILE_ATTEMPTS)
170
+ continue;
171
+ throw e;
172
+ }
173
+ try {
174
+ node_fs_1.default.rmSync(tmp, { force: true });
175
+ }
176
+ catch { /* ignore */ }
177
+ throw e;
178
+ }
179
+ try {
180
+ // chmod rather than options.mode: open(2) masks mode with the process
181
+ // umask, chmod applies the preserved bits exactly.
182
+ if (priorMode !== undefined)
183
+ node_fs_1.default.chmodSync(tmp, priorMode);
184
+ shellCmdProjection.retryRenameSync(tmp, target);
185
+ // Successful rename: the tmp path no longer exists, but leave it in the
186
+ // Set so _cleanTmpFiles can recognise it as installer-owned if it somehow
187
+ // lingers (e.g. a rename succeeded but left a stale entry on some FS).
188
+ }
189
+ catch (e) {
190
+ try {
191
+ node_fs_1.default.rmSync(tmp, { force: true });
192
+ }
193
+ catch { /* ignore */ }
194
+ throw e;
195
+ }
196
+ return;
156
197
  }
157
198
  }
158
199
  // ---------------------------------------------------------------------------
@@ -333,20 +374,41 @@ function parseTomlValue(text, i) {
333
374
  }
334
375
  throw new Error(`parseTomlValue: unexpected character '${ch}' at position ${i}`);
335
376
  }
377
+ /**
378
+ * Normalize a directory that will be joined with `/…` — posix separators, no
379
+ * trailing slash. `FNM_DIR=/custom/fnm/` would otherwise bake
380
+ * `/custom/fnm//aliases/default/bin/node` (#3704 review). Cosmetic — `existsSync`
381
+ * resolves the doubled separator and every shell collapses it — but the value is
382
+ * written into a user's settings.json and read by humans.
383
+ *
384
+ * Shared by both fnm branches deliberately: they build the same alias paths from
385
+ * different roots, and a trim applied to only one is a difference with no reason
386
+ * behind it.
387
+ */
388
+ function normalizeRootDir(dir) {
389
+ return shellCmdProjection.posixNormalize(dir).replace(/\/+$/, '');
390
+ }
336
391
  function normalizeNodePath(execPath, opts) {
337
392
  if (!execPath)
338
393
  return execPath;
339
394
  const env = (opts && opts.env) || process.env;
340
395
  const existsSync = (opts && opts.existsSync) || node_fs_1.default.existsSync;
341
396
  const normalizedForMatch = shellCmdProjection.posixNormalize(execPath);
342
- if (/\/fnm_multishells\/[0-9]+_[0-9]+\/node(\.exe)?$/i.test(normalizedForMatch)) {
397
+ // #977: fnm's Windows shim IS `process.execPath` there — Windows does not
398
+ // realpath through it — so this branch stays. #3704 adds `(?:bin\/)?`: fnm's
399
+ // POSIX shim is `<multishell>/bin/node`, so the original pattern could not match
400
+ // it even when a caller hands one in explicitly (#3662's `execPath` option
401
+ // exists to do exactly that, normalizing a path from another environment).
402
+ if (/\/fnm_multishells\/[0-9]+_[0-9]+\/(?:bin\/)?node(\.exe)?$/i.test(normalizedForMatch)) {
343
403
  const candidates = [];
344
404
  if (env.FNM_DIR) {
345
- candidates.push(`${env.FNM_DIR}/aliases/default/node.exe`);
346
- candidates.push(`${env.FNM_DIR}/aliases/default/bin/node`);
405
+ const fnmRoot = normalizeRootDir(env.FNM_DIR);
406
+ candidates.push(`${fnmRoot}/aliases/default/node.exe`);
407
+ candidates.push(`${fnmRoot}/aliases/default/bin/node`);
347
408
  }
348
- if (env.APPDATA) {
349
- candidates.push(`${env.APPDATA}/fnm/aliases/default/node.exe`);
409
+ const appdata = shellCmdProjection.envGet(env, 'APPDATA');
410
+ if (appdata) {
411
+ candidates.push(`${normalizeRootDir(appdata)}/fnm/aliases/default/node.exe`);
350
412
  }
351
413
  for (const candidate of candidates) {
352
414
  if (candidate && existsSync(candidate))
@@ -354,6 +416,42 @@ function normalizeNodePath(execPath, opts) {
354
416
  }
355
417
  return execPath;
356
418
  }
419
+ // #3704: fnm pins a concrete version at
420
+ // <FNM_DIR>/node-versions/<ver>/installation/bin/node (Windows:
421
+ // .../installation/node.exe). On macOS/Linux this — not the shim above — is what
422
+ // `process.execPath` reports, because Node realpaths through
423
+ // `fnm_multishells`. So the shim branch above is unreachable on POSIX and the
424
+ // raw versioned path was baked into every managed hook: `fnm uninstall <ver>`,
425
+ // or fnm's own pruning, then 404s every hook — the ephemeral-path failure #977
426
+ // exists to prevent, and the same one #1619 (mise) and #2185 (Homebrew) fixed
427
+ // for their managers by matching the VERSIONED path rather than a shim.
428
+ //
429
+ // The stable alias is <FNM_DIR>/aliases/default/..., which fnm repoints on
430
+ // `fnm default`. Derive <FNM_DIR> from execPath first (#2185's rule — the path
431
+ // IS the install location, and FNM_DIR may be unset or point at a different
432
+ // install), keeping the env as a secondary candidate. Rewrite only when the
433
+ // alias exists; otherwise fall through to the raw execPath, exactly like the
434
+ // mise and volta branches, so a rewrite never turns a stale-but-working pin
435
+ // into an immediately broken one.
436
+ const fnmVersioned = normalizedForMatch.match(/^(.*)\/node-versions\/[^/]+\/installation\/(?:bin\/)?node(\.exe)?$/i);
437
+ if (fnmVersioned) {
438
+ const isExe = Boolean(fnmVersioned[2]);
439
+ const roots = [fnmVersioned[1]];
440
+ if (env.FNM_DIR)
441
+ roots.push(normalizeRootDir(env.FNM_DIR));
442
+ const fnmAliasCandidates = [];
443
+ for (const root of roots) {
444
+ // Probe the spelling matching the input first, then the other — a layout
445
+ // is one or the other, and guessing wrong would skip a real alias.
446
+ const leaf = isExe ? ['node.exe', 'bin/node'] : ['bin/node', 'node.exe'];
447
+ for (const tail of leaf)
448
+ fnmAliasCandidates.push(`${root}/aliases/default/${tail}`);
449
+ }
450
+ for (const candidate of fnmAliasCandidates) {
451
+ if (existsSync(candidate))
452
+ return candidate;
453
+ }
454
+ }
357
455
  // Homebrew (macOS Intel /usr/local, Apple Silicon /opt/homebrew, Linuxbrew
358
456
  // /home/linuxbrew/.linuxbrew, and any custom HOMEBREW_PREFIX) pins node at
359
457
  // <prefix>/Cellar/node(<@ver>)?/<ver>/bin/node, then deletes prior versions on
@@ -399,12 +497,76 @@ function normalizeNodePath(execPath, opts) {
399
497
  return execPath;
400
498
  }
401
499
  function resolveNodeRunner(opts) {
402
- const execPath = typeof process.execPath === 'string' ? process.execPath : '';
500
+ const execPath = (opts && opts.execPath) || (typeof process.execPath === 'string' ? process.execPath : '');
403
501
  if (!execPath)
404
502
  return null;
405
503
  const stablePath = normalizeNodePath(execPath, opts);
406
504
  return JSON.stringify(shellCmdProjection.posixNormalize(stablePath));
407
505
  }
506
+ /**
507
+ * #3662 — the runtime-resolving node runner token for managed JS hooks.
508
+ *
509
+ * A bake-time absolute runner (`resolveNodeRunner`) only works in the
510
+ * environment that ran the installer; a config root shared across
511
+ * environments (the `--portable-hooks` scenario, or any settings.json under a
512
+ * mounted `$HOME`) carries a path that 404s with exit 127 everywhere else.
513
+ * This token is a POSIX `sh` command substitution that resolves node at
514
+ * hook-fire time, trying IN ORDER:
515
+ *
516
+ * 1. the baked installer path (absolute — keeps the #2979/#3002/#3017/#3022
517
+ * minimal-PATH guarantee: where the baked path exists it still wins,
518
+ * under any PATH, GUI launch included);
519
+ * 2. `command -v node` (quoted — one word even with spaces in the result);
520
+ * 3. the well-known stable layouts (`/usr/local/bin/node`, `/usr/bin/node`).
521
+ *
522
+ * The FIRST executable candidate wins; if none resolves the substitution
523
+ * yields an empty word and the hook fails exactly as a stale absolute path
524
+ * does today — no bare `node` token is ever emitted or depended on.
525
+ *
526
+ * One shape for every platform: emitted hook commands execute via POSIX `sh`
527
+ * (Claude-on-win32 runs Git Bash per #166/#580; `hookCommandNeedsPowerShellCallOperator`
528
+ * is an unused opt-in), and the baked path is posixNormalize'd before escaping
529
+ * (escapePosixDoubleQuoted — the Shell Command Projection seam owns quoting).
530
+ * The portable resolver script (hooks/gsd-node-runner.sh) resolves through a
531
+ * SUPERSET of this candidate list — keep the two lists consistent.
532
+ */
533
+ function buildNodeRunnerChainToken(opts) {
534
+ const execPath = (opts && opts.execPath) || (typeof process.execPath === 'string' ? process.execPath : '');
535
+ if (!execPath)
536
+ return null;
537
+ const stablePath = shellCmdProjection.posixNormalize(normalizeNodePath(execPath, opts));
538
+ const baked = escapePosixDoubleQuoted(stablePath);
539
+ // Absolute candidates only (leading / or a win32 drive letter): a relative
540
+ // `command -v node` hit (legal under a relative PATH entry) must never
541
+ // promote repo-cwd content into the runner slot. The gate uses parameter
542
+ // expansion + [ ] — deliberately NO `case` (its `)` terminates the command
543
+ // substitution under macOS's stock bash 3.2 /bin/sh, breaking the hook).
544
+ // \${…} below stays a literal shell parameter expansion, not TS interpolation.
545
+ return `"$(for n in "${baked}" "$(command -v node)" /usr/local/bin/node /usr/bin/node; do [ -x "$n" ] && { [ "\${n#/}" != "$n" ] || [ "\${n#?:}" != "$n" ]; } && printf '%s' "$n" && break; done)"`;
546
+ }
547
+ /**
548
+ * #3662 — the install-time node path as a shell-safe QUOTED token, carrying
549
+ * the same double-quote escaping as the chain token (`escapePosixDoubleQuoted`
550
+ * — $ ` " \), NOT bare JSON quoting. The portable resolver's first argument
551
+ * is executed by the host shell before the resolver sees argv, so a path
552
+ * containing shell metacharacters must arrive escaped.
553
+ */
554
+ function buildBakedNodeToken(opts) {
555
+ const execPath = (opts && opts.execPath) || (typeof process.execPath === 'string' ? process.execPath : '');
556
+ if (!execPath)
557
+ return null;
558
+ const stablePath = shellCmdProjection.posixNormalize(normalizeNodePath(execPath, opts));
559
+ return `"${escapePosixDoubleQuoted(stablePath)}"`;
560
+ }
561
+ /**
562
+ * #3662 — basename of the portable node resolver staged into the install's
563
+ * hooks/ directory. Under `--portable-hooks`, managed JS hook commands route
564
+ * through it (`bash "<hooks>/gsd-node-runner.sh" "<baked-node>" "<script>.js"`)
565
+ * so the SAME staged file works for every install: the install-time node path
566
+ * travels as the resolver's first argument (tried first — the minimal-PATH
567
+ * guarantee), ahead of `command -v node` and the well-known fallback list.
568
+ */
569
+ const NODE_RUNNER_RESOLVER_HOOK = 'gsd-node-runner.sh';
408
570
  function resolveBashRunner(opts) {
409
571
  const platform = (opts && opts.platform) || process.platform;
410
572
  if (platform !== 'win32')
@@ -429,8 +591,13 @@ function resolveBashRunner(opts) {
429
591
  }
430
592
  return null;
431
593
  }
432
- function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) {
433
- if (!settings || !settings.hooks || !absoluteRunner)
594
+ // #3662 — recognize the two runtime-resolving command shapes the installer
595
+ // emits, so the rewriter never churns (or un-does) an entry that already
596
+ // works in every environment sharing the config root.
597
+ const CHAIN_RUNNER_COMMAND = /^"\$\(for n in [\s\S]*?printf '%s' "\$n" && break; done\)"\s+\S/;
598
+ const RESOLVER_RUNNER_COMMAND = /^(?:"[^"]*bash(\.exe)?"|bash)\s+"[^"]*gsd-node-runner\.sh"\s+"[^"]*"\s+\S/;
599
+ function rewriteLegacyManagedNodeHookCommands(settings, runnerToken, opts) {
600
+ if (!settings || !settings.hooks || !runnerToken)
434
601
  return false;
435
602
  if (!opts)
436
603
  opts = {};
@@ -452,29 +619,31 @@ function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) {
452
619
  if (hadPowerShellCallOperator) {
453
620
  trimmed = trimmed.replace(/^&\s+/, '').trim();
454
621
  }
622
+ if (CHAIN_RUNNER_COMMAND.test(trimmed) || RESOLVER_RUNNER_COMMAND.test(trimmed))
623
+ continue;
455
624
  const m = trimmed.match(/^node\s+("([^"]+)"|'([^']+)'|(\S+))\s*$/) ||
456
625
  trimmed.match(/^("([^"]+)"|'([^']+)'|(\S+))\s+("([^"]+)"|'([^']+)'|(\S+))\s*$/);
457
626
  if (!m)
458
627
  continue;
459
- let _runnerToken, scriptToken, scriptPath;
628
+ let scriptToken, scriptPath;
460
629
  if (/^node\s+/.test(trimmed)) {
461
- _runnerToken = 'node';
462
630
  scriptToken = m[1];
463
631
  scriptPath = m[2] || m[3] || m[4] || '';
464
632
  }
465
633
  else {
466
- _runnerToken = m[1];
467
- const runnerPath = shellCmdProjection.posixNormalize(m[2] || m[3] || m[4] || '');
468
- const stableRunner = normalizeNodePath(runnerPath);
469
- if (stableRunner === runnerPath && platform !== 'win32')
470
- continue;
634
+ // #3662: the pre-fix two-token shape baked an absolute node path at
635
+ // install time. A foreign-but-stable runner (valid in the
636
+ // environment that wrote it, absent here) used to be SKIPPED — the
637
+ // exact mechanism behind the mixed state where no environment can
638
+ // run all hooks. Every two-token managed entry now re-projects onto
639
+ // the runtime-resolving runner, whatever environment baked it.
471
640
  scriptToken = m[5];
472
641
  scriptPath = m[6] || m[7] || m[8] || '';
473
642
  }
474
643
  if (!isManagedHookBasename(scriptPath, { surface: 'settings-json' }))
475
644
  continue;
476
645
  const projectedCommand = projectLegacySettingsHookCommand({
477
- absoluteRunner,
646
+ runnerToken,
478
647
  scriptPath,
479
648
  scriptToken,
480
649
  runtime: opts.runtime || 'generic',
@@ -863,27 +1032,75 @@ function buildHookCommand(configDir, hookName, opts) {
863
1032
  }
864
1033
  return JSON.stringify(shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName);
865
1034
  }
866
- const nodeRunner = resolveNodeRunner();
867
- const runner = isShellHook ? resolveBashRunner(opts) : nodeRunner;
868
- if (runner === null)
869
- return null;
1035
+ // .sh hooks keep the pre-#3662 shape everywhere: the bash runner resolves
1036
+ // at install time like today, and `bash` itself is a PATH-stable binary
1037
+ // (the absolute Git-Bash discovery covers win32 — #580/#3393).
1038
+ if (isShellHook) {
1039
+ const runner = resolveBashRunner(opts);
1040
+ if (runner === null)
1041
+ return null;
1042
+ if (opts.portableHooks) {
1043
+ const portableBaseDir = projectPortableHookBaseDir({
1044
+ configDir,
1045
+ homeDir: node_os_1.default.homedir(),
1046
+ });
1047
+ return projectManagedHookCommand({
1048
+ absoluteRunner: runner,
1049
+ scriptPath: `${portableBaseDir}/hooks/${hookName}`,
1050
+ runtime: opts.runtime || 'generic',
1051
+ platform,
1052
+ hookShell,
1053
+ });
1054
+ }
1055
+ const hooksPath = shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName;
1056
+ return projectManagedHookCommand({
1057
+ absoluteRunner: runner,
1058
+ scriptPath: hooksPath,
1059
+ runtime,
1060
+ platform,
1061
+ hookShell,
1062
+ });
1063
+ }
1064
+ // JS hooks (#3662): the node runner is resolved at hook-fire time, never
1065
+ // baked as a bare absolute path — an install-environment absolute path is
1066
+ // exactly what breaks with exit 127 when the config root is shared across
1067
+ // environments with different node layouts.
870
1068
  if (opts.portableHooks) {
1069
+ // Portable installs route through the staged resolver: the baked absolute
1070
+ // path travels as the resolver's FIRST argument (tried first, so the
1071
+ // minimal-PATH guarantee holds), then `command -v node`, then the
1072
+ // well-known list — one staged file, no per-install templating. The
1073
+ // token is shell-escaped like the chain (the host shell expands the
1074
+ // argument before bash sees argv), not merely JSON-quoted.
1075
+ const bakedToken = buildBakedNodeToken(opts);
1076
+ if (bakedToken === null)
1077
+ return null;
871
1078
  const portableBaseDir = projectPortableHookBaseDir({
872
1079
  configDir,
873
1080
  homeDir: node_os_1.default.homedir(),
874
1081
  });
875
- return projectManagedHookCommand({
876
- absoluteRunner: runner,
877
- scriptPath: `${portableBaseDir}/hooks/${hookName}`,
878
- runtime: opts.runtime || 'generic',
1082
+ // Absolute Git-Bash discovery on win32 when available (#580); `bash` on
1083
+ // PATH otherwise — the same assumption .sh hooks already make.
1084
+ const resolverRunner = resolveBashRunner(opts) || 'bash';
1085
+ return shellCmdProjection.projectShellCommandText({
1086
+ runnerToken: resolverRunner,
1087
+ argTokens: [
1088
+ JSON.stringify(`${portableBaseDir}/hooks/${NODE_RUNNER_RESOLVER_HOOK}`),
1089
+ bakedToken,
1090
+ JSON.stringify(`${portableBaseDir}/hooks/${hookName}`),
1091
+ ],
1092
+ runtime,
879
1093
  platform,
880
1094
  hookShell,
881
1095
  });
882
1096
  }
1097
+ const chainRunner = buildNodeRunnerChainToken(opts);
1098
+ if (chainRunner === null)
1099
+ return null;
883
1100
  const hooksPath = shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName;
884
- return projectManagedHookCommand({
885
- absoluteRunner: runner,
886
- scriptPath: hooksPath,
1101
+ return shellCmdProjection.projectShellCommandText({
1102
+ runnerToken: chainRunner,
1103
+ argTokens: [JSON.stringify(hooksPath)],
887
1104
  runtime,
888
1105
  platform,
889
1106
  hookShell,
@@ -1094,6 +1311,128 @@ function reconcileCursorHooksJson(hooksJsonPath, managedEntries) {
1094
1311
  }
1095
1312
  return { changed: changed, wrote: shouldWrite, path: hooksJsonPath };
1096
1313
  }
1314
+ /**
1315
+ * Stage the `hooks/lib/` helpers a set of staged hook scripts require, walking
1316
+ * the require graph TRANSITIVELY to a fixed point.
1317
+ *
1318
+ * Extracted from writeCursorHooksJson (#3911 review, commit 704859e9c) so the
1319
+ * reduced Codex hook bundle can share one implementation instead of growing a
1320
+ * second, divergent copy (#4087 / #4098). Both reduced bundles hand-pick which
1321
+ * hook SCRIPTS they ship, and neither can hand-pick their helpers correctly for
1322
+ * long: `hooks/lib/hook-exit.js` requires `./cli-exit.js`, which requires
1323
+ * `./exit-code-registry.js` — with NO `./lib/` prefix, because from inside
1324
+ * `lib/` the sibling is already local. A single-pass scan for the `./lib/…`
1325
+ * spelling used FROM a hook script stages hook-exit.js and stops, and the
1326
+ * installed hook then dies on MODULE_NOT_FOUND at load, before its own
1327
+ * try/catch, on every event it is registered for.
1328
+ *
1329
+ * Scans CONTENT rather than paths so a caller can seed from whatever it staged,
1330
+ * transformed or not, without this helper knowing the caller's layout.
1331
+ *
1332
+ * @returns the lib filenames actually staged, in staging order.
1333
+ */
1334
+ function stageTransitiveHookLibs(opts) {
1335
+ const { seedSources, srcLibDir, destLibDir, runtimeLabel, transform } = opts;
1336
+ const requiredLibFiles = new Set();
1337
+ const scannedLibFiles = new Set();
1338
+ const staged = [];
1339
+ // `./X` means DIFFERENT things depending on where the scanned file lives, and
1340
+ // conflating them stages the wrong file. From a hook SCRIPT in hooks/, a bare
1341
+ // `./X` is a sibling hook-level artifact — Codex's gsd-check-update-worker.js
1342
+ // requires `./managed-hooks-registry.cjs`, which lives in hooks/, not
1343
+ // hooks/lib/ — so only the explicit `./lib/X` spelling is a lib requirement.
1344
+ // From inside a LIB file, the sibling is already local, so `./X` IS a lib
1345
+ // requirement (hook-exit.js -> ./cli-exit.js -> ./exit-code-registry.js); that
1346
+ // is the case 704859e9c added and it must keep working. Cursor never exposed
1347
+ // the difference because none of its staged scripts has a bare sibling
1348
+ // require; Codex's does, and the fail-loud guard below caught it immediately
1349
+ // by demanding managed-hooks-registry.cjs out of hooks/dist/lib.
1350
+ // Fresh per call: a module-level /g regex carries lastIndex across calls and
1351
+ // would silently skip matches on the second install in one process.
1352
+ const seedRequireRe = /require\(\s*['"]\.\/lib\/([A-Za-z0-9._-]+)['"]\s*\)/g;
1353
+ const libRequireRe = /require\(\s*['"]\.\/(?:lib\/)?([A-Za-z0-9._-]+)['"]\s*\)/g;
1354
+ // A NESTED helper path is outside the flat layout hooks/lib/ has and the
1355
+ // build emits, and the character classes above cannot express it — so it
1356
+ // would be a SILENT miss, staging nothing and shipping a hook that dies at
1357
+ // load. Detected separately and refused loudly instead: a silent miss is the
1358
+ // failure mode this whole function exists to remove (review of #4087).
1359
+ const nestedRequireRe = /require\(\s*['"]\.\/lib\/[A-Za-z0-9._-]+\/[^'"]*['"]\s*\)/;
1360
+ const scanForLibRequires = (source, fromLib) => {
1361
+ if (nestedRequireRe.test(source)) {
1362
+ throw new Error(`A staged ${runtimeLabel} hook requires a NESTED hooks/lib path. hooks/lib/ is flat and `
1363
+ + 'this stager only resolves flat helper names, so the nested helper would never be '
1364
+ + 'staged and the hook would throw MODULE_NOT_FOUND at load. Flatten the helper or '
1365
+ + 'extend this stager deliberately.');
1366
+ }
1367
+ const re = fromLib ? libRequireRe : seedRequireRe;
1368
+ re.lastIndex = 0;
1369
+ let m;
1370
+ while ((m = re.exec(source)) !== null) {
1371
+ const candidate = m[1];
1372
+ // A capture with no alphanumeric character is not a module name — it is
1373
+ // prose. This scan reads whole file text, comments included, and
1374
+ // hooks/lib/injection-patterns.js's own header documents this mechanism
1375
+ // with the literal string `require('./lib/...')`, which captures `...`
1376
+ // and would send the resolver hunting for `hooks/lib/...` and fail the
1377
+ // install (measured; that helper is not staged for either reduced bundle
1378
+ // today, so it is latent rather than live).
1379
+ //
1380
+ // KNOWN LIMIT, disclosed rather than papered over: this does NOT make the
1381
+ // scan comment-aware. A comment naming a REAL helper — `require(
1382
+ // './lib/git-cmd.js')` in prose — still registers it and would over-stage
1383
+ // that helper. Closing that needs a comment-stripping pass; the
1384
+ // line-based stripper in scripts/lint-hooks-runtime-build-seam.cjs is the
1385
+ // precedent (its header explains why the naive two-regex strip corrupts
1386
+ // these very files), but promoting a lint-script helper into installer
1387
+ // runtime code is a larger change than this fix.
1388
+ if (!/[A-Za-z0-9]/.test(candidate))
1389
+ continue;
1390
+ requiredLibFiles.add(candidate);
1391
+ }
1392
+ };
1393
+ for (const source of seedSources)
1394
+ scanForLibRequires(source, false);
1395
+ if (requiredLibFiles.size === 0)
1396
+ return staged;
1397
+ node_fs_1.default.mkdirSync(destLibDir, { recursive: true });
1398
+ // Iterate to a fixed point: staging a lib file can add MORE required lib
1399
+ // files (its own requires), which must themselves be staged and scanned.
1400
+ let libFile = [...requiredLibFiles].find((f) => !scannedLibFiles.has(f));
1401
+ while (libFile !== undefined) {
1402
+ scannedLibFiles.add(libFile);
1403
+ // Node's own extension resolution: `require('./lib/x')` is a valid, working
1404
+ // CommonJS spelling today, and matching only the extension-bearing form
1405
+ // resolved `x` literally, found nothing, and failed the install on a
1406
+ // legitimate require (review of #4087). Try the bare name first so an
1407
+ // extension-bearing capture still wins, then .js/.cjs.
1408
+ let resolvedName;
1409
+ for (const candidate of [libFile, `${libFile}.js`, `${libFile}.cjs`]) {
1410
+ if (node_fs_1.default.existsSync(node_path_1.default.join(srcLibDir, candidate))) {
1411
+ resolvedName = candidate;
1412
+ break;
1413
+ }
1414
+ }
1415
+ const libSrc = node_path_1.default.join(srcLibDir, resolvedName ?? libFile);
1416
+ if (resolvedName === undefined) {
1417
+ // FAIL LOUD. Skipping here would ship hook scripts whose top-level
1418
+ // require() throws before their own try/catch, wedging every session —
1419
+ // and the install would still exit 0, so nobody would know until a user
1420
+ // hit it. A missing helper source is a packaging bug; surface it.
1421
+ throw new Error(`hooks/lib/${libFile} is required by a staged ${runtimeLabel} hook but is missing from ${srcLibDir}. `
1422
+ + 'Installing would ship a hook that throws MODULE_NOT_FOUND at load.');
1423
+ }
1424
+ let libContent = node_fs_1.default.readFileSync(libSrc, 'utf8');
1425
+ if (transform)
1426
+ libContent = transform(libContent);
1427
+ // Written under its RESOLVED name so an extensionless require still lands a
1428
+ // file Node can resolve at the destination.
1429
+ node_fs_1.default.writeFileSync(node_path_1.default.join(destLibDir, resolvedName), libContent);
1430
+ staged.push(resolvedName);
1431
+ scanForLibRequires(libContent, true);
1432
+ libFile = [...requiredLibFiles].find((f) => !scannedLibFiles.has(f));
1433
+ }
1434
+ return staged;
1435
+ }
1097
1436
  function writeCursorHooksJson(targetDir, src, opts) {
1098
1437
  opts = opts || {};
1099
1438
  const hooksDir = node_path_1.default.join(targetDir, 'hooks');
@@ -1119,42 +1458,25 @@ function writeCursorHooksJson(targetDir, src, opts) {
1119
1458
  installedScripts.add(script);
1120
1459
  }
1121
1460
  }
1122
- // Stage the hooks/lib/ helpers the staged scripts require (#2587). Cursor sets
1123
- // hostBehaviors.skipSharedHooksInstall, so it never reaches the installer's
1124
- // bulk hooks/lib copy — without this, a script requiring './lib/…' would throw
1125
- // MODULE_NOT_FOUND at load, BEFORE its own try/catch, and wedge every Cursor
1126
- // session on the one runtime these hooks exist for. Driven off what the staged
1127
- // scripts actually require so a future helper cannot be silently omitted.
1128
- const requiredLibFiles = new Set();
1129
- for (const script of installedScripts) {
1130
- const staged = node_fs_1.default.readFileSync(node_path_1.default.join(hooksDir, script), 'utf8');
1131
- // Tolerant of interior whitespace and either quote style: a hook author
1132
- // writing `require( "./lib/x.js" )` must still get its helper staged, since
1133
- // a miss here surfaces as MODULE_NOT_FOUND at hook load, not at install.
1134
- const re = /require\(\s*['"]\.\/lib\/([A-Za-z0-9._-]+)['"]\s*\)/g;
1135
- let m;
1136
- while ((m = re.exec(staged)) !== null)
1137
- requiredLibFiles.add(m[1]);
1138
- }
1139
- if (requiredLibFiles.size > 0) {
1140
- const srcLibDir = node_path_1.default.join(srcHooksDir, 'lib');
1141
- const destLibDir = node_path_1.default.join(hooksDir, 'lib');
1142
- node_fs_1.default.mkdirSync(destLibDir, { recursive: true });
1143
- for (const libFile of requiredLibFiles) {
1144
- const libSrc = node_path_1.default.join(srcLibDir, libFile);
1145
- if (!node_fs_1.default.existsSync(libSrc)) {
1146
- // FAIL LOUD. Skipping here would ship hook scripts whose top-level
1147
- // require() throws before their own try/catch, wedging every session —
1148
- // and the install would still exit 0, so nobody would know until a user
1149
- // hit it. A missing helper source is a packaging bug; surface it.
1150
- throw new Error(`hooks/lib/${libFile} is required by a staged Cursor hook but is missing from ${srcLibDir}. `
1151
- + 'Installing would ship a hook that throws MODULE_NOT_FOUND at load.');
1152
- }
1153
- let libContent = node_fs_1.default.readFileSync(libSrc, 'utf8');
1154
- libContent = libContent.replace(/gsd:/gi, 'gsd-');
1155
- node_fs_1.default.writeFileSync(node_path_1.default.join(destLibDir, libFile), libContent);
1156
- }
1157
- }
1461
+ // Stage the hooks/lib/ helpers the staged scripts require (#2587), TRANSITIVELY
1462
+ // (#3911 review): a lib helper can itself require a sibling under lib/ (e.g.
1463
+ // hooks/lib/hook-exit.js requires './cli-exit.js', which requires
1464
+ // './exit-code-registry.js') — a require with NO './lib/' prefix, because from
1465
+ // inside lib/ the sibling is already local. The original single-pass scan only
1466
+ // ever matched the "./lib/…" spelling used FROM a hook script, so it staged
1467
+ // hook-exit.js but never walked hook-exit.js's own requires, and an installed
1468
+ // Cursor hook wedged on MODULE_NOT_FOUND for './cli-exit.js' at load — before
1469
+ // its own try/catch. This walks a worklist: hook scripts seed it with their
1470
+ // "./lib/X" requires, and every lib file staged is itself scanned for further
1471
+ // "./lib/X" OR bare "./X" (sibling-within-lib) requires, so the requirement
1472
+ // graph is derived to a fixed point instead of one hand-tuned level deep.
1473
+ stageTransitiveHookLibs({
1474
+ seedSources: [...installedScripts].map((script) => node_fs_1.default.readFileSync(node_path_1.default.join(hooksDir, script), 'utf8')),
1475
+ srcLibDir: node_path_1.default.join(srcHooksDir, 'lib'),
1476
+ destLibDir: node_path_1.default.join(hooksDir, 'lib'),
1477
+ runtimeLabel: 'Cursor',
1478
+ transform: (content) => content.replace(/gsd:/gi, 'gsd-'),
1479
+ });
1158
1480
  // #2717: write the CommonJS marker into hooks/ alongside the staged .js
1159
1481
  // scripts. Cursor sets skipSharedHooksInstall, so it never reaches
1160
1482
  // installSharedHooksBundle (the only other writer of this marker); without
@@ -1357,6 +1679,26 @@ function writeWindsurfHooksJson(targetDir, src, opts) {
1357
1679
  installedScripts.add(script);
1358
1680
  }
1359
1681
  }
1682
+ // Stage the hooks/lib/ helpers these scripts require (#4087 review). Windsurf
1683
+ // sets hostBehaviors.skipSharedHooksInstall, so like Cursor it never reaches
1684
+ // installSharedHooksBundle — the only other stager of hooks/lib — and it was
1685
+ // staging neither. Both Cascade guards require helpers at module load:
1686
+ // gsd-windsurf-pre-write.js requires ./lib/hook-exit.js and ./lib/git-probe.js,
1687
+ // gsd-windsurf-pre-command.js requires ./lib/hook-exit.js. Measured against a
1688
+ // real `--windsurf --global` install before this call existed: the installer
1689
+ // exited 0, hooks/ held only the two scripts, and running either one exited 1
1690
+ // with "Cannot find module './lib/hook-exit.js'" — the same failure #4087
1691
+ // reports for Codex, on every pre_write_code / pre_run_command event.
1692
+ //
1693
+ // The transform matches the one applied to the scripts above: a helper must be
1694
+ // rewritten the same way as its caller or the two disagree on the spelling.
1695
+ stageTransitiveHookLibs({
1696
+ seedSources: [...installedScripts].map((script) => node_fs_1.default.readFileSync(node_path_1.default.join(hooksDir, script), 'utf8')),
1697
+ srcLibDir: node_path_1.default.join(srcHooksDir, 'lib'),
1698
+ destLibDir: node_path_1.default.join(hooksDir, 'lib'),
1699
+ runtimeLabel: 'Windsurf',
1700
+ transform: (content) => content.replace(/gsd:/gi, 'gsd-'),
1701
+ });
1360
1702
  // #2717: write the CommonJS marker into hooks/ alongside the staged .js
1361
1703
  // scripts. Windsurf sets skipSharedHooksInstall, so it never reaches
1362
1704
  // installSharedHooksBundle (the only other writer of this marker); without
@@ -1507,6 +1849,37 @@ function applySettingsJsonHooks(settings, opts) {
1507
1849
  if (!settings.hooks.SessionStart) {
1508
1850
  settings.hooks.SessionStart = [];
1509
1851
  }
1852
+ // #3981: Claude Code treats a timed-out hook as NON-blocking — the tool
1853
+ // call continues through the normal permission flow. The blocking
1854
+ // PreToolUse guards therefore need a budget a host stall cannot exceed,
1855
+ // not one sized to the hook's own ~0.1 s runtime. Observed stalls reached
1856
+ // 84.3 s; 120 s is the top of the issue's prescribed 60–120 range and
1857
+ // returns every observed verdict. Registration below uses this constant,
1858
+ // and the migration pass right here raises existing managed entries.
1859
+ const BLOCKING_GUARD_TIMEOUT_S = 120;
1860
+ const blockingGuardNames = [
1861
+ 'gsd-prompt-guard',
1862
+ 'gsd-workflow-guard',
1863
+ 'gsd-worktree-path-guard',
1864
+ 'gsd-agent-isolation-guard',
1865
+ 'gsd-write-guard',
1866
+ 'gsd-secret-read-guard',
1867
+ 'gsd-validate-commit',
1868
+ ];
1869
+ for (const entries of Object.values(settings.hooks)) {
1870
+ if (!Array.isArray(entries))
1871
+ continue;
1872
+ for (const entry of entries) {
1873
+ if (!entry || !Array.isArray(entry.hooks))
1874
+ continue;
1875
+ for (const h of entry.hooks) {
1876
+ if (blockingGuardNames.some((name) => referencesHook(h, name)) &&
1877
+ h.timeout === 5) {
1878
+ h.timeout = BLOCKING_GUARD_TIMEOUT_S;
1879
+ }
1880
+ }
1881
+ }
1882
+ }
1510
1883
  const hasGsdUpdateHook = settings.hooks.SessionStart.some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-check-update')));
1511
1884
  // Guard: only register if the hook file was actually installed (#1754).
1512
1885
  // When hooks/dist/ is missing from the npm package (as in v1.32.0), the
@@ -1587,7 +1960,7 @@ function applySettingsJsonHooks(settings, opts) {
1587
1960
  {
1588
1961
  type: 'command',
1589
1962
  command: promptGuardCommand,
1590
- timeout: 5
1963
+ timeout: BLOCKING_GUARD_TIMEOUT_S
1591
1964
  }
1592
1965
  ]
1593
1966
  });
@@ -1664,7 +2037,7 @@ function applySettingsJsonHooks(settings, opts) {
1664
2037
  {
1665
2038
  type: 'command',
1666
2039
  command: workflowGuardCommand,
1667
- timeout: 5
2040
+ timeout: BLOCKING_GUARD_TIMEOUT_S
1668
2041
  }
1669
2042
  ]
1670
2043
  });
@@ -1689,7 +2062,7 @@ function applySettingsJsonHooks(settings, opts) {
1689
2062
  {
1690
2063
  type: 'command',
1691
2064
  command: worktreePathGuardCommand,
1692
- timeout: 5
2065
+ timeout: BLOCKING_GUARD_TIMEOUT_S
1693
2066
  }
1694
2067
  ]
1695
2068
  });
@@ -1719,7 +2092,7 @@ function applySettingsJsonHooks(settings, opts) {
1719
2092
  {
1720
2093
  type: 'command',
1721
2094
  command: agentIsolationGuardCommand,
1722
- timeout: 5
2095
+ timeout: BLOCKING_GUARD_TIMEOUT_S
1723
2096
  }
1724
2097
  ]
1725
2098
  });
@@ -1746,7 +2119,7 @@ function applySettingsJsonHooks(settings, opts) {
1746
2119
  {
1747
2120
  type: 'command',
1748
2121
  command: writeGuardCommand,
1749
- timeout: 5
2122
+ timeout: BLOCKING_GUARD_TIMEOUT_S
1750
2123
  }
1751
2124
  ]
1752
2125
  });
@@ -1755,6 +2128,33 @@ function applySettingsJsonHooks(settings, opts) {
1755
2128
  else if (!hasWriteGuardHook && !node_fs_1.default.existsSync(writeGuardFile)) {
1756
2129
  console.warn(` ${yellow}⚠${reset} Skipped write guard hook — gsd-write-guard.js not found at target`);
1757
2130
  }
2131
+ // Configure PreToolUse hook for secret-file read protection (#4221).
2132
+ // Hard-blocks Read/Grep/Bash reads of .env, .env.<suffix> and .secrets.
2133
+ // Replaces the Read(.env*) permission deny rules the installer used to
2134
+ // write (#768): on Claude Code >= 2.1.259 ANY Read() deny rule makes every
2135
+ // `cd DIR && grep …` compound prompt for approval, even in auto mode; a
2136
+ // hook denial is not a permission rule and never arms that check.
2137
+ const secretReadGuardCommand = isGlobal
2138
+ ? buildHookCommand(targetDir, 'gsd-secret-read-guard.js', hookOpts)
2139
+ : localCmd('gsd-secret-read-guard.js');
2140
+ const hasSecretReadGuardHook = settings.hooks[preToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-secret-read-guard')));
2141
+ const secretReadGuardFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-secret-read-guard.js');
2142
+ if (!hasSecretReadGuardHook && node_fs_1.default.existsSync(secretReadGuardFile) && secretReadGuardCommand) {
2143
+ settings.hooks[preToolEvent].push({
2144
+ matcher: 'Read|Grep|Bash',
2145
+ hooks: [
2146
+ {
2147
+ type: 'command',
2148
+ command: secretReadGuardCommand,
2149
+ timeout: BLOCKING_GUARD_TIMEOUT_S
2150
+ }
2151
+ ]
2152
+ });
2153
+ console.log(` ${green}✓${reset} Configured secret read guard hook (.env / .secrets read protection)`);
2154
+ }
2155
+ else if (!hasSecretReadGuardHook && !node_fs_1.default.existsSync(secretReadGuardFile)) {
2156
+ console.warn(` ${yellow}⚠${reset} Skipped secret read guard hook — gsd-secret-read-guard.js not found at target`);
2157
+ }
1758
2158
  // Configure commit validation hook (Conventional Commits enforcement, opt-in)
1759
2159
  const validateCommitCommand = isGlobal
1760
2160
  ? buildHookCommand(targetDir, 'gsd-validate-commit.sh', hookOpts)
@@ -1771,7 +2171,7 @@ function applySettingsJsonHooks(settings, opts) {
1771
2171
  {
1772
2172
  type: 'command',
1773
2173
  command: validateCommitCommand,
1774
- timeout: 5
2174
+ timeout: BLOCKING_GUARD_TIMEOUT_S
1775
2175
  }
1776
2176
  ]
1777
2177
  });
@@ -2100,6 +2500,7 @@ function buildKimiHooksTomlBlock(targetDir, opts) {
2100
2500
  { event: 'PreToolUse', command: cmd('gsd-read-guard.js'), matcher: 'WriteFile|StrReplaceFile', timeout: 5 },
2101
2501
  { event: 'PreToolUse', command: cmd('gsd-worktree-path-guard.js'), matcher: 'WriteFile|StrReplaceFile', timeout: 5 },
2102
2502
  { event: 'PreToolUse', command: cmd('gsd-write-guard.js'), matcher: 'WriteFile', timeout: 5 },
2503
+ { event: 'PreToolUse', command: cmd('gsd-secret-read-guard.js'), matcher: 'ReadFile|Grep|Shell', timeout: 5 },
2103
2504
  { event: 'PreToolUse', command: cmd('gsd-workflow-guard.js'), matcher: 'Shell|WriteFile|StrReplaceFile', timeout: 5 },
2104
2505
  { event: 'PreToolUse', command: cmd('gsd-validate-commit.sh'), matcher: 'Shell', timeout: 5 },
2105
2506
  // PostToolUse
@@ -2302,6 +2703,7 @@ module.exports = {
2302
2703
  KIMI_HOOKS_TOML_MARKER_BEGIN,
2303
2704
  KIMI_HOOKS_TOML_MARKER_END,
2304
2705
  // Shared
2706
+ stageTransitiveHookLibs,
2305
2707
  buildHookCommand,
2306
2708
  applySettingsJsonHooks,
2307
2709
  referencesHook,
@@ -2309,7 +2711,9 @@ module.exports = {
2309
2711
  reconcileManagedShellHookCommands,
2310
2712
  normalizeNodePath,
2311
2713
  resolveNodeRunner,
2714
+ buildNodeRunnerChainToken,
2312
2715
  resolveBashRunner,
2716
+ NODE_RUNNER_RESOLVER_HOOK,
2313
2717
  // Atomic write seam (shared with bin/install.js so all writes participate
2314
2718
  // in install.js's _cleanTmpFiles() scoped temp-cleanup).
2315
2719
  atomicWriteFileSync,