@opengsd/gsd-core 1.9.1 → 1.11.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 (426) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +27 -3
  5. package/agents/gsd-debug-session-manager.md +11 -0
  6. package/agents/gsd-debugger.md +12 -246
  7. package/agents/gsd-doc-synthesizer.md +2 -4
  8. package/agents/gsd-executor.md +12 -10
  9. package/agents/gsd-integration-checker.md +3 -0
  10. package/agents/gsd-mempalace-curator.md +5 -2
  11. package/agents/gsd-phase-researcher.md +20 -1
  12. package/agents/gsd-plan-checker.md +46 -0
  13. package/agents/gsd-planner.md +49 -54
  14. package/agents/gsd-roadmapper.md +21 -3
  15. package/agents/gsd-user-profiler.md +3 -0
  16. package/agents/gsd-verifier.md +26 -73
  17. package/bin/install.js +1272 -1238
  18. package/bin/lib/ui-safety-gate.cjs +2 -0
  19. package/commands/gsd/code-review.md +1 -1
  20. package/commands/gsd/execute-phase.md +1 -1
  21. package/commands/gsd/map-codebase.md +1 -1
  22. package/commands/gsd/mempalace-capture.md +2 -2
  23. package/commands/gsd/mempalace-recall.md +1 -1
  24. package/commands/gsd/new-milestone.md +2 -2
  25. package/commands/gsd/plan-phase.md +1 -1
  26. package/commands/gsd/quick.md +1 -1
  27. package/commands/gsd/review-backlog.md +2 -1
  28. package/commands/gsd/verify-work.md +1 -1
  29. package/gsd-core/bin/gsd-tools.cjs +1009 -115
  30. package/gsd-core/bin/lib/active-workstream-store.cjs +153 -12
  31. package/gsd-core/bin/lib/agent-install-check.cjs +268 -38
  32. package/gsd-core/bin/lib/api-coverage.cjs +123 -5
  33. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  34. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  35. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  36. package/gsd-core/bin/lib/audit.cjs +926 -202
  37. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  38. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  39. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  40. package/gsd-core/bin/lib/capability-registry.cjs +608 -148
  41. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  42. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  43. package/gsd-core/bin/lib/capability-validator.cjs +507 -24
  44. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  45. package/gsd-core/bin/lib/check-command-router.cjs +114 -38
  46. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  47. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  48. package/gsd-core/bin/lib/command-aliases.cjs +94 -0
  49. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  50. package/gsd-core/bin/lib/commands.cjs +665 -99
  51. package/gsd-core/bin/lib/commonjs-marker.cjs +142 -0
  52. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  53. package/gsd-core/bin/lib/config-loader.cjs +76 -0
  54. package/gsd-core/bin/lib/config.cjs +22 -2
  55. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  56. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  57. package/gsd-core/bin/lib/core-utils.cjs +217 -40
  58. package/gsd-core/bin/lib/decisions.cjs +23 -0
  59. package/gsd-core/bin/lib/docs.cjs +3 -2
  60. package/gsd-core/bin/lib/external-job.cjs +19 -4
  61. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  62. package/gsd-core/bin/lib/frontmatter.cjs +239 -32
  63. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  64. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  65. package/gsd-core/bin/lib/git-base-branch.cjs +160 -15
  66. package/gsd-core/bin/lib/graphify.cjs +142 -27
  67. package/gsd-core/bin/lib/gsd2-import.cjs +37 -5
  68. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  69. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  70. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  71. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  72. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  73. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  74. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  75. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  76. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  77. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  78. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  79. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  80. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  81. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  82. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  83. package/gsd-core/bin/lib/init.cjs +1325 -169
  84. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  85. package/gsd-core/bin/lib/install-engine.cjs +805 -264
  86. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  87. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  88. package/gsd-core/bin/lib/install-profiles.cjs +160 -57
  89. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  90. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  91. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  92. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  93. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  94. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  95. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  96. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  97. package/gsd-core/bin/lib/installer-migrations.cjs +206 -13
  98. package/gsd-core/bin/lib/io.cjs +38 -3
  99. package/gsd-core/bin/lib/markdown-sectionizer.cjs +8 -1
  100. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  101. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  102. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  103. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  104. package/gsd-core/bin/lib/milestone.cjs +821 -109
  105. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  106. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  107. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  108. package/gsd-core/bin/lib/pattern.cjs +122 -0
  109. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  110. package/gsd-core/bin/lib/phase-id.cjs +507 -36
  111. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  112. package/gsd-core/bin/lib/phase-locator.cjs +258 -58
  113. package/gsd-core/bin/lib/phase.cjs +891 -156
  114. package/gsd-core/bin/lib/plan-dependency-graph.cjs +303 -0
  115. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  116. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  117. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  118. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  119. package/gsd-core/bin/lib/planning-workspace.cjs +60 -6
  120. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  121. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  122. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  123. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  124. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +85 -0
  125. package/gsd-core/bin/lib/review-lane-descriptor.cjs +108 -0
  126. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  127. package/gsd-core/bin/lib/review-lane-runner.cjs +447 -68
  128. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  129. package/gsd-core/bin/lib/roadmap-command-router.cjs +76 -9
  130. package/gsd-core/bin/lib/roadmap-parser.cjs +1035 -194
  131. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  132. package/gsd-core/bin/lib/roadmap.cjs +405 -84
  133. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +795 -100
  134. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  135. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +440 -57
  136. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  137. package/gsd-core/bin/lib/runtime-homes.cjs +220 -41
  138. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +220 -44
  139. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  140. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  141. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  142. package/gsd-core/bin/lib/security.cjs +104 -5
  143. package/gsd-core/bin/lib/shell-command-projection.cjs +388 -30
  144. package/gsd-core/bin/lib/smart-entry.cjs +154 -22
  145. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  146. package/gsd-core/bin/lib/state-document.cjs +152 -8
  147. package/gsd-core/bin/lib/state-transition.cjs +424 -105
  148. package/gsd-core/bin/lib/state.cjs +1927 -401
  149. package/gsd-core/bin/lib/surface.cjs +35 -10
  150. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  151. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  152. package/gsd-core/bin/lib/uat-predicate.cjs +20 -4
  153. package/gsd-core/bin/lib/uat.cjs +706 -64
  154. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  155. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  156. package/gsd-core/bin/lib/unusable-input.cjs +33 -0
  157. package/gsd-core/bin/lib/update-context.cjs +8 -2
  158. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  159. package/gsd-core/bin/lib/validate.cjs +20 -6
  160. package/gsd-core/bin/lib/vendor/README.md +37 -0
  161. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  162. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  163. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  164. package/gsd-core/bin/lib/verification.cjs +287 -20
  165. package/gsd-core/bin/lib/verify.cjs +368 -880
  166. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  167. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +203 -19
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +576 -31
  169. package/gsd-core/bin/lib/workstream.cjs +8 -2
  170. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  171. package/gsd-core/bin/lib/worktree-safety.cjs +450 -125
  172. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  173. package/gsd-core/bin/shared/config-schema.manifest.json +9 -1
  174. package/gsd-core/references/agent-contracts.md +43 -26
  175. package/gsd-core/references/artifact-types.md +10 -3
  176. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  177. package/gsd-core/references/checkpoints.md +2 -2
  178. package/gsd-core/references/context-budget.md +1 -1
  179. package/gsd-core/references/debugger-techniques.md +255 -0
  180. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  181. package/gsd-core/references/doc-conflict-engine.md +1 -1
  182. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  184. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  185. package/gsd-core/references/execute-phase-response-language.md +1 -1
  186. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  187. package/gsd-core/references/gate-prompts.md +1 -1
  188. package/gsd-core/references/git-planning-commit.md +2 -1
  189. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  190. package/gsd-core/references/model-profiles.md +12 -4
  191. package/gsd-core/references/mvp-concepts.md +9 -9
  192. package/gsd-core/references/planner-guidance.md +3 -9
  193. package/gsd-core/references/planner-preconditions.md +1 -1
  194. package/gsd-core/references/planner-reviews.md +1 -1
  195. package/gsd-core/references/planning-config.md +8 -6
  196. package/gsd-core/references/research-documentation-lookup.md +5 -3
  197. package/gsd-core/references/revision-loop.md +1 -1
  198. package/gsd-core/references/specless-probe-fallback.md +8 -7
  199. package/gsd-core/references/universal-anti-patterns.md +3 -3
  200. package/gsd-core/references/verifier-phase-gates.md +192 -0
  201. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  202. package/gsd-core/references/verify-mvp-mode.md +1 -1
  203. package/gsd-core/references/workstream-flag.md +22 -6
  204. package/gsd-core/references/worktree-branch-check.md +2 -2
  205. package/gsd-core/templates/discussion-log.md +1 -1
  206. package/gsd-core/templates/phase-prompt.md +2 -4
  207. package/gsd-core/templates/state.md +4 -4
  208. package/gsd-core/templates/summary-complex.md +2 -0
  209. package/gsd-core/templates/summary-minimal.md +2 -0
  210. package/gsd-core/templates/summary-standard.md +2 -0
  211. package/gsd-core/templates/summary.md +2 -0
  212. package/gsd-core/templates/verification-report.md +9 -1
  213. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  214. package/gsd-core/workflows/audit-milestone.md +3 -0
  215. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  216. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  217. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  218. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  219. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  220. package/gsd-core/workflows/autonomous.md +33 -70
  221. package/gsd-core/workflows/cleanup.md +62 -3
  222. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  223. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +93 -0
  224. package/gsd-core/workflows/code-review-fix.md +37 -10
  225. package/gsd-core/workflows/code-review.md +74 -166
  226. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  227. package/gsd-core/workflows/complete-milestone.md +160 -95
  228. package/gsd-core/workflows/debug.md +16 -17
  229. package/gsd-core/workflows/diagnose-issues.md +56 -8
  230. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  231. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  232. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  233. package/gsd-core/workflows/discuss-phase-assumptions.md +7 -17
  234. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  235. package/gsd-core/workflows/docs-update.md +8 -51
  236. package/gsd-core/workflows/edit-phase.md +26 -1
  237. package/gsd-core/workflows/eval-review.md +3 -5
  238. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +64 -7
  239. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  240. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  241. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  242. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +21 -0
  243. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  244. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  245. package/gsd-core/workflows/execute-phase.md +103 -187
  246. package/gsd-core/workflows/execute-plan.md +36 -4
  247. package/gsd-core/workflows/explore.md +131 -4
  248. package/gsd-core/workflows/fast.md +10 -2
  249. package/gsd-core/workflows/health.md +73 -4
  250. package/gsd-core/workflows/help/modes/full.md +6 -1
  251. package/gsd-core/workflows/import.md +4 -4
  252. package/gsd-core/workflows/ingest-docs.md +7 -6
  253. package/gsd-core/workflows/mvp-phase.md +6 -3
  254. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  255. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  256. package/gsd-core/workflows/new-milestone.md +35 -47
  257. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  258. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  259. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  260. package/gsd-core/workflows/new-project.md +27 -240
  261. package/gsd-core/workflows/next.md +12 -0
  262. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  263. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  264. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  265. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  266. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  267. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  268. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  269. package/gsd-core/workflows/plan-phase.md +89 -209
  270. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  271. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  272. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  273. package/gsd-core/workflows/progress.md +45 -159
  274. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  275. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  276. package/gsd-core/workflows/quick/steps/quick-verification.md +67 -0
  277. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  278. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  279. package/gsd-core/workflows/quick.md +55 -405
  280. package/gsd-core/workflows/resume-project.md +3 -0
  281. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  282. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  283. package/gsd-core/workflows/review.md +41 -13
  284. package/gsd-core/workflows/section-manifest.json +219 -0
  285. package/gsd-core/workflows/secure-phase.md +1 -1
  286. package/gsd-core/workflows/session-report.md +2 -1
  287. package/gsd-core/workflows/settings.md +66 -2
  288. package/gsd-core/workflows/ship.md +104 -44
  289. package/gsd-core/workflows/sketch.md +1 -1
  290. package/gsd-core/workflows/spec-phase.md +41 -20
  291. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  292. package/gsd-core/workflows/spike.md +50 -16
  293. package/gsd-core/workflows/sync-skills.md +106 -13
  294. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  295. package/gsd-core/workflows/transition.md +53 -31
  296. package/gsd-core/workflows/ui-phase.md +13 -12
  297. package/gsd-core/workflows/ui-review.md +2 -2
  298. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  299. package/gsd-core/workflows/update.md +19 -8
  300. package/gsd-core/workflows/validate-phase.md +1 -1
  301. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  302. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  303. package/gsd-core/workflows/verify-work.md +17 -65
  304. package/hooks/dist/gsd-agent-isolation-guard.js +517 -0
  305. package/hooks/dist/gsd-check-update-worker.js +64 -12
  306. package/hooks/dist/gsd-check-update.js +19 -1
  307. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  308. package/hooks/dist/gsd-cursor-subagent-start.js +607 -26
  309. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  310. package/hooks/dist/gsd-prompt-guard.js +21 -20
  311. package/hooks/dist/gsd-read-injection-scanner.js +45 -24
  312. package/hooks/dist/gsd-statusline.js +90 -6
  313. package/hooks/dist/gsd-update-banner.js +22 -1
  314. package/hooks/dist/gsd-workflow-guard.js +134 -36
  315. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  316. package/hooks/dist/gsd-write-guard.js +359 -0
  317. package/hooks/dist/lib/git-cmd.js +92 -59
  318. package/hooks/dist/lib/injection-patterns.js +45 -0
  319. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  320. package/hooks/dist/lib/isolation-sentinel.js +277 -0
  321. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  322. package/hooks/gsd-agent-isolation-guard.js +517 -0
  323. package/hooks/gsd-check-update-worker.js +64 -12
  324. package/hooks/gsd-check-update.js +19 -1
  325. package/hooks/gsd-cursor-pre-tool.js +0 -3
  326. package/hooks/gsd-cursor-subagent-start.js +607 -26
  327. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  328. package/hooks/gsd-prompt-guard.js +21 -20
  329. package/hooks/gsd-read-injection-scanner.js +45 -24
  330. package/hooks/gsd-statusline.js +90 -6
  331. package/hooks/gsd-update-banner.js +22 -1
  332. package/hooks/gsd-workflow-guard.js +134 -36
  333. package/hooks/gsd-worktree-path-guard.js +2 -1
  334. package/hooks/gsd-write-guard.js +359 -0
  335. package/hooks/hooks.json +12 -0
  336. package/hooks/lib/git-cmd.js +92 -59
  337. package/hooks/lib/injection-patterns.js +45 -0
  338. package/hooks/lib/isolation-deny-reason.js +39 -0
  339. package/hooks/lib/isolation-sentinel.js +277 -0
  340. package/hooks/managed-hooks-registry.cjs +2 -0
  341. package/package.json +31 -10
  342. package/pi/gsd.cjs +71 -12
  343. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  344. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  345. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  346. package/scripts/build-hooks.js +9 -0
  347. package/scripts/changeset/lint.cjs +68 -6
  348. package/scripts/changeset/serialize.cjs +5 -1
  349. package/scripts/check-alias-drift.cjs +7 -43
  350. package/scripts/check-contract-drift.cjs +297 -0
  351. package/scripts/ci-test-scope.cjs +19 -2
  352. package/scripts/command-contract-helpers.cjs +903 -1
  353. package/scripts/gen-adr-index.cjs +728 -38
  354. package/scripts/gen-capability-matrix.cjs +1 -1
  355. package/scripts/gen-capability-registry.cjs +3 -15
  356. package/scripts/gen-context-index.cjs +439 -0
  357. package/scripts/gen-health-docs.cjs +390 -0
  358. package/scripts/gen-inventory-manifest.cjs +150 -4
  359. package/scripts/gen-loop-host-contract.cjs +4 -24
  360. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  361. package/scripts/gen-registry.cjs +3 -14
  362. package/scripts/gen-section-manifest.cjs +638 -0
  363. package/scripts/generate-package-identity.cjs +4 -2
  364. package/scripts/lib/alias-drift-families.cjs +46 -0
  365. package/scripts/lib/drift-scan.cjs +278 -0
  366. package/scripts/lint-allow-test-rule-refs.allowlist.json +15 -54
  367. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  368. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  369. package/scripts/lint-canary-version-leak.cjs +73 -0
  370. package/scripts/lint-command-contract.cjs +96 -13
  371. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  372. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  373. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  374. package/scripts/lint-default-flip-documentation.cjs +193 -0
  375. package/scripts/lint-docs-command-form.cjs +195 -0
  376. package/scripts/lint-docs-required.cjs +9 -1
  377. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  378. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  379. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  380. package/scripts/lint-example-parser-parity.cjs +395 -0
  381. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  382. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  383. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  384. package/scripts/lint-milestone-window-drift.cjs +468 -0
  385. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  386. package/scripts/lint-plan-count-drift.cjs +318 -0
  387. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  388. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  389. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  390. package/scripts/lint-regression-test-names.cjs +15 -13
  391. package/scripts/lint-removed-but-needed.cjs +320 -0
  392. package/scripts/lint-state-field-drift.cjs +805 -0
  393. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  394. package/scripts/lint-test-file-count.allowlist.json +40 -3
  395. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  396. package/scripts/lint-vendored-deps.cjs +124 -0
  397. package/scripts/mutation-matrix.cjs +13 -0
  398. package/scripts/pr-changed-files.cjs +63 -0
  399. package/scripts/pr-template-policy.cjs +14 -4
  400. package/scripts/prompt-injection-scan.sh +52 -6
  401. package/scripts/require-issue-link-policy.cjs +192 -0
  402. package/scripts/state-write-path-drift-baseline.json +19 -0
  403. package/scripts/sync-runtime-launcher.cjs +2 -4
  404. package/skills/gsd-autonomous/SKILL.md +0 -1
  405. package/skills/gsd-code-review/SKILL.md +1 -1
  406. package/skills/gsd-execute-phase/SKILL.md +1 -2
  407. package/skills/gsd-map-codebase/SKILL.md +1 -1
  408. package/skills/gsd-mempalace-capture/SKILL.md +2 -2
  409. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  410. package/skills/gsd-new-milestone/SKILL.md +2 -2
  411. package/skills/gsd-next/SKILL.md +0 -1
  412. package/skills/gsd-plan-phase/SKILL.md +1 -2
  413. package/skills/gsd-progress/SKILL.md +0 -1
  414. package/skills/gsd-quick/SKILL.md +1 -1
  415. package/skills/gsd-review-backlog/SKILL.md +2 -1
  416. package/skills/gsd-stats/SKILL.md +0 -1
  417. package/skills/gsd-verify-work/SKILL.md +1 -1
  418. package/vscode/package.json +1 -1
  419. package/gsd-core/workflows/discovery-phase.md +0 -298
  420. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  421. package/gsd-core/workflows/verify-phase.md +0 -577
  422. package/scripts/affected-tests-lib.cjs +0 -554
  423. package/scripts/gen-emitted-baseline.cjs +0 -145
  424. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  425. package/scripts/run-affected-tests.cjs +0 -7
  426. package/scripts/run-tests.cjs +0 -1050
