@opengsd/gsd-core 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (395) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-code-fixer.md +1 -1
  4. package/agents/gsd-debug-session-manager.md +1 -1
  5. package/agents/gsd-debugger.md +1 -1
  6. package/agents/gsd-dom-verifier.md +169 -0
  7. package/agents/gsd-eval-auditor.md +1 -1
  8. package/agents/gsd-executor.md +17 -9
  9. package/agents/gsd-framework-selector.md +1 -3
  10. package/agents/gsd-intel-updater.md +1 -1
  11. package/agents/gsd-mempalace-curator.md +0 -1
  12. package/agents/gsd-pattern-mapper.md +11 -0
  13. package/agents/gsd-phase-researcher.md +3 -1
  14. package/agents/gsd-plan-checker.md +15 -55
  15. package/agents/gsd-planner.md +6 -4
  16. package/agents/gsd-project-researcher.md +1 -1
  17. package/agents/gsd-research-synthesizer.md +2 -2
  18. package/agents/gsd-roadmapper.md +15 -11
  19. package/agents/gsd-ui-checker.md +63 -4
  20. package/agents/gsd-ui-researcher.md +41 -3
  21. package/agents/gsd-verifier.md +1 -1
  22. package/bin/install.js +609 -134
  23. package/commands/gsd/discuss-phase.md +1 -1
  24. package/commands/gsd/import.md +1 -1
  25. package/commands/gsd/quick.md +8 -4
  26. package/gsd-core/bin/gsd-tools.cjs +567 -51
  27. package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
  28. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  29. package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
  30. package/gsd-core/bin/lib/api-coverage.cjs +30 -9
  31. package/gsd-core/bin/lib/artifacts.cjs +2 -0
  32. package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
  33. package/gsd-core/bin/lib/audit.cjs +163 -41
  34. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  35. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  36. package/gsd-core/bin/lib/capability-registry.cjs +336 -95
  37. package/gsd-core/bin/lib/capability-state.cjs +18 -3
  38. package/gsd-core/bin/lib/capability-validator.cjs +205 -18
  39. package/gsd-core/bin/lib/check-command-router.cjs +145 -5
  40. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  41. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  42. package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
  43. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  44. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  45. package/gsd-core/bin/lib/commands.cjs +543 -44
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
  47. package/gsd-core/bin/lib/config-loader.cjs +118 -29
  48. package/gsd-core/bin/lib/config.cjs +92 -2
  49. package/gsd-core/bin/lib/configuration.cjs +129 -37
  50. package/gsd-core/bin/lib/core-utils.cjs +84 -7
  51. package/gsd-core/bin/lib/edge-probe.cjs +9 -1
  52. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  53. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  54. package/gsd-core/bin/lib/frontmatter.cjs +840 -305
  55. package/gsd-core/bin/lib/gap-checker.cjs +27 -3
  56. package/gsd-core/bin/lib/git-base-branch.cjs +174 -39
  57. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
  58. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +6 -3
  59. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
  60. package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
  61. package/gsd-core/bin/lib/host-integration.cjs +39 -6
  62. package/gsd-core/bin/lib/init-command-router.cjs +118 -21
  63. package/gsd-core/bin/lib/init.cjs +120 -41
  64. package/gsd-core/bin/lib/install-engine.cjs +68 -3
  65. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -1
  66. package/gsd-core/bin/lib/install-profiles.cjs +78 -4
  67. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  68. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  69. package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
  70. package/gsd-core/bin/lib/intel.cjs +101 -26
  71. package/gsd-core/bin/lib/io.cjs +160 -15
  72. package/gsd-core/bin/lib/learnings.cjs +85 -14
  73. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  74. package/gsd-core/bin/lib/markdown-table.cjs +52 -4
  75. package/gsd-core/bin/lib/milestone.cjs +90 -5
  76. package/gsd-core/bin/lib/model-catalog.cjs +177 -19
  77. package/gsd-core/bin/lib/model-resolver.cjs +10 -28
  78. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  79. package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
  80. package/gsd-core/bin/lib/phase-id.cjs +70 -4
  81. package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
  82. package/gsd-core/bin/lib/phase-locator.cjs +138 -17
  83. package/gsd-core/bin/lib/phase.cjs +405 -84
  84. package/gsd-core/bin/lib/plan-document.cjs +263 -0
  85. package/gsd-core/bin/lib/plan-scan.cjs +13 -2
  86. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  87. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  88. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -14
  89. package/gsd-core/bin/lib/planning-workspace.cjs +56 -0
  90. package/gsd-core/bin/lib/probe-core.cjs +4 -1
  91. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  92. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  93. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  94. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
  95. package/gsd-core/bin/lib/review-lane-descriptor.cjs +9 -9
  96. package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
  97. package/gsd-core/bin/lib/roadmap-parser.cjs +79 -16
  98. package/gsd-core/bin/lib/roadmap.cjs +74 -19
  99. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +96 -8
  100. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +34 -1
  101. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +287 -55
  102. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  103. package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
  104. package/gsd-core/bin/lib/shell-command-projection.cjs +71 -8
  105. package/gsd-core/bin/lib/smart-entry.cjs +12 -22
  106. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  107. package/gsd-core/bin/lib/state-command-router.cjs +47 -18
  108. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  109. package/gsd-core/bin/lib/state-document.cjs +186 -0
  110. package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
  111. package/gsd-core/bin/lib/state-transition.cjs +517 -101
  112. package/gsd-core/bin/lib/state.cjs +946 -163
  113. package/gsd-core/bin/lib/surface.cjs +10 -2
  114. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  115. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  116. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  117. package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
  118. package/gsd-core/bin/lib/uat.cjs +1376 -125
  119. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  120. package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
  121. package/gsd-core/bin/lib/unusable-input.cjs +13 -0
  122. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  123. package/gsd-core/bin/lib/vendor/README.md +43 -5
  124. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  125. package/gsd-core/bin/lib/verification.cjs +14 -1
  126. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  127. package/gsd-core/bin/lib/verify.cjs +95 -40
  128. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  129. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  130. package/gsd-core/bin/lib/worktree-safety.cjs +177 -21
  131. package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
  132. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  133. package/gsd-core/bin/shared/exit-codes.json +8 -0
  134. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  135. package/gsd-core/bin/shared/model-catalog.json +8 -1
  136. package/gsd-core/references/agent-contracts.md +3 -2
  137. package/gsd-core/references/api-coverage.md +24 -2
  138. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  139. package/gsd-core/references/checkpoints.md +37 -19
  140. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  141. package/gsd-core/references/edge-probe.md +8 -0
  142. package/gsd-core/references/execute-mvp-tdd.md +1 -3
  143. package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
  144. package/gsd-core/references/execute-phase-wave-guard.md +11 -9
  145. package/gsd-core/references/failing-direction.md +78 -0
  146. package/gsd-core/references/gate-prompts.md +1 -1
  147. package/gsd-core/references/git-integration.md +5 -5
  148. package/gsd-core/references/git-planning-commit.md +3 -3
  149. package/gsd-core/references/gsd-run-resolver.md +1 -1
  150. package/gsd-core/references/loop-hook-dispatch.md +22 -0
  151. package/gsd-core/references/model-profiles.md +1 -1
  152. package/gsd-core/references/nyquist-compliance.md +74 -0
  153. package/gsd-core/references/offer-next.md +3 -5
  154. package/gsd-core/references/phase-argument-parsing.md +3 -3
  155. package/gsd-core/references/planner-failing-direction.md +53 -0
  156. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  157. package/gsd-core/references/planner-revision.md +1 -1
  158. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  159. package/gsd-core/references/planning-config.md +37 -8
  160. package/gsd-core/references/reviewer-instances.md +31 -0
  161. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  162. package/gsd-core/references/tdd.md +1 -3
  163. package/gsd-core/references/ui-brand.md +65 -21
  164. package/gsd-core/references/ui-consideration-probe.md +1 -1
  165. package/gsd-core/references/universal-anti-patterns.md +2 -2
  166. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  167. package/gsd-core/references/verify-mvp-mode.md +1 -1
  168. package/gsd-core/references/workstream-flag.md +11 -11
  169. package/gsd-core/templates/README.md +1 -1
  170. package/gsd-core/templates/SECURITY.md +3 -3
  171. package/gsd-core/templates/UI-SPEC.md +25 -3
  172. package/gsd-core/templates/VALIDATION.md +3 -3
  173. package/gsd-core/templates/phase-prompt.md +3 -0
  174. package/gsd-core/templates/state.md +7 -0
  175. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  176. package/gsd-core/workflows/add-backlog.md +1 -1
  177. package/gsd-core/workflows/add-phase.md +3 -3
  178. package/gsd-core/workflows/add-tests.md +3 -8
  179. package/gsd-core/workflows/add-todo.md +1 -1
  180. package/gsd-core/workflows/ai-integration-phase.md +4 -9
  181. package/gsd-core/workflows/audit-fix.md +12 -3
  182. package/gsd-core/workflows/audit-milestone.md +9 -9
  183. package/gsd-core/workflows/audit-uat.md +17 -2
  184. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  185. package/gsd-core/workflows/autonomous.md +10 -26
  186. package/gsd-core/workflows/check-todos.md +1 -1
  187. package/gsd-core/workflows/cleanup.md +2 -2
  188. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
  189. package/gsd-core/workflows/code-review-fix.md +1 -1
  190. package/gsd-core/workflows/code-review.md +121 -40
  191. package/gsd-core/workflows/complete-milestone.md +15 -10
  192. package/gsd-core/workflows/debug.md +5 -3
  193. package/gsd-core/workflows/diagnose-issues.md +12 -6
  194. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  195. package/gsd-core/workflows/discuss-phase/modes/chain.md +3 -7
  196. package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
  197. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  198. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -2
  199. package/gsd-core/workflows/discuss-phase.md +1 -1
  200. package/gsd-core/workflows/do.md +3 -6
  201. package/gsd-core/workflows/docs-update.md +5 -4
  202. package/gsd-core/workflows/edit-phase.md +1 -1
  203. package/gsd-core/workflows/eval-review.md +4 -9
  204. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  205. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +113 -11
  206. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  207. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  208. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  209. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +22 -4
  210. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  211. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  212. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
  213. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  214. package/gsd-core/workflows/execute-phase.md +38 -54
  215. package/gsd-core/workflows/execute-plan.md +17 -12
  216. package/gsd-core/workflows/explore.md +1 -1
  217. package/gsd-core/workflows/extract-learnings.md +1 -1
  218. package/gsd-core/workflows/fast.md +2 -2
  219. package/gsd-core/workflows/forensics.md +1 -1
  220. package/gsd-core/workflows/graduation.md +5 -5
  221. package/gsd-core/workflows/health.md +3 -6
  222. package/gsd-core/workflows/import.md +14 -11
  223. package/gsd-core/workflows/inbox.md +4 -5
  224. package/gsd-core/workflows/ingest-docs.md +44 -11
  225. package/gsd-core/workflows/insert-phase.md +5 -5
  226. package/gsd-core/workflows/list-seeds.md +5 -3
  227. package/gsd-core/workflows/list-workspaces.md +1 -1
  228. package/gsd-core/workflows/manager.md +12 -23
  229. package/gsd-core/workflows/map-codebase.md +1 -1
  230. package/gsd-core/workflows/milestone-summary.md +1 -1
  231. package/gsd-core/workflows/mvp-phase.md +2 -2
  232. package/gsd-core/workflows/new-milestone.md +9 -21
  233. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  234. package/gsd-core/workflows/new-project.md +12 -26
  235. package/gsd-core/workflows/new-workspace.md +1 -1
  236. package/gsd-core/workflows/next.md +2 -2
  237. package/gsd-core/workflows/pause-work.md +1 -1
  238. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  239. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  240. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
  241. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
  242. package/gsd-core/workflows/plan-phase.md +121 -42
  243. package/gsd-core/workflows/plan-review-convergence.md +46 -9
  244. package/gsd-core/workflows/plant-seed.md +2 -2
  245. package/gsd-core/workflows/pr-branch.md +187 -51
  246. package/gsd-core/workflows/profile-user.md +16 -14
  247. package/gsd-core/workflows/progress.md +27 -12
  248. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  249. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -3
  250. package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
  251. package/gsd-core/workflows/quick/steps/research-phase.md +2 -4
  252. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  253. package/gsd-core/workflows/quick.md +20 -29
  254. package/gsd-core/workflows/remove-phase.md +4 -4
  255. package/gsd-core/workflows/remove-workspace.md +2 -2
  256. package/gsd-core/workflows/resume-project.md +8 -12
  257. package/gsd-core/workflows/review.md +193 -15
  258. package/gsd-core/workflows/scan.md +1 -1
  259. package/gsd-core/workflows/secure-phase.md +2 -2
  260. package/gsd-core/workflows/settings-advanced.md +7 -9
  261. package/gsd-core/workflows/settings-integrations.md +64 -31
  262. package/gsd-core/workflows/settings.md +3 -5
  263. package/gsd-core/workflows/ship.md +12 -6
  264. package/gsd-core/workflows/sketch-wrap-up.md +11 -17
  265. package/gsd-core/workflows/sketch.md +12 -18
  266. package/gsd-core/workflows/smart-entry.md +3 -5
  267. package/gsd-core/workflows/spec-phase.md +23 -1
  268. package/gsd-core/workflows/spike-wrap-up.md +7 -11
  269. package/gsd-core/workflows/spike.md +20 -31
  270. package/gsd-core/workflows/stats.md +2 -2
  271. package/gsd-core/workflows/sync-skills.md +1 -1
  272. package/gsd-core/workflows/thread.md +11 -7
  273. package/gsd-core/workflows/transition.md +5 -5
  274. package/gsd-core/workflows/ui-phase.md +10 -16
  275. package/gsd-core/workflows/ui-review.md +6 -10
  276. package/gsd-core/workflows/ultraplan-phase.md +5 -13
  277. package/gsd-core/workflows/undo.md +8 -16
  278. package/gsd-core/workflows/update.md +6 -10
  279. package/gsd-core/workflows/validate-phase.md +2 -2
  280. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  281. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  282. package/gsd-core/workflows/verify-work.md +57 -18
  283. package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
  284. package/hooks/dist/gsd-config-reload.js +18 -12
  285. package/hooks/dist/gsd-context-monitor.js +19 -10
  286. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  287. package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
  288. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  289. package/hooks/dist/gsd-cursor-stop.js +2 -1
  290. package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
  291. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
  292. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  293. package/hooks/dist/gsd-graphify-update.sh +22 -18
  294. package/hooks/dist/gsd-node-runner.sh +76 -0
  295. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  296. package/hooks/dist/gsd-prompt-guard.js +16 -7
  297. package/hooks/dist/gsd-read-guard.js +16 -7
  298. package/hooks/dist/gsd-read-injection-scanner.js +17 -8
  299. package/hooks/dist/gsd-session-state.sh +1 -0
  300. package/hooks/dist/gsd-statusline.js +215 -26
  301. package/hooks/dist/gsd-validate-commit.sh +80 -6
  302. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  303. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  304. package/hooks/dist/gsd-workflow-guard.js +34 -16
  305. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  306. package/hooks/dist/gsd-write-guard.js +35 -25
  307. package/hooks/dist/lib/cli-exit.js +560 -0
  308. package/hooks/dist/lib/exit-code-registry.js +98 -0
  309. package/hooks/dist/lib/git-probe.js +84 -0
  310. package/hooks/dist/lib/hook-exit.js +81 -0
  311. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  312. package/hooks/gsd-agent-isolation-guard.js +77 -38
  313. package/hooks/gsd-config-reload.js +18 -12
  314. package/hooks/gsd-context-monitor.js +19 -10
  315. package/hooks/gsd-cursor-post-tool.js +3 -1
  316. package/hooks/gsd-cursor-pre-tool.js +3 -1
  317. package/hooks/gsd-cursor-session-start.js +2 -1
  318. package/hooks/gsd-cursor-stop.js +2 -1
  319. package/hooks/gsd-cursor-subagent-start.js +28 -23
  320. package/hooks/gsd-cursor-subagent-stop.js +3 -1
  321. package/hooks/gsd-ensure-canonical-path.js +2 -1
  322. package/hooks/gsd-graphify-update.sh +22 -18
  323. package/hooks/gsd-node-runner.sh +76 -0
  324. package/hooks/gsd-phase-boundary.sh +1 -0
  325. package/hooks/gsd-prompt-guard.js +16 -7
  326. package/hooks/gsd-read-guard.js +16 -7
  327. package/hooks/gsd-read-injection-scanner.js +17 -8
  328. package/hooks/gsd-session-state.sh +1 -0
  329. package/hooks/gsd-statusline.js +215 -26
  330. package/hooks/gsd-validate-commit.sh +80 -6
  331. package/hooks/gsd-windsurf-pre-command.js +16 -11
  332. package/hooks/gsd-windsurf-pre-write.js +22 -13
  333. package/hooks/gsd-workflow-guard.js +34 -16
  334. package/hooks/gsd-worktree-path-guard.js +36 -21
  335. package/hooks/gsd-write-guard.js +35 -25
  336. package/hooks/lib/cli-exit.js +560 -0
  337. package/hooks/lib/exit-code-registry.js +98 -0
  338. package/hooks/lib/git-probe.js +84 -0
  339. package/hooks/lib/hook-exit.js +81 -0
  340. package/hooks/managed-hooks-registry.cjs +3 -0
  341. package/package.json +12 -7
  342. package/scripts/base64-scan.sh +74 -12
  343. package/scripts/build-hooks.js +5 -0
  344. package/scripts/check-glossary-refs.cjs +77 -15
  345. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  346. package/scripts/ci-check-job-near-cap.cjs +49 -0
  347. package/scripts/ci-pr-mergeability.cjs +262 -0
  348. package/scripts/ci-test-scope.cjs +45 -12
  349. package/scripts/ci-timeout-report.cjs +230 -0
  350. package/scripts/docs-guard-registry.cjs +396 -0
  351. package/scripts/gen-capability-registry.cjs +8 -6
  352. package/scripts/gen-exit-code-docs.cjs +318 -0
  353. package/scripts/gen-exit-code-registry.cjs +891 -0
  354. package/scripts/gen-features.cjs +836 -0
  355. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  356. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  357. package/scripts/gen-loop-host-contract.cjs +134 -1
  358. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  359. package/scripts/gen-state-md-docs.cjs +727 -0
  360. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  361. package/scripts/lib/ci-job-timing.cjs +72 -0
  362. package/scripts/lib/cli-exit.cjs +546 -44
  363. package/scripts/lib/drift-scan.cjs +32 -2
  364. package/scripts/lib/exit-code-registry.cjs +98 -0
  365. package/scripts/lib/ndjson-reporter.cjs +119 -0
  366. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  367. package/scripts/lint-docs-guard-registration.cjs +495 -0
  368. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
  369. package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
  370. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  371. package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
  372. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  373. package/scripts/lint-phase-enumeration-drift.cjs +21 -8
  374. package/scripts/lint-planning-prompt-drift.cjs +38 -1
  375. package/scripts/lint-removed-but-needed.cjs +184 -16
  376. package/scripts/lint-seam-enforcement.cjs +182 -0
  377. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  378. package/scripts/lint-source-test-name-collision.cjs +241 -0
  379. package/scripts/lint-state-write-path-drift.cjs +337 -432
  380. package/scripts/lint-test-file-count.allowlist.json +122 -4
  381. package/scripts/lint-test-file-count.cjs +25 -3
  382. package/scripts/lint-unreachable-guard-drift.cjs +51 -64
  383. package/scripts/lint-vendored-deps.cjs +208 -35
  384. package/scripts/mutation-matrix.cjs +599 -50
  385. package/scripts/prompt-injection-scan.sh +75 -14
  386. package/scripts/secret-scan.sh +75 -13
  387. package/scripts/select-docs-guards.cjs +56 -0
  388. package/scripts/sync-runtime-launcher.cjs +22 -3
  389. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  390. package/skills/gsd-import/SKILL.md +1 -1
  391. package/skills/gsd-quick/SKILL.md +8 -4
  392. package/vscode/package.json +1 -1
  393. package/bin/lib/ui-safety-gate.cjs +0 -109
  394. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  395. package/scripts/state-write-path-drift-baseline.json +0 -19
