@opengsd/gsd-core 1.10.0 → 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 (328) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-debug-session-manager.md +11 -0
  4. package/agents/gsd-doc-synthesizer.md +2 -4
  5. package/agents/gsd-executor.md +5 -5
  6. package/agents/gsd-mempalace-curator.md +5 -2
  7. package/agents/gsd-phase-researcher.md +20 -1
  8. package/agents/gsd-plan-checker.md +37 -0
  9. package/agents/gsd-planner.md +44 -46
  10. package/agents/gsd-user-profiler.md +3 -0
  11. package/agents/gsd-verifier.md +12 -3
  12. package/bin/install.js +841 -971
  13. package/bin/lib/ui-safety-gate.cjs +2 -0
  14. package/commands/gsd/code-review.md +1 -1
  15. package/commands/gsd/execute-phase.md +1 -1
  16. package/commands/gsd/map-codebase.md +1 -1
  17. package/commands/gsd/mempalace-capture.md +1 -1
  18. package/commands/gsd/mempalace-recall.md +1 -1
  19. package/commands/gsd/new-milestone.md +1 -1
  20. package/commands/gsd/quick.md +1 -1
  21. package/commands/gsd/review-backlog.md +2 -1
  22. package/commands/gsd/verify-work.md +1 -1
  23. package/gsd-core/bin/gsd-tools.cjs +469 -88
  24. package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
  25. package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
  26. package/gsd-core/bin/lib/api-coverage.cjs +3 -5
  27. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  28. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  29. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  30. package/gsd-core/bin/lib/audit.cjs +876 -240
  31. package/gsd-core/bin/lib/broken-windows.cjs +1 -1
  32. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  33. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  34. package/gsd-core/bin/lib/capability-registry.cjs +575 -101
  35. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  36. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  37. package/gsd-core/bin/lib/capability-validator.cjs +495 -22
  38. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  39. package/gsd-core/bin/lib/check-command-router.cjs +71 -37
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  41. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  42. package/gsd-core/bin/lib/command-aliases.cjs +22 -0
  43. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  44. package/gsd-core/bin/lib/commands.cjs +651 -86
  45. package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  47. package/gsd-core/bin/lib/config-loader.cjs +75 -0
  48. package/gsd-core/bin/lib/config.cjs +10 -1
  49. package/gsd-core/bin/lib/core-utils.cjs +127 -29
  50. package/gsd-core/bin/lib/decisions.cjs +23 -0
  51. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  52. package/gsd-core/bin/lib/frontmatter.cjs +155 -20
  53. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  54. package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
  55. package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
  56. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  57. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  58. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  59. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  60. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  61. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  62. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  63. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  64. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  65. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  66. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  67. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  68. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  69. package/gsd-core/bin/lib/init.cjs +321 -129
  70. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  71. package/gsd-core/bin/lib/install-engine.cjs +745 -258
  72. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  73. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  74. package/gsd-core/bin/lib/install-profiles.cjs +134 -57
  75. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  76. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  77. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  78. package/gsd-core/bin/lib/installer-migrations.cjs +138 -31
  79. package/gsd-core/bin/lib/io.cjs +10 -0
  80. package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
  81. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  82. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  83. package/gsd-core/bin/lib/milestone.cjs +754 -70
  84. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  85. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  86. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  87. package/gsd-core/bin/lib/pattern.cjs +122 -0
  88. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  89. package/gsd-core/bin/lib/phase-id.cjs +444 -36
  90. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  91. package/gsd-core/bin/lib/phase-locator.cjs +125 -18
  92. package/gsd-core/bin/lib/phase.cjs +646 -143
  93. package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
  94. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  95. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  96. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  97. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  98. package/gsd-core/bin/lib/planning-workspace.cjs +56 -6
  99. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  100. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  101. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  102. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
  103. package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
  104. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  105. package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
  106. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  107. package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
  108. package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
  109. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  110. package/gsd-core/bin/lib/roadmap.cjs +385 -94
  111. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
  112. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  113. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
  114. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  115. package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
  116. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
  117. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  118. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  119. package/gsd-core/bin/lib/security.cjs +104 -5
  120. package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
  121. package/gsd-core/bin/lib/smart-entry.cjs +142 -22
  122. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  123. package/gsd-core/bin/lib/state-document.cjs +152 -8
  124. package/gsd-core/bin/lib/state-transition.cjs +371 -117
  125. package/gsd-core/bin/lib/state.cjs +1794 -357
  126. package/gsd-core/bin/lib/surface.cjs +23 -9
  127. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  128. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  129. package/gsd-core/bin/lib/uat-predicate.cjs +9 -3
  130. package/gsd-core/bin/lib/uat.cjs +399 -56
  131. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  132. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  133. package/gsd-core/bin/lib/unusable-input.cjs +24 -0
  134. package/gsd-core/bin/lib/update-context.cjs +8 -2
  135. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  136. package/gsd-core/bin/lib/validate.cjs +20 -6
  137. package/gsd-core/bin/lib/vendor/README.md +37 -0
  138. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  139. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  140. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  141. package/gsd-core/bin/lib/verification.cjs +258 -8
  142. package/gsd-core/bin/lib/verify.cjs +368 -888
  143. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
  144. package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
  145. package/gsd-core/bin/lib/workstream.cjs +2 -2
  146. package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
  147. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  148. package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
  149. package/gsd-core/references/agent-contracts.md +43 -26
  150. package/gsd-core/references/checkpoints.md +2 -2
  151. package/gsd-core/references/context-budget.md +1 -1
  152. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  153. package/gsd-core/references/doc-conflict-engine.md +1 -1
  154. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  155. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  156. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  157. package/gsd-core/references/execute-phase-response-language.md +1 -1
  158. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  159. package/gsd-core/references/gate-prompts.md +1 -1
  160. package/gsd-core/references/git-planning-commit.md +2 -1
  161. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  162. package/gsd-core/references/model-profiles.md +12 -4
  163. package/gsd-core/references/mvp-concepts.md +9 -9
  164. package/gsd-core/references/planner-guidance.md +3 -9
  165. package/gsd-core/references/planner-preconditions.md +1 -1
  166. package/gsd-core/references/planner-reviews.md +1 -1
  167. package/gsd-core/references/planning-config.md +8 -6
  168. package/gsd-core/references/revision-loop.md +1 -1
  169. package/gsd-core/references/specless-probe-fallback.md +1 -1
  170. package/gsd-core/references/universal-anti-patterns.md +3 -3
  171. package/gsd-core/references/verifier-phase-gates.md +192 -0
  172. package/gsd-core/references/verify-mvp-mode.md +1 -1
  173. package/gsd-core/references/workstream-flag.md +22 -6
  174. package/gsd-core/templates/discussion-log.md +1 -1
  175. package/gsd-core/templates/phase-prompt.md +2 -4
  176. package/gsd-core/templates/state.md +4 -4
  177. package/gsd-core/templates/verification-report.md +9 -1
  178. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  179. package/gsd-core/workflows/autonomous.md +1 -1
  180. package/gsd-core/workflows/cleanup.md +62 -3
  181. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
  182. package/gsd-core/workflows/code-review-fix.md +37 -10
  183. package/gsd-core/workflows/code-review.md +38 -12
  184. package/gsd-core/workflows/complete-milestone.md +141 -18
  185. package/gsd-core/workflows/debug.md +7 -5
  186. package/gsd-core/workflows/diagnose-issues.md +35 -9
  187. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  188. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  189. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -1
  190. package/gsd-core/workflows/edit-phase.md +26 -1
  191. package/gsd-core/workflows/eval-review.md +3 -5
  192. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +31 -6
  193. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  194. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +2 -0
  195. package/gsd-core/workflows/execute-phase.md +38 -50
  196. package/gsd-core/workflows/execute-plan.md +36 -4
  197. package/gsd-core/workflows/explore.md +131 -4
  198. package/gsd-core/workflows/fast.md +10 -2
  199. package/gsd-core/workflows/health.md +73 -4
  200. package/gsd-core/workflows/import.md +4 -4
  201. package/gsd-core/workflows/ingest-docs.md +5 -5
  202. package/gsd-core/workflows/mvp-phase.md +6 -3
  203. package/gsd-core/workflows/new-milestone.md +14 -9
  204. package/gsd-core/workflows/new-project.md +14 -14
  205. package/gsd-core/workflows/next.md +12 -0
  206. package/gsd-core/workflows/plan-phase.md +41 -17
  207. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  208. package/gsd-core/workflows/progress.md +34 -6
  209. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
  210. package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
  211. package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
  212. package/gsd-core/workflows/quick.md +35 -15
  213. package/gsd-core/workflows/review.md +26 -5
  214. package/gsd-core/workflows/secure-phase.md +1 -1
  215. package/gsd-core/workflows/session-report.md +2 -1
  216. package/gsd-core/workflows/settings.md +66 -2
  217. package/gsd-core/workflows/ship.md +104 -44
  218. package/gsd-core/workflows/spec-phase.md +30 -12
  219. package/gsd-core/workflows/sync-skills.md +63 -8
  220. package/gsd-core/workflows/transition.md +46 -11
  221. package/gsd-core/workflows/ui-phase.md +5 -5
  222. package/gsd-core/workflows/ui-review.md +2 -2
  223. package/gsd-core/workflows/update.md +1 -1
  224. package/gsd-core/workflows/validate-phase.md +1 -1
  225. package/gsd-core/workflows/verify-work.md +9 -7
  226. package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
  227. package/hooks/dist/gsd-check-update-worker.js +56 -13
  228. package/hooks/dist/gsd-check-update.js +19 -1
  229. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  230. package/hooks/dist/gsd-cursor-subagent-start.js +77 -2
  231. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  232. package/hooks/dist/gsd-prompt-guard.js +21 -20
  233. package/hooks/dist/gsd-read-injection-scanner.js +38 -24
  234. package/hooks/dist/gsd-statusline.js +18 -0
  235. package/hooks/dist/gsd-update-banner.js +22 -1
  236. package/hooks/dist/gsd-workflow-guard.js +134 -36
  237. package/hooks/dist/lib/git-cmd.js +92 -59
  238. package/hooks/dist/lib/injection-patterns.js +45 -0
  239. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  240. package/hooks/dist/lib/isolation-sentinel.js +9 -0
  241. package/hooks/gsd-agent-isolation-guard.js +103 -14
  242. package/hooks/gsd-check-update-worker.js +56 -13
  243. package/hooks/gsd-check-update.js +19 -1
  244. package/hooks/gsd-cursor-pre-tool.js +0 -3
  245. package/hooks/gsd-cursor-subagent-start.js +77 -2
  246. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  247. package/hooks/gsd-prompt-guard.js +21 -20
  248. package/hooks/gsd-read-injection-scanner.js +38 -24
  249. package/hooks/gsd-statusline.js +18 -0
  250. package/hooks/gsd-update-banner.js +22 -1
  251. package/hooks/gsd-workflow-guard.js +134 -36
  252. package/hooks/lib/git-cmd.js +92 -59
  253. package/hooks/lib/injection-patterns.js +45 -0
  254. package/hooks/lib/isolation-deny-reason.js +39 -0
  255. package/hooks/lib/isolation-sentinel.js +9 -0
  256. package/package.json +21 -9
  257. package/pi/gsd.cjs +19 -5
  258. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  259. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  260. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  261. package/scripts/changeset/lint.cjs +60 -5
  262. package/scripts/check-alias-drift.cjs +7 -43
  263. package/scripts/check-contract-drift.cjs +297 -0
  264. package/scripts/ci-test-scope.cjs +19 -2
  265. package/scripts/command-contract-helpers.cjs +903 -1
  266. package/scripts/gen-adr-index.cjs +728 -38
  267. package/scripts/gen-capability-registry.cjs +3 -15
  268. package/scripts/gen-context-index.cjs +2 -11
  269. package/scripts/gen-health-docs.cjs +390 -0
  270. package/scripts/gen-inventory-manifest.cjs +50 -4
  271. package/scripts/gen-loop-host-contract.cjs +4 -24
  272. package/scripts/gen-registry.cjs +3 -14
  273. package/scripts/lib/alias-drift-families.cjs +46 -0
  274. package/scripts/lib/drift-scan.cjs +278 -0
  275. package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
  276. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  277. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  278. package/scripts/lint-canary-version-leak.cjs +73 -0
  279. package/scripts/lint-command-contract.cjs +96 -13
  280. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  281. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  282. package/scripts/lint-default-flip-documentation.cjs +193 -0
  283. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  284. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  285. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  286. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  287. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  288. package/scripts/lint-milestone-window-drift.cjs +468 -0
  289. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  290. package/scripts/lint-plan-count-drift.cjs +318 -0
  291. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  292. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  293. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  294. package/scripts/lint-regression-test-names.cjs +15 -13
  295. package/scripts/lint-removed-but-needed.cjs +320 -0
  296. package/scripts/lint-state-field-drift.cjs +805 -0
  297. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  298. package/scripts/lint-test-file-count.allowlist.json +21 -10
  299. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  300. package/scripts/lint-vendored-deps.cjs +124 -0
  301. package/scripts/pr-changed-files.cjs +63 -0
  302. package/scripts/pr-template-policy.cjs +14 -4
  303. package/scripts/prompt-injection-scan.sh +25 -0
  304. package/scripts/require-issue-link-policy.cjs +192 -0
  305. package/scripts/state-write-path-drift-baseline.json +19 -0
  306. package/scripts/sync-runtime-launcher.cjs +2 -4
  307. package/skills/gsd-autonomous/SKILL.md +0 -1
  308. package/skills/gsd-code-review/SKILL.md +1 -1
  309. package/skills/gsd-execute-phase/SKILL.md +1 -2
  310. package/skills/gsd-map-codebase/SKILL.md +1 -1
  311. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  312. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  313. package/skills/gsd-new-milestone/SKILL.md +1 -1
  314. package/skills/gsd-next/SKILL.md +0 -1
  315. package/skills/gsd-plan-phase/SKILL.md +0 -1
  316. package/skills/gsd-progress/SKILL.md +0 -1
  317. package/skills/gsd-quick/SKILL.md +1 -1
  318. package/skills/gsd-review-backlog/SKILL.md +2 -1
  319. package/skills/gsd-stats/SKILL.md +0 -1
  320. package/skills/gsd-verify-work/SKILL.md +1 -1
  321. package/vscode/package.json +1 -1
  322. package/gsd-core/workflows/discovery-phase.md +0 -298
  323. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  324. package/gsd-core/workflows/verify-phase.md +0 -574
  325. package/scripts/affected-tests-lib.cjs +0 -554
  326. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  327. package/scripts/run-affected-tests.cjs +0 -7
  328. package/scripts/run-tests.cjs +0 -1051
