@opengsd/gsd-core 1.10.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 (544) 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 +12 -1
  5. package/agents/gsd-debugger.md +1 -1
  6. package/agents/gsd-doc-synthesizer.md +2 -4
  7. package/agents/gsd-dom-verifier.md +169 -0
  8. package/agents/gsd-eval-auditor.md +1 -1
  9. package/agents/gsd-executor.md +22 -14
  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 +5 -3
  13. package/agents/gsd-pattern-mapper.md +11 -0
  14. package/agents/gsd-phase-researcher.md +23 -2
  15. package/agents/gsd-plan-checker.md +50 -53
  16. package/agents/gsd-planner.md +50 -50
  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 +63 -4
  21. package/agents/gsd-ui-researcher.md +41 -3
  22. package/agents/gsd-user-profiler.md +3 -0
  23. package/agents/gsd-verifier.md +13 -4
  24. package/bin/install.js +1448 -1103
  25. package/commands/gsd/code-review.md +1 -1
  26. package/commands/gsd/discuss-phase.md +1 -1
  27. package/commands/gsd/execute-phase.md +1 -1
  28. package/commands/gsd/import.md +1 -1
  29. package/commands/gsd/map-codebase.md +1 -1
  30. package/commands/gsd/mempalace-capture.md +1 -1
  31. package/commands/gsd/mempalace-recall.md +1 -1
  32. package/commands/gsd/new-milestone.md +1 -1
  33. package/commands/gsd/quick.md +9 -5
  34. package/commands/gsd/review-backlog.md +2 -1
  35. package/commands/gsd/verify-work.md +1 -1
  36. package/gsd-core/bin/gsd-tools.cjs +1035 -138
  37. package/gsd-core/bin/lib/active-workstream-store.cjs +146 -22
  38. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  39. package/gsd-core/bin/lib/agent-install-check.cjs +392 -32
  40. package/gsd-core/bin/lib/api-coverage.cjs +33 -14
  41. package/gsd-core/bin/lib/artifacts.cjs +5 -0
  42. package/gsd-core/bin/lib/assumption-delta.cjs +32 -15
  43. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  44. package/gsd-core/bin/lib/audit.cjs +1026 -268
  45. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  46. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  47. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  48. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  49. package/gsd-core/bin/lib/capability-registry.cjs +845 -130
  50. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  51. package/gsd-core/bin/lib/capability-state.cjs +18 -3
  52. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  53. package/gsd-core/bin/lib/capability-validator.cjs +700 -40
  54. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  55. package/gsd-core/bin/lib/check-command-router.cjs +216 -42
  56. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  57. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  58. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  59. package/gsd-core/bin/lib/codex-agent-toml.cjs +735 -0
  60. package/gsd-core/bin/lib/command-aliases.cjs +22 -0
  61. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  62. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  63. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  64. package/gsd-core/bin/lib/commands.cjs +1172 -108
  65. package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
  66. package/gsd-core/bin/lib/complexity-trigger.cjs +1192 -0
  67. package/gsd-core/bin/lib/config-loader.cjs +187 -23
  68. package/gsd-core/bin/lib/config.cjs +102 -3
  69. package/gsd-core/bin/lib/configuration.cjs +129 -37
  70. package/gsd-core/bin/lib/core-utils.cjs +208 -33
  71. package/gsd-core/bin/lib/decisions.cjs +23 -0
  72. package/gsd-core/bin/lib/edge-probe.cjs +9 -1
  73. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  74. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  75. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  76. package/gsd-core/bin/lib/frontmatter.cjs +899 -229
  77. package/gsd-core/bin/lib/gap-checker.cjs +95 -10
  78. package/gsd-core/bin/lib/git-base-branch.cjs +276 -39
  79. package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
  80. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  81. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  82. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +149 -0
  83. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  84. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  85. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  86. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +268 -0
  87. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  88. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  89. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +187 -0
  90. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  91. package/gsd-core/bin/lib/health-diagnostic.cjs +451 -0
  92. package/gsd-core/bin/lib/host-integration.cjs +39 -6
  93. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  94. package/gsd-core/bin/lib/init-command-router.cjs +118 -21
  95. package/gsd-core/bin/lib/init.cjs +439 -168
  96. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  97. package/gsd-core/bin/lib/install-engine.cjs +811 -259
  98. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  99. package/gsd-core/bin/lib/install-model-override-resolver.cjs +235 -0
  100. package/gsd-core/bin/lib/install-profiles.cjs +212 -61
  101. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  102. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  103. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  104. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  105. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  106. package/gsd-core/bin/lib/installer-migrations.cjs +148 -38
  107. package/gsd-core/bin/lib/intel.cjs +101 -26
  108. package/gsd-core/bin/lib/io.cjs +170 -15
  109. package/gsd-core/bin/lib/learnings.cjs +85 -14
  110. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  111. package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
  112. package/gsd-core/bin/lib/markdown-table.cjs +183 -22
  113. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  114. package/gsd-core/bin/lib/milestone.cjs +842 -73
  115. package/gsd-core/bin/lib/model-catalog.cjs +232 -16
  116. package/gsd-core/bin/lib/model-resolver.cjs +193 -68
  117. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  118. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  119. package/gsd-core/bin/lib/pattern.cjs +122 -0
  120. package/gsd-core/bin/lib/phase-estimation.cjs +18 -9
  121. package/gsd-core/bin/lib/phase-id.cjs +514 -40
  122. package/gsd-core/bin/lib/phase-lifecycle.cjs +52 -19
  123. package/gsd-core/bin/lib/phase-locator.cjs +262 -34
  124. package/gsd-core/bin/lib/phase.cjs +1038 -214
  125. package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
  126. package/gsd-core/bin/lib/plan-document.cjs +263 -0
  127. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  128. package/gsd-core/bin/lib/plan-scan.cjs +98 -3
  129. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  130. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  131. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  132. package/gsd-core/bin/lib/planning-snapshot.cjs +894 -0
  133. package/gsd-core/bin/lib/planning-workspace.cjs +112 -6
  134. package/gsd-core/bin/lib/probe-core.cjs +5 -2
  135. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  136. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  137. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  138. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  139. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +766 -0
  140. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
  141. package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -13
  142. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  143. package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
  144. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  145. package/gsd-core/bin/lib/roadmap-command-router.cjs +59 -11
  146. package/gsd-core/bin/lib/roadmap-parser.cjs +1006 -184
  147. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  148. package/gsd-core/bin/lib/roadmap.cjs +442 -96
  149. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +702 -52
  150. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  151. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +459 -55
  152. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  153. package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
  154. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +402 -58
  155. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  156. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  157. package/gsd-core/bin/lib/runtime-slash.cjs +96 -8
  158. package/gsd-core/bin/lib/security.cjs +104 -5
  159. package/gsd-core/bin/lib/shell-command-projection.cjs +342 -7
  160. package/gsd-core/bin/lib/smart-entry.cjs +133 -23
  161. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  162. package/gsd-core/bin/lib/state-command-router.cjs +52 -19
  163. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  164. package/gsd-core/bin/lib/state-document.cjs +338 -8
  165. package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
  166. package/gsd-core/bin/lib/state-transition.cjs +846 -176
  167. package/gsd-core/bin/lib/state.cjs +2589 -369
  168. package/gsd-core/bin/lib/surface.cjs +33 -11
  169. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  170. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  171. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  172. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  173. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  174. package/gsd-core/bin/lib/uat-predicate.cjs +67 -23
  175. package/gsd-core/bin/lib/uat.cjs +1761 -167
  176. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  177. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  178. package/gsd-core/bin/lib/ui-safety-gate.cjs +51 -12
  179. package/gsd-core/bin/lib/unusable-input.cjs +37 -0
  180. package/gsd-core/bin/lib/update-context.cjs +8 -2
  181. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  182. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  183. package/gsd-core/bin/lib/validate.cjs +20 -6
  184. package/gsd-core/bin/lib/vendor/README.md +75 -0
  185. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  186. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  187. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  188. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  189. package/gsd-core/bin/lib/verification.cjs +272 -9
  190. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  191. package/gsd-core/bin/lib/verify.cjs +453 -918
  192. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
  193. package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
  194. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  195. package/gsd-core/bin/lib/workstream.cjs +2 -2
  196. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  197. package/gsd-core/bin/lib/worktree-safety.cjs +341 -18
  198. package/gsd-core/bin/shared/config-defaults.manifest.json +8 -1
  199. package/gsd-core/bin/shared/config-schema.manifest.json +12 -1
  200. package/gsd-core/bin/shared/exit-codes.json +8 -0
  201. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  202. package/gsd-core/bin/shared/model-catalog.json +8 -1
  203. package/gsd-core/references/agent-contracts.md +44 -26
  204. package/gsd-core/references/api-coverage.md +24 -2
  205. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  206. package/gsd-core/references/checkpoints.md +39 -21
  207. package/gsd-core/references/context-budget.md +1 -1
  208. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  209. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  210. package/gsd-core/references/doc-conflict-engine.md +1 -1
  211. package/gsd-core/references/edge-probe.md +8 -0
  212. package/gsd-core/references/execute-mvp-tdd.md +4 -6
  213. package/gsd-core/references/execute-phase-between-wave-reset.md +15 -14
  214. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  215. package/gsd-core/references/execute-phase-response-language.md +1 -1
  216. package/gsd-core/references/execute-phase-wave-guard.md +17 -11
  217. package/gsd-core/references/failing-direction.md +78 -0
  218. package/gsd-core/references/gate-prompts.md +1 -1
  219. package/gsd-core/references/git-integration.md +5 -5
  220. package/gsd-core/references/git-planning-commit.md +5 -4
  221. package/gsd-core/references/gsd-run-resolver.md +1 -1
  222. package/gsd-core/references/loop-hook-dispatch.md +61 -2
  223. package/gsd-core/references/model-profiles.md +12 -4
  224. package/gsd-core/references/mvp-concepts.md +9 -9
  225. package/gsd-core/references/nyquist-compliance.md +74 -0
  226. package/gsd-core/references/offer-next.md +3 -5
  227. package/gsd-core/references/phase-argument-parsing.md +3 -3
  228. package/gsd-core/references/planner-failing-direction.md +53 -0
  229. package/gsd-core/references/planner-guidance.md +3 -9
  230. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  231. package/gsd-core/references/planner-preconditions.md +1 -1
  232. package/gsd-core/references/planner-reviews.md +1 -1
  233. package/gsd-core/references/planner-revision.md +1 -1
  234. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  235. package/gsd-core/references/planning-config.md +44 -13
  236. package/gsd-core/references/reviewer-instances.md +31 -0
  237. package/gsd-core/references/revision-loop.md +1 -1
  238. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  239. package/gsd-core/references/specless-probe-fallback.md +1 -1
  240. package/gsd-core/references/tdd.md +1 -3
  241. package/gsd-core/references/ui-brand.md +65 -21
  242. package/gsd-core/references/ui-consideration-probe.md +1 -1
  243. package/gsd-core/references/universal-anti-patterns.md +5 -5
  244. package/gsd-core/references/verifier-phase-gates.md +192 -0
  245. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  246. package/gsd-core/references/verify-mvp-mode.md +2 -2
  247. package/gsd-core/references/workstream-flag.md +33 -17
  248. package/gsd-core/templates/README.md +1 -1
  249. package/gsd-core/templates/SECURITY.md +3 -3
  250. package/gsd-core/templates/UI-SPEC.md +25 -3
  251. package/gsd-core/templates/VALIDATION.md +3 -3
  252. package/gsd-core/templates/discussion-log.md +1 -1
  253. package/gsd-core/templates/phase-prompt.md +5 -4
  254. package/gsd-core/templates/state.md +11 -4
  255. package/gsd-core/templates/verification-report.md +9 -1
  256. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  257. package/gsd-core/workflows/add-backlog.md +1 -1
  258. package/gsd-core/workflows/add-phase.md +3 -3
  259. package/gsd-core/workflows/add-tests.md +3 -8
  260. package/gsd-core/workflows/add-todo.md +1 -1
  261. package/gsd-core/workflows/ai-integration-phase.md +13 -20
  262. package/gsd-core/workflows/audit-fix.md +12 -3
  263. package/gsd-core/workflows/audit-milestone.md +9 -9
  264. package/gsd-core/workflows/audit-uat.md +17 -2
  265. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  266. package/gsd-core/workflows/autonomous.md +11 -27
  267. package/gsd-core/workflows/check-todos.md +1 -1
  268. package/gsd-core/workflows/cleanup.md +64 -5
  269. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +14 -4
  270. package/gsd-core/workflows/code-review-fix.md +38 -11
  271. package/gsd-core/workflows/code-review.md +159 -52
  272. package/gsd-core/workflows/complete-milestone.md +151 -23
  273. package/gsd-core/workflows/debug.md +12 -8
  274. package/gsd-core/workflows/diagnose-issues.md +47 -15
  275. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  276. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -8
  277. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  278. package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
  279. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  280. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  281. package/gsd-core/workflows/discuss-phase.md +1 -1
  282. package/gsd-core/workflows/do.md +3 -6
  283. package/gsd-core/workflows/docs-update.md +5 -4
  284. package/gsd-core/workflows/edit-phase.md +27 -2
  285. package/gsd-core/workflows/eval-review.md +7 -14
  286. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  287. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +142 -15
  288. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  289. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  290. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  291. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +24 -4
  292. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  293. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  294. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
  295. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  296. package/gsd-core/workflows/execute-phase.md +72 -100
  297. package/gsd-core/workflows/execute-plan.md +52 -15
  298. package/gsd-core/workflows/explore.md +131 -4
  299. package/gsd-core/workflows/extract-learnings.md +1 -1
  300. package/gsd-core/workflows/fast.md +10 -2
  301. package/gsd-core/workflows/forensics.md +1 -1
  302. package/gsd-core/workflows/graduation.md +5 -5
  303. package/gsd-core/workflows/health.md +76 -10
  304. package/gsd-core/workflows/import.md +18 -15
  305. package/gsd-core/workflows/inbox.md +4 -5
  306. package/gsd-core/workflows/ingest-docs.md +49 -16
  307. package/gsd-core/workflows/insert-phase.md +5 -5
  308. package/gsd-core/workflows/list-seeds.md +5 -3
  309. package/gsd-core/workflows/list-workspaces.md +1 -1
  310. package/gsd-core/workflows/manager.md +12 -23
  311. package/gsd-core/workflows/map-codebase.md +1 -1
  312. package/gsd-core/workflows/milestone-summary.md +1 -1
  313. package/gsd-core/workflows/mvp-phase.md +8 -5
  314. package/gsd-core/workflows/new-milestone.md +22 -29
  315. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  316. package/gsd-core/workflows/new-project.md +26 -40
  317. package/gsd-core/workflows/new-workspace.md +1 -1
  318. package/gsd-core/workflows/next.md +14 -2
  319. package/gsd-core/workflows/pause-work.md +1 -1
  320. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  321. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  322. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
  323. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
  324. package/gsd-core/workflows/plan-phase.md +162 -59
  325. package/gsd-core/workflows/plan-review-convergence.md +96 -11
  326. package/gsd-core/workflows/plant-seed.md +2 -2
  327. package/gsd-core/workflows/pr-branch.md +187 -51
  328. package/gsd-core/workflows/profile-user.md +16 -14
  329. package/gsd-core/workflows/progress.md +61 -18
  330. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  331. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +5 -7
  332. package/gsd-core/workflows/quick/steps/quick-verification.md +28 -9
  333. package/gsd-core/workflows/quick/steps/research-phase.md +4 -6
  334. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  335. package/gsd-core/workflows/quick.md +55 -44
  336. package/gsd-core/workflows/remove-phase.md +4 -4
  337. package/gsd-core/workflows/remove-workspace.md +2 -2
  338. package/gsd-core/workflows/resume-project.md +8 -12
  339. package/gsd-core/workflows/review.md +219 -20
  340. package/gsd-core/workflows/scan.md +1 -1
  341. package/gsd-core/workflows/secure-phase.md +3 -3
  342. package/gsd-core/workflows/session-report.md +2 -1
  343. package/gsd-core/workflows/settings-advanced.md +7 -9
  344. package/gsd-core/workflows/settings-integrations.md +64 -31
  345. package/gsd-core/workflows/settings.md +69 -7
  346. package/gsd-core/workflows/ship.md +116 -50
  347. package/gsd-core/workflows/sketch-wrap-up.md +11 -17
  348. package/gsd-core/workflows/sketch.md +12 -18
  349. package/gsd-core/workflows/smart-entry.md +3 -5
  350. package/gsd-core/workflows/spec-phase.md +53 -13
  351. package/gsd-core/workflows/spike-wrap-up.md +7 -11
  352. package/gsd-core/workflows/spike.md +20 -31
  353. package/gsd-core/workflows/stats.md +2 -2
  354. package/gsd-core/workflows/sync-skills.md +64 -9
  355. package/gsd-core/workflows/thread.md +11 -7
  356. package/gsd-core/workflows/transition.md +49 -14
  357. package/gsd-core/workflows/ui-phase.md +15 -21
  358. package/gsd-core/workflows/ui-review.md +8 -12
  359. package/gsd-core/workflows/ultraplan-phase.md +5 -13
  360. package/gsd-core/workflows/undo.md +8 -16
  361. package/gsd-core/workflows/update.md +7 -11
  362. package/gsd-core/workflows/validate-phase.md +3 -3
  363. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  364. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  365. package/gsd-core/workflows/verify-work.md +66 -25
  366. package/hooks/dist/gsd-agent-isolation-guard.js +158 -30
  367. package/hooks/dist/gsd-check-update-worker.js +56 -13
  368. package/hooks/dist/gsd-check-update.js +19 -1
  369. package/hooks/dist/gsd-config-reload.js +18 -12
  370. package/hooks/dist/gsd-context-monitor.js +19 -10
  371. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  372. package/hooks/dist/gsd-cursor-pre-tool.js +2 -3
  373. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  374. package/hooks/dist/gsd-cursor-stop.js +2 -1
  375. package/hooks/dist/gsd-cursor-subagent-start.js +83 -3
  376. package/hooks/dist/gsd-cursor-subagent-stop.js +6 -3
  377. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  378. package/hooks/dist/gsd-graphify-update.sh +22 -18
  379. package/hooks/dist/gsd-node-runner.sh +76 -0
  380. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  381. package/hooks/dist/gsd-prompt-guard.js +37 -27
  382. package/hooks/dist/gsd-read-guard.js +16 -7
  383. package/hooks/dist/gsd-read-injection-scanner.js +55 -32
  384. package/hooks/dist/gsd-session-state.sh +1 -0
  385. package/hooks/dist/gsd-statusline.js +231 -24
  386. package/hooks/dist/gsd-update-banner.js +22 -1
  387. package/hooks/dist/gsd-validate-commit.sh +80 -6
  388. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  389. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  390. package/hooks/dist/gsd-workflow-guard.js +162 -46
  391. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  392. package/hooks/dist/gsd-write-guard.js +35 -25
  393. package/hooks/dist/lib/cli-exit.js +560 -0
  394. package/hooks/dist/lib/exit-code-registry.js +98 -0
  395. package/hooks/dist/lib/git-cmd.js +92 -59
  396. package/hooks/dist/lib/git-probe.js +84 -0
  397. package/hooks/dist/lib/hook-exit.js +81 -0
  398. package/hooks/dist/lib/injection-patterns.js +45 -0
  399. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  400. package/hooks/dist/lib/isolation-sentinel.js +9 -0
  401. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  402. package/hooks/gsd-agent-isolation-guard.js +158 -30
  403. package/hooks/gsd-check-update-worker.js +56 -13
  404. package/hooks/gsd-check-update.js +19 -1
  405. package/hooks/gsd-config-reload.js +18 -12
  406. package/hooks/gsd-context-monitor.js +19 -10
  407. package/hooks/gsd-cursor-post-tool.js +3 -1
  408. package/hooks/gsd-cursor-pre-tool.js +2 -3
  409. package/hooks/gsd-cursor-session-start.js +2 -1
  410. package/hooks/gsd-cursor-stop.js +2 -1
  411. package/hooks/gsd-cursor-subagent-start.js +83 -3
  412. package/hooks/gsd-cursor-subagent-stop.js +6 -3
  413. package/hooks/gsd-ensure-canonical-path.js +2 -1
  414. package/hooks/gsd-graphify-update.sh +22 -18
  415. package/hooks/gsd-node-runner.sh +76 -0
  416. package/hooks/gsd-phase-boundary.sh +1 -0
  417. package/hooks/gsd-prompt-guard.js +37 -27
  418. package/hooks/gsd-read-guard.js +16 -7
  419. package/hooks/gsd-read-injection-scanner.js +55 -32
  420. package/hooks/gsd-session-state.sh +1 -0
  421. package/hooks/gsd-statusline.js +231 -24
  422. package/hooks/gsd-update-banner.js +22 -1
  423. package/hooks/gsd-validate-commit.sh +80 -6
  424. package/hooks/gsd-windsurf-pre-command.js +16 -11
  425. package/hooks/gsd-windsurf-pre-write.js +22 -13
  426. package/hooks/gsd-workflow-guard.js +162 -46
  427. package/hooks/gsd-worktree-path-guard.js +36 -21
  428. package/hooks/gsd-write-guard.js +35 -25
  429. package/hooks/lib/cli-exit.js +560 -0
  430. package/hooks/lib/exit-code-registry.js +98 -0
  431. package/hooks/lib/git-cmd.js +92 -59
  432. package/hooks/lib/git-probe.js +84 -0
  433. package/hooks/lib/hook-exit.js +81 -0
  434. package/hooks/lib/injection-patterns.js +45 -0
  435. package/hooks/lib/isolation-deny-reason.js +39 -0
  436. package/hooks/lib/isolation-sentinel.js +9 -0
  437. package/hooks/managed-hooks-registry.cjs +3 -0
  438. package/package.json +28 -11
  439. package/pi/gsd.cjs +19 -5
  440. package/scripts/base64-scan.sh +74 -12
  441. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  442. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  443. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  444. package/scripts/build-hooks.js +5 -0
  445. package/scripts/changeset/lint.cjs +60 -5
  446. package/scripts/check-alias-drift.cjs +7 -43
  447. package/scripts/check-contract-drift.cjs +297 -0
  448. package/scripts/check-glossary-refs.cjs +77 -15
  449. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  450. package/scripts/ci-check-job-near-cap.cjs +49 -0
  451. package/scripts/ci-pr-mergeability.cjs +262 -0
  452. package/scripts/ci-test-scope.cjs +64 -14
  453. package/scripts/ci-timeout-report.cjs +230 -0
  454. package/scripts/command-contract-helpers.cjs +903 -1
  455. package/scripts/docs-guard-registry.cjs +396 -0
  456. package/scripts/gen-adr-index.cjs +728 -38
  457. package/scripts/gen-capability-registry.cjs +11 -21
  458. package/scripts/gen-context-index.cjs +2 -11
  459. package/scripts/gen-exit-code-docs.cjs +318 -0
  460. package/scripts/gen-exit-code-registry.cjs +891 -0
  461. package/scripts/gen-features.cjs +836 -0
  462. package/scripts/gen-health-docs.cjs +390 -0
  463. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  464. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  465. package/scripts/gen-inventory-manifest.cjs +50 -4
  466. package/scripts/gen-loop-host-contract.cjs +138 -25
  467. package/scripts/gen-registry.cjs +3 -14
  468. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  469. package/scripts/gen-state-md-docs.cjs +727 -0
  470. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  471. package/scripts/lib/alias-drift-families.cjs +46 -0
  472. package/scripts/lib/ci-job-timing.cjs +72 -0
  473. package/scripts/lib/cli-exit.cjs +546 -44
  474. package/scripts/lib/drift-scan.cjs +308 -0
  475. package/scripts/lib/exit-code-registry.cjs +98 -0
  476. package/scripts/lib/ndjson-reporter.cjs +119 -0
  477. package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
  478. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  479. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  480. package/scripts/lint-canary-version-leak.cjs +73 -0
  481. package/scripts/lint-command-contract.cjs +96 -13
  482. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  483. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  484. package/scripts/lint-default-flip-documentation.cjs +193 -0
  485. package/scripts/lint-docs-guard-registration.cjs +495 -0
  486. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
  487. package/scripts/lint-eslint-glob-coverage.allowlist.json +38 -0
  488. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  489. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  490. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  491. package/scripts/lint-health-diagnostic-rule-table.cjs +461 -0
  492. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  493. package/scripts/lint-milestone-window-drift.cjs +468 -0
  494. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  495. package/scripts/lint-phase-enumeration-drift.cjs +492 -0
  496. package/scripts/lint-plan-count-drift.cjs +318 -0
  497. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  498. package/scripts/lint-planning-prompt-drift.cjs +471 -0
  499. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  500. package/scripts/lint-regression-test-names.cjs +15 -13
  501. package/scripts/lint-removed-but-needed.cjs +488 -0
  502. package/scripts/lint-seam-enforcement.cjs +182 -0
  503. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  504. package/scripts/lint-source-test-name-collision.cjs +241 -0
  505. package/scripts/lint-state-field-drift.cjs +805 -0
  506. package/scripts/lint-state-write-path-drift.cjs +950 -0
  507. package/scripts/lint-test-file-count.allowlist.json +137 -8
  508. package/scripts/lint-test-file-count.cjs +25 -3
  509. package/scripts/lint-unreachable-guard-drift.cjs +830 -0
  510. package/scripts/lint-vendored-deps.cjs +297 -0
  511. package/scripts/mutation-matrix.cjs +599 -50
  512. package/scripts/pr-changed-files.cjs +63 -0
  513. package/scripts/pr-template-policy.cjs +14 -4
  514. package/scripts/prompt-injection-scan.sh +100 -14
  515. package/scripts/require-issue-link-policy.cjs +192 -0
  516. package/scripts/secret-scan.sh +75 -13
  517. package/scripts/select-docs-guards.cjs +56 -0
  518. package/scripts/sync-runtime-launcher.cjs +24 -7
  519. package/skills/gsd-autonomous/SKILL.md +0 -1
  520. package/skills/gsd-code-review/SKILL.md +1 -1
  521. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  522. package/skills/gsd-execute-phase/SKILL.md +1 -2
  523. package/skills/gsd-import/SKILL.md +1 -1
  524. package/skills/gsd-map-codebase/SKILL.md +1 -1
  525. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  526. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  527. package/skills/gsd-new-milestone/SKILL.md +1 -1
  528. package/skills/gsd-next/SKILL.md +0 -1
  529. package/skills/gsd-plan-phase/SKILL.md +0 -1
  530. package/skills/gsd-progress/SKILL.md +0 -1
  531. package/skills/gsd-quick/SKILL.md +9 -5
  532. package/skills/gsd-review-backlog/SKILL.md +2 -1
  533. package/skills/gsd-stats/SKILL.md +0 -1
  534. package/skills/gsd-verify-work/SKILL.md +1 -1
  535. package/vscode/package.json +1 -1
  536. package/bin/lib/ui-safety-gate.cjs +0 -107
  537. package/gsd-core/workflows/discovery-phase.md +0 -298
  538. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  539. package/gsd-core/workflows/verify-phase.md +0 -574
  540. package/scripts/affected-tests-lib.cjs +0 -554
  541. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  542. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  543. package/scripts/run-affected-tests.cjs +0 -7
  544. package/scripts/run-tests.cjs +0 -1051