@@ -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);
165
+ }
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;
153
178
  }
154
- catch { /* ignore */ }
155
- throw e;
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,
@@ -1119,28 +1336,39 @@ function writeCursorHooksJson(targetDir, src, opts) {
1119
1336
  installedScripts.add(script);
1120
1337
  }
1121
1338
  }
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.
1339
+ // Stage the hooks/lib/ helpers the staged scripts require (#2587), TRANSITIVELY
1340
+ // (#3911 review): a lib helper can itself require a sibling under lib/ (e.g.
1341
+ // hooks/lib/hook-exit.js requires './cli-exit.js', which requires
1342
+ // './exit-code-registry.js') — a require with NO './lib/' prefix, because from
1343
+ // inside lib/ the sibling is already local. The original single-pass scan only
1344
+ // ever matched the "./lib/…" spelling used FROM a hook script, so it staged
1345
+ // hook-exit.js but never walked hook-exit.js's own requires, and an installed
1346
+ // Cursor hook wedged on MODULE_NOT_FOUND for './cli-exit.js' at load — before
1347
+ // its own try/catch. This walks a worklist: hook scripts seed it with their
1348
+ // "./lib/X" requires, and every lib file staged is itself scanned for further
1349
+ // "./lib/X" OR bare "./X" (sibling-within-lib) requires, so the requirement
1350
+ // graph is derived to a fixed point instead of one hand-tuned level deep.
1128
1351
  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;
