@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
@@ -19,6 +19,7 @@ exports.PATH_ACTION_REASON = void 0;
19
19
  exports.toPosixPath = toPosixPath;
20
20
  exports.toNativePath = toNativePath;
21
21
  exports.posixNormalize = posixNormalize;
22
+ exports.toComparablePathKey = toComparablePathKey;
22
23
  exports.hookCommandNeedsPowerShellCallOperator = hookCommandNeedsPowerShellCallOperator;
23
24
  exports.formatHookCommandForRuntime = formatHookCommandForRuntime;
24
25
  exports.shellHookOmitsBashRunner = shellHookOmitsBashRunner;
@@ -43,11 +44,15 @@ exports.projectPersistentPathExportActions = projectPersistentPathExportActions;
43
44
  exports.isSpawnTimeout = isSpawnTimeout;
44
45
  exports.execGit = execGit;
45
46
  exports.execNpm = execNpm;
47
+ exports.envGet = envGet;
48
+ exports.resolveExecutableBinary = resolveExecutableBinary;
49
+ exports.projectSpawnInvocation = projectSpawnInvocation;
46
50
  exports.execTool = execTool;
47
51
  exports.resolveGsdToolsPath = resolveGsdToolsPath;
48
52
  exports.dispatchGsdCommand = dispatchGsdCommand;
49
53
  exports.probeTty = probeTty;
50
54
  exports.normalizeContent = normalizeContent;
55
+ exports.contentChangedAfterNormalize = contentChangedAfterNormalize;
51
56
  exports.retryRenameSync = retryRenameSync;
52
57
  exports.platformWriteSync = platformWriteSync;
53
58
  exports.platformReadSync = platformReadSync;
@@ -58,6 +63,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
58
63
  // can intercept calls from this seam — destructured imports capture references
59
64
  // at load time and become un-mockable.
60
65
  const node_child_process_1 = __importDefault(require("node:child_process"));