@@ -15,6 +15,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
15
15
  return (mod && mod.__esModule) ? mod : { "default": mod };
16
16
  };
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.PATH_ACTION_REASON = void 0;
18
19
  exports.toPosixPath = toPosixPath;
19
20
  exports.toNativePath = toNativePath;
20
21
  exports.posixNormalize = posixNormalize;
@@ -35,11 +36,15 @@ exports.projectCodexHookTomlCommand = projectCodexHookTomlCommand;
35
36
  exports.escapePowerShellSingleQuoted = escapePowerShellSingleQuoted;
36
37
  exports.escapePosixDoubleQuoted = escapePosixDoubleQuoted;
37
38
  exports.escapeSingleQuotedShellLiteral = escapeSingleQuotedShellLiteral;
39
+ exports.projectPathExportLine = projectPathExportLine;
38
40
  exports.renderShellActionLines = renderShellActionLines;
39
41
  exports.projectPathActionProjection = projectPathActionProjection;
40
42
  exports.projectPersistentPathExportActions = projectPersistentPathExportActions;
43
+ exports.isSpawnTimeout = isSpawnTimeout;
41
44
  exports.execGit = execGit;
42
45
  exports.execNpm = execNpm;
46
+ exports.resolveExecutableBinary = resolveExecutableBinary;
47
+ exports.projectSpawnInvocation = projectSpawnInvocation;
43
48
  exports.execTool = execTool;