1352
+ const scannedLibFiles = new Set();
1353
+ const libRequireRe = /require\(\s*['"]\.\/(?:lib\/)?([A-Za-z0-9._-]+)['"]\s*\)/g;
1354
+ function scanForLibRequires(source) {
1355
+ libRequireRe.lastIndex = 0;
1135
1356
  let m;
1136
- while ((m = re.exec(staged)) !== null)
1357
+ while ((m = libRequireRe.exec(source)) !== null)
1137
1358
  requiredLibFiles.add(m[1]);
1138
1359
  }
1360
+ for (const script of installedScripts) {
1361
+ scanForLibRequires(node_fs_1.default.readFileSync(node_path_1.default.join(hooksDir, script), 'utf8'));
1362
+ }
1139
1363
  if (requiredLibFiles.size > 0) {
1140
1364
  const srcLibDir = node_path_1.default.join(srcHooksDir, 'lib');
1141
1365
  const destLibDir = node_path_1.default.join(hooksDir, 'lib');
1142
1366
  node_fs_1.default.mkdirSync(destLibDir, { recursive: true });
1143
- for (const libFile of requiredLibFiles) {
1367
+ // Iterate to a fixed point: staging a lib file can add MORE required lib
1368
+ // files (its own requires), which must themselves be staged and scanned.
1369
+ let libFile = [...requiredLibFiles].find((f) => !scannedLibFiles.has(f));
1370
+ while (libFile !== undefined) {
1371
+ scannedLibFiles.add(libFile);
1144
1372
  const libSrc = node_path_1.default.join(srcLibDir, libFile);
1145
1373
  if (!node_fs_1.default.existsSync(libSrc)) {
1146
1374
  // FAIL LOUD. Skipping here would ship hook scripts whose top-level
@@ -1153,6 +1381,8 @@ function writeCursorHooksJson(targetDir, src, opts) {
1153
1381
  let libContent = node_fs_1.default.readFileSync(libSrc, 'utf8');
1154
1382
  libContent = libContent.replace(/gsd:/gi, 'gsd-');
1155
1383
  node_fs_1.default.writeFileSync(node_path_1.default.join(destLibDir, libFile), libContent);
1384
+ scanForLibRequires(libContent);
1385
+ libFile = [...requiredLibFiles].find((f) => !scannedLibFiles.has(f));
1156
1386
  }
1157
1387
  }
1158
1388
  // #2717: write the CommonJS marker into hooks/ alongside the staged .js
@@ -2309,7 +2539,9 @@ module.exports = {
2309
2539
  reconcileManagedShellHookCommands,
2310
2540
  normalizeNodePath,
2311
2541
  resolveNodeRunner,
2542
+ buildNodeRunnerChainToken,
2312
2543
  resolveBashRunner,
2544
+ NODE_RUNNER_RESOLVER_HOOK,
2313
2545
  // Atomic write seam (shared with bin/install.js so all writes participate
2314
2546
  // in install.js's _cleanTmpFiles() scoped temp-cleanup).
2315
2547
  atomicWriteFileSync,