66
+ const pattern_cjs_1 = require("./pattern.cjs");
61
67
  /**
62
68
  * Convert a filesystem path to POSIX form (forward slashes) by translating the
63
69
  * platform-native separator. Single seam for native→POSIX conversion.
@@ -92,6 +98,20 @@ function toNativePath(p) {
92
98
  function posixNormalize(p) {
93
99
  return p.replace(/\\/g, '/');
94
100
  }
101
+ /**
102
+ * #3663 — comparison key for a path that must equal another path regardless
103
+ * of spelling: separator form (forward slashes via posixNormalize), relative
104
+ * segments and trailing separators (path.resolve + strip), and — ONLY on
105
+ * win32's case-insensitive filesystem — letter casing. POSIX stays
106
+ * case-sensitive: differently-cased paths are genuinely different
107
+ * directories there. This module owns the platform-conditional fold so the
108
+ * policy lives at the seam instead of accreting per-call-site copies (the
109
+ * class init.cts's normalizeForCompare/toComparableRaw predate).
110
+ */
111
+ function toComparablePathKey(p, platform = process.platform) {
112
+ const normalized = posixNormalize(node_path_1.default.resolve(p)).replace(/\/+$/g, '');
113
+ return platform === 'win32' ? normalized.toLowerCase() : normalized;
114
+ }
95
115
  /**
96
116
  * Return true when a managed hook command must be prefixed with PowerShell's
97
117
  * call operator so a quoted executable token is invokable by the target
@@ -215,6 +235,13 @@ const MANAGED_HOOK_BASENAMES_BY_SURFACE = {
215
235
  'gsd-read-injection-scanner.js',
216
236
  'gsd-update-banner.js',
217
237
  'gsd-workflow-guard.js',
238
+ // #3662: the three PreToolUse guards are GSD-managed JS hooks (registered
239
+ // only-if-absent by applySettingsJsonHooks) but sat outside this set, so
240
+ // the #2979 runner rewriter never reached them — the issue's reported
241
+ // mixed state lived on exactly these hooks.
242
+ 'gsd-write-guard.js',
243
+ 'gsd-agent-isolation-guard.js',
244
+ 'gsd-worktree-path-guard.js',
218
245
  ]),
219
246
  'codex-toml': new Set([
220
247
  'gsd-check-update.js',
@@ -234,6 +261,14 @@ const MANAGED_HOOK_COMMAND_BASENAMES_BY_SURFACE = {
234
261
  'gsd-session-state.sh',
235
262
  'gsd-validate-commit.sh',
236
263
  'gsd-phase-boundary.sh',
264
+ // #3662: same three guards as MANAGED_HOOK_BASENAMES_BY_SURFACE above —
265
+ // their absence here meant isManagedHookCommand never recognized them, so
266
+ // the settings.json→settings.local.json migration filter (and uninstall
267
+ // cleanup) skipped GSD's own guard entries. Folded-in fix, surfaced by the
268
+ // #3662 issue thread.
269
+ 'gsd-write-guard.js',
270
+ 'gsd-agent-isolation-guard.js',
271
+ 'gsd-worktree-path-guard.js',
237
272
  ]),
238
273
  'codex-toml': new Set([
239
274
  'gsd-check-update.js',
@@ -306,7 +341,7 @@ function isManagedHookCommand(commandText, opts = {}) {
306
341
  return false;
307
342
  }
308
343
  for (const basename of managedBasenames) {
309
- const escapedBasename = basename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
344
+ const escapedBasename = (0, pattern_cjs_1.escapeRegex)(basename);
310
345
  const pattern = new RegExp(`(^|[\\\\/\\s"'` + '`' + `])${escapedBasename}(?=$|[\\s"'` + '`' + `])`);
311
346
  if (pattern.test(normalizedCommand))
312
347
  return true;
@@ -327,9 +362,15 @@ const ANCHORED_HOOK_SCRIPT_TOKEN = /^"\$[A-Za-z_][A-Za-z0-9_]*"\//;
327
362
  * Non-Windows keeps the original script token shape when provided (single
328
363
  * quote / bareword / quoted), while Windows normalizes to double-quoted
329
364
  * forward-slash path tokens for stable cross-shell behavior.
365
+ *
366
+ * #3662: `runnerToken` is the runner the current install would emit — since
367
+ * #3662 that is the runtime-resolving chain token (see
368
+ * buildNodeRunnerChainToken), not a bare absolute path, so re-projected
369
+ * entries converge onto a runner that resolves in every environment sharing
370
+ * the config root.
330
371
  */