44
49
  exports.resolveGsdToolsPath = resolveGsdToolsPath;
45
50
  exports.dispatchGsdCommand = dispatchGsdCommand;
@@ -55,6 +60,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
55
60
  // can intercept calls from this seam — destructured imports capture references
56
61
  // at load time and become un-mockable.
57
62
  const node_child_process_1 = __importDefault(require("node:child_process"));
63
+ const pattern_cjs_1 = require("./pattern.cjs");
58
64
  /**
59
65
  * Convert a filesystem path to POSIX form (forward slashes) by translating the
60
66
  * platform-native separator. Single seam for native→POSIX conversion.
@@ -303,7 +309,7 @@ function isManagedHookCommand(commandText, opts = {}) {
303
309
  return false;
304
310
  }
305
311
  for (const basename of managedBasenames) {
306
- const escapedBasename = basename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
312
+ const escapedBasename = (0, pattern_cjs_1.escapeRegex)(basename);
307
313
  const pattern = new RegExp(`(^|[\\\\/\\s"'` + '`' + `])${escapedBasename}(?=$|[\\s"'` + '`' + `])`);
308
314
  if (pattern.test(normalizedCommand))
309
315
  return true;
@@ -351,8 +357,34 @@ function projectLegacySettingsHookCommand({ absoluteRunner, scriptPath, scriptTo
351
357
  platform,
352
358
  });
353
359
  }
360
+ // Implements the TOML v1.0.0 basic-string escaping grammar (toml.md, "Basic
361
+ // strings" section, https://toml.io/en/v1.0.0#string): a basic string must
362
+ // escape the quotation mark, backslash, and control characters other than
363
+ // tab (U+0000-U+0008, U+000A-U+001F, U+007F). Compact escapes are used where
364
+ // TOML defines them (\b \t \n \f \r \" \\); every other character in the
365
+ // required ranges falls back to \uXXXX. See #3118 — an earlier version
366
+ // escaped only backslash and quote, so a raw newline/CR/NUL in a value
367
+ // produced an unparseable config.toml.
368
+ const TOML_COMPACT_ESCAPES = {
369
+ '\x08': '\\b',
370
+ '\x09': '\\t',
371
+ '\x0A': '\\n',
372
+ '\x0C': '\\f',
373
+ '\x0D': '\\r',
374
+ };
375
+ // U+0000-U+0008, U+000A-U+001F, U+007F — control characters other than tab
376
+ // (U+0009), which the grammar permits unescaped.
377
+ const TOML_MUST_ESCAPE_CONTROL_CHARS = /[\x00-\x08\x0A-\x1F\x7F]/g;
354
378
  function escapeTomlDoubleQuotedString(value) {
355
- return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
379
+ return String(value)
380
+ .replace(/\\/g, '\\\\')
381
+ .replace(/"/g, '\\"')
382
+ .replace(TOML_MUST_ESCAPE_CONTROL_CHARS, (ch) => {
383
+ const compact = TOML_COMPACT_ESCAPES[ch];
384
+ if (compact)
385
+ return compact;
386
+ return `\\u${ch.codePointAt(0).toString(16).padStart(4, '0')}`;
387
+ });
356
388
  }
357
389
  function projectCodexHookTomlCommand({ absoluteRunner, scriptPath, platform = process.platform }) {
358
390
  const command = projectManagedHookCommand({
@@ -372,6 +404,27 @@ function escapePosixDoubleQuoted(value) {
372
404
  function escapeSingleQuotedShellLiteral(value) {
373
405
  return String(value).replace(/'/g, "'\\''");
374
406
  }
407
+ /**
408
+ * The `export PATH="<dir>:$PATH"` line every persistence lane appends, plus the escaped directory
409
+ * token it embeds. One builder because three lanes emit this line: a lane that re-escapes it
410
+ * locally is how #3118 shipped a `$(…)` into ~/.bashrc, where it ran on every new shell. The
411
+ * escaping is for the line's FINAL context — a double-quoted string in an rc file — not for
412
+ * whatever transport (an `echo`, a paste) it passes through on the way there.
413
+ */
414
+ function projectPathExportLine(targetDir) {
415
+ const escapedDir = escapePosixDoubleQuoted(String(targetDir));
416
+ return { escapedDir, line: `export PATH="${escapedDir}:$PATH"` };
417
+ }
418
+ /**
419
+ * Why a PATH suggestion produced no actions. An empty `shellActions` alone folds two different
420
+ * facts together — "no target directory was given" and "this target directory cannot be
421
+ * expressed as a shell command" — and a caller that cannot tell them apart prints a header with
422
+ * nothing under it (#3118).
423
+ */
424
+ exports.PATH_ACTION_REASON = Object.freeze({
425
+ NO_TARGET_DIR: 'no_target_dir',
426
+ WIN32_RESERVED_QUOTE: 'win32_reserved_quote',
427
+ });
375
428
  function renderShellActionLines(shellActions = []) {
376
429
  return shellActions.map((action) => {
377
430
  if (!action || !action.command)
@@ -381,12 +434,18 @@ function renderShellActionLines(shellActions = []) {
381
434
  }
382
435
  function projectPathActionProjection({ mode = 'repair', targetDir, platform = process.platform, }) {
383
436
  if (!targetDir)
384
- return { shellActions: [], actionLines: [] };
437
+ return { shellActions: [], actionLines: [], reason: exports.PATH_ACTION_REASON.NO_TARGET_DIR };
438
+ // #3118: `"` is reserved on Windows, so a path containing one cannot exist — and it would close
439
+ // cmd's quoted region in the `powershell -Command "…"` lane below, turning the rest into cmd
440
+ // input. There is no correct command to suggest for an impossible path: fail closed rather than
441
+ // emit one whose quoting can be broken.
442
+ if (platform === 'win32' && String(targetDir).includes('"'))
443
+ return { shellActions: [], actionLines: [], reason: exports.PATH_ACTION_REASON.WIN32_RESERVED_QUOTE };
385
444
  const isWin32 = platform === 'win32';
386
445
  let shellActions;
387
446
  if (isWin32) {
388
447
  const psTargetDir = escapePowerShellSingleQuoted(targetDir);
389
- const bashTargetDir = escapeSingleQuotedShellLiteral(posixNormalize(String(targetDir)));
448
+ const bashExportLine = escapeSingleQuotedShellLiteral(projectPathExportLine(posixNormalize(String(targetDir))).line);
390
449
  shellActions = [
391
450
  {
392
451
  label: 'PowerShell',
@@ -401,42 +460,51 @@ function projectPathActionProjection({ mode = 'repair', targetDir, platform = pr
401
460
  {
402
461
  label: 'Git Bash',
403
462
  shell: 'bash',
404
- command: `echo 'export PATH="${bashTargetDir}:$PATH"' >> ~/.bashrc`,
463
+ command: `echo '${bashExportLine}' >> ~/.bashrc`,
405
464
  },
406
465
  ];
407
466
  }
408
467
  else if (mode === 'persist') {
409
- const bashTargetDir = escapeSingleQuotedShellLiteral(String(targetDir));
468
+ const exportLine = escapeSingleQuotedShellLiteral(projectPathExportLine(targetDir).line);
469
+ const fishTargetDir = escapeSingleQuotedShellLiteral(String(targetDir));
410
470
  shellActions = [
411
471
  {
412
472
  label: 'zsh',
413
473
  shell: 'zsh',
414
- command: `echo 'export PATH="${bashTargetDir}:$PATH"' >> ~/.zshrc`,
474
+ command: `echo '${exportLine}' >> ~/.zshrc`,
415
475
  },
416
476
  {
417
477
  label: 'bash',
418
478
  shell: 'bash',
419
- command: `echo 'export PATH="${bashTargetDir}:$PATH"' >> ~/.bashrc`,
479
+ command: `echo '${exportLine}' >> ~/.bashrc`,
420
480
  },
421
481
  // #323: fish has no `export`/`$PATH`-list syntax. `fish_add_path` is the
422
482
  // fish-native API (>= fish 3.2, 2021) that persists to the universal
423
483
  // variable store and de-duplicates. The directory is single-quoted with
424
484
  // the same POSIX literal escaping as the zsh/bash siblings — `'\''` is
425
485
  // also a valid escaped single quote in fish between quote spans.
486
+ //
487
+ // #3118 review MINOR: a `targetDir` with a leading `-` (e.g. `-v`) is a
488
+ // legal directory name, but fish's argparse-based option scanning
489
+ // treats a leading-dash token as a flag REGARDLESS of quoting, so
490
+ // `fish_add_path '-v'` misparses it and prints "No paths to add, not
491
+ // setting anything." (exit 1) instead of adding the path. `--` is
492
+ // fish's standard end-of-options separator; verified empirically
493
+ // against a real fish 4.8.1 install that `fish_add_path -- '-v'`
494
+ // succeeds where the unseparated form fails.
426
495
  {
427
496
  label: 'fish',
428
497
  shell: 'fish',
429
- command: `fish_add_path '${bashTargetDir}'`,
498
+ command: `fish_add_path -- '${fishTargetDir}'`,
430
499
  },
431
500
  ];
432
501
  }
433
502
  else {
434
- const posixTargetDir = escapePosixDoubleQuoted(targetDir);
435
503
  shellActions = [
436
504
  {
437
505
  label: null,
438
506
  shell: 'posix',
439
- command: `export PATH="${posixTargetDir}:$PATH"`,
507
+ command: projectPathExportLine(targetDir).line,
440
508
  },
441
509
  ];
442
510
  }
@@ -451,18 +519,46 @@ function projectPersistentPathExportActions({ targetDir, platform = process.plat
451
519
  targetDir,
452
520
  platform,
453
521
  });
454
- return { shellActions: projected.shellActions };
522
+ return projected.reason === undefined
523
+ ? { shellActions: projected.shellActions }
524
+ : { shellActions: projected.shellActions, reason: projected.reason };
525
+ }
526
+ /**
527
+ * Returns true when a spawn/exec result indicates the subprocess was killed
528
+ * by a timeout, i.e. it never completed and reported a real answer. This is
529
+ * the single shared definition of "did this subprocess time out" — worktree
530
+ * safety (src/worktree-safety.cts) and worktree base-ref detection
531
+ * (src/worktree-base-ref.cts) both call this instead of maintaining their
532
+ * own copies (#3050 — "Generative Fix Divergence").
533
+ *
534
+ * Only `error.code === 'ETIMEDOUT'` is checked. Node.js guarantees this
535
+ * cross-platform when `spawnSync`'s `timeout` option fires. The `signal ===
536
+ * 'SIGTERM'` check some earlier code paired with it is platform-fragile —
537
+ * Windows does not necessarily report SIGTERM the same way — and pairing it
538
+ * in as a REQUIRED conjunct risks a false NEGATIVE (a timeout that silently
539
+ * fails to trip the guard) on that platform. There is no false-positive risk
540
+ * from dropping it: an externally-delivered SIGTERM (not a timeout) leaves
541
+ * `error` null, so `error.code === 'ETIMEDOUT'` alone still won't match it.
542
+ */
543
+ function isSpawnTimeout(result) {
544
+ return result.error?.code === 'ETIMEDOUT';
455
545
  }
456
546
  function _spawnResult(result, program) {
457
547
  if (result.error && result.error.code === 'ENOENT') {
458
- return { exitCode: 127, stdout: '', stderr: `${program}: not found`, signal: null, error: result.error };
548
+ return { exitCode: 127, stdout: '', stderr: `${program}: not found`, signal: null, error: result.error, timedOut: false };
459
549
  }
550
+ const signal = result.signal ?? null;
551
+ const error = result.error ?? null;
460
552
  return {
461
553
  exitCode: result.status ?? 1,
462
554
  stdout: (result.stdout ?? '').toString().trim(),
463
555
  stderr: (result.stderr ?? '').toString().trim(),
464
- signal: result.signal ?? null,
465
- error: result.error ?? null,
556
+ signal,
557
+ error,
558
+ // Reuse the single shared timeout predicate (isSpawnTimeout, below) rather
559
+ // than re-deriving it here — see that function's docstring for why only
560
+ // error.code === 'ETIMEDOUT' is checked (not signal === 'SIGTERM').
561
+ timedOut: isSpawnTimeout({ error }),
466
562
  };
467
563
  }
468
564
  function execGit(args, opts = {}) {
@@ -496,15 +592,283 @@ function execNpm(args, opts = {}) {
496
592
  });
497
593
  return _spawnResult(result, 'npm');
498
594
  }
595
+ /**
596
+ * Default PATHEXT when Windows does not supply one. Matches the value
597
+ * `gsd-tools.cjs`'s `resolveSpawnBinary` shipped in #3275; kept identical so the
598
+ * delegation is a behavior-preserving move rather than a redefinition.
599
+ */
600
+ const DEFAULT_PATHEXT = '.EXE;.CMD;.BAT;.COM';
601
+ /** Windows extensions that must be mediated through cmd.exe rather than spawned. */
602
+ const CMD_MEDIATED_EXT = /\.(cmd|bat)$/i;
603
+ function _isFile(candidate, requireExecutable = false, platform = process.platform) {
604
+ try {
605
+ if (!node_fs_1.default.statSync(candidate).isFile())
606
+ return false;
607
+ if (requireExecutable && platform !== 'win32')
608
+ node_fs_1.default.accessSync(candidate, node_fs_1.default.constants.X_OK);
609
+ return true;
610
+ }
611
+ catch {
612
+ // Missing, unreadable (EACCES), a broken link, or (when requireExecutable
613
+ // is set) not executable — all mean "not this one".
614
+ return false;
615
+ }
616
+ }
617
+ /**
618
+ * Read an environment variable by name, case-insensitively.
619
+ *
620
+ * Windows environment variable names are case-insensitive and conventionally
621
+ * cased `Path` / `ComSpec`, and `process.env` is a case-insensitive proxy that
622
+ * hides the difference. Spreading it (`{ ...process.env, ...opts.env }`, which
623
+ * `execTool` does whenever a caller supplies `opts.env`) produces a PLAIN object
624
+ * that keeps the OS's actual casing and loses the proxy — so an exact-case
625
+ * `env['PATH']` lookup returns undefined there and the PATH scan silently sees
626
+ * nothing. Caught by the Windows CI lane on #3617; the #3445 tests never hit it
627
+ * because they pass uppercase keys explicitly.
628
+ *
629
+ * Exact match wins when present, so a caller who sets the canonical name pays
630
+ * no scan.
631
+ */
632
+ function _envGet(env, name) {
633
+ const exact = env[name];
634
+ if (exact !== undefined)
635
+ return exact;
636
+ const lower = name.toLowerCase();
637
+ for (const key of Object.keys(env)) {
638
+ if (key.toLowerCase() === lower)
639
+ return env[key];
640
+ }
641
+ return undefined;
642
+ }
643
+ /** cmd.exe's own quoting rule inside a `/c` string: a literal quote is doubled. */
644
+ function _cmdQuoteToken(token) {
645
+ return `"${token.replace(/"/g, '""')}"`;
646
+ }
647
+ /**
648
+ * Build a single verbatim command-line string for `cmd.exe /c` where every
649
+ * token is force-quoted, then wrap the whole thing in one more outer pair.
650
+ * cmd.exe strips exactly one outer quote pair when the string begins with a
651
+ * quote and contains at least two — so the outer wrap disappears and what's
652
+ * left is a sequence of individually-quoted tokens. Force-quoting is the
653
+ * point: an unquoted `a&b` is split by cmd's own metacharacter parsing, but a
654
+ * quoted `"a&b"` is one literal argument.
655
+ */
656
+ function _buildVerbatimCmdLine(target, args) {
657
+ return `"${[target, ...args].map(_cmdQuoteToken).join(' ')}"`;
658
+ }
659
+ /**
660
+ * #3411: resolve a DECLARED command name to the file a spawn can actually start.
661
+ * The single canonical answer to "where is this binary?" for the whole tree.
662
+ *
663
+ * This is the seam `CONTEXT.md` declares as "All OS-facing I/O; single platform
664
+ * seam". Four divergent implementations of this logic existed (#3411): `execNpm`'s
665
+ * `shell:true`, `execTool`'s absence of any handling, `gsd-tools.cjs`'s private
666
+ * scan, and `fallow-runner.cts`'s own candidate array. #3275 folded two of them
667
+ * together in `bin/`; this lifts that resolver into the seam so `bin/` delegates
668
+ * instead of owning a copy.
669
+ *
670
+ * **win32** tries PATHEXT entries ONLY — never the bare name. npm global installs
671
+ * drop an EXTENSIONLESS POSIX sh shim (`...\npm\codex`) next to `codex.CMD`; a
672
+ * bare-name-first scan resolves to it, the cmd.exe mediation gate sees no `.cmd`,
673
+ * and the ENOENT returns unchanged (field-reported on Windows 11 — see #3275).
674
+ * A name that ALREADY carries a PATHEXT-listed extension is tried as-is first, so
675
+ * `foo.exe` resolves to `foo.exe` rather than being probed as `foo.exe.EXE`; a
676
+ * suffix that is not in PATHEXT (`foo.txt`) is not an extension and only feeds the
677
+ * append loop.
678
+ *
679
+ * **POSIX** answers EXISTENCE by scanning PATH for the bare name. `execTool` does
680
+ * NOT consult this on POSIX — the bare name goes to spawnSync unchanged and Node's
681
+ * own PATH search does the work, so macOS/Linux behavior is untouched (#3275
682
+ * acceptance contract).
683
+ *
684
+ * Path-like names (any `/` or `\`) bypass the PATH scan: the name is already an
685
+ * address, so it passes through when it names an existing file.
686
+ *
687
+ * **`opts.prependPaths`** — directories searched BEFORE `env.PATH`, in array
688
+ * order (e.g. a project-local `node_modules/.bin`). Defaults to `[]`, so
689
+ * Phase 1's callers (`execTool`, and `gsd-tools.cjs`'s `resolveSpawnBinary` /
690
+ * `deps.spawn` / `hasBinary`), which set neither new option, are byte-identical
691
+ * to today. The existing per-directory candidate logic (win32 as-is-then-append-
692
+ * PATHEXT; POSIX bare name) applies to prepended directories exactly as it does
693
+ * to `PATH` segments — there is no special-casing.
694
+ *
695
+ * **`opts.requireExecutable`** — when `true` and the platform is not `win32`,
696
+ * a candidate must additionally pass `fs.accessSync(candidate, fs.constants.X_OK)`
697
+ * to count as a match. On `win32` this is a no-op (mode bits do not mean
698
+ * execute on Windows — the same carve-out `fallow-runner`'s prior private
699
+ * resolver already had). Defaults to `false`, so `accessSync` is never called
700
+ * unless a caller opts in. It is opt-in rather than the default because making
701
+ * `X_OK` unconditional would break #3445's suite: those tests stage candidates
702
+ * with plain `fs.writeFileSync` and never set an exec bit (the repo bans
703
+ * `chmod` in tests), so every one of them would resolve to `null` on POSIX.
704
+ *
705
+ * **`opts.pathOverride`** — "search THIS PATH, but read everything else —
706
+ * PATHEXT included — from the ambient environment." When set (`!== undefined`),
707
+ * the PATH search segments come from splitting `pathOverride` on `path.delimiter`
708
+ * instead of from `env.PATH`; `pathOverride: ''` means an explicit EMPTY search
709
+ * path (zero segments), never a fallback to `env.PATH` — use `!== undefined`,
710
+ * not truthiness, to tell "caller supplied a PATH string" apart from "caller
711
+ * supplied nothing". `opts.prependPaths` still comes first. PATHEXT resolution
712
+ * is UNAFFECTED by this option — it still reads from `env` (which defaults to
713
+ * `process.env`) exactly as it does when `pathOverride` is omitted. This exists
714
+ * so a caller that already has its own search path in hand (e.g.
715
+ * `resolveFallowBinary`'s `envPath`) does not have to hand-thread PATHEXT
716
+ * alongside it — a private PATHEXT read is the very shape
717
+ * `local/no-private-binary-resolution` forbids outside this seam.
718
+ *
719
+ * @returns the resolved path, or `null` when nothing matched. Callers fall back to
720
+ * the declared name on `null` so a genuine ENOENT still surfaces (#3086).
721
+ */
722
+ function resolveExecutableBinary(name, opts = {}) {
723
+ if (!name)
724
+ return null;
725
+ const requireExecutable = opts.requireExecutable ?? false;
726
+ const platform = opts.platform ?? process.platform;
727
+ if (name.includes('/') || name.includes('\\')) {
728
+ return _isFile(name, requireExecutable, platform) ? name : null;
729
+ }
730
+ const env = opts.env ?? process.env;
731
+ const rawPath = opts.pathOverride !== undefined ? opts.pathOverride : (_envGet(env, 'PATH') || '');
732
+ const pathSegments = String(rawPath).split(node_path_1.default.delimiter).filter(Boolean);
733
+ const segments = [...(opts.prependPaths ?? []), ...pathSegments];
734
+ if (platform !== 'win32') {
735
+ for (const dir of segments) {
736
+ const candidate = node_path_1.default.join(dir, name);
737
+ if (_isFile(candidate, requireExecutable, platform))
738
+ return candidate;
739
+ }
740
+ return null;
741
+ }
742
+ const exts = String(_envGet(env, 'PATHEXT') || DEFAULT_PATHEXT).split(';').filter(Boolean);
743
+ // A name already ending in a PATHEXT-listed extension is an address, not a stem:
744
+ // probing `foo.exe` as `foo.exe.EXE` would miss the file sitting right there.
745
+ // Compared case-insensitively because PATHEXT casing is not guaranteed.
746
+ const lower = name.toLowerCase();
747
+ const carriesKnownExt = exts.some((ext) => lower.endsWith(ext.toLowerCase()));
748
+ for (const dir of segments) {
749
+ if (carriesKnownExt) {
750
+ const asIs = node_path_1.default.join(dir, name);
751
+ if (_isFile(asIs, requireExecutable, platform))
752
+ return asIs;
753
+ }
754
+ for (const ext of exts) {
755
+ const candidate = node_path_1.default.join(dir, name + ext);
756
+ if (_isFile(candidate, requireExecutable, platform))
757
+ return candidate;
758
+ }
759
+ }
760
+ return null;
761
+ }
762
+ /**
763
+ * #3411: project a declared `(command, args)` into the pair `spawnSync` can
764
+ * actually execute on this platform.
765
+ *
766
+ * Resolution alone does not make Windows work: `CreateProcess` cannot execute a
767
+ * `.cmd`/`.bat` at all, so the cmd.exe mediation is inseparable from the lookup.
768
+ * Exporting only the resolver would leave every caller to re-derive that half —
769
+ * which is precisely how #3411's four copies accumulated.
770
+ *
771
+ * cmd.exe is invoked as an ordinary program with an EXPLICIT argv array, never via
772
+ * `shell: true`. `shell:true` on Windows is the mechanism behind CVE-2024-27980
773
+ * (argument injection through `.bat`/`.cmd`), and Node 26 deprecates it with an
774
+ * args array (DEP0190) because arguments are concatenated rather than escaped.
775
+ *
776
+ * Mediation fires when the target — the resolved path, or the declared name when
777
+ * resolution found nothing — carries a `.cmd`/`.bat` extension. A BARE name that
778
+ * resolved to nothing is passed through verbatim so the spawn fails with ENOENT;
779
+ * mediating it would turn `{exitCode:127, 'foo: not found'}` into cmd.exe's exit
780
+ * 9009 and silently change the not-found contract callers depend on.
781
+ *
782
+ * POSIX is a strict no-op: the declared command is returned unchanged and the
783
+ * environment is never consulted.
784
+ *
785
+ * The mediated command line is built VERBATIM rather than left to libuv: libuv's
786
+ * `quote_cmd_arg` only force-quotes an argument that contains a space, tab, or
787
+ * quote — it does not know about cmd.exe metacharacters (`&`, `|`, `>`, `<`,
788
+ * `^`, ...) at all, so an argument like `a&calc` reaches cmd.exe unquoted and
789
+ * gets re-parsed as two commands (the CVE-2024-27980 argument-injection class).
790
+ * Node's own CVE-2024-27980 escaping does not help here because it only fires
791
+ * when the spawned FILE itself is a `.bat`/`.cmd` — in this seam the spawned
792
+ * file is `cmd.exe`, not the target. Building the line ourselves and passing
793
+ * `windowsVerbatimArguments: true` (the shape Rust's std uses for the sibling
794
+ * CVE-2024-24576) means every token is force-quoted inside one outer pair, so
795
+ * a metacharacter inside a quoted token can never split the command line.
796
+ *
797
+ * KNOWN LIMIT: `%VAR%` still expands inside a cmd `/c` string, and there is no
798
+ * escape for `%` outside a batch file — an argument containing `%FOO%` is
799
+ * substituted with the environment value regardless of quoting. That is an
800
+ * information-disclosure limit, not arbitrary execution, and it's the same
801
+ * limit Rust's std documents for its own `CommandExt::raw_arg` escape hatch.
802
+ *
803
+ * CALLER CHOICE: this function's return value carries two independently
804
+ * adoptable pieces of information, and a caller may take either, both, or
805
+ * neither. `windowsVerbatimArguments: true` marks the cases where mediation
806
+ * was REQUIRED — the caller MUST adopt `command`+`args` together, since a
807
+ * `.cmd`/`.bat` genuinely cannot be spawned any other way. A merely-resolved
808
+ * `.exe` path (no mediation flag set) is only an OFFER: a caller may decline
809
+ * it and keep spawning the declared name instead, to hold its own observable
810
+ * contract stable. `execTool` (this file, below) is exactly such a caller —
811
+ * it adopts the mediated pair when `windowsVerbatimArguments` is set, but
812
+ * otherwise passes the declared `program`/`args` through untouched.
813
+ */
814
+ function projectSpawnInvocation(command, args = [], opts = {}) {
815
+ const platform = opts.platform ?? process.platform;
816
+ if (platform !== 'win32')
817
+ return { command, args, resolved: null };
818
+ const env = opts.env ?? process.env;
819
+ const resolved = resolveExecutableBinary(command, { platform, env });
820
+ // Mediate against the resolved path when we have one, else against the declared
821
+ // name. An unresolved name is mediated ONLY when it already declares .cmd/.bat:
822
+ // PATH-only resolution misses a batch file sitting in the current directory,
823
+ // which `cmd.exe /c` still finds — the behavior gsd-tools.cjs shipped before
824
+ // this consolidation, preserved here rather than silently narrowed.
825
+ const target = resolved ?? command;
826
+ if (!CMD_MEDIATED_EXT.test(node_path_1.default.basename(target))) {
827
+ // A BARE name that resolved to nothing is passed through untouched so the
828
+ // spawn fails with ENOENT. Mediating it would turn {exitCode:127,
829
+ // '<name>: not found'} into cmd.exe's exit 9009 and silently change the
830
+ // not-found contract `_spawnResult` and its 53 dependent files rely on.
831
+ return resolved ? { command: resolved, args, resolved } : { command, args, resolved: null };
832
+ }
833
+ // A CR/LF cannot be represented inside a Windows command line at all — cmd.exe
834
+ // treats it as a line terminator, so mediating it would silently truncate the
835
+ // argument rather than pass it through. Fail visibly instead: fall back to the
836
+ // unmediated shape so the spawn either fails with ENOENT (bare unresolved name)
837
+ // or hands the raw string to CreateProcess, whichever the caller was already
838
+ // prepared to see for a non-.cmd/.bat target.
839
+ if (/[\r\n]/.test(target) || args.some((a) => /[\r\n]/.test(a))) {
840
+ return resolved ? { command: resolved, args, resolved } : { command, args, resolved: null };
841
+ }
842
+ return {
843
+ command: String(_envGet(env, 'ComSpec') || 'cmd.exe'),
844
+ args: ['/d', '/s', '/c', _buildVerbatimCmdLine(target, args)],
845
+ resolved,
846
+ windowsVerbatimArguments: true,
847
+ };
848
+ }
499
849
  function execTool(program, args, opts = {}) {
500
- const result = node_child_process_1.default.spawnSync(program, args, {
850
+ // #3411: Windows cannot spawn a .cmd/.bat at all — CreateProcess refuses it —
851
+ // so those are mediated through cmd.exe. Everything else keeps the DECLARED
852
+ // program name: libuv's CreateProcess path already performs PATH + PATHEXT
853
+ // search, so resolving a .exe here would buy nothing and would change what
854
+ // this seam's 167 dependents observe being spawned. `tests/graphify.test.cjs`
855
+ // pins that contract by spying on spawnSync's first argument. POSIX never
856
+ // reaches the mediation branch at all.
857
+ const spawnEnv = opts.env ? { ...process.env, ...opts.env } : undefined;
858
+ const invocation = projectSpawnInvocation(program, args, { env: spawnEnv ?? process.env });
859
+ const mediated = invocation.windowsVerbatimArguments === true;
860
+ const result = node_child_process_1.default.spawnSync(mediated ? invocation.command : program, mediated ? invocation.args : args, {
501
861
  cwd: opts.cwd,
502
- env: opts.env ? { ...process.env, ...opts.env } : undefined,
862
+ env: spawnEnv,
503
863
  encoding: 'utf-8',
504
864
  stdio: 'pipe',
505
865
  timeout: opts.timeout ?? 30_000,
506
866
  windowsHide: true,
867
+ ...(mediated ? { windowsVerbatimArguments: true } : {}),
507
868
  });
869
+ // Stamp the DECLARED name, never the resolved path: `_spawnResult` renders
870
+ // `${program}: not found`, and callers across 53 files match on the string they
871
+ // passed. Resolution must not leak an absolute path into that message.
508
872
  return _spawnResult(result, program);
509
873
  }
510
874
  /**
@@ -549,9 +913,9 @@ function resolveGsdToolsPath() {
549
913
  * NEVER throws. Degrades to `{ ok:false, ... }` on:
550
914
  * - a missing/invalid "family" (validated locally, no subprocess spawned)
551
915
  * - ENOENT / a missing gsd-tools.cjs (via the injectable `gsdToolsPath`)
552
- * - a wall-clock timeout (`timedOut:true`, mirroring the
553
- * `signal === 'SIGTERM' && error.code === 'ETIMEDOUT'` idiom already used
554
- * by worktree-safety.cts)
916
+ * - a wall-clock timeout (`timedOut:true`, via the shared `isSpawnTimeout`
917
+ * predicate defined above in this file — also used by worktree-safety.cts
918
+ * and worktree-base-ref.cts)
555
919
  * - any other unanticipated throw from the underlying spawn (defensive
556
920
  * try/catch — execTool itself is spawnSync-based and does not throw).
557
921
  */
@@ -592,16 +956,9 @@ function dispatchGsdCommand({ family, subcommand, args = [], cwd, timeout = 30_0
592
956
  timedOut: false,
593
957
  };
594
958
  }
595
- // Mirrors the established `result.error && (result.error as
596
- // NodeJS.ErrnoException).code === ...` idiom (graphify.cts, worktree-safety.cts):
597
- // narrow away null via `!== null` FIRST, then cast — asserting `Error | null`
598
- // to `NodeJS.ErrnoException | null` directly (paired with optional chaining)
599
- // trips a typescript-eslint no-unnecessary-type-assertion false positive for
600
- // this exact narrowing shape (all of ErrnoException's extra fields over Error
601
- // are optional).
602
- const timedOut = result.signal === 'SIGTERM'
603
- && result.error !== null
604
- && result.error.code === 'ETIMEDOUT';
959
+ // Delegates to the single shared predicate defined above in this file
960
+ // (#3050 — "Generative Fix Divergence") instead of a local inline copy.
961
+ const timedOut = isSpawnTimeout(result);
605
962
  return {
606
963
  ok: result.exitCode === 0 && !timedOut,
607
964
  stdout: result.stdout,
@@ -618,6 +975,7 @@ function probeTty(opts = {}) {
618
975
  const ttyPath = node_child_process_1.default.execFileSync('tty', [], {
619
976
  encoding: 'utf-8',
620
977
  stdio: ['inherit', 'pipe', 'ignore'],
978
+ timeout: 5_000,
621
979
  }).trim();
622
980
  if (!ttyPath || ttyPath === 'not a tty')
623
981
  return null;