@@ -290,6 +290,56 @@ function _resetRuntimeWarningCacheForTests() {
290
290
  _warnedConfigKeys.clear();
291
291
  _warnedUnknownConfigKeys.clear();
292
292
  _warnedUnusableConfig.clear();
293
+ _warnedShadowedGlobalKeys.clear();
294
+ }
295
+ // ─── #3532 (10b): shadowed global-defaults diagnostic ────────────────────────
296
+ // The keys Branch D's `_globalBaseCfg` demonstrably honors from
297
+ // ~/.gsd/defaults.json when no project config exists. Under a project
298
+ // .planning/config.json (Branch A — every real project) the global file is
299
+ // never opened, so each of these set globally is silently inert for resolution.
300
+ // `effort` is in Branch D's honored set but is EXCLUDED from the shadow warning:
301
+ // the install-time effort sync (readGsdEffectiveEffortConfig) DOES merge the
302
+ // global file, so warning on it would be false for the channel users actually
303
+ // control via `effort sync`. Keep this list in lockstep with `_globalBaseCfg`
304
+ // below — the per-key canary in tests/config-loader.test.cjs fails first on
305
+ // drift in either direction.
306
+ const GLOBAL_DEFAULTS_RESOLUTION_KEYS = [
307
+ 'model_profile', 'commit_docs', 'research', 'plan_checker', 'verifier',
308
+ 'nyquist_validation', 'post_planning_gaps', 'parallelization', 'text_mode',
309
+ 'resolve_model_ids', 'context_window', 'subagent_timeout', 'model_overrides',
310
+ 'models', 'granularity', 'granularities', 'planning', 'dynamic_routing',
311
+ 'effort', 'fast_mode', 'agent_skills', 'response_language', 'runtime',
312
+ 'model_profile_overrides', 'model_policy',
313
+ ];
314
+ // Module-level dedup keyed on the SORTED shadowed-key set: a later call with
315
+ // the same shadowed set stays quiet, while a config that grows a new shadowed
316
+ // key re-arms the warning. Stronger than _warnedUnknownConfigKeys (which keys
317
+ // on insertion order) — same discipline, order-independent key.
318
+ const _warnedShadowedGlobalKeys = new Set();
319
+ function _warnShadowedGlobalDefaults(globalDefaults, globalPath) {
320
+ const shadowed = GLOBAL_DEFAULTS_RESOLUTION_KEYS.filter(k => k !== 'effort' && Object.prototype.hasOwnProperty.call(globalDefaults, k));
321
+ // Branch D also honors the nested alias workflow.post_planning_gaps (the
322
+ // `?? globalDefaults['workflow']?.['post_planning_gaps']` fallback in
323
+ // _globalBaseCfg) — a global file using only the nested form is equally
324
+ // shadowed, so it reports under its dotted name.
325
+ if (!shadowed.includes('post_planning_gaps')) {
326
+ const wf = globalDefaults['workflow'];
327
+ if (wf && typeof wf === 'object' && !Array.isArray(wf) &&
328
+ Object.prototype.hasOwnProperty.call(wf, 'post_planning_gaps')) {
329
+ shadowed.push('workflow.post_planning_gaps');
330
+ }
331
+ }
332
+ if (shadowed.length === 0)
333
+ return;
334
+ const dedupKey = shadowed.slice().sort().join(',');
335
+ if (_warnedShadowedGlobalKeys.has(dedupKey))
336
+ return;
337
+ _warnedShadowedGlobalKeys.add(dedupKey);
338
+ try {
339
+ process.stderr.write(`gsd-tools: warning: ${globalPath} sets ${shadowed.join(', ')} but a project config ` +
340
+ `takes precedence here — those global keys are ignored for model resolution. (#3532)\n`);
341
+ }
342
+ catch { /* stderr might be closed in some test harnesses */ }
293
343
  }