@@ -21,9 +21,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
21
21
  * BEHAVIOR-PRESERVING RELOCATION: all logic is copied verbatim from
22
22
  * bin/install.js. No behavior change, no descriptor reads, no new IO.
23
23
  *
24
- * bin/install.js re-exports every symbol from this module so existing
25
- * consumers that do require('../bin/install.js').writeCursorHooksJson
26
- * (etc.) continue to work unchanged.
24
+ * #2876 (epic #2866 Phase 7): bin/install.js previously re-exported every
25
+ * symbol from this module, but a repo-wide audit found zero production
26
+ * consumers of those re-exports — no `require('../bin/install.js').
27
+ * writeCursorHooksJson` (or any sibling) exists outside a doc comment
28
+ * anywhere in the tree. The re-exports were test-suite-only pass-throughs;
29
+ * tests now require this module directly instead.
27
30
  */
28
31
  const node_fs_1 = __importDefault(require("node:fs"));
29
32
  const node_path_1 = __importDefault(require("node:path"));
@@ -35,7 +38,7 @@ const commonjs_marker_cjs_1 = require("./commonjs-marker.cjs");
35
38
  const imperative_hook_bus_cjs_1 = require("./host-integration-adapters/imperative-hook-bus.cjs");
36
39
  // eslint-disable-next-line @typescript-eslint/no-require-imports