331
- function projectLegacySettingsHookCommand({ absoluteRunner, scriptPath, scriptToken, runtime = 'generic', platform = process.platform, }) {
332
- if (!absoluteRunner || !scriptPath)
372
+ function projectLegacySettingsHookCommand({ runnerToken, scriptPath, scriptToken, runtime = 'generic', platform = process.platform, }) {
373
+ if (!runnerToken || !scriptPath)
333
374
  return null;
334
375
  const normalizedScriptPath = platform === 'win32' ? posixNormalize(scriptPath) : scriptPath;
335
376
  // #1693: a script path already carrying a `"$CLAUDE_PROJECT_DIR"`-anchored
@@ -348,7 +389,7 @@ function projectLegacySettingsHookCommand({ absoluteRunner, scriptPath, scriptTo
348
389
  : JSON.stringify(normalizedScriptPath))
349
390
  : (scriptToken || JSON.stringify(normalizedScriptPath));
350
391
  return projectShellCommandText({
351
- runnerToken: absoluteRunner,
392
+ runnerToken,
352
393
  argTokens: [commandScriptToken],
353
394
  runtime,
354
395
  platform,
@@ -589,15 +630,287 @@ function execNpm(args, opts = {}) {
589
630
  });
590
631
  return _spawnResult(result, 'npm');
591
632
  }
633
+ /**
634
+ * Default PATHEXT when Windows does not supply one. Matches the value
635
+ * `gsd-tools.cjs`'s `resolveSpawnBinary` shipped in #3275; kept identical so the
636
+ * delegation is a behavior-preserving move rather than a redefinition.
637
+ */
638
+ const DEFAULT_PATHEXT = '.EXE;.CMD;.BAT;.COM';
639
+ /** Windows extensions that must be mediated through cmd.exe rather than spawned. */
640
+ const CMD_MEDIATED_EXT = /\.(cmd|bat)$/i;
641
+ function _isFile(candidate, requireExecutable = false, platform = process.platform) {
642
+ try {
643
+ if (!node_fs_1.default.statSync(candidate).isFile())
644
+ return false;
645
+ if (requireExecutable && platform !== 'win32')
646
+ node_fs_1.default.accessSync(candidate, node_fs_1.default.constants.X_OK);
647
+ return true;
648
+ }
649
+ catch {
650
+ // Missing, unreadable (EACCES), a broken link, or (when requireExecutable
651
+ // is set) not executable — all mean "not this one".
652
+ return false;
653
+ }
654
+ }
655
+ /**
656
+ * Read an environment variable by name, case-insensitively.
657
+ *
658
+ * Windows environment variable names are case-insensitive and conventionally
659
+ * cased `Path` / `ComSpec`, and `process.env` is a case-insensitive proxy that
660
+ * hides the difference. Spreading it (`{ ...process.env, ...opts.env }`, which
661
+ * `execTool` does whenever a caller supplies `opts.env`) produces a PLAIN object
662
+ * that keeps the OS's actual casing and loses the proxy — so an exact-case
663
+ * `env['PATH']` lookup returns undefined there and the PATH scan silently sees
664
+ * nothing. Caught by the Windows CI lane on #3617; the #3445 tests never hit it
665
+ * because they pass uppercase keys explicitly.
666
+ *
667
+ * Exact match wins when present, so a caller who sets the canonical name pays
668
+ * no scan.
669
+ *
670
+ * Exported so callers outside this seam can route an exact-case-sensitive env
671
+ * read through it instead of accessing a non-`process.env` object directly
672
+ * (see `local/no-exact-case-env-access`).
673
+ */
674
+ function envGet(env, name) {
675
+ const exact = env[name];
676
+ if (exact !== undefined)
677
+ return exact;
678
+ const lower = name.toLowerCase();
679
+ for (const key of Object.keys(env)) {
680
+ if (key.toLowerCase() === lower)
681
+ return env[key];
682
+ }
683
+ return undefined;
684
+ }
685
+ /** cmd.exe's own quoting rule inside a `/c` string: a literal quote is doubled. */
686
+ function _cmdQuoteToken(token) {
687
+ return `"${token.replace(/"/g, '""')}"`;
688
+ }
689
+ /**
690
+ * Build a single verbatim command-line string for `cmd.exe /c` where every
691
+ * token is force-quoted, then wrap the whole thing in one more outer pair.
692
+ * cmd.exe strips exactly one outer quote pair when the string begins with a
693
+ * quote and contains at least two — so the outer wrap disappears and what's
694
+ * left is a sequence of individually-quoted tokens. Force-quoting is the
695
+ * point: an unquoted `a&b` is split by cmd's own metacharacter parsing, but a
696
+ * quoted `"a&b"` is one literal argument.
697
+ */
698
+ function _buildVerbatimCmdLine(target, args) {
699
+ return `"${[target, ...args].map(_cmdQuoteToken).join(' ')}"`;
700
+ }
701
+ /**
702
+ * #3411: resolve a DECLARED command name to the file a spawn can actually start.
703
+ * The single canonical answer to "where is this binary?" for the whole tree.
704
+ *
705
+ * This is the seam `CONTEXT.md` declares as "All OS-facing I/O; single platform
706
+ * seam". Four divergent implementations of this logic existed (#3411): `execNpm`'s
707
+ * `shell:true`, `execTool`'s absence of any handling, `gsd-tools.cjs`'s private
708
+ * scan, and `fallow-runner.cts`'s own candidate array. #3275 folded two of them
709
+ * together in `bin/`; this lifts that resolver into the seam so `bin/` delegates
710
+ * instead of owning a copy.
711
+ *
712
+ * **win32** tries PATHEXT entries ONLY — never the bare name. npm global installs
713
+ * drop an EXTENSIONLESS POSIX sh shim (`...\npm\codex`) next to `codex.CMD`; a
714
+ * bare-name-first scan resolves to it, the cmd.exe mediation gate sees no `.cmd`,
715
+ * and the ENOENT returns unchanged (field-reported on Windows 11 — see #3275).
716
+ * A name that ALREADY carries a PATHEXT-listed extension is tried as-is first, so
717
+ * `foo.exe` resolves to `foo.exe` rather than being probed as `foo.exe.EXE`; a
718
+ * suffix that is not in PATHEXT (`foo.txt`) is not an extension and only feeds the
719
+ * append loop.
720
+ *
721
+ * **POSIX** answers EXISTENCE by scanning PATH for the bare name. `execTool` does
722
+ * NOT consult this on POSIX — the bare name goes to spawnSync unchanged and Node's
723
+ * own PATH search does the work, so macOS/Linux behavior is untouched (#3275
724
+ * acceptance contract).
725
+ *
726
+ * Path-like names (any `/` or `\`) bypass the PATH scan: the name is already an
727
+ * address, so it passes through when it names an existing file.
728
+ *
729
+ * **`opts.prependPaths`** — directories searched BEFORE `env.PATH`, in array
730
+ * order (e.g. a project-local `node_modules/.bin`). Defaults to `[]`, so
731
+ * Phase 1's callers (`execTool`, and `gsd-tools.cjs`'s `resolveSpawnBinary` /
732
+ * `deps.spawn` / `hasBinary`), which set neither new option, are byte-identical
733
+ * to today. The existing per-directory candidate logic (win32 as-is-then-append-
734
+ * PATHEXT; POSIX bare name) applies to prepended directories exactly as it does
735
+ * to `PATH` segments — there is no special-casing.
736
+ *
737
+ * **`opts.requireExecutable`** — when `true` and the platform is not `win32`,
738
+ * a candidate must additionally pass `fs.accessSync(candidate, fs.constants.X_OK)`
739
+ * to count as a match. On `win32` this is a no-op (mode bits do not mean
740
+ * execute on Windows — the same carve-out `fallow-runner`'s prior private
741
+ * resolver already had). Defaults to `false`, so `accessSync` is never called
742
+ * unless a caller opts in. It is opt-in rather than the default because making
743
+ * `X_OK` unconditional would break #3445's suite: those tests stage candidates
744
+ * with plain `fs.writeFileSync` and never set an exec bit (the repo bans
745
+ * `chmod` in tests), so every one of them would resolve to `null` on POSIX.
746
+ *
747
+ * **`opts.pathOverride`** — "search THIS PATH, but read everything else —
748
+ * PATHEXT included — from the ambient environment." When set (`!== undefined`),
749
+ * the PATH search segments come from splitting `pathOverride` on `path.delimiter`
750
+ * instead of from `env.PATH`; `pathOverride: ''` means an explicit EMPTY search
751
+ * path (zero segments), never a fallback to `env.PATH` — use `!== undefined`,
752
+ * not truthiness, to tell "caller supplied a PATH string" apart from "caller
753
+ * supplied nothing". `opts.prependPaths` still comes first. PATHEXT resolution
754
+ * is UNAFFECTED by this option — it still reads from `env` (which defaults to
755
+ * `process.env`) exactly as it does when `pathOverride` is omitted. This exists
756
+ * so a caller that already has its own search path in hand (e.g.
757
+ * `resolveFallowBinary`'s `envPath`) does not have to hand-thread PATHEXT
758
+ * alongside it — a private PATHEXT read is the very shape
759
+ * `local/no-private-binary-resolution` forbids outside this seam.
760
+ *
761
+ * @returns the resolved path, or `null` when nothing matched. Callers fall back to
762
+ * the declared name on `null` so a genuine ENOENT still surfaces (#3086).
763
+ */
764
+ function resolveExecutableBinary(name, opts = {}) {
765
+ if (!name)
766
+ return null;
767
+ const requireExecutable = opts.requireExecutable ?? false;
768
+ const platform = opts.platform ?? process.platform;
769
+ if (name.includes('/') || name.includes('\\')) {
770
+ return _isFile(name, requireExecutable, platform) ? name : null;
771
+ }
772
+ const env = opts.env ?? process.env;
773
+ const rawPath = opts.pathOverride !== undefined ? opts.pathOverride : (envGet(env, 'PATH') || '');
774
+ const pathSegments = String(rawPath).split(node_path_1.default.delimiter).filter(Boolean);
775
+ const segments = [...(opts.prependPaths ?? []), ...pathSegments];
776
+ if (platform !== 'win32') {
777
+ for (const dir of segments) {
778
+ const candidate = node_path_1.default.join(dir, name);
779
+ if (_isFile(candidate, requireExecutable, platform))
780
+ return candidate;
781
+ }
782
+ return null;
783
+ }
784
+ const exts = String(envGet(env, 'PATHEXT') || DEFAULT_PATHEXT).split(';').filter(Boolean);
785
+ // A name already ending in a PATHEXT-listed extension is an address, not a stem:
786
+ // probing `foo.exe` as `foo.exe.EXE` would miss the file sitting right there.
787
+ // Compared case-insensitively because PATHEXT casing is not guaranteed.
788
+ const lower = name.toLowerCase();
789
+ const carriesKnownExt = exts.some((ext) => lower.endsWith(ext.toLowerCase()));
790
+ for (const dir of segments) {
791
+ if (carriesKnownExt) {
792
+ const asIs = node_path_1.default.join(dir, name);
793
+ if (_isFile(asIs, requireExecutable, platform))
794
+ return asIs;
795
+ }
796
+ for (const ext of exts) {
797
+ const candidate = node_path_1.default.join(dir, name + ext);
798
+ if (_isFile(candidate, requireExecutable, platform))
799
+ return candidate;
800
+ }
801
+ }
802
+ return null;
803
+ }
804
+ /**
805
+ * #3411: project a declared `(command, args)` into the pair `spawnSync` can
806
+ * actually execute on this platform.
807
+ *
808
+ * Resolution alone does not make Windows work: `CreateProcess` cannot execute a
809
+ * `.cmd`/`.bat` at all, so the cmd.exe mediation is inseparable from the lookup.
810
+ * Exporting only the resolver would leave every caller to re-derive that half —
811
+ * which is precisely how #3411's four copies accumulated.
812
+ *
813
+ * cmd.exe is invoked as an ordinary program with an EXPLICIT argv array, never via
814
+ * `shell: true`. `shell:true` on Windows is the mechanism behind CVE-2024-27980
815
+ * (argument injection through `.bat`/`.cmd`), and Node 26 deprecates it with an
816
+ * args array (DEP0190) because arguments are concatenated rather than escaped.
817
+ *
818
+ * Mediation fires when the target — the resolved path, or the declared name when
819
+ * resolution found nothing — carries a `.cmd`/`.bat` extension. A BARE name that
820
+ * resolved to nothing is passed through verbatim so the spawn fails with ENOENT;
821
+ * mediating it would turn `{exitCode:127, 'foo: not found'}` into cmd.exe's exit
822
+ * 9009 and silently change the not-found contract callers depend on.
823
+ *
824
+ * POSIX is a strict no-op: the declared command is returned unchanged and the
825
+ * environment is never consulted.
826
+ *
827
+ * The mediated command line is built VERBATIM rather than left to libuv: libuv's
828
+ * `quote_cmd_arg` only force-quotes an argument that contains a space, tab, or
829
+ * quote — it does not know about cmd.exe metacharacters (`&`, `|`, `>`, `<`,
830
+ * `^`, ...) at all, so an argument like `a&calc` reaches cmd.exe unquoted and
831
+ * gets re-parsed as two commands (the CVE-2024-27980 argument-injection class).
832
+ * Node's own CVE-2024-27980 escaping does not help here because it only fires
833
+ * when the spawned FILE itself is a `.bat`/`.cmd` — in this seam the spawned
834
+ * file is `cmd.exe`, not the target. Building the line ourselves and passing
835
+ * `windowsVerbatimArguments: true` (the shape Rust's std uses for the sibling
836
+ * CVE-2024-24576) means every token is force-quoted inside one outer pair, so
837
+ * a metacharacter inside a quoted token can never split the command line.
838
+ *
839
+ * KNOWN LIMIT: `%VAR%` still expands inside a cmd `/c` string, and there is no
840
+ * escape for `%` outside a batch file — an argument containing `%FOO%` is
841
+ * substituted with the environment value regardless of quoting. That is an
842
+ * information-disclosure limit, not arbitrary execution, and it's the same
843
+ * limit Rust's std documents for its own `CommandExt::raw_arg` escape hatch.
844
+ *
845
+ * CALLER CHOICE: this function's return value carries two independently
846
+ * adoptable pieces of information, and a caller may take either, both, or
847
+ * neither. `windowsVerbatimArguments: true` marks the cases where mediation
848
+ * was REQUIRED — the caller MUST adopt `command`+`args` together, since a
849
+ * `.cmd`/`.bat` genuinely cannot be spawned any other way. A merely-resolved
850
+ * `.exe` path (no mediation flag set) is only an OFFER: a caller may decline
851
+ * it and keep spawning the declared name instead, to hold its own observable
852
+ * contract stable. `execTool` (this file, below) is exactly such a caller —
853
+ * it adopts the mediated pair when `windowsVerbatimArguments` is set, but
854
+ * otherwise passes the declared `program`/`args` through untouched.
855
+ */
856
+ function projectSpawnInvocation(command, args = [], opts = {}) {
857
+ const platform = opts.platform ?? process.platform;
858
+ if (platform !== 'win32')
859
+ return { command, args, resolved: null };
860
+ const env = opts.env ?? process.env;
861
+ const resolved = resolveExecutableBinary(command, { platform, env });
862
+ // Mediate against the resolved path when we have one, else against the declared
863
+ // name. An unresolved name is mediated ONLY when it already declares .cmd/.bat:
864
+ // PATH-only resolution misses a batch file sitting in the current directory,
865
+ // which `cmd.exe /c` still finds — the behavior gsd-tools.cjs shipped before
866
+ // this consolidation, preserved here rather than silently narrowed.
867
+ const target = resolved ?? command;
868
+ if (!CMD_MEDIATED_EXT.test(node_path_1.default.basename(target))) {
869
+ // A BARE name that resolved to nothing is passed through untouched so the
870
+ // spawn fails with ENOENT. Mediating it would turn {exitCode:127,
871
+ // '<name>: not found'} into cmd.exe's exit 9009 and silently change the
872
+ // not-found contract `_spawnResult` and its 53 dependent files rely on.
873
+ return resolved ? { command: resolved, args, resolved } : { command, args, resolved: null };
874
+ }
875
+ // A CR/LF cannot be represented inside a Windows command line at all — cmd.exe
876
+ // treats it as a line terminator, so mediating it would silently truncate the
877
+ // argument rather than pass it through. Fail visibly instead: fall back to the
878
+ // unmediated shape so the spawn either fails with ENOENT (bare unresolved name)
879
+ // or hands the raw string to CreateProcess, whichever the caller was already
880
+ // prepared to see for a non-.cmd/.bat target.
881
+ if (/[\r\n]/.test(target) || args.some((a) => /[\r\n]/.test(a))) {
882
+ return resolved ? { command: resolved, args, resolved } : { command, args, resolved: null };
883
+ }
884
+ return {
885
+ command: String(envGet(env, 'ComSpec') || 'cmd.exe'),
886
+ args: ['/d', '/s', '/c', _buildVerbatimCmdLine(target, args)],
887
+ resolved,
888
+ windowsVerbatimArguments: true,
889
+ };
890
+ }
592
891
  function execTool(program, args, opts = {}) {
593
- const result = node_child_process_1.default.spawnSync(program, args, {
892
+ // #3411: Windows cannot spawn a .cmd/.bat at all — CreateProcess refuses it —
893
+ // so those are mediated through cmd.exe. Everything else keeps the DECLARED
894
+ // program name: libuv's CreateProcess path already performs PATH + PATHEXT
895
+ // search, so resolving a .exe here would buy nothing and would change what
896
+ // this seam's 167 dependents observe being spawned. `tests/graphify.test.cjs`
897
+ // pins that contract by spying on spawnSync's first argument. POSIX never
898
+ // reaches the mediation branch at all.
899
+ const spawnEnv = opts.env ? { ...process.env, ...opts.env } : undefined;
900
+ const invocation = projectSpawnInvocation(program, args, { env: spawnEnv ?? process.env });
901
+ const mediated = invocation.windowsVerbatimArguments === true;
902
+ const result = node_child_process_1.default.spawnSync(mediated ? invocation.command : program, mediated ? invocation.args : args, {
594
903
  cwd: opts.cwd,
595
- env: opts.env ? { ...process.env, ...opts.env } : undefined,
904
+ env: spawnEnv,
596
905
  encoding: 'utf-8',
597
906
  stdio: 'pipe',
598
907
  timeout: opts.timeout ?? 30_000,
599
908
  windowsHide: true,
909
+ ...(mediated ? { windowsVerbatimArguments: true } : {}),
600
910
  });
911
+ // Stamp the DECLARED name, never the resolved path: `_spawnResult` renders
912
+ // `${program}: not found`, and callers across 53 files match on the string they
913
+ // passed. Resolution must not leak an absolute path into that message.
601
914
  return _spawnResult(result, program);
602
915
  }
603
916
  /**
@@ -704,6 +1017,7 @@ function probeTty(opts = {}) {
704
1017
  const ttyPath = node_child_process_1.default.execFileSync('tty', [], {
705
1018
  encoding: 'utf-8',
706
1019
  stdio: ['inherit', 'pipe', 'ignore'],
1020
+ timeout: 5_000,
707
1021
  }).trim();
708
1022
  if (!ttyPath || ttyPath === 'not a tty')
709
1023
  return null;
@@ -750,7 +1064,11 @@ function _normalizeMd(content) {
750
1064
  if (i === 0 || !insideFence[i - 1])
751
1065
  result.push('');
752
1066
  }
753
- if (/^(\s*[-*+]\s|\s*\d+\.\s)/.test(line) && i > 0 && prevTrimmed !== '' && !/^(\s*[-*+]\s|\s*\d+\.\s)/.test(prev) && prevTrimmed !== '---')
1067
+ // #3854: the `!/^\s/.test(prev)` guard mirrors the after-a-bullet rule below —
1068
+ // an indented non-bullet line is a CONTINUATION of the previous list item, not a
1069
+ // preceding paragraph, so no separating blank may be injected before this bullet
1070
+ // (that injection converted every tight multi-line list to a loose one on write).
1071
+ if (/^(\s*[-*+]\s|\s*\d+\.\s)/.test(line) && i > 0 && prevTrimmed !== '' && !/^(\s*[-*+]\s|\s*\d+\.\s)/.test(prev) && !/^\s/.test(prev) && prevTrimmed !== '---')
754
1072
  result.push('');
755
1073
  result.push(line);
756
1074
  if (/^#{1,6}\s/.test(trimmed) && i < lines.length - 1 && (lines[i + 1] ?? '').trimEnd() !== '')
@@ -780,6 +1098,23 @@ function normalizeContent(filePath, content, opts = {}) {
780
1098
  }
781
1099
  return { content: normalized, encoding };
782
1100
  }
1101
+ /**
1102
+ * True iff persisting `after` via `platformWriteSync` would land different
1103
+ * on-disk bytes than `before` already has (or would have, normalized the
1104
+ * same way). `platformWriteSync` runs Markdown normalization (CRLF strip,
1105
+ * blank-line-run collapse, single trailing newline) before writing, so a
1106
+ * caller comparing raw pre-normalize strings (`after !== before`) can report
1107
+ * `true` even when the persisted bytes are byte-identical — e.g. a
1108
+ * transform that regenerates a section fresh on every call, including a
1109
+ * genuine no-op re-run, in a different-but-equivalent raw shape than the
1110
+ * already-normalized on-disk original (#3685 / #3691). Any "did this write
1111
+ * change the file?" flag MUST go through this seam (or an equivalent
1112
+ * post-write re-read of the actual on-disk bytes) instead of a raw `!==` —
1113
+ * do not simplify this back to a direct string comparison.
1114
+ */
1115
+ function contentChangedAfterNormalize(filePath, before, after) {
1116
+ return normalizeContent(filePath, after).content !== normalizeContent(filePath, before).content;
1117
+ }
783
1118
  // Rename errnos that are transient on Windows: a concurrent reader (or an AV
784
1119
  // scanner / indexer) holding the target open makes renameSync fail briefly.
785
1120
  // Same idiom as capability-ledger.cts / capability-consent.cts.