294
344
  // ─── FIX 2: Federated overlay helpers ────────────────────────────────────────
295
345
  /**
@@ -751,6 +801,13 @@ function loadConfigResolved(cwd, options = {}) {
751
801
  fast_mode: (parsed['fast_mode']) || null,
752
802
  agent_skills: (parsed['agent_skills']) || {},
753
803
  agent_skills_security: (parsed['agent_skills_security']) || null,
804
+ // #3587: phase_commit_docs.<phase-id> — a dynamic-key family shaped like
805
+ // agent_skills above (`{ "<phase-id>": boolean }`). Must be threaded here
806
+ // explicitly: `_baseConfig` is a hand-maintained allowlist, so a key that
807
+ // is only in config-schema.manifest.json's dynamicKeyPatterns (and not
808
+ // projected here) is silently dropped on read — the exact `features`-key
809
+ // failure mode this module's own A3 test guards against.
810
+ phase_commit_docs: (parsed['phase_commit_docs']) || {},
754
811
  manager: (parsed['manager']) || {},
755
812
  response_language: get('response_language') || null,
756
813
  claude_md_path: get('claude_md_path') || null,
@@ -777,6 +834,22 @@ function loadConfigResolved(cwd, options = {}) {
777
834
  // A1 vs A2: disambiguate by whether a real workstream was requested.
778
835
  // Fix 4: empty-string ws ('') resolves the root path → source:'root'.
779
836
  const source = wsRequested ? 'workstream' : 'root';
837
+ // #3532 (10b): a parsed project config means Branch D never runs, so every
838
+ // key ~/.gsd/defaults.json sets that Branch D would honor is silently inert
839
+ // here. Observation only — one deduped stderr warning; precedence is
840
+ // untouched. Faults in the global file stay silent in this branch (the
841
+ // project config governs; the nearer file is the actionable one).
842
+ try {
843
+ const shadowHome = process.env['GSD_HOME'] || node_os_1.default.homedir();
844
+ const shadowPath = node_path_1.default.join(shadowHome, '.gsd', 'defaults.json');
845
+ const shadowRead = _readConfigFile(shadowPath);
846
+ if (shadowRead.kind === 'ok') {
847
+ _warnShadowedGlobalDefaults(shadowRead.data, shadowPath);
848
+ }
849
+ }
850
+ catch {
851
+ // Observation only — never let the diagnostic perturb resolution.
852
+ }
780
853
  // This config parsed — but a DIFFERENT file on the resolution path may not
781
854
  // have. A workstream config that loads cleanly while the root config it
782
855
  // inherits from is corrupt is still a degraded resolution: the root's
@@ -908,6 +981,8 @@ module.exports = {
908
981
  _getNestedConfigDefault,
909
982
  _deepMergeConfig,
910
983
  _warnedUnknownConfigKeys,
984
+ _warnedShadowedGlobalKeys,
985
+ GLOBAL_DEFAULTS_RESOLUTION_KEYS,
911
986
  _warnUnknownProfileOverrides,
912
987
  _resetRuntimeWarningCacheForTests,
913
988
  _warnedConfigKeys,
@@ -68,6 +68,9 @@ const SCHEMA_DEFAULTS = {
68
68
  'planner.stall_detect_interval_minutes': 5,
69
69
  'planner.stall_threshold_minutes': 10,
70
70
  'git.create_tag': true,
71
+ // #1689: per-plan agent_hint executor routing — default-on. A no-op for plans
72
+ // without an agent_hint field, so existing dispatch is byte-identical.
73
+ 'workflow.agent_hint_routing': true,
71
74
  // Derived from the defaults manifest rather than restated, so the manifest
72
75
  // stays the single source of truth for the smart-zone budget (#2630).
73
76
  'workflow.smart_zone_tokens': CONFIG_DEFAULTS.smart_zone_tokens,
@@ -652,7 +655,7 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
652
655
  const val = value;
653
656
  validateKnownConfigKeyPath(kp);
654
657
  if (!isValidConfigKey(kp, cwd)) {
655
- error(`Unknown config key: "${kp}". Valid keys: ${[...VALID_CONFIG_KEYS].sort().join(', ')}, agent_skills.<agent-type>, features.<feature_name>`, ERROR_REASON.CONFIG_INVALID_KEY);
658
+ error(`Unknown config key: "${kp}". Valid keys: ${[...VALID_CONFIG_KEYS].sort().join(', ')}, agent_skills.<agent-type>, features.<feature_name>, phase_commit_docs.<phase-id>`, ERROR_REASON.CONFIG_INVALID_KEY);
656
659
  }
657
660
  // Parse value (handle booleans, numbers, and JSON arrays/objects)
658
661
  let parsedValue = val;
@@ -736,6 +739,12 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
736
739
  error(`Invalid workflow.post_planning_gaps '${val}'. Must be a boolean (true or false).`);
737
740
  }
738
741
  }
742
+ // Per-plan executor routing via agent_hint frontmatter (#1689)
743
+ if (kp === 'workflow.agent_hint_routing') {
744
+ if (typeof parsedValue !== 'boolean') {
745
+ error(`Invalid workflow.agent_hint_routing '${val}'. Must be a boolean (true or false).`);
746
+ }
747
+ }
739
748
  // #3086 — git.create_tag: boolean only
740
749
  if (kp === 'git.create_tag') {
741
750
  if (typeof parsedValue !== 'boolean') {
@@ -23,7 +23,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
23
23
  const node_path_1 = __importDefault(require("node:path"));
24
24
  // eslint-disable-next-line @typescript-eslint/no-require-imports
25
25
  const phaseIdModule = require("./phase-id.cjs");
26
- const { comparePhaseNum } = phaseIdModule;
26
+ const { comparePhaseNum, scopeToPhase } = phaseIdModule;
27
27
  // eslint-disable-next-line @typescript-eslint/no-require-imports
28
28
  const planningWorkspace = require("./planning-workspace.cjs");
29
29
  const { findContextMdIn } = planningWorkspace;
@@ -72,17 +72,30 @@ function extractOneLinerFromBody(content) {
72
72
  if (!content)
73
73
  return null;
74
74
  const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
75
- const body = normalized.replace(/^---\n[\s\S]*?\n---\n*/, '');
76
- const match = body.match(/^#[^\n]*\n+\*\*([^*\n]+)\*\*([^\n]*)/m);
77
- if (!match)
78
- return null;
79
- const boldInner = match[1].trim();
80
- const afterBold = match[2];
81
- if (/:\s*$/.test(boldInner)) {
82
- const prose = afterBold.trim();
83
- return prose.length > 0 ? prose : null;
75
+ const body = normalized.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n*/, '');
76
+ // #3170: anchor to a summary-shaped heading (Summary / Overview /
77
+ // Accomplishments) so an incidental first heading (a rule list, task
78
+ // breakdown, deviation note) does not contribute its first bold run as the
79
+ // deliverable one-liner. Iterate headings in document order and extract from
80
+ // the first summary-shaped one that has a bold run; fall back to null (not the
81
+ // wrong text) when no such heading exists.
82
+ const headingRe = /^#+\s*([^\n]*)\n+\*\*([^*\n]+)\*\*([^\n]*)/gm;
83
+ let match;
84
+ while ((match = headingRe.exec(body)) !== null) {
85
+ if (!/summary|overview|accomplish/i.test(match[1]))
86
+ continue;
87
+ const boldInner = match[2].trim();
88
+ const afterBold = match[3];
89
+ if (/:\s*$/.test(boldInner)) {
90
+ const prose = afterBold.trim();
91
+ if (prose.length > 0)
92
+ return prose;
93
+ }
94
+ else if (boldInner.length > 0) {
95
+ return boldInner;
96
+ }
84
97
  }
85
- return boldInner.length > 0 ? boldInner : null;
98
+ return null;
86
99
  }