37
40
  const shellCmdProjection = require("./shell-command-projection.cjs");
38
- const { isManagedHookBasename, isManagedHookCommand, projectLegacySettingsHookCommand, projectManagedHookCommand, projectPortableHookBaseDir, projectCodexHookTomlCommand, shellHookOmitsBashRunner, escapeTomlDoubleQuotedString, } = shellCmdProjection;
41
+ const { isManagedHookBasename, isManagedHookCommand, projectLegacySettingsHookCommand, projectManagedHookCommand, projectPortableHookBaseDir, projectCodexHookTomlCommand, shellHookOmitsBashRunner, escapeTomlDoubleQuotedString, escapePosixDoubleQuoted, } = shellCmdProjection;
39
42
  // ---------------------------------------------------------------------------
40
43
  // Terminal color constants (mirrors install.js for console output parity)
41
44
  // ---------------------------------------------------------------------------
@@ -133,23 +136,64 @@ function _capabilityTitle(runtime) {
133
136
  let __atomicWriteCounter = 0;
134
137
  // Set<string> — absolute paths of .tmp-<pid>-<n> files this process created.
135
138
  const __atomicWrittenTmps = new Set();
139
+ // Retry budget for the EEXIST (squatted temp path) branch in atomicWriteFileSync.
140
+ const MAX_TEMP_FILE_ATTEMPTS = 4;
136
141
  function atomicWriteFileSync(target, data, options) {
137
- __atomicWriteCounter += 1;
138
- const tmp = `${target}.tmp-${process.pid}-${__atomicWriteCounter}`;
139
- __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;
140
147
  try {
141
- node_fs_1.default.writeFileSync(tmp, data, options);
142
- shellCmdProjection.retryRenameSync(tmp, target);
143
- // Successful rename: the tmp path no longer exists, but leave it in the
144
- // Set so _cleanTmpFiles can recognise it as installer-owned if it somehow
145
- // lingers (e.g. a rename succeeded but left a stale entry on some FS).
146
- }
147
- 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);
148
163
  try {
149
- node_fs_1.default.rmSync(tmp, { force: true });
164
+ node_fs_1.default.writeFileSync(tmp, data, exclusiveOptions);
150
165
  }
151
- catch { /* ignore */ }
152
- 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;
153
197
  }
