@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
@@ -13,7 +13,8 @@
13
13
  *
14
14
  * Dependencies (leaf modules only):
15
15
  * - node:fs / node:os / node:path (stdlib)
16
- * - ./configuration.cjs (normalizeLegacyKeys, CONFIG_DEFAULTS as CANONICAL_CONFIG_DEFAULTS)
16
+ * - ./configuration.cjs (normalizeLegacyKeys, isConfigSection, CONFIG_DEFAULTS as CANONICAL_CONFIG_DEFAULTS)
17
+ * - ./unusable-input.cjs (warnUnusableInput, UNUSABLE_REASON — #3760)
17
18
  * - ./config-schema.cjs (VALID_CONFIG_KEYS, DYNAMIC_KEY_PATTERNS)
18
19
  * - ./planning-workspace.cjs (planningDir, planningRoot)
19
20
  * - ./shell-command-projection.cjs (execGit, platformWriteSync, platformReadSync)
@@ -39,6 +40,12 @@ const configuration_cjs_1 = require("./configuration.cjs");
39
40
  const configSchema = require("./config-schema.cjs");
40
41
  const { VALID_CONFIG_KEYS, DYNAMIC_KEY_PATTERNS, isCentralConfigKey: _isCentralConfigKeyFn } = configSchema;
41
42
  const model_catalog_cjs_1 = require("./model-catalog.cjs");
43
+ // #3760: the ADR-1411 out-of-band diagnostic seam. loadConfig returns `.config`
44
+ // alone, so an in-band `skipped` record would be unreachable to nearly every
45
+ // caller — "a reason no caller reads is an unreachable field" (ADR-1411).
46
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
47
+ const unusableInputModule = require("./unusable-input.cjs");
48
+ const { UNUSABLE_REASON: _UNUSABLE_REASON, warnUnusableInput: _warnUnusableInput } = unusableInputModule;
42
49
  // ─── Federated Config (ADR-857 phase 3b) ─────────────────────────────────────
43
50
  // eslint-disable-next-line @typescript-eslint/no-require-imports
44
51
  const federatedConfigModule = require("./federated-config.cjs");
@@ -77,6 +84,7 @@ function _resetFederatedRegistryForTests() {
77
84
  * - git.* → flat git keys (branching_strategy, templates)
78
85
  * - workflow.* → flat names (research, verifier, …)
79
86
  * - planning.sub_repos → sub_repos
87
+ * - planning.pr_strict → pr_strict
80
88
  * - planning.commit_docs / search_gitignored → top-level flat keys
81
89
  */
82
90
  // CANONICAL_CONFIG_DEFAULTS is typed as Record<string, unknown> from configuration.cjs;
@@ -91,6 +99,23 @@ function _getNestedConfigDefault(section, field) {
91
99
  }
92
100
  return undefined;
93
101
  }
102
+ /** Shared flat-then-nested config lookup; exported for parity tests. */
103
+ function _getConfigValue(parsed, key, nested) {
104
+ if (parsed[key] !== undefined)
105
+ return parsed[key];
106
+ if (nested && parsed[nested.section] && typeof parsed[nested.section] === 'object' && parsed[nested.section] !== null) {
107
+ return parsed[nested.section][nested.field];
108
+ }
109
+ return undefined;
110
+ }
111
+ /** Shared nested-only config lookup; exported for parity tests. */
112
+ function _getConfigNested(parsed, section, field) {
113
+ const sec = parsed[section];
114
+ if (sec !== null && typeof sec === 'object' && !Array.isArray(sec)) {
115
+ return sec[field];
116
+ }
117
+ return undefined;
118
+ }
94
119
  const CONFIG_DEFAULTS = {
95
120
  model_profile: _getConfigDefault('model_profile'),
96
121
  commit_docs: _getConfigDefault('commit_docs'),
@@ -111,6 +136,7 @@ const CONFIG_DEFAULTS = {
111
136
  exa_search: _getConfigDefault('exa_search'),
112
137
  text_mode: _getNestedConfigDefault('workflow', 'text_mode'),
113
138
  sub_repos: _getNestedConfigDefault('planning', 'sub_repos'),
139
+ pr_strict: _getNestedConfigDefault('planning', 'pr_strict'),
114
140
  resolve_model_ids: _getConfigDefault('resolve_model_ids'),
115
141
  context_window: _getConfigDefault('context_window'),
116
142
  phase_naming: _getConfigDefault('phase_naming'),
@@ -120,7 +146,10 @@ const CONFIG_DEFAULTS = {
120
146
  security_asvs_level: _getNestedConfigDefault('workflow', 'security_asvs_level'),
121
147
  security_block_on: _getNestedConfigDefault('workflow', 'security_block_on'),
122
148
  post_planning_gaps: _getNestedConfigDefault('workflow', 'post_planning_gaps'),
149
+ research_before_questions: _getNestedConfigDefault('workflow', 'research_before_questions'), // #3894
123
150
  smart_zone_tokens: _getNestedConfigDefault('workflow', 'smart_zone_tokens'),
151
+ inline_plan_threshold: _getNestedConfigDefault('workflow', 'inline_plan_threshold'), // #3801
152
+ max_prompt_tokens: _getNestedConfigDefault('review', 'max_prompt_tokens'),
124
153
  };
125
154
  /**
126
155
  * Deep-merge two plain config objects. `overlay` wins on key conflict.
@@ -290,6 +319,59 @@ function _resetRuntimeWarningCacheForTests() {
290
319
  _warnedConfigKeys.clear();
291
320
  _warnedUnknownConfigKeys.clear();
292
321
  _warnedUnusableConfig.clear();
322
+ _warnedShadowedGlobalKeys.clear();
323
+ }
324
+ // ─── #3532 (10b): shadowed global-defaults diagnostic ────────────────────────
325
+ // The keys Branch D's `_globalBaseCfg` demonstrably honors from
326
+ // ~/.gsd/defaults.json when no project config exists. Under a project
327
+ // .planning/config.json (Branch A — every real project) the global file is
328
+ // never opened, so each of these set globally is silently inert for resolution.
329
+ // `effort` is in Branch D's honored set but is EXCLUDED from the shadow warning:
330
+ // the install-time effort sync (readGsdEffectiveEffortConfig) DOES merge the
331
+ // global file, so warning on it would be false for the channel users actually
332
+ // control via `effort sync`. Keep this list in lockstep with `_globalBaseCfg`
333
+ // below — the per-key canary in tests/config-loader.test.cjs fails first on
334
+ // drift in either direction.
335
+ const GLOBAL_DEFAULTS_RESOLUTION_KEYS = [
336
+ 'model_profile', 'commit_docs', 'research', 'plan_checker', 'verifier',
337
+ 'nyquist_validation', 'post_planning_gaps', 'research_before_questions', 'parallelization', 'text_mode',
338
+ 'resolve_model_ids', 'context_window', 'subagent_timeout', 'model_overrides',
339
+ 'models', 'granularity', 'granularities', 'planning', 'dynamic_routing',
340
+ 'effort', 'fast_mode', 'agent_skills', 'response_language', 'runtime',
341
+ 'model_profile_overrides', 'model_policy',
342
+ ];
343
+ // Module-level dedup keyed on the SORTED shadowed-key set: a later call with
344
+ // the same shadowed set stays quiet, while a config that grows a new shadowed
345
+ // key re-arms the warning. Stronger than _warnedUnknownConfigKeys (which keys
346
+ // on insertion order) — same discipline, order-independent key.
347
+ const _warnedShadowedGlobalKeys = new Set();
348
+ function _warnShadowedGlobalDefaults(globalDefaults, globalPath) {
349
+ const shadowed = GLOBAL_DEFAULTS_RESOLUTION_KEYS.filter(k => k !== 'effort' && Object.prototype.hasOwnProperty.call(globalDefaults, k));
350
+ // Branch D also honors the nested alias workflow.post_planning_gaps (the
351
+ // `?? globalDefaults['workflow']?.['post_planning_gaps']` fallback in
352
+ // _globalBaseCfg) — a global file using only the nested form is equally
353
+ // shadowed, so it reports under its dotted name.
354
+ // #3894: research_before_questions gets the same nested-alias reporting.
355
+ const nestedAliasKeys = ['post_planning_gaps', 'research_before_questions'];
356
+ const wf = globalDefaults['workflow'];
357
+ if (wf && typeof wf === 'object' && !Array.isArray(wf)) {
358
+ for (const k of nestedAliasKeys) {
359
+ if (!shadowed.includes(k) && Object.prototype.hasOwnProperty.call(wf, k)) {
360
+ shadowed.push(`workflow.${k}`);
361
+ }
362
+ }
363
+ }
364
+ if (shadowed.length === 0)
365
+ return;
366
+ const dedupKey = shadowed.slice().sort().join(',');
367
+ if (_warnedShadowedGlobalKeys.has(dedupKey))
368
+ return;
369
+ _warnedShadowedGlobalKeys.add(dedupKey);
370
+ try {
371
+ process.stderr.write(`gsd-tools: warning: ${globalPath} sets ${shadowed.join(', ')} but a project config ` +
372
+ `takes precedence here — those global keys are ignored for model resolution. (#3532)\n`);
373
+ }
374
+ catch { /* stderr might be closed in some test harnesses */ }
293
375
  }
294
376
  // ─── FIX 2: Federated overlay helpers ────────────────────────────────────────
295
377
  /**
@@ -510,8 +592,18 @@ function _warnUnusableConfig(fault) {
510
592
  * diagnostic names the file. Before this, a trailing comma in config.json was
511
593
  * byte-identical to the file not existing: builtin defaults, degraded:false,
512
594
  * and the user's entire configuration silently discarded.
595
+ *
596
+ * `options.persist: false` (#3648) suppresses the two normalize-then-write-back
597
+ * side effects below. Resolution is otherwise identical — same precedence, same
598
+ * returned object — the migrated shape simply stays in memory. Callers that only
599
+ * ASK the config something (a predicate, a status readout) pass it so that a read
600
+ * cannot dirty the working tree; the ~30 callers that omit it keep persisting, so
601
+ * a legacy config is still migrated exactly once by ordinary use.
513
602
  */
514
603
  function loadConfigResolved(cwd, options = {}) {
604
+ // Opt-OUT, not opt-in: omitting the option must preserve the historical
605
+ // write-back for every existing caller.
606
+ const persist = options['persist'] !== false;
515
607
  // NOTE: loadConfigResolved resolves from cwd AS-IS (no walk-up).
516
608
  // Callers that need ancestor-anchoring (e.g. cmdAgentSkills) must do so
517
609
  // themselves via findProjectRoot() before calling this function.
@@ -564,24 +656,37 @@ function loadConfigResolved(cwd, options = {}) {
564
656
  if (rootRead.kind !== 'ok')
565
657
  throw new Error('root config absent or unusable');
566
658
  rootParsed = rootRead.data;
567
- const { parsed: rootNormalized, normalizations: rootNorms } = (0, configuration_cjs_1.normalizeLegacyKeys)(rootParsed);
659
+ const { parsed: rootNormalized, normalizations: rootNorms, skipped: rootSkipped } = (0, configuration_cjs_1.normalizeLegacyKeys)(rootParsed);
660
+ if (rootSkipped.length > 0) {
661
+ _warnUnusableInput({ reason: _UNUSABLE_REASON.CONFIG_SECTION_NOT_OBJECT, source: rootConfigPath });
662
+ }
568
663
  if (rootNorms.length > 0) {
569
664
  for (const norm of rootNorms) {
570
665
  if (norm.requiresFilesystem && !rootNormalized.planning?.['sub_repos']) {
571
666
  const detected = getDetectedSubRepos();
572
667
  if (detected.length > 0) {
573
- if (!rootNormalized.planning)
668
+ // #3760: `if (!planning) planning = {}` treated a non-empty STRING as an
669
+ // already-present section, and the next line then assigned onto a
670
+ // primitive — a strict-mode TypeError the enclosing catch swallowed,
671
+ // discarding the user's whole config. `requiresFilesystem` now only
672
+ // reaches here when the section is absent or an object (configuration.cts
673
+ // block 3 refuses otherwise and reports it via `skipped`), so this
674
+ // narrowing chooses between merge and create and never discards.
675
+ if (!(0, configuration_cjs_1.isConfigSection)(rootNormalized.planning)) {
574
676
  rootNormalized.planning = {};
677
+ }
575
678
  rootNormalized.planning['sub_repos'] = detected;
576
679
  rootNormalized.planning['commit_docs'] = false;
577
680
  }
578
681
  }
579
682
  }
580
683
  rootParsed = rootNormalized;
581
- try {
582
- (0, shell_command_projection_cjs_1.platformWriteSync)(rootConfigPath, JSON.stringify(rootParsed, null, 2));
684
+ if (persist) {
685
+ try {
686
+ (0, shell_command_projection_cjs_1.platformWriteSync)(rootConfigPath, JSON.stringify(rootParsed, null, 2));
687
+ }
688
+ catch { /* ignore */ }
583
689
  }
584
- catch { /* ignore */ }
585
690
  }
586
691
  else {
587
692
  rootParsed = rootNormalized;
@@ -608,7 +713,10 @@ function loadConfigResolved(cwd, options = {}) {
608
713
  const fileHadKeys = Object.keys(read.data).length > 0;
609
714
  let configDirty = false;
610
715
  {
611
- const { parsed: normalized, normalizations } = (0, configuration_cjs_1.normalizeLegacyKeys)(fileData);
716
+ const { parsed: normalized, normalizations, skipped } = (0, configuration_cjs_1.normalizeLegacyKeys)(fileData);
717
+ if (skipped.length > 0) {
718
+ _warnUnusableInput({ reason: _UNUSABLE_REASON.CONFIG_SECTION_NOT_OBJECT, source: configPath });
719
+ }
612
720
  if (normalizations.length > 0) {
613
721
  Object.keys(fileData).forEach(k => delete fileData[k]);
614
722
  Object.assign(fileData, normalized);
@@ -617,7 +725,8 @@ function loadConfigResolved(cwd, options = {}) {
617
725
  if (norm.requiresFilesystem && !fileData.planning?.['sub_repos']) {
618
726
  const detected = getDetectedSubRepos();
619
727
  if (detected.length > 0) {
620
- if (!fileData.planning)
728
+ // #3760 — see the identical guard on the root-config path above.
729
+ if (!(0, configuration_cjs_1.isConfigSection)(fileData.planning))
621
730
  fileData.planning = {};
622
731
  fileData.planning['sub_repos'] = detected;
623
732
  fileData.planning['commit_docs'] = false;
@@ -632,14 +741,17 @@ function loadConfigResolved(cwd, options = {}) {
632
741
  if (detected.length > 0) {
633
742
  const sorted = [...currentSubRepos].sort();
634
743
  if (JSON.stringify(sorted) !== JSON.stringify(detected)) {
635
- if (!fileData.planning)
744
+ // #3760 — reachable only when `planning` already yielded a non-empty
745
+ // sub_repos array, so it is an object here; the narrowing keeps the
746
+ // assignment total rather than relying on that from three frames away.
747
+ if (!(0, configuration_cjs_1.isConfigSection)(fileData.planning))
636
748
  fileData.planning = {};
637
749
  fileData.planning['sub_repos'] = detected;
638
750
  configDirty = true;
639
751
  }
640
752
  }
641
753
  }
642
- if (configDirty) {
754
+ if (configDirty && persist) {
643
755
  try {
644
756
  (0, shell_command_projection_cjs_1.platformWriteSync)(configPath, JSON.stringify(fileData, null, 2));
645
757
  }
@@ -684,17 +796,19 @@ function loadConfigResolved(cwd, options = {}) {
684
796
  }
685
797
  }
686
798
  _warnUnknownProfileOverrides(parsed, '.planning/config.json');
687
- const get = (key, nested) => {
688
- if (parsed[key] !== undefined)
689
- return parsed[key];
690
- if (nested && parsed[nested.section] && typeof parsed[nested.section] === 'object' && parsed[nested.section] !== null) {
691
- const sec = parsed[nested.section];
692
- if (sec[nested.field] !== undefined) {
693
- return sec[nested.field];
694
- }
695
- }
696
- return undefined;
697
- };
799
+ const get = (key, nested) => _getConfigValue(parsed, key, nested);
800
+ /**
801
+ * Nested-ONLY read — no top-level fallback (#3648).
802
+ *
803
+ * `get()`'s flat-then-nested order exists for keys that have a legacy flat
804
+ * spelling `normalizeLegacyKeys` migrates (`branching_strategy`,
805
+ * `base_branch`, …); for those, honouring the flat key is back-compat. A key
806
+ * introduced with no legacy form has nothing to be compatible WITH, so
807
+ * routing it through `get()` would invent an undocumented top-level alias
808
+ * that silently outranks the canonical nested key. Use this instead for new
809
+ * `<section>.<field>` keys (round-4 external review).
810
+ */
811
+ const getNested = (section, field) => _getConfigNested(parsed, section, field);
698
812
  const parallelization = (() => {
699
813
  const val = get('parallelization');
700
814
  if (typeof val === 'boolean')
@@ -715,6 +829,8 @@ function loadConfigResolved(cwd, options = {}) {
715
829
  })(),
716
830
  search_gitignored: get('search_gitignored', { section: 'planning', field: 'search_gitignored' }) ?? defaults.search_gitignored,
717
831
  branching_strategy: get('branching_strategy', { section: 'git', field: 'branching_strategy' }) ?? defaults.branching_strategy,
832
+ base_branch: get('base_branch', { section: 'git', field: 'base_branch' }),
833
+ protected_branches: getNested('git', 'protected_branches'),
718
834
  phase_branch_template: get('phase_branch_template', { section: 'git', field: 'phase_branch_template' }) ?? defaults.phase_branch_template,
719
835
  milestone_branch_template: get('milestone_branch_template', { section: 'git', field: 'milestone_branch_template' }) ?? defaults.milestone_branch_template,
720
836
  quick_branch_template: get('quick_branch_template', { section: 'git', field: 'quick_branch_template' }) ?? defaults.quick_branch_template,
@@ -733,6 +849,7 @@ function loadConfigResolved(cwd, options = {}) {
733
849
  _auto_chain_active: get('_auto_chain_active', { section: 'workflow', field: '_auto_chain_active' }) ?? false,
734
850
  mode: get('mode') ?? 'interactive',
735
851
  sub_repos: get('sub_repos', { section: 'planning', field: 'sub_repos' }) ?? defaults.sub_repos,
852
+ pr_strict: get('pr_strict', { section: 'planning', field: 'pr_strict' }) ?? defaults.pr_strict,
736
853
  resolve_model_ids: get('resolve_model_ids') ?? defaults.resolve_model_ids,
737
854
  context_window: get('context_window') ?? defaults.context_window,
738
855
  phase_naming: get('phase_naming') ?? defaults.phase_naming,
@@ -751,11 +868,25 @@ function loadConfigResolved(cwd, options = {}) {
751
868
  fast_mode: (parsed['fast_mode']) || null,
752
869
  agent_skills: (parsed['agent_skills']) || {},
753
870
  agent_skills_security: (parsed['agent_skills_security']) || null,
871
+ // #3587: phase_commit_docs.<phase-id> — a dynamic-key family shaped like
872
+ // agent_skills above (`{ "<phase-id>": boolean }`). Must be threaded here
873
+ // explicitly: `_baseConfig` is a hand-maintained allowlist, so a key that
874
+ // is only in config-schema.manifest.json's dynamicKeyPatterns (and not
875
+ // projected here) is silently dropped on read — the exact `features`-key
876
+ // failure mode this module's own A3 test guards against.
877
+ phase_commit_docs: (parsed['phase_commit_docs']) || {},
754
878
  manager: (parsed['manager']) || {},
755
879
  response_language: get('response_language') || null,
756
880
  claude_md_path: get('claude_md_path') || null,
757
881
  claude_md_assembly: (parsed['claude_md_assembly']) || null,
758
882
  phase_id_convention: get('phase_id_convention') ?? null,
883
+ // #3691: the documented central review key. Declared here (not federated —
884
+ // it is central, see config-schema.manifest.json validKeys) so the existing
885
+ // `review.*` per-lane keys the federated overlay below adds land as SIBLINGS
886
+ // on this same object rather than being clobbered by it.
887
+ review: {
888
+ max_prompt_tokens: get('max_prompt_tokens', { section: 'review', field: 'max_prompt_tokens' }) ?? defaults.max_prompt_tokens,
889
+ },
759
890
  };
760
891
  // ADR-857 phase 3b: federated config overlay
761
892
  try {
@@ -777,6 +908,22 @@ function loadConfigResolved(cwd, options = {}) {
777
908
  // A1 vs A2: disambiguate by whether a real workstream was requested.
778
909
  // Fix 4: empty-string ws ('') resolves the root path → source:'root'.
779
910
  const source = wsRequested ? 'workstream' : 'root';
911
+ // #3532 (10b): a parsed project config means Branch D never runs, so every
912
+ // key ~/.gsd/defaults.json sets that Branch D would honor is silently inert
913
+ // here. Observation only — one deduped stderr warning; precedence is
914
+ // untouched. Faults in the global file stay silent in this branch (the
915
+ // project config governs; the nearer file is the actionable one).
916
+ try {
917
+ const shadowHome = process.env['GSD_HOME'] || node_os_1.default.homedir();
918
+ const shadowPath = node_path_1.default.join(shadowHome, '.gsd', 'defaults.json');
919
+ const shadowRead = _readConfigFile(shadowPath);
920
+ if (shadowRead.kind === 'ok') {
921
+ _warnShadowedGlobalDefaults(shadowRead.data, shadowPath);
922
+ }
923
+ }
924
+ catch {
925
+ // Observation only — never let the diagnostic perturb resolution.
926
+ }
780
927
  // This config parsed — but a DIFFERENT file on the resolution path may not
781
928
  // have. A workstream config that loads cleanly while the root config it
782
929
  // inherits from is corrupt is still a degraded resolution: the root's
@@ -800,8 +947,15 @@ function loadConfigResolved(cwd, options = {}) {
800
947
  // Fix 2: Early intercept — workstream requested but ws config.json absent (or dir absent)
801
948
  // AND root config was loaded. Covers BOTH "dir exists, no config.json" AND "dir absent".
802
949
  // This delivers the #1366 acceptance criterion: nonexistent GSD_WORKSTREAM yields root, degraded.
950
+ //
951
+ // Both fallback recursions below forward `options` and override ONLY `workstream`.
952
+ // A bare `{ workstream: null }` silently dropped every other option, so a caller's
953
+ // `persist: false` was discarded on exactly this path and the root config was
954
+ // rewritten by a read (#3648, found by external review). The explicit
955
+ // `workstream: null` still wins the `hasOwnProperty` check at the top of this
956
+ // function, so spreading cannot let `workstreamContext` reintroduce a workstream.
803
957
  if (wsRequested && rootParsed) {
804
- const fb = loadConfigResolved(cwd, { workstream: null });
958
+ const fb = loadConfigResolved(cwd, { ...options, workstream: null });
805
959
  return fallback({ config: fb.config, source: 'root', degraded: true });
806
960
  }
807
961
  // Branch B, C, D, E
@@ -809,7 +963,7 @@ function loadConfigResolved(cwd, options = {}) {
809
963
  if (rootParsed) {
810
964
  // Branch B: workstream requested but ws config.json absent; root config present.
811
965
  // (Only reached when wsRequested is false — e.g. ws='' with .planning/workstreams//config.json)
812
- const fb = loadConfigResolved(cwd, { workstream: null });
966
+ const fb = loadConfigResolved(cwd, { ...options, workstream: null });
813
967
  return fallback({ config: fb.config, source: 'root', degraded: true });
814
968
  }
815
969
  // Branch C: .planning/ exists but no config.json and no root config — federated/builtin defaults
@@ -847,6 +1001,12 @@ function loadConfigResolved(cwd, options = {}) {
847
1001
  post_planning_gaps: (globalDefaults['post_planning_gaps'])
848
1002
  ?? globalDefaults['workflow']?.['post_planning_gaps']
849
1003
  ?? defaults.post_planning_gaps,
1004
+ // #3894: same nested-alias shape as post_planning_gaps above — the key
1005
+ // was silently dropped from global defaults, so it was unavailable at
1006
+ // user scope AND inert at project scope on the /gsd-quick path.
1007
+ research_before_questions: (globalDefaults['research_before_questions'])
1008
+ ?? globalDefaults['workflow']?.['research_before_questions']
1009
+ ?? defaults.research_before_questions,
850
1010
  parallelization: (globalDefaults['parallelization']) ?? defaults.parallelization,
851
1011
  text_mode: (globalDefaults['text_mode']) ?? defaults.text_mode,
852
1012
  resolve_model_ids: (globalDefaults['resolve_model_ids']) ?? defaults.resolve_model_ids,
@@ -906,8 +1066,12 @@ module.exports = {
906
1066
  CONFIG_DEFAULTS,
907
1067
  _getConfigDefault,
908
1068
  _getNestedConfigDefault,
1069
+ _getConfigValue,
1070
+ _getConfigNested,
909
1071
  _deepMergeConfig,
910
1072
  _warnedUnknownConfigKeys,
1073
+ _warnedShadowedGlobalKeys,
1074
+ GLOBAL_DEFAULTS_RESOLUTION_KEYS,
911
1075
  _warnUnknownProfileOverrides,
912
1076
  _resetRuntimeWarningCacheForTests,
913
1077
  _warnedConfigKeys,
@@ -16,6 +16,9 @@ const node_os_1 = __importDefault(require("node:os"));
16
16
  const io = require("./io.cjs");
17
17
  const { output, error, ERROR_REASON } = io;
18
18
  // eslint-disable-next-line @typescript-eslint/no-require-imports
19
+ const cliExitMod = require("./cli-exit.cjs");
20
+ const { ExitError } = cliExitMod;
21
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
19
22
  const configLoader = require("./config-loader.cjs");
20
23
  const { CONFIG_DEFAULTS } = configLoader;
21
24
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
@@ -31,6 +34,13 @@ const { VALID_CONFIG_KEYS, isValidConfigKey, getCapabilityConfigSchema } = confi
31
34
  const secrets_cjs_1 = require("./secrets.cjs");
32
35
  const review_reviewer_selection_cjs_1 = require("./review-reviewer-selection.cjs");
33
36
  const configuration_cjs_1 = require("./configuration.cjs");
37
+ // #3760: the ADR-1411 out-of-band diagnostic. It lives here rather than inside
38
+ // `migrateOnDisk` because `configuration.cjs` must stay loadable from an install
39
+ // layout holding only itself plus its manifests (#3571) — see the note at the top
40
+ // of configuration.cts.
41
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
42
+ const unusableInputModule = require("./unusable-input.cjs");
43
+ const { UNUSABLE_REASON, warnUnusableInput } = unusableInputModule;
34
44
  // ─── Constants ────────────────────────────────────────────────────────────────
35
45
  const CONFIG_KEY_SUGGESTIONS = {
36
46
  'workflow.nyquist_validation_enabled': 'workflow.nyquist_validation',
@@ -68,9 +78,21 @@ const SCHEMA_DEFAULTS = {
68
78
  'planner.stall_detect_interval_minutes': 5,
69
79
  'planner.stall_threshold_minutes': 10,
70
80
  'git.create_tag': true,
81
+ // #1689: per-plan agent_hint executor routing — default-on. A no-op for plans
82
+ // without an agent_hint field, so existing dispatch is byte-identical.
83
+ 'workflow.agent_hint_routing': true,
71
84
  // Derived from the defaults manifest rather than restated, so the manifest
72
85
  // stays the single source of truth for the smart-zone budget (#2630).
73
86
  'workflow.smart_zone_tokens': CONFIG_DEFAULTS.smart_zone_tokens,
87
+ // #2971: /gsd:pr-branch reads this key directly; an absent key must resolve to the
88
+ // manifest default rather than "Key not found". Derived from the defaults manifest so
89
+ // the manifest stays the single source of truth.
90
+ 'planning.pr_strict': CONFIG_DEFAULTS.pr_strict,
91
+ // #3801: execute-plan reads this key on every run; an absent key must resolve
92
+ // to the manifest default (2) rather than "Key not Found" — previously the
93
+ // effective default existed only as the workflow's shell fallback and the
94
+ // docs disagreed (settings-advanced said 3). Manifest stays the one owner.
95
+ 'workflow.inline_plan_threshold': CONFIG_DEFAULTS.inline_plan_threshold,
74
96
  };
75
97
  /**
76
98
  * Resolve a schema-level default for an absent key (#2256). Checks the legacy
@@ -118,6 +140,35 @@ function validateKnownConfigKeyPath(keyPath) {
118
140
  error(`Unknown config key: ${keyPath}. Did you mean ${suggested}?`, ERROR_REASON.CONFIG_INVALID_KEY);
119
141
  }
120
142
  }
143
+ /**
144
+ * Is `value` an acceptable `git.protected_branches` list (#3552)?
145
+ *
146
+ * A non-empty array whose every element is a string with non-whitespace
147
+ * content. Exported so a property test can pin this predicate against the
148
+ * resolver's own per-entry filter in `git-base-branch.cts` — `config-set` must
149
+ * only accept lists the resolver will honour in full, with nothing rejected.
150
+ * The two are deliberately different shapes (all-or-nothing here, per-entry
151
+ * there, because a direct file edit bypasses this check), so nothing keeps them
152
+ * agreeing except a test that asks both.
153
+ */
154
+ function isValidProtectedBranches(value) {
155
+ if (!Array.isArray(value) || value.length === 0)
156
+ return false;
157
+ const entries = value;
158
+ // Index, do NOT use `.every()`. `.every()` SKIPS holes, so a sparse array
159
+ // (`["main", , "develop"]`) passed this check while the resolver's `for...of`
160
+ // — which yields `undefined` for a hole — rejected that element. The two
161
+ // surfaces then disagreed about the same value. JSON cannot express a hole,
162
+ // so neither surface meets one in production, but "unreachable" is not a
163
+ // reason to leave two definitions of the same predicate contradicting each
164
+ // other (round-4 external review).
165
+ for (let i = 0; i < entries.length; i += 1) {
166
+ const branch = entries[i];
167
+ if (typeof branch !== 'string' || branch.trim().length === 0)
168
+ return false;
169
+ }
170
+ return true;
171
+ }
121
172
  function validateShipPrBodySections(value) {
122
173
  if (!Array.isArray(value)) {
123
174
  error('Invalid ship.pr_body_sections value. Expected a JSON array of section objects.');
@@ -652,7 +703,7 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
652
703
  const val = value;
653
704
  validateKnownConfigKeyPath(kp);
654
705
  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);
706
+ 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
707
  }
657
708
  // Parse value (handle booleans, numbers, and JSON arrays/objects)
658
709
  let parsedValue = val;
@@ -736,12 +787,23 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
736
787
  error(`Invalid workflow.post_planning_gaps '${val}'. Must be a boolean (true or false).`);
737
788
  }
738
789
  }
790
+ // Per-plan executor routing via agent_hint frontmatter (#1689)
791
+ if (kp === 'workflow.agent_hint_routing') {
792
+ if (typeof parsedValue !== 'boolean') {
793
+ error(`Invalid workflow.agent_hint_routing '${val}'. Must be a boolean (true or false).`);
794
+ }
795
+ }
739
796
  // #3086 — git.create_tag: boolean only
740
797
  if (kp === 'git.create_tag') {
741
798
  if (typeof parsedValue !== 'boolean') {
742
799
  error(`Invalid git.create_tag '${val}'. Must be a boolean (true or false).`);
743
800
  }
744
801
  }
802
+ if (kp === 'git.protected_branches') {
803
+ if (!isValidProtectedBranches(parsedValue)) {
804
+ error(`Invalid git.protected_branches '${val}'. Must be a non-empty array of non-empty branch names.`);
805
+ }
806
+ }
745
807
  if (kp === 'ship.pr_body_sections') {
746
808
  validateShipPrBodySections(parsedValue);
747
809
  }
@@ -917,6 +979,16 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
917
979
  }
918
980
  }
919
981
  catch (err) {
982
+ // ADR-3889: error() now throws ExitError (carries no message) instead of
983
+ // calling process.exit() directly. The message-sniffing check below
984
+ // (`.startsWith('No config.json')`) can never match an ExitError raised
985
+ // by the "no config.json" error() call above it — ExitError.message
986
+ // defaults to `process exit ${code}` when no message is passed — so
987
+ // without this unconditional guard that ExitError falls through and gets
988
+ // re-wrapped as a WRONG reason (CONFIG_PARSE_FAILED instead of
989
+ // CONFIG_NO_FILE) with a nonsense message, plus a duplicate stderr write.
990
+ if (err instanceof ExitError)
991
+ throw err;
920
992
  if (err.message.startsWith('No config.json'))
921
993
  throw err;
922
994
  error('Failed to read config.json: ' + err.message, ERROR_REASON.CONFIG_PARSE_FAILED);
@@ -1111,16 +1183,42 @@ function cmdConfigPath(cwd, _raw, workstreamContext = null) {
1111
1183
  */
1112
1184
  function cmdMigrateConfig(cwd, raw) {
1113
1185
  const ws = process.env['GSD_WORKSTREAM'] || null;
1114
- const report = (0, configuration_cjs_1.migrateOnDisk)(cwd, ws || undefined);
1186
+ // #3749: resolve the migration target through the project-aware resolver so
1187
+ // GSD_PROJECT scopes the write; migrateOnDisk itself cannot (see its
1188
+ // configPathOverride note).
1189
+ const scopedConfigPath = node_path_1.default.join(planningDir(cwd, ws || undefined), 'config.json');
1190
+ const report = (0, configuration_cjs_1.migrateOnDisk)(cwd, ws || undefined, scopedConfigPath);
1191
+ // #3760: deduplicated on (path, reason), so a repeated invocation stays quiet.
1192
+ if (report.skipped.length > 0) {
1193
+ warnUnusableInput({
1194
+ reason: UNUSABLE_REASON.CONFIG_SECTION_NOT_OBJECT,
1195
+ source: node_path_1.default.join(planningDir(cwd, ws || undefined), 'config.json'),
1196
+ });
1197
+ }
1115
1198
  if (raw) {
1116
- if (!report.migrated) {
1199
+ // #3760: a refused migration is NOT an already-canonical config. Reporting
1200
+ // "no legacy keys found" when a legacy key was found and declined would send
1201
+ // the user away believing there is nothing to fix — and the thing to fix is
1202
+ // the one thing only they can fix, by hand.
1203
+ const declined = report.skipped;
1204
+ const declinedLines = declined.map(s => ` ${s.from} → ${s.to} SKIPPED: '${s.section}' holds a ${s.sectionType}, not an object`);
1205
+ if (!report.migrated && declined.length === 0) {
1117
1206
  const msg = 'No legacy keys found — config is already canonical.';
1118
1207
  output(msg, true, msg);
1119
1208
  }
1209
+ else if (!report.migrated) {
1210
+ const lines = [
1211
+ 'Not migrated — every legacy key found was left in place:',
1212
+ ...declinedLines,
1213
+ 'Fix the section by hand, then re-run. Nothing was written.',
1214
+ ].join('\n');
1215
+ output(lines, true, lines);
1216
+ }
1120
1217
  else {
1121
1218
  const lines = [
1122
1219
  `Migrated: ${String(report.wrote)}`,
1123
1220
  ...report.normalizations.map(n => ` ${n.from} → ${n.to}`),
1221
+ ...declinedLines,
1124
1222
  ].join('\n');
1125
1223
  output(lines, true, lines);
1126
1224
  }
@@ -1142,4 +1240,5 @@ module.exports = {
1142
1240
  // Exported for programmatic use by capability-writer and tests
1143
1241
  setConfigValue,
1144
1242
  setConfigValues,
1243
+ isValidProtectedBranches,
1145
1244
  };