87
100
  // ─── Misc utilities ───────────────────────────────────────────────────────────
88
101
  function pathExistsInternal(cwd, targetPath) {
@@ -151,27 +164,66 @@ function transliterateForSlug(text) {
151
164
  }
152
165
  return out;
153
166
  }
154
- // ─── Phase file helpers ──────────────────────────────────────────────────────
155
- /** Filter a file list to just PLAN.md / *-PLAN.md entries. */
156
- function filterPlanFiles(files) {
157
- return files.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
158
- }
159
- /** Filter a file list to just SUMMARY.md / *-SUMMARY.md entries. */
160
- function filterSummaryFiles(files) {
161
- return files.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
162
- }
163
167
  /**
164
168
  * Read a phase directory and return counts/flags for common file types.
169
+ *
170
+ * #3183 (ADR-3180 Decision 2): `plans`/`summaries` are derived from the
171
+ * canonical `scanPhasePlans` rather than a local re-derivation, so this
172
+ * primitive can no longer diverge from the single owner of live-plan
173
+ * counting. `scanPhasePlans`
174
+ * lives in plan-scan.cjs, which itself imports `countMatchedSummaries` from
175
+ * THIS module — a top-level import here would be circular, so the require
176
+ * is deferred (lazy, inside the function body) to break the cycle at load
177
+ * time. This mirrors the lazy-require seam already used elsewhere in this
178
+ * repo (see src/audit-command-router.cts) for the same "module A needs
179
+ * module B which needs module A" shape.
180
+ *
181
+ * `hasResearch`/`hasContext`/`hasVerification`/`hasReviews` stay on the raw
182
+ * `readdirSync` listing — they are not plan-scan concerns.
183
+ *
184
+ * #3511 BLOCKER-2: the raw listing is scoped through `scopeToPhase` (keyed on
185
+ * `path.basename(phaseDir)`) before any of the four artifact predicates run,
186
+ * so a stray cross-phase file (e.g. `04-VERIFICATION.md` sitting inside phase
187
+ * 03's directory) cannot flip `hasResearch`/`hasContext`/`hasVerification`/
188
+ * `hasReviews` true for a phase it does not belong to — the same membership
189
+ * rule every other aggregate phase-directory scan (`uat.cts`, `audit.cts`,
190
+ * `phase.cts`, `state.cts`) already routes through. `hasContext` is scoped by
191
+ * passing the already-scoped array into `findContextMdIn` at this call site
192
+ * only — `findContextMdIn` itself is unchanged and its other 4 call sites
193
+ * (roadmap.cts, gap-checker.cts, init.cts) are unaffected.
194
+ *
195
+ * Degrades on an unreadable directory instead of throwing: empty arrays,
196
+ * every flag false, scope UNREADABLE (mirroring scanPhasePlans's own
197
+ * degrade path).
165
198
  */