154
198
  }
155
199
  // ---------------------------------------------------------------------------
@@ -330,20 +374,41 @@ function parseTomlValue(text, i) {
330
374
  }
331
375
  throw new Error(`parseTomlValue: unexpected character '${ch}' at position ${i}`);
332
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
+ }
333
391
  function normalizeNodePath(execPath, opts) {
334
392
  if (!execPath)
335
393
  return execPath;
336
394
  const env = (opts && opts.env) || process.env;
337
395
  const existsSync = (opts && opts.existsSync) || node_fs_1.default.existsSync;
338
396
  const normalizedForMatch = shellCmdProjection.posixNormalize(execPath);
339
- 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)) {
340
403
  const candidates = [];
341
404
  if (env.FNM_DIR) {
342
- candidates.push(`${env.FNM_DIR}/aliases/default/node.exe`);
343
- 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`);
344
408
  }
345
- if (env.APPDATA) {
346
- 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`);
347
412
  }
348
413
  for (const candidate of candidates) {
349
414
  if (candidate && existsSync(candidate))
@@ -351,6 +416,42 @@ function normalizeNodePath(execPath, opts) {
351
416
  }
352
417
  return execPath;
353
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
+ }
354
455
  // Homebrew (macOS Intel /usr/local, Apple Silicon /opt/homebrew, Linuxbrew
355
456
  // /home/linuxbrew/.linuxbrew, and any custom HOMEBREW_PREFIX) pins node at
356
457
  // <prefix>/Cellar/node(<@ver>)?/<ver>/bin/node, then deletes prior versions on
@@ -396,12 +497,76 @@ function normalizeNodePath(execPath, opts) {
396
497
  return execPath;
397
498
  }
398
499
  function resolveNodeRunner(opts) {
399
- const execPath = typeof process.execPath === 'string' ? process.execPath : '';
500
+ const execPath = (opts && opts.execPath) || (typeof process.execPath === 'string' ? process.execPath : '');
400
501
  if (!execPath)
401
502
  return null;
402
503
  const stablePath = normalizeNodePath(execPath, opts);
403
504
  return JSON.stringify(shellCmdProjection.posixNormalize(stablePath));
404
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';
405
570
  function resolveBashRunner(opts) {
406
571
  const platform = (opts && opts.platform) || process.platform;
407
572
  if (platform !== 'win32')
@@ -426,8 +591,13 @@ function resolveBashRunner(opts) {
426
591
  }
427
592
  return null;
428
593
  }
429
- function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) {
430
- 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)
431
601
  return false;
432
602
  if (!opts)
433
603
  opts = {};
@@ -449,29 +619,31 @@ function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) {
449
619
  if (hadPowerShellCallOperator) {
450
620
  trimmed = trimmed.replace(/^&\s+/, '').trim();
451
621
  }
622
+ if (CHAIN_RUNNER_COMMAND.test(trimmed) || RESOLVER_RUNNER_COMMAND.test(trimmed))
623
+ continue;
452
624
  const m = trimmed.match(/^node\s+("([^"]+)"|'([^']+)'|(\S+))\s*$/) ||
453
625
  trimmed.match(/^("([^"]+)"|'([^']+)'|(\S+))\s+("([^"]+)"|'([^']+)'|(\S+))\s*$/);
454
626
  if (!m)
455
627
  continue;
456
- let _runnerToken, scriptToken, scriptPath;
628
+ let scriptToken, scriptPath;
457
629
  if (/^node\s+/.test(trimmed)) {
458
- _runnerToken = 'node';
459
630
  scriptToken = m[1];
460
631
  scriptPath = m[2] || m[3] || m[4] || '';
461
632
  }
462
633
  else {
463
- _runnerToken = m[1];
464
- const runnerPath = shellCmdProjection.posixNormalize(m[2] || m[3] || m[4] || '');
465
- const stableRunner = normalizeNodePath(runnerPath);
466
- if (stableRunner === runnerPath && platform !== 'win32')
467
- 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.
468
640
  scriptToken = m[5];
469
641
  scriptPath = m[6] || m[7] || m[8] || '';
470
642
  }
471
643
  if (!isManagedHookBasename(scriptPath, { surface: 'settings-json' }))
472
644
  continue;
473
645
  const projectedCommand = projectLegacySettingsHookCommand({
474
- absoluteRunner,
646
+ runnerToken,
475
647
  scriptPath,
476
648
  scriptToken,
477
649
  runtime: opts.runtime || 'generic',
@@ -488,6 +660,98 @@ function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) {
488
660
  }
489
661
  return changed;
490
662
  }
663
+ // ---------------------------------------------------------------------------
664
+ // Shared: reconcileManagedShellHookCommands (#3329)
665
+ // ---------------------------------------------------------------------------
666
+ /**
667
+ * Rewrite already-registered managed `.sh` hook `command` strings to the shape
668
+ * the current installer would generate (#3329).
669
+ *
670
+ * applySettingsJsonHooks registers the four `.sh` managed hooks only-if-absent,
671
+ * so an entry registered by an older installer keeps its old command forever —
672
+ * `/gsd-update` (which re-invokes the installer) never re-derived it. On
673
+ * Claude/win32 that left the pre-#580/#3393 bash-runner-prefixed commands
674
+ * (`bash "<script>.sh"`, `"<git>/bash.exe" "<script>.sh"`) in place, spawning a
675
+ * nested bash on every hook fire. The #2979 rewriter above cannot help: it is
676
+ * Node-only by design (its basename gate contains only `.js` filenames).
677
+ *
678
+ * Scoping / safety:
679
+ * - Inert unless `shellHookOmitsBashRunner({ platform, runtime, isShellHook:
680
+ * true })` — the exact combination whose correct command shape changed. Where
681
+ * the bash runner is still correct (non-Windows, non-claude), nothing is
682
+ * rewritten, so the reconcile cannot churn unrelated installs.
683
+ * - Only entries whose parsed script token's basename exactly equals one of the
684
+ * expected managed `.sh` filenames are touched. A user hook would have to
685
+ * live at a path ending in exactly `gsd-session-state.sh` etc. — i.e. the
686
+ * GSD-installed file — to match. Extra-token commands (env prefixes, extra
687
+ * args) and args-form launcher entries (#976) never match the strict
688
+ * `[runner ]<script>` two-token shape and are left alone.
689
+ * - A null/empty expected command disables rewriting for that hook (a
690
+ * bash-runner-unavailable install must not have its entry nulled).
691
+ *
692
+ * @param settings settings.json-shaped object; mutated in place
693
+ * @param expected map of managed `.sh` filename → the command this install
694
+ * would register today (from buildHookCommand / buildLocalShellHookCommand)
695
+ * @param opts platform/runtime override (default: current process)
696
+ * @returns true when any command was rewritten
697
+ */
698
+ function reconcileManagedShellHookCommands(settings, expected, opts) {
699
+ if (!settings || !settings.hooks || !expected)
700
+ return false;
701
+ if (!opts)
702
+ opts = {};
703
+ const platform = opts.platform || process.platform;
704
+ const runtime = opts.runtime || 'generic';
705
+ if (!shellHookOmitsBashRunner({ platform, runtime, isShellHook: true }))
706
+ return false;
707
+ const expectedByBasename = new Map();
708
+ for (const [hookFile, command] of Object.entries(expected)) {
709
+ if (typeof command === 'string' && command.length > 0) {
710
+ expectedByBasename.set(hookFile, command);
711
+ }
712
+ }
713
+ if (expectedByBasename.size === 0)
714
+ return false;
715
+ let changed = false;
716
+ for (const entries of Object.values(settings.hooks)) {
717
+ if (!Array.isArray(entries))
718
+ continue;
719
+ for (const entry of entries) {
720
+ if (!entry || !Array.isArray(entry.hooks))
721
+ continue;
722
+ for (const h of entry.hooks) {
723
+ if (!h || typeof h.command !== 'string')
724
+ continue;
725
+ if (Array.isArray(h.args) && h.args.length > 0)
726
+ continue;
727
+ let trimmed = h.command.trim();
728
+ const hadPowerShellCallOperator = platform === 'win32' && /^&\s+/.test(trimmed);
729
+ if (hadPowerShellCallOperator) {
730
+ trimmed = trimmed.replace(/^&\s+/, '').trim();
731
+ }
732
+ // Strict `[runner ]<script>` shape: an optional single runner token
733
+ // (bare, 'single-quoted', or "double-quoted") followed by the script
734
+ // token. Anything else (env prefixes, extra flags, pipelines) does not
735
+ // match and is left untouched.
736
+ const m = trimmed.match(/^(?:(?:"([^"]+)"|'([^']+)'|(\S+))\s+)?(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/);
737
+ if (!m)
738
+ continue;
739
+ const scriptToken = m[4] || m[5] || m[6] || '';
740
+ if (!scriptToken)
741
+ continue;
742
+ const basename = shellCmdProjection.posixNormalize(scriptToken).split('/').pop() || '';
743
+ const expectedCommand = expectedByBasename.get(basename);
744
+ if (!expectedCommand)
745
+ continue;
746
+ if (h.command === expectedCommand)
747
+ continue;
748
+ h.command = expectedCommand;
749
+ changed = true;
750
+ }
751
+ }
752
+ }
753
+ return changed;
754
+ }
491
755
  function buildCodexHookBlock(targetDir, opts) {
492
756
  const absoluteRunner = opts && opts.absoluteRunner;
493
757
  if (!absoluteRunner)
@@ -768,27 +1032,75 @@ function buildHookCommand(configDir, hookName, opts) {
768
1032
  }
769
1033
  return JSON.stringify(shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName);
770
1034
  }
771
- const nodeRunner = resolveNodeRunner();
772
- const runner = isShellHook ? resolveBashRunner(opts) : nodeRunner;
773
- if (runner === null)
774
- 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.
775
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;
776
1078
  const portableBaseDir = projectPortableHookBaseDir({
777
1079
  configDir,
778
1080
  homeDir: node_os_1.default.homedir(),
779
1081
  });
780
- return projectManagedHookCommand({
781
- absoluteRunner: runner,
782
- scriptPath: `${portableBaseDir}/hooks/${hookName}`,
783
- 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,
784
1093
  platform,
785
1094
  hookShell,
786
1095
  });
787
1096
  }
1097
+ const chainRunner = buildNodeRunnerChainToken(opts);
1098
+ if (chainRunner === null)
1099
+ return null;
788
1100
  const hooksPath = shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName;
789
- return projectManagedHookCommand({
790
- absoluteRunner: runner,
791
- scriptPath: hooksPath,
1101
+ return shellCmdProjection.projectShellCommandText({
1102
+ runnerToken: chainRunner,
1103
+ argTokens: [JSON.stringify(hooksPath)],
792
1104
  runtime,
793
1105
  platform,
794
1106
  hookShell,
@@ -1024,28 +1336,39 @@ function writeCursorHooksJson(targetDir, src, opts) {
1024
1336
  installedScripts.add(script);
1025
1337
  }
1026
1338
  }
1027
- // Stage the hooks/lib/ helpers the staged scripts require (#2587). Cursor sets
1028
- // hostBehaviors.skipSharedHooksInstall, so it never reaches the installer's
1029
- // bulk hooks/lib copy — without this, a script requiring './lib/…' would throw
1030
- // MODULE_NOT_FOUND at load, BEFORE its own try/catch, and wedge every Cursor
1031
- // session on the one runtime these hooks exist for. Driven off what the staged
1032
- // 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.
1033
1351
  const requiredLibFiles = new Set();
1034
- for (const script of installedScripts) {
1035
- const staged = node_fs_1.default.readFileSync(node_path_1.default.join(hooksDir, script), 'utf8');
1036
- // Tolerant of interior whitespace and either quote style: a hook author
1037
- // writing `require( "./lib/x.js" )` must still get its helper staged, since
1038
- // a miss here surfaces as MODULE_NOT_FOUND at hook load, not at install.
1039
- 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;
1040
1356
  let m;
1041
- while ((m = re.exec(staged)) !== null)
1357
+ while ((m = libRequireRe.exec(source)) !== null)
1042
1358
  requiredLibFiles.add(m[1]);
1043
1359
  }
1360
+ for (const script of installedScripts) {
1361
+ scanForLibRequires(node_fs_1.default.readFileSync(node_path_1.default.join(hooksDir, script), 'utf8'));
1362
+ }
1044
1363
  if (requiredLibFiles.size > 0) {
1045
1364
  const srcLibDir = node_path_1.default.join(srcHooksDir, 'lib');
1046
1365
  const destLibDir = node_path_1.default.join(hooksDir, 'lib');
1047
1366
  node_fs_1.default.mkdirSync(destLibDir, { recursive: true });
1048
- 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);
1049
1372
  const libSrc = node_path_1.default.join(srcLibDir, libFile);
1050
1373
  if (!node_fs_1.default.existsSync(libSrc)) {
1051
1374
  // FAIL LOUD. Skipping here would ship hook scripts whose top-level
@@ -1058,6 +1381,8 @@ function writeCursorHooksJson(targetDir, src, opts) {
1058
1381
  let libContent = node_fs_1.default.readFileSync(libSrc, 'utf8');
1059
1382
  libContent = libContent.replace(/gsd:/gi, 'gsd-');
1060
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));
1061
1386
  }
1062
1387
  }
1063
1388
  // #2717: write the CommonJS marker into hooks/ alongside the staged .js
@@ -1764,6 +2089,22 @@ function applySettingsJsonHooks(settings, opts) {
1764
2089
  else if (!hasPhaseBoundaryHook && !phaseBoundaryCommand) {
1765
2090
  console.warn(` ${yellow}⚠${reset} Skipped phase boundary hook — Bash executable path unavailable (#3393)`);
1766
2091
  }
2092
+ // #3329: the four `.sh` sites above register only-if-absent, so an entry
2093
+ // registered by an older installer keeps its old command forever —
2094
+ // /gsd-update (which re-invokes the installer) never re-derived it. On
2095
+ // Claude/win32 that left the pre-#580/#3393 bash-runner-prefixed commands
2096
+ // in settings.json indefinitely. Reconcile existing managed `.sh` entries
2097
+ // to the command this install would generate today. Inert wherever the
2098
+ // bash runner is still the correct shape; scoped to exact managed
2099
+ // basenames so user-authored hooks are never touched.
2100
+ if (reconcileManagedShellHookCommands(settings, {
2101
+ 'gsd-validate-commit.sh': validateCommitCommand,
2102
+ 'gsd-graphify-update.sh': graphifyUpdateCommand,
2103
+ 'gsd-session-state.sh': sessionStateCommand,
2104
+ 'gsd-phase-boundary.sh': phaseBoundaryCommand,
2105
+ }, { platform: hookOpts.platform, runtime })) {
2106
+ console.log(` ${green}✓${reset} Reconciled managed .sh hook commands to current format (#3329)`);
2107
+ }
1767
2108
  // ── Extended hook events: SubagentStop / Stop / PreCompact / SubagentStart
1768
2109
  // (#788 + #770 + #2092) ────────────────────────────────────────────────
1769
2110
  // Claude Code (since #770) and Qwen Code (since #788) both support the
@@ -2195,9 +2536,12 @@ module.exports = {
2195
2536
  applySettingsJsonHooks,
2196
2537
  referencesHook,
2197
2538
  rewriteLegacyManagedNodeHookCommands,
2539
+ reconcileManagedShellHookCommands,
2198
2540
  normalizeNodePath,
2199
2541
  resolveNodeRunner,
2542
+ buildNodeRunnerChainToken,
2200
2543
  resolveBashRunner,
2544
+ NODE_RUNNER_RESOLVER_HOOK,
2201
2545
  // Atomic write seam (shared with bin/install.js so all writes participate
2202
2546
  // in install.js's _cleanTmpFiles() scoped temp-cleanup).
2203
2547
  atomicWriteFileSync,