166
199
  function getPhaseFileStats(phaseDir) {
167
- const files = node_fs_1.default.readdirSync(phaseDir);
200
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
201
+ const scanPhasePlans = require('./plan-scan.cjs');
202
+ const scan = scanPhasePlans(phaseDir);
203
+ let files;
204
+ try {
205
+ files = node_fs_1.default.readdirSync(phaseDir);
206
+ }
207
+ catch {
208
+ return {
209
+ plans: scan.planFiles,
210
+ summaries: scan.summaryFiles,
211
+ hasResearch: false,
212
+ hasContext: false,
213
+ hasVerification: false,
214
+ hasReviews: false,
215
+ scope: scan.scope,
216
+ };
217
+ }
218
+ const scopedFiles = scopeToPhase(files, node_path_1.default.basename(phaseDir));
168
219
  return {
169
- plans: filterPlanFiles(files),
170
- summaries: filterSummaryFiles(files),
171
- hasResearch: files.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'),
172
- hasContext: findContextMdIn(files) !== null,
173
- hasVerification: files.some(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md'),
174
- hasReviews: files.some(f => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md'),
220
+ plans: scan.planFiles,
221
+ summaries: scan.summaryFiles,
222
+ hasResearch: scopedFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'),
223
+ hasContext: findContextMdIn(scopedFiles) !== null,
224
+ hasVerification: scopedFiles.some(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md'),
225
+ hasReviews: scopedFiles.some(f => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md'),
226
+ scope: scan.scope,
175
227
  };
176
228
  }
177
229
  /**
@@ -297,6 +349,35 @@ function summaryCandidates(plan) {
297
349
  const extended = base.match(/^(\d+)-PLAN-(\d+)/i);
298
350
  if (extended)
299
351
  candidates.push(dir + extended[1] + '-' + extended[2] + '-SUMMARY.md');
352
+ // #3183: canonical-id form. Restores the coverage of the pre-migration
353
+ // bespoke I001 rule (verify.cts, pre-#3183, via validate.cjs's now-unused
354
+ // `canonicalPlanStem` — behaviourally identical to `extractCanonicalPlanId`,
355
+ // confirmed empirically), which matched a plan carrying a descriptive slug
356
+ // after its <phase>-<plan> id — e.g. `68-01-scaffolding-PLAN.md` — against
357
+ // a summary named only by the bare id — `68-01-SUMMARY.md`. None of the
358
+ // three candidates above produce that filename.
359
+ //
360
+ // Narrowed to the case `extractCanonicalPlanId` actually extracted an
361
+ // <id>-<id> pair (its result differs from the plan's own PLAN-stripped
362
+ // base). When no pair is found it falls back to returning that same base
363
+ // unchanged, which would otherwise push a redundant candidate identical to
364
+ // the `<stem>-SUMMARY.md` form above (e.g. `setup-PLAN.md` -> canonical
365
+ // 'setup' -> 'setup-SUMMARY.md', already candidate #2) rather than the
366
+ // original rule's actual behavior of matching only real id pairs.
367
+ //
368
+ // Collision, matching the original rule byte-for-behaviour: two plans that
369
+ // share the same <phase>-<plan> id but differ only in their descriptive
370
+ // slug (`68-01-alpha-PLAN.md` + `68-01-beta-PLAN.md`) both generate the
371
+ // SAME candidate `68-01-SUMMARY.md` and therefore BOTH read as summarized
372
+ // off one shared summary file. This is not a new regression: the
373
+ // pre-migration bespoke rule collapsed the same way (it populated one
374
+ // `summaryBases` Set keyed by canonical stem, so any plan whose canonical
375
+ // stem hit the set counted as matched, with no cardinality check against
376
+ // how many plans shared that stem).
377
+ const planStem = base.replace(/-PLAN$/i, '');
378
+ const canonicalId = extractCanonicalPlanId(base + '.md');
379
+ if (canonicalId !== planStem)
380
+ candidates.push(dir + canonicalId + '-SUMMARY.md');
300
381
  return candidates;
301
382
  }
302
383
  /**
@@ -314,6 +395,24 @@ function findUnsummarizedPlans(planFiles, summaryFiles) {
314
395
  const summarySet = new Set(summaryFiles);
315
396
  return planFiles.filter((plan) => !summaryCandidates(plan).some((c) => summarySet.has(c)));
316
397
  }
398
+ /**
399
+ * #3183: the mirror image of `findUnsummarizedPlans` — the summary files in
400
+ * `summaryFiles` that do NOT pair with ANY plan in `planFiles`, using the
401
+ * identical `summaryCandidates` matching rule as `countMatchedSummaries` /
402
+ * `findUnsummarizedPlans`. Callers that must name orphaned summaries (a
403
+ * stray non-plan summary, or a summary whose plan was renamed/removed) need
404
+ * this instead of a bespoke exact-suffix Set-diff, which cannot recognize
405
+ * the nested or extended naming forms `summaryCandidates` already handles —
406
+ * a divergence that produced false "orphan summary" warnings.
407
+ */
408
+ function findOrphanSummaries(planFiles, summaryFiles) {
409
+ const claimed = new Set();
410
+ for (const plan of planFiles) {
411
+ for (const candidate of summaryCandidates(plan))
412
+ claimed.add(candidate);
413
+ }
414
+ return summaryFiles.filter((s) => !claimed.has(s));
415
+ }
317
416
  module.exports = {
318
417
  toPosixPath,
319
418
  detectSubRepos,
@@ -321,12 +420,11 @@ module.exports = {
321
420
  pathExistsInternal,
322
421
  generateSlugInternal,
323
422
  transliterateForSlug,
324
- filterPlanFiles,
325
- filterSummaryFiles,
326
423
  getPhaseFileStats,
327
424
  readSubdirectories,
328
425
  timeAgo,
329
426
  extractCanonicalPlanId,
330
427
  countMatchedSummaries,
331
428
  findUnsummarizedPlans,
429
+ findOrphanSummaries,
332
430
  };
@@ -21,6 +21,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.extractDecisions = extractDecisions;
22
22
  exports.parseDecisions = parseDecisions;
23
23
  const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
24
+ const token_scanner_cjs_1 = require("./token-scanner.cjs");
24
25
  const DISCRETION_HEADINGS = new Set([
25
26
  "claude's discretion",
26
27
  'claudes discretion',
@@ -89,12 +90,14 @@ function parseDecisionLines(block) {
89
90
  let category = '';
90
91
  let inDiscretion = false;
91
92
  let current = null;
93
+ let openIndent = null;
92
94
  let parseMisses = 0;
93
95
  const flush = () => {
94
96
  if (current) {
95
97
  current.text = current.text.trim();
96
98
  out.push(current);
97
99
  current = null;
100
+ openIndent = null;
98
101
  }
99
102
  };
100
103
  for (const line of lines) {
@@ -115,6 +118,23 @@ function parseDecisionLines(block) {
115
118
  inDiscretion = DISCRETION_HEADINGS.has(normalized);
116
119
  continue;
117
120
  }
121
+ // Nested bullet under an open decision (#3212 Phase 3, #3169): a bullet
122
+ // indented deeper than the currently-open decision's own bullet is that
123
+ // decision's elaboration (e.g. a cross-reference to a sibling decision),
124
+ // not a fresh declaration attempt — fold it into current.text exactly
125
+ // like a continuation line, before it ever reaches the declaration/
126
+ // parse-miss regexes below. A bullet at the SAME or a SHALLOWER indent
127
+ // is unaffected — tested exactly as before this fix. See design doc
128
+ // .gsd/phase/chore-3414-tokenizer-first-seam/40-design.md §1.3 for why
129
+ // nesting depth, not bullet content, is the signal that distinguishes
130
+ // this from a genuinely malformed top-level declaration.
131
+ if (current &&
132
+ openIndent !== null &&
133
+ trimmed.startsWith('-') &&
134
+ (0, token_scanner_cjs_1.indentWidth)(line) > openIndent) {
135
+ current.text += ' ' + trimmed;
136
+ continue;
137
+ }
118
138
  // Colon form: `- **D-NN[ [tags]]:** text`
119
139
  const colonMatch = line.match(bulletColonRe);
120
140
  if (colonMatch) {
@@ -125,6 +145,7 @@ function parseDecisionLines(block) {
125
145
  : [];
126
146
  const trackable = !inDiscretion && !tags.some((t) => NON_TRACKABLE_TAGS.has(t));
127
147
  current = { id, text: colonMatch[3], category, tags, trackable };
148
+ openIndent = (0, token_scanner_cjs_1.indentWidth)(line);
128
149
  continue;
129
150
  }
130
151
  // Em-dash form: `- **D-NN[ [tags]] — title** body`
@@ -140,6 +161,7 @@ function parseDecisionLines(block) {
140
161
  // title itself is embedded in the bold run but we report the body as text
141
162
  // (consistent with how the gate cares only about coverage, not title/body split).
142
163
  current = { id, text: emDashMatch[3] || '', category, tags, trackable };
164
+ openIndent = (0, token_scanner_cjs_1.indentWidth)(line);
143
165
  continue;
144
166
  }
145
167
  // Titled-colon form: `- **D-NN[ [tags]]: Title.** body` (#1639). Checked LAST — it is
@@ -155,6 +177,7 @@ function parseDecisionLines(block) {
155
177
  : [];
156
178
  const trackable = !inDiscretion && !tags.some((t) => NON_TRACKABLE_TAGS.has(t));
157
179
  current = { id, text: titledColonMatch[3] || '', category, tags, trackable };
180
+ openIndent = (0, token_scanner_cjs_1.indentWidth)(line);
158
181
  continue;
159
182
  }
160
183
  // Parse-miss guard (FIX B + #1343): a line that looks like a `D-NN` decision
@@ -8,6 +8,13 @@
8
8
  *
9
9
  * Parses the real fallow `audit --format json` schema (schema_version 3
10
10
  * envelope, nested dead_code/duplication sections). See fallow 2.70.0+.
11
+ *
12
+ * #3411 Phase 2 (#3618): binary resolution no longer lives here — it delegates
13
+ * to the platform seam's `resolveExecutableBinary` (shell-command-projection.cts).
14
+ * This file's prior private resolver deliberately included an extensionless
15
+ * `fallow` as a win32 candidate; the seam does not, and that is a fix, not a
16
+ * regression — an extensionless file sitting beside `fallow.cmd` is npm's POSIX
17
+ * `sh` shim, which `CreateProcess` cannot run (#3275).
11
18
  */
12
19
  var __importDefault = (this && this.__importDefault) || function (mod) {
13
20
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -19,51 +26,20 @@ exports.normalizeFallowReport = normalizeFallowReport;
19
26
  exports.normalizeFallowReportFile = normalizeFallowReportFile;
20
27
  const node_fs_1 = __importDefault(require("node:fs"));
21
28
  const node_path_1 = __importDefault(require("node:path"));
22
- function candidateNames() {
23
- return process.platform === 'win32'
24
- ? ['fallow.exe', 'fallow.cmd', 'fallow.bat', 'fallow']
25
- : ['fallow'];
26
- }
27
- function isExecutableFile(filePath) {
28
- try {
29
- const stat = node_fs_1.default.statSync(filePath);
30
- if (!stat.isFile())
31
- return false;
32
- if (process.platform === 'win32')
33
- return true;
34
- node_fs_1.default.accessSync(filePath, node_fs_1.default.constants.X_OK);
35
- return true;
36
- }
37
- catch {
38
- return false;
39
- }
40
- }
41
- function findInPath(envPath) {
42
- if (!envPath)
43
- return null;
44
- const names = candidateNames();
45
- const segments = envPath.split(node_path_1.default.delimiter).filter(Boolean);
46
- for (const segment of segments) {
47
- for (const name of names) {
48
- const candidate = node_path_1.default.join(segment, name);
49
- if (isExecutableFile(candidate))
50
- return candidate;
51
- }
52
- }
53
- return null;
54
- }
55
- function findInNodeModules(cwd) {
56
- const names = candidateNames();
57
- const binDir = node_path_1.default.join(cwd, 'node_modules', '.bin');
58
- for (const name of names) {
59
- const candidate = node_path_1.default.join(binDir, name);
60
- if (isExecutableFile(candidate))
61
- return candidate;
62
- }
63
- return null;
64
- }
29
+ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
65
30
  function resolveFallowBinary({ cwd, envPath = process.env['PATH'] ?? '' }) {
66
- return findInNodeModules(cwd) || findInPath(envPath) || null;
31
+ return (0, shell_command_projection_cjs_1.resolveExecutableBinary)('fallow', {
32
+ prependPaths: [node_path_1.default.join(cwd, 'node_modules', '.bin')],
33
+ // #3619 (epic #3411 Phase 3): pathOverride carries envPath as the search
34
+ // path while leaving `env` unset, so the seam falls back to its default
35
+ // `env` (process.env) for everything else — PATHEXT included. Ambient
36
+ // PATHEXT therefore still governs win32 resolution exactly as before, but
37
+ // this file never reads it itself: that's what keeps this module clean
38
+ // under local/no-private-binary-resolution, which forbids a PATHEXT read
39
+ // outside the seam.
40
+ pathOverride: envPath,
41
+ requireExecutable: true,
42
+ });
67
43
  }
68
44
  function requireFallowBinary({ cwd, envPath = process.env['PATH'] ?? '' }) {
69
45
  const binary = resolveFallowBinary({ cwd, envPath });