@opengsd/gsd-core 1.11.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 (395) 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 +1 -1
  5. package/agents/gsd-debugger.md +1 -1
  6. package/agents/gsd-dom-verifier.md +169 -0
  7. package/agents/gsd-eval-auditor.md +1 -1
  8. package/agents/gsd-executor.md +17 -9
  9. package/agents/gsd-framework-selector.md +1 -3
  10. package/agents/gsd-intel-updater.md +1 -1
  11. package/agents/gsd-mempalace-curator.md +0 -1
  12. package/agents/gsd-pattern-mapper.md +11 -0
  13. package/agents/gsd-phase-researcher.md +3 -1
  14. package/agents/gsd-plan-checker.md +15 -55
  15. package/agents/gsd-planner.md +6 -4
  16. package/agents/gsd-project-researcher.md +1 -1
  17. package/agents/gsd-research-synthesizer.md +2 -2
  18. package/agents/gsd-roadmapper.md +15 -11
  19. package/agents/gsd-ui-checker.md +63 -4
  20. package/agents/gsd-ui-researcher.md +41 -3
  21. package/agents/gsd-verifier.md +1 -1
  22. package/bin/install.js +609 -134
  23. package/commands/gsd/discuss-phase.md +1 -1
  24. package/commands/gsd/import.md +1 -1
  25. package/commands/gsd/quick.md +8 -4
  26. package/gsd-core/bin/gsd-tools.cjs +567 -51
  27. package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
  28. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  29. package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
  30. package/gsd-core/bin/lib/api-coverage.cjs +30 -9
  31. package/gsd-core/bin/lib/artifacts.cjs +2 -0
  32. package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
  33. package/gsd-core/bin/lib/audit.cjs +163 -41
  34. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  35. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  36. package/gsd-core/bin/lib/capability-registry.cjs +336 -95
  37. package/gsd-core/bin/lib/capability-state.cjs +18 -3
  38. package/gsd-core/bin/lib/capability-validator.cjs +205 -18
  39. package/gsd-core/bin/lib/check-command-router.cjs +145 -5
  40. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  41. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  42. package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
  43. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  44. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  45. package/gsd-core/bin/lib/commands.cjs +543 -44
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
  47. package/gsd-core/bin/lib/config-loader.cjs +118 -29
  48. package/gsd-core/bin/lib/config.cjs +92 -2
  49. package/gsd-core/bin/lib/configuration.cjs +129 -37
  50. package/gsd-core/bin/lib/core-utils.cjs +84 -7
  51. package/gsd-core/bin/lib/edge-probe.cjs +9 -1
  52. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  53. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  54. package/gsd-core/bin/lib/frontmatter.cjs +840 -305
  55. package/gsd-core/bin/lib/gap-checker.cjs +27 -3
  56. package/gsd-core/bin/lib/git-base-branch.cjs +174 -39
  57. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
  58. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +6 -3
  59. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
  60. package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
  61. package/gsd-core/bin/lib/host-integration.cjs +39 -6
  62. package/gsd-core/bin/lib/init-command-router.cjs +118 -21
  63. package/gsd-core/bin/lib/init.cjs +120 -41
  64. package/gsd-core/bin/lib/install-engine.cjs +68 -3
  65. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -1
  66. package/gsd-core/bin/lib/install-profiles.cjs +78 -4
  67. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  68. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  69. package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
  70. package/gsd-core/bin/lib/intel.cjs +101 -26
  71. package/gsd-core/bin/lib/io.cjs +160 -15
  72. package/gsd-core/bin/lib/learnings.cjs +85 -14
  73. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  74. package/gsd-core/bin/lib/markdown-table.cjs +52 -4
  75. package/gsd-core/bin/lib/milestone.cjs +90 -5
  76. package/gsd-core/bin/lib/model-catalog.cjs +177 -19
  77. package/gsd-core/bin/lib/model-resolver.cjs +10 -28
  78. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  79. package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
  80. package/gsd-core/bin/lib/phase-id.cjs +70 -4
  81. package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
  82. package/gsd-core/bin/lib/phase-locator.cjs +138 -17
  83. package/gsd-core/bin/lib/phase.cjs +405 -84
  84. package/gsd-core/bin/lib/plan-document.cjs +263 -0
  85. package/gsd-core/bin/lib/plan-scan.cjs +13 -2
  86. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  87. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  88. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -14
  89. package/gsd-core/bin/lib/planning-workspace.cjs +56 -0
  90. package/gsd-core/bin/lib/probe-core.cjs +4 -1
  91. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  92. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  93. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  94. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
  95. package/gsd-core/bin/lib/review-lane-descriptor.cjs +9 -9
  96. package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
  97. package/gsd-core/bin/lib/roadmap-parser.cjs +79 -16
  98. package/gsd-core/bin/lib/roadmap.cjs +74 -19
  99. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +96 -8
  100. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +34 -1
  101. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +287 -55
  102. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  103. package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
  104. package/gsd-core/bin/lib/shell-command-projection.cjs +71 -8
  105. package/gsd-core/bin/lib/smart-entry.cjs +12 -22
  106. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  107. package/gsd-core/bin/lib/state-command-router.cjs +47 -18
  108. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  109. package/gsd-core/bin/lib/state-document.cjs +186 -0
  110. package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
  111. package/gsd-core/bin/lib/state-transition.cjs +517 -101
  112. package/gsd-core/bin/lib/state.cjs +946 -163
  113. package/gsd-core/bin/lib/surface.cjs +10 -2
  114. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  115. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  116. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  117. package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
  118. package/gsd-core/bin/lib/uat.cjs +1376 -125
  119. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  120. package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
  121. package/gsd-core/bin/lib/unusable-input.cjs +13 -0
  122. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  123. package/gsd-core/bin/lib/vendor/README.md +43 -5
  124. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  125. package/gsd-core/bin/lib/verification.cjs +14 -1
  126. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  127. package/gsd-core/bin/lib/verify.cjs +95 -40
  128. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  129. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  130. package/gsd-core/bin/lib/worktree-safety.cjs +177 -21
  131. package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
  132. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  133. package/gsd-core/bin/shared/exit-codes.json +8 -0
  134. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  135. package/gsd-core/bin/shared/model-catalog.json +8 -1
  136. package/gsd-core/references/agent-contracts.md +3 -2
  137. package/gsd-core/references/api-coverage.md +24 -2
  138. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  139. package/gsd-core/references/checkpoints.md +37 -19
  140. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  141. package/gsd-core/references/edge-probe.md +8 -0
  142. package/gsd-core/references/execute-mvp-tdd.md +1 -3
  143. package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
  144. package/gsd-core/references/execute-phase-wave-guard.md +11 -9
  145. package/gsd-core/references/failing-direction.md +78 -0
  146. package/gsd-core/references/gate-prompts.md +1 -1
  147. package/gsd-core/references/git-integration.md +5 -5
  148. package/gsd-core/references/git-planning-commit.md +3 -3
  149. package/gsd-core/references/gsd-run-resolver.md +1 -1
  150. package/gsd-core/references/loop-hook-dispatch.md +22 -0
  151. package/gsd-core/references/model-profiles.md +1 -1
  152. package/gsd-core/references/nyquist-compliance.md +74 -0
  153. package/gsd-core/references/offer-next.md +3 -5
  154. package/gsd-core/references/phase-argument-parsing.md +3 -3
  155. package/gsd-core/references/planner-failing-direction.md +53 -0
  156. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  157. package/gsd-core/references/planner-revision.md +1 -1
  158. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  159. package/gsd-core/references/planning-config.md +37 -8
  160. package/gsd-core/references/reviewer-instances.md +31 -0
  161. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  162. package/gsd-core/references/tdd.md +1 -3
  163. package/gsd-core/references/ui-brand.md +65 -21
  164. package/gsd-core/references/ui-consideration-probe.md +1 -1
  165. package/gsd-core/references/universal-anti-patterns.md +2 -2
  166. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  167. package/gsd-core/references/verify-mvp-mode.md +1 -1
  168. package/gsd-core/references/workstream-flag.md +11 -11
  169. package/gsd-core/templates/README.md +1 -1
  170. package/gsd-core/templates/SECURITY.md +3 -3
  171. package/gsd-core/templates/UI-SPEC.md +25 -3
  172. package/gsd-core/templates/VALIDATION.md +3 -3
  173. package/gsd-core/templates/phase-prompt.md +3 -0
  174. package/gsd-core/templates/state.md +7 -0
  175. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  176. package/gsd-core/workflows/add-backlog.md +1 -1
  177. package/gsd-core/workflows/add-phase.md +3 -3
  178. package/gsd-core/workflows/add-tests.md +3 -8
  179. package/gsd-core/workflows/add-todo.md +1 -1
  180. package/gsd-core/workflows/ai-integration-phase.md +4 -9
  181. package/gsd-core/workflows/audit-fix.md +12 -3
  182. package/gsd-core/workflows/audit-milestone.md +9 -9
  183. package/gsd-core/workflows/audit-uat.md +17 -2
  184. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  185. package/gsd-core/workflows/autonomous.md +10 -26
  186. package/gsd-core/workflows/check-todos.md +1 -1
  187. package/gsd-core/workflows/cleanup.md +2 -2
  188. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
  189. package/gsd-core/workflows/code-review-fix.md +1 -1
  190. package/gsd-core/workflows/code-review.md +121 -40
  191. package/gsd-core/workflows/complete-milestone.md +15 -10
  192. package/gsd-core/workflows/debug.md +5 -3
  193. package/gsd-core/workflows/diagnose-issues.md +12 -6
  194. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  195. package/gsd-core/workflows/discuss-phase/modes/chain.md +3 -7
  196. package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
  197. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  198. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -2
  199. package/gsd-core/workflows/discuss-phase.md +1 -1
  200. package/gsd-core/workflows/do.md +3 -6
  201. package/gsd-core/workflows/docs-update.md +5 -4
  202. package/gsd-core/workflows/edit-phase.md +1 -1
  203. package/gsd-core/workflows/eval-review.md +4 -9
  204. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  205. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +113 -11
  206. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  207. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  208. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  209. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +22 -4
  210. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  211. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  212. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
  213. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  214. package/gsd-core/workflows/execute-phase.md +38 -54
  215. package/gsd-core/workflows/execute-plan.md +17 -12
  216. package/gsd-core/workflows/explore.md +1 -1
  217. package/gsd-core/workflows/extract-learnings.md +1 -1
  218. package/gsd-core/workflows/fast.md +2 -2
  219. package/gsd-core/workflows/forensics.md +1 -1
  220. package/gsd-core/workflows/graduation.md +5 -5
  221. package/gsd-core/workflows/health.md +3 -6
  222. package/gsd-core/workflows/import.md +14 -11
  223. package/gsd-core/workflows/inbox.md +4 -5
  224. package/gsd-core/workflows/ingest-docs.md +44 -11
  225. package/gsd-core/workflows/insert-phase.md +5 -5
  226. package/gsd-core/workflows/list-seeds.md +5 -3
  227. package/gsd-core/workflows/list-workspaces.md +1 -1
  228. package/gsd-core/workflows/manager.md +12 -23
  229. package/gsd-core/workflows/map-codebase.md +1 -1
  230. package/gsd-core/workflows/milestone-summary.md +1 -1
  231. package/gsd-core/workflows/mvp-phase.md +2 -2
  232. package/gsd-core/workflows/new-milestone.md +9 -21
  233. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  234. package/gsd-core/workflows/new-project.md +12 -26
  235. package/gsd-core/workflows/new-workspace.md +1 -1
  236. package/gsd-core/workflows/next.md +2 -2
  237. package/gsd-core/workflows/pause-work.md +1 -1
  238. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  239. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  240. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
  241. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
  242. package/gsd-core/workflows/plan-phase.md +121 -42
  243. package/gsd-core/workflows/plan-review-convergence.md +46 -9
  244. package/gsd-core/workflows/plant-seed.md +2 -2
  245. package/gsd-core/workflows/pr-branch.md +187 -51
  246. package/gsd-core/workflows/profile-user.md +16 -14
  247. package/gsd-core/workflows/progress.md +27 -12
  248. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  249. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -3
  250. package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
  251. package/gsd-core/workflows/quick/steps/research-phase.md +2 -4
  252. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  253. package/gsd-core/workflows/quick.md +20 -29
  254. package/gsd-core/workflows/remove-phase.md +4 -4
  255. package/gsd-core/workflows/remove-workspace.md +2 -2
  256. package/gsd-core/workflows/resume-project.md +8 -12
  257. package/gsd-core/workflows/review.md +193 -15
  258. package/gsd-core/workflows/scan.md +1 -1
  259. package/gsd-core/workflows/secure-phase.md +2 -2
  260. package/gsd-core/workflows/settings-advanced.md +7 -9
  261. package/gsd-core/workflows/settings-integrations.md +64 -31
  262. package/gsd-core/workflows/settings.md +3 -5
  263. package/gsd-core/workflows/ship.md +12 -6
  264. package/gsd-core/workflows/sketch-wrap-up.md +11 -17
  265. package/gsd-core/workflows/sketch.md +12 -18
  266. package/gsd-core/workflows/smart-entry.md +3 -5
  267. package/gsd-core/workflows/spec-phase.md +23 -1
  268. package/gsd-core/workflows/spike-wrap-up.md +7 -11
  269. package/gsd-core/workflows/spike.md +20 -31
  270. package/gsd-core/workflows/stats.md +2 -2
  271. package/gsd-core/workflows/sync-skills.md +1 -1
  272. package/gsd-core/workflows/thread.md +11 -7
  273. package/gsd-core/workflows/transition.md +5 -5
  274. package/gsd-core/workflows/ui-phase.md +10 -16
  275. package/gsd-core/workflows/ui-review.md +6 -10
  276. package/gsd-core/workflows/ultraplan-phase.md +5 -13
  277. package/gsd-core/workflows/undo.md +8 -16
  278. package/gsd-core/workflows/update.md +6 -10
  279. package/gsd-core/workflows/validate-phase.md +2 -2
  280. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  281. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  282. package/gsd-core/workflows/verify-work.md +57 -18
  283. package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
  284. package/hooks/dist/gsd-config-reload.js +18 -12
  285. package/hooks/dist/gsd-context-monitor.js +19 -10
  286. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  287. package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
  288. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  289. package/hooks/dist/gsd-cursor-stop.js +2 -1
  290. package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
  291. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
  292. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  293. package/hooks/dist/gsd-graphify-update.sh +22 -18
  294. package/hooks/dist/gsd-node-runner.sh +76 -0
  295. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  296. package/hooks/dist/gsd-prompt-guard.js +16 -7
  297. package/hooks/dist/gsd-read-guard.js +16 -7
  298. package/hooks/dist/gsd-read-injection-scanner.js +17 -8
  299. package/hooks/dist/gsd-session-state.sh +1 -0
  300. package/hooks/dist/gsd-statusline.js +215 -26
  301. package/hooks/dist/gsd-validate-commit.sh +80 -6
  302. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  303. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  304. package/hooks/dist/gsd-workflow-guard.js +34 -16
  305. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  306. package/hooks/dist/gsd-write-guard.js +35 -25
  307. package/hooks/dist/lib/cli-exit.js +560 -0
  308. package/hooks/dist/lib/exit-code-registry.js +98 -0
  309. package/hooks/dist/lib/git-probe.js +84 -0
  310. package/hooks/dist/lib/hook-exit.js +81 -0
  311. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  312. package/hooks/gsd-agent-isolation-guard.js +77 -38
  313. package/hooks/gsd-config-reload.js +18 -12
  314. package/hooks/gsd-context-monitor.js +19 -10
  315. package/hooks/gsd-cursor-post-tool.js +3 -1
  316. package/hooks/gsd-cursor-pre-tool.js +3 -1
  317. package/hooks/gsd-cursor-session-start.js +2 -1
  318. package/hooks/gsd-cursor-stop.js +2 -1
  319. package/hooks/gsd-cursor-subagent-start.js +28 -23
  320. package/hooks/gsd-cursor-subagent-stop.js +3 -1
  321. package/hooks/gsd-ensure-canonical-path.js +2 -1
  322. package/hooks/gsd-graphify-update.sh +22 -18
  323. package/hooks/gsd-node-runner.sh +76 -0
  324. package/hooks/gsd-phase-boundary.sh +1 -0
  325. package/hooks/gsd-prompt-guard.js +16 -7
  326. package/hooks/gsd-read-guard.js +16 -7
  327. package/hooks/gsd-read-injection-scanner.js +17 -8
  328. package/hooks/gsd-session-state.sh +1 -0
  329. package/hooks/gsd-statusline.js +215 -26
  330. package/hooks/gsd-validate-commit.sh +80 -6
  331. package/hooks/gsd-windsurf-pre-command.js +16 -11
  332. package/hooks/gsd-windsurf-pre-write.js +22 -13
  333. package/hooks/gsd-workflow-guard.js +34 -16
  334. package/hooks/gsd-worktree-path-guard.js +36 -21
  335. package/hooks/gsd-write-guard.js +35 -25
  336. package/hooks/lib/cli-exit.js +560 -0
  337. package/hooks/lib/exit-code-registry.js +98 -0
  338. package/hooks/lib/git-probe.js +84 -0
  339. package/hooks/lib/hook-exit.js +81 -0
  340. package/hooks/managed-hooks-registry.cjs +3 -0
  341. package/package.json +12 -7
  342. package/scripts/base64-scan.sh +74 -12
  343. package/scripts/build-hooks.js +5 -0
  344. package/scripts/check-glossary-refs.cjs +77 -15
  345. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  346. package/scripts/ci-check-job-near-cap.cjs +49 -0
  347. package/scripts/ci-pr-mergeability.cjs +262 -0
  348. package/scripts/ci-test-scope.cjs +45 -12
  349. package/scripts/ci-timeout-report.cjs +230 -0
  350. package/scripts/docs-guard-registry.cjs +396 -0
  351. package/scripts/gen-capability-registry.cjs +8 -6
  352. package/scripts/gen-exit-code-docs.cjs +318 -0
  353. package/scripts/gen-exit-code-registry.cjs +891 -0
  354. package/scripts/gen-features.cjs +836 -0
  355. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  356. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  357. package/scripts/gen-loop-host-contract.cjs +134 -1
  358. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  359. package/scripts/gen-state-md-docs.cjs +727 -0
  360. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  361. package/scripts/lib/ci-job-timing.cjs +72 -0
  362. package/scripts/lib/cli-exit.cjs +546 -44
  363. package/scripts/lib/drift-scan.cjs +32 -2
  364. package/scripts/lib/exit-code-registry.cjs +98 -0
  365. package/scripts/lib/ndjson-reporter.cjs +119 -0
  366. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  367. package/scripts/lint-docs-guard-registration.cjs +495 -0
  368. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
  369. package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
  370. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  371. package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
  372. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  373. package/scripts/lint-phase-enumeration-drift.cjs +21 -8
  374. package/scripts/lint-planning-prompt-drift.cjs +38 -1
  375. package/scripts/lint-removed-but-needed.cjs +184 -16
  376. package/scripts/lint-seam-enforcement.cjs +182 -0
  377. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  378. package/scripts/lint-source-test-name-collision.cjs +241 -0
  379. package/scripts/lint-state-write-path-drift.cjs +337 -432
  380. package/scripts/lint-test-file-count.allowlist.json +122 -4
  381. package/scripts/lint-test-file-count.cjs +25 -3
  382. package/scripts/lint-unreachable-guard-drift.cjs +51 -64
  383. package/scripts/lint-vendored-deps.cjs +208 -35
  384. package/scripts/mutation-matrix.cjs +599 -50
  385. package/scripts/prompt-injection-scan.sh +75 -14
  386. package/scripts/secret-scan.sh +75 -13
  387. package/scripts/select-docs-guards.cjs +56 -0
  388. package/scripts/sync-runtime-launcher.cjs +22 -3
  389. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  390. package/skills/gsd-import/SKILL.md +1 -1
  391. package/skills/gsd-quick/SKILL.md +8 -4
  392. package/vscode/package.json +1 -1
  393. package/bin/lib/ui-safety-gate.cjs +0 -109
  394. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  395. package/scripts/state-write-path-drift-baseline.json +0 -19
@@ -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 io = require("./io.cjs");
26
- const { output, error } = io;
26
+ const { output, error, formatDiagnosticToken } = io;
27
27
  const pattern_cjs_1 = require("./pattern.cjs");
28
28
  // eslint-disable-next-line @typescript-eslint/no-require-imports
29
29
  const planningWorkspace = require("./planning-workspace.cjs");
@@ -54,6 +54,17 @@ function parseRequirements(reqMd) {
54
54
  // The **ID** is extracted from the bullet text caller-side — the seam provides
55
55
  // the raw text; we parse the bold-ID prefix from it here.
56
56
  const boldIdRe = new RegExp(`^\\*\\*(${ID_PATTERN})\\*\\*\\s*(.*)$`);
57
+ // The shipped template (gsd-core/templates/requirements.md) writes
58
+ // `- [ ] **AUTH-01**: User can sign up` — a single separator delimiter
59
+ // between the bold ID and the description. Strip AT MOST ONE leading
60
+ // delimiter (plus its surrounding whitespace) before the final `.trim()`,
61
+ // mirroring roadmap-parser.cts's `stripLeadingDelimiter` delimiter set
62
+ // (em dash, en dash, colon, hyphen) — that helper is not exported, and its
63
+ // own `+`-quantified strip removes an entire delimiter RUN, which would
64
+ // also eat a second, meaningful marker (`**X-01**: -- weird` must keep the
65
+ // `--`), so the set is mirrored here with a single-occurrence match instead
66
+ // of reused verbatim.
67
+ const ONE_LEADING_DELIMITER_RE = /^\s*[—–:-]\s*/;
57
68
  for (const bullet of (0, markdown_sectionizer_cjs_1.iterateBullets)(reqMd)) {
58
69
  if (bullet.marker !== 'checkbox-unchecked' && bullet.marker !== 'checkbox-checked')
59
70
  continue;
@@ -65,7 +76,8 @@ function parseRequirements(reqMd) {
65
76
  continue;
66
77
  if (!seen.has(id)) {
67
78
  seen.add(id);
68
- out.push({ id, text: (m[2] || '').trim() });
79
+ const rawText = m[2] || '';
80
+ out.push({ id, text: rawText.replace(ONE_LEADING_DELIMITER_RE, '').trim() });
69
81
  }
70
82
  }
71
83
  // Pipe-table-row path and separator-row skip stay caller-side
@@ -290,6 +302,7 @@ function runGapAnalysis(cwd, phaseDir, options = {}) {
290
302
  table: '',
291
303
  summary: 'workflow.post_planning_gaps disabled — skipping post-planning gap analysis',
292
304
  counts: { total: 0, covered: 0, uncovered: 0 },
305
+ phase_dir_read_error: null,
293
306
  };
294
307
  }
295
308
  const absPhaseDir = node_path_1.default.isAbsolute(phaseDir) ? phaseDir : node_path_1.default.join(cwd, phaseDir);
@@ -313,11 +326,18 @@ function runGapAnalysis(cwd, phaseDir, options = {}) {
313
326
  // Read the phase directory once; reuse the listing for both context detection
314
327
  // and plan-file enumeration (avoids redundant readdirSync calls).
315
328
  let phaseDirFiles = [];
329
+ // #3885 (ADR-3473 §8.5): the existsSync guard above already means a catch
330
+ // here is NEVER "genuinely absent" (ENOENT) — this directory exists, so any
331
+ // failure to list it is a real read error (EACCES/EIO/...) and must be
332
+ // named, not folded into the same `[]` an absent directory produces.
333
+ let phaseDirReadError = null;
316
334
  try {
317
335
  if (node_fs_1.default.existsSync(absPhaseDir))
318
336
  phaseDirFiles = node_fs_1.default.readdirSync(absPhaseDir);
319
337
  }
320
- catch { /* unreadable */ }
338
+ catch (err) {
339
+ phaseDirReadError = `Could not read phase directory ${formatDiagnosticToken(absPhaseDir)}: ${formatDiagnosticToken(err?.message ?? String(err))}`;
340
+ }
321
341
  // #3511-class: scope the raw listing to this phase dir before the
322
342
  // phase-numbered -CONTEXT.md predicate. `phaseDirFiles` itself stays raw —
323
343
  // it is also reused below only as a `.length > 0` guard ahead of
@@ -384,6 +404,7 @@ function runGapAnalysis(cwd, phaseDir, options = {}) {
384
404
  table: formatGapTable(rows) + '\n' + coverageSummary + '\n\n' + mismatchMsg,
385
405
  summary: coverageSummary + '; extracted 0 of N — possible format mismatch',
386
406
  counts: { total: rows.length, covered, uncovered },
407
+ phase_dir_read_error: phaseDirReadError,
387
408
  };
388
409
  }
389
410
  return {
@@ -392,6 +413,7 @@ function runGapAnalysis(cwd, phaseDir, options = {}) {
392
413
  table: mismatchMsg,
393
414
  summary: 'extracted 0 of N — possible format mismatch',
394
415
  counts: { total: 0, covered: 0, uncovered: 0 },
416
+ phase_dir_read_error: phaseDirReadError,
395
417
  };
396
418
  }
397
419
  // #1365: if no items at all, surface a clean no-check message.
@@ -408,6 +430,7 @@ function runGapAnalysis(cwd, phaseDir, options = {}) {
408
430
  table: '## Post-Planning Gap Analysis\n\nNo requirements or decisions to check.\n',
409
431
  summary: 'no requirements or decisions to check',
410
432
  counts: { total: 0, covered: 0, uncovered: 0 },
433
+ phase_dir_read_error: phaseDirReadError,
411
434
  };
412
435
  }
413
436
  const rows = sortRows([
@@ -425,6 +448,7 @@ function runGapAnalysis(cwd, phaseDir, options = {}) {
425
448
  table: formatGapTable(rows) + '\n' + summary + '\n',
426
449
  summary,
427
450
  counts: { total: rows.length, covered, uncovered },
451
+ phase_dir_read_error: phaseDirReadError,
428
452
  };
429
453
  }
430
454
  function cmdGapAnalysis(cwd, args, raw) {
@@ -34,57 +34,134 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
34
34
  return (mod && mod.__esModule) ? mod : { "default": mod };
35
35
  };
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
- exports.readConfigBaseBranch = readConfigBaseBranch;
37
+ exports._readGitNested = _readGitNested;
38
+ exports._readGitKey = _readGitKey;
38
39
  exports.trySymbolicRef = trySymbolicRef;
39
40
  exports.tryRemoteShow = tryRemoteShow;
40
41
  exports.tryLocalBranch = tryLocalBranch;
41
42
  exports.resolveBaseBranchDiagnostics = resolveBaseBranchDiagnostics;
42
43
  exports.resolveBaseBranch = resolveBaseBranch;
44
+ exports.resolveProtectedBranchStatus = resolveProtectedBranchStatus;
43
45
  exports.gitWorktreeInfoInternal = gitWorktreeInfoInternal;
44
46
  exports.phaseStartCommit = phaseStartCommit;
45
47
  exports.changedFilesSince = changedFilesSince;
46
48
  exports.cmdGitBaseBranch = cmdGitBaseBranch;
47
- const node_fs_1 = __importDefault(require("node:fs"));
48
49
  const node_path_1 = __importDefault(require("node:path"));
50
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
51
+ const configLoader = require("./config-loader.cjs");
52
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
53
+ const io = require("./io.cjs");
54
+ const configuration_cjs_1 = require("./configuration.cjs");
55
+ const security_cjs_1 = require("./security.cjs");
49
56
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
50
- // ─── Helpers ──────────────────────────────────────────────────────────────────
57
+ const { error, ERROR_REASON } = io;
58
+ const { loadConfig: loadConfigSeam } = configLoader;
59
+ /** Render a rejected config value for a diagnostic without throwing on exotic input. */
60
+ function renderRejected(value) {
61
+ try {
62
+ return (0, security_cjs_1.sanitizeLabel)(JSON.stringify(value) ?? String(value));
63
+ }
64
+ catch {
65
+ return (0, security_cjs_1.sanitizeLabel)(String(value));
66
+ }
67
+ }
68
+ /** Nested-only read of `git.<field>`, mirroring `loadConfigResolved`'s `getNested`. */
69
+ function _readGitNested(config, field) {
70
+ const git = config['git'];
71
+ if (git !== null && typeof git === 'object' && !Array.isArray(git)) {
72
+ return git[field];
73
+ }
74
+ return undefined;
75
+ }
76
+ /**
77
+ * Flat-then-nested read, mirroring `loadConfigResolved`'s own `get()`.
78
+ *
79
+ * Used for `base_branch` ONLY, and only because it HAS a legacy flat spelling:
80
+ * `normalizeLegacyKeys` normally hoists it away, so a flat key that SURVIVED
81
+ * normalization is one the migration refused (a non-object `git` section,
82
+ * #3760) and is the user's last remaining expression of intent.
83
+ *
84
+ * `protected_branches` deliberately does NOT use this. It is new in #3552 with
85
+ * no legacy form, so honouring a top-level spelling would invent an
86
+ * undocumented alias that silently outranks the canonical nested key
87
+ * (round-4 external review).
88
+ */
89
+ function _readGitKey(config, field) {
90
+ if (config[field] !== undefined)
91
+ return config[field];
92
+ return _readGitNested(config, field);
93
+ }
51
94
  /**
52
- * Safely look up `git.base_branch` from the project's config.json.
53
- * Returns the configured value (a non-empty, non-null string) or null.
95
+ * Read the effective root/workstream configuration once for branch policy.
96
+ *
97
+ * Production takes the `loadConfig` branch, with `persist: false` — this is a
98
+ * PREDICATE, invoked on every `execute-phase` and every `ship` run, and a
99
+ * question must not rewrite the file it is asking about. Without it, any project
100
+ * carrying a legacy flat key (`base_branch`, `branching_strategy`, `depth`, …)
101
+ * has `.planning/config.json` silently normalized and rewritten by a call whose
102
+ * entire contract is to answer a boolean (#3648 review Blocker 1).
103
+ *
104
+ * The `readFile` branch is a unit-test seam, NOT a second production path, and
105
+ * it is deliberately narrower than `loadConfig`. It covers exactly two of
106
+ * production's steps — `normalizeLegacyKeys`, then the flat-then-nested lookup
107
+ * — over a single `<cwd>/.planning/config.json`. It does NOT apply
108
+ * root/workstream `_deepMergeConfig`, builtin or `~/.gsd/defaults.json`
109
+ * defaults, or `mergeFederatedConfig`. Tests that assert on any of those must
110
+ * drive `loadConfig` instead; the seam's own tests are scoped to normalization
111
+ * and shape validation, which is all it reproduces (#3648 review Major 3).
54
112
  */
55
- function readConfigBaseBranch(planningDir, deps) {
56
- const readFile = deps?.readFile ??
57
- ((p) => { try {
58
- return node_fs_1.default.readFileSync(p, 'utf8');
113
+ function readEffectiveGitConfig(cwd, deps) {
114
+ let config = {};
115
+ if (deps?.readFile && !deps.loadConfig) {
116
+ const raw = deps.readFile(node_path_1.default.join(cwd, '.planning', 'config.json'));
117
+ if (raw) {
118
+ try {
119
+ const parsed = JSON.parse(raw);
120
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
121
+ const { parsed: normalized } = (0, configuration_cjs_1.normalizeLegacyKeys)(parsed);
122
+ config = {
123
+ base_branch: _readGitKey(normalized, 'base_branch'),
124
+ protected_branches: _readGitNested(normalized, 'protected_branches'),
125
+ };
126
+ }
127
+ }
128
+ catch { /* malformed direct edit contributes no policy values */ }
59
129
  }
60
- catch {
61
- return null;
62
- } });
63
- const configPath = node_path_1.default.join(planningDir, 'config.json');
64
- const raw = readFile(configPath);
65
- if (!raw)
66
- return null;
67
- let cfg;
68
- try {
69
- cfg = JSON.parse(raw);
70
130
  }
71
- catch {
72
- return null;
131
+ else {
132
+ try {
133
+ config = (deps?.loadConfig ?? loadConfigSeam)(cwd, { persist: false });
134
+ }
135
+ catch { /* configuration loading is fail-soft for branch resolution */ }
73
136
  }
74
- if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg))
75
- return null;
76
- const top = cfg;
77
- // Support both "git.base_branch" (nested) and "base_branch" (flat legacy)
78
- const gitSection = top.git;
79
- if (gitSection && typeof gitSection === 'object' && !Array.isArray(gitSection)) {
80
- const nested = gitSection.base_branch;
81
- if (typeof nested === 'string' && nested.trim())
82
- return nested.trim();
137
+ const rawBaseBranch = config.base_branch;
138
+ const baseBranch = typeof rawBaseBranch === 'string' && rawBaseBranch.trim()
139
+ ? rawBaseBranch.trim()
140
+ : null;
141
+ // A protection predicate must not fail OPEN. `config-set` validation is
142
+ // bypassable by a direct edit of .planning/config.json, so one bad element
143
+ // discarding the whole list would silently answer "not protected" for names
144
+ // the user believes are protected — the exact failure #3552 exists to close,
145
+ // reintroduced through a different door (#3648 review Blocker 3). Drop only
146
+ // the bad elements, and report every rejection so it cannot pass unnoticed.
147
+ const rawProtectedBranches = config.protected_branches;
148
+ const protectedBranches = [];
149
+ const rejectedProtectedBranches = [];
150
+ if (Array.isArray(rawProtectedBranches)) {
151
+ for (const branch of rawProtectedBranches) {
152
+ if (typeof branch === 'string' && branch.trim() !== '') {
153
+ protectedBranches.push(branch.trim());
154
+ }
155
+ else {
156
+ rejectedProtectedBranches.push(renderRejected(branch));
157
+ }
158
+ }
159
+ }
160
+ else if (rawProtectedBranches !== undefined && rawProtectedBranches !== null) {
161
+ // Not a list at all — contributes no names, but is still a misconfiguration.
162
+ rejectedProtectedBranches.push(renderRejected(rawProtectedBranches));
83
163
  }
84
- const flat = top.base_branch;
85
- if (typeof flat === 'string' && flat.trim())
86
- return flat.trim();
87
- return null;
164
+ return { baseBranch, protectedBranches, rejectedProtectedBranches };
88
165
  }
89
166
  /**
90
167
  * Try `git symbolic-ref --short refs/remotes/origin/HEAD` (no network).
@@ -170,7 +247,7 @@ function tryLocalBranch(cwd, execGit) {
170
247
  * Consults the full precedence ladder and always returns a non-empty string.
171
248
  * Never throws.
172
249
  */
173
- function resolveBaseBranchDiagnostics(cwd, deps) {
250
+ function resolveBaseBranchDiagnosticsWithConfig(cwd, configured, deps) {
174
251
  const rawExecGit = deps?.execGit ?? shell_command_projection_cjs_1.execGit;
175
252
  // A genuine execGit failure (timeout, or the call could not even spawn —
176
253
  // e.g. git missing, surfaced as exitCode 127 with `error` set) is distinct
@@ -186,10 +263,7 @@ function resolveBaseBranchDiagnostics(cwd, deps) {
186
263
  anyGitFailure = true;
187
264
  return r;
188
265
  };
189
- // Derive .planning dir relative to cwd (mirrors planningDir() in planning-workspace.cjs)
190
- const planningDir = node_path_1.default.join(cwd, '.planning');
191
266
  // 1. Config override
192
- const configured = readConfigBaseBranch(planningDir, deps);
193
267
  if (configured)
194
268
  return { branch: configured, verified: true };
195
269
  // 2. symbolic-ref (fast, no network)
@@ -210,6 +284,10 @@ function resolveBaseBranchDiagnostics(cwd, deps) {
210
284
  // not answer (#3057 B4).
211
285
  return { branch: 'main', verified: !anyGitFailure };
212
286
  }
287
+ function resolveBaseBranchDiagnostics(cwd, deps) {
288
+ const { baseBranch } = readEffectiveGitConfig(cwd, deps);
289
+ return resolveBaseBranchDiagnosticsWithConfig(cwd, baseBranch, deps);
290
+ }
213
291
  /**
214
292
  * Resolve the default/base branch for the repository at `cwd`.
215
293
  *
@@ -220,6 +298,19 @@ function resolveBaseBranchDiagnostics(cwd, deps) {
220
298
  function resolveBaseBranch(cwd, deps) {
221
299
  return resolveBaseBranchDiagnostics(cwd, deps).branch;
222
300
  }
301
+ /** Resolve the base branch plus configured protected-branch extensions. */
302
+ function resolveProtectedBranchStatus(cwd, currentBranch, deps) {
303
+ const effectiveConfig = readEffectiveGitConfig(cwd, deps);
304
+ const { branch: baseBranch, verified } = resolveBaseBranchDiagnosticsWithConfig(cwd, effectiveConfig.baseBranch, deps);
305
+ const protectedBranches = [...new Set([baseBranch, ...effectiveConfig.protectedBranches])];
306
+ return {
307
+ baseBranch,
308
+ protectedBranches,
309
+ rejectedProtectedBranches: effectiveConfig.rejectedProtectedBranches,
310
+ isProtected: protectedBranches.includes(currentBranch),
311
+ verified,
312
+ };
313
+ }
223
314
  /**
224
315
  * Detect whether `cwd` sits inside a git worktree, and if so, return the
225
316
  * absolute path of the worktree root.
@@ -352,7 +443,51 @@ function changedFilesSince(cwd, sinceRef, execGit) {
352
443
  * Resolves the default branch and writes it to stdout (raw string, newline-terminated).
353
444
  * Called by workflows via `gsd_run query git.base-branch`.
354
445
  */
355
- function cmdGitBaseBranch(cwd, _args, deps) {
446
+ function cmdGitBaseBranch(cwd, args, deps) {
447
+ if (args[0] === '--is-protected') {
448
+ if (args.length > 2) {
449
+ error('Usage: git base-branch --is-protected [<branch>]', ERROR_REASON.USAGE);
450
+ }
451
+ const writeDiagnostic = deps?.writeDiagnostic ?? ((s) => process.stderr.write(s));
452
+ // `git branch --show-current` prints nothing on a detached HEAD, so the
453
+ // call sites legitimately pass an explicit empty string: no protected
454
+ // branch is named '', the answer is false, and that is not a fault worth
455
+ // reporting. The flag with NO argument is a different thing — a caller bug
456
+ // that `args[1] ?? ''` used to collapse into the detached-HEAD case. Same
457
+ // handling, but said out loud so the two can be told apart.
458
+ //
459
+ // This diagnostic deliberately does NOT state the answer. The empty branch
460
+ // matches no protected name, but the fail-closed guard below still renders
461
+ // `true` when the base branch could not be verified — so promising "false"
462
+ // here would contradict what this same call prints on stdout (#3648 review).
463
+ if (args.length < 2) {
464
+ writeDiagnostic(`⚠ git-base-branch: --is-protected was called without a branch argument; ` +
465
+ `treating it as an empty branch name. Pass the branch to test, ` +
466
+ `e.g. --is-protected "$CURRENT_BRANCH".\n`);
467
+ }
468
+ const status = resolveProtectedBranchStatus(cwd, args[1] ?? '', deps);
469
+ if (status.rejectedProtectedBranches.length > 0) {
470
+ writeDiagnostic(`⚠ git-base-branch: ignoring ${status.rejectedProtectedBranches.length} unusable ` +
471
+ `git.protected_branches entr${status.rejectedProtectedBranches.length === 1 ? 'y' : 'ies'} ` +
472
+ `(${status.rejectedProtectedBranches.join(', ')}) — each must be a non-empty branch name. ` +
473
+ `The remaining names are still enforced. See #3552.\n`);
474
+ }
475
+ // A protection guard must fail closed: if the base branch could not be
476
+ // verified against this repository (a git query timed out or failed to
477
+ // run — #3057 B4), report "protected" rather than silently trusting an
478
+ // unverified guess that might happen to not match the current branch.
479
+ const rendered = String(status.verified ? status.isProtected : true);
480
+ if (!status.verified) {
481
+ writeDiagnostic(`⚠ git-base-branch: --is-protected could not verify repository branch metadata; ` +
482
+ `defaulting to protected (fail-closed). See #3057.\n`);
483
+ }
484
+ const write = deps?.write ?? ((s) => process.stdout.write(s));
485
+ write(rendered + '\n');
486
+ return rendered;
487
+ }
488
+ if (args.length > 0) {
489
+ error(`Unknown flag for git.base-branch: ${args[0]}`, ERROR_REASON.USAGE);
490
+ }
356
491
  const { branch, verified } = resolveBaseBranchDiagnostics(cwd, deps);
357
492
  if (!verified) {
358
493
  const writeDiagnostic = deps?.writeDiagnostic ?? ((s) => process.stderr.write(s));
@@ -35,7 +35,7 @@ const healthDiagnosticMod = require("../health-diagnostic-types.cjs");
35
35
  const { SEVERITY, adviseRemedy } = healthDiagnosticMod;
36
36
  // eslint-disable-next-line @typescript-eslint/no-require-imports
37
37
  const phaseIdMod = require("../phase-id.cjs");
38
- const { isSentinelPhaseId } = phaseIdMod;
38
+ const { isSentinelPhaseDir } = phaseIdMod;
39
39
  // ─── C001 — gap in disk phase numbering (integer sequence) ────────────────
40
40
  // (verify.cts:1504-1519)
41
41
  function checkC001(snapshot) {
@@ -47,8 +47,12 @@ function checkC001(snapshot) {
47
47
  return [];
48
48
  const integerPhases = snapshot.allPhaseDirNames.value
49
49
  // #3225: exclude sentinel phase ids (999.x/0.x) — never part of the
50
- // sequential numbering, mirrors verify.cts:1510 verbatim.
51
- .filter((p) => !p.includes('.') && !isSentinelPhaseId(p))
50
+ // sequential numbering, mirrors verify.cts:1510 verbatim. The dot filter
51
+ // already drops every code-prefixed (bracket) name before the sentinel
52
+ // test, so the dir-aware recognizer below is defense-in-depth for the
53
+ // dotless legacy forms — kept so this guard cannot regress if the dot
54
+ // filter is ever loosened (#3639).
55
+ .filter((p) => !p.includes('.') && !isSentinelPhaseDir(p))
52
56
  .map((p) => parseInt(p, 10))
53
57
  .filter((n) => !Number.isNaN(n))
54
58
  .sort((a, b) => a - b);
@@ -104,7 +104,7 @@ const planningScopeMod = require("../planning-scope.cjs");
104
104
  const { SCOPE } = planningScopeMod;
105
105
  // eslint-disable-next-line @typescript-eslint/no-require-imports
106
106
  const phaseIdMod = require("../phase-id.cjs");
107
- const { matchPhaseDirs, normalizePhaseName, extractPhaseToken, isSentinelPhaseId } = phaseIdMod;
107
+ const { matchPhaseDirs, normalizePhaseName, extractPhaseToken, isSentinelPhaseId, isSentinelPhaseDir } = phaseIdMod;
108
108
  // eslint-disable-next-line @typescript-eslint/no-require-imports
109
109
  const validateMod = require("../validate.cjs");
110
110
  const { phaseVariants } = validateMod;
@@ -231,8 +231,11 @@ function checkW007(snapshot) {
231
231
  // `verify.cts:1373-1397`) — same token, relocated read, not reinvented.
232
232
  const token = extractPhaseToken(dirName);
233
233
  // #3225: a sentinel dir on disk (999-interim, 0-drafts) is defined as
234
- // never-on-roadmap; it must not trigger W007.
235
- if (isSentinelPhaseId(token))
234
+ // never-on-roadmap; it must not trigger W007. #3639: judged on the DIR
235
+ // NAME via the dir-aware recognizer — the extracted token is
236
+ // milestone-stripped, so a bracket sentinel (GSD.999-07-icebox) was
237
+ // invisible to the id predicate and false-fired as an orphan.
238
+ if (isSentinelPhaseDir(dirName))
236
239
  continue;
237
240
  if (claimedDirs.has(dirName))
238
241
  continue;
@@ -35,10 +35,8 @@
35
35
  * src/health-diagnostic-rules/worktree-health.cts, compiled to
36
36
  * gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs (gitignored).
37
37
  */
38
- var __importDefault = (this && this.__importDefault) || function (mod) {
39
- return (mod && mod.__esModule) ? mod : { "default": mod };
40
- };
41
- const node_path_1 = __importDefault(require("node:path"));
38
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
39
+ const shellCmdProjection = require("../shell-command-projection.cjs");
42
40
  // eslint-disable-next-line @typescript-eslint/no-require-imports
43
41
  const healthDiagnosticMod = require("../health-diagnostic-types.cjs");
44
42
  const { SEVERITY, adviseRemedy } = healthDiagnosticMod;
@@ -121,15 +119,31 @@ function checkW017(snapshot) {
121
119
  // against `process.cwd()`. Per this batch's brief: the interpolated command
122
120
  // (with the real path) lives in `message`; `remedy.args.command` stays a
123
121
  // static `<path>` template, mirroring the split the brief specifies.
122
+ // ─── #3663 — path-comparison provenance helper ─────────────────────────────
123
+ //
124
+ // snapshot.cwd is raw process-cwd-derived — path.resolve() normalizes
125
+ // separators and relative segments but NOT casing, and a process launched via
126
+ // a differently-cased path echoes that spelling back. finding.path is
127
+ // `git worktree list`-derived, which self-normalizes to the canonical
128
+ // on-disk casing (forward slashes). Comparing those two spellings strictly
129
+ // misclassifies the ACTIVE worktree as stale on win32 — so the comparison
130
+ // folds case ONLY on win32 (case-insensitive filesystem, via the Shell
131
+ // Command Projection seam's toComparablePathKey — the platform-conditional
132
+ // fold policy lives there, not per call site) and stays case-sensitive on
133
+ // POSIX, where differently-cased paths are genuinely different directories.
134
+ // No realpath resolution — that would change symlink matching behavior.
135
+ function isActiveWorktreePath(activeCwd, worktreePath, platform = process.platform) {
136
+ const active = shellCmdProjection.toComparablePathKey(activeCwd, platform);
137
+ const worktree = shellCmdProjection.toComparablePathKey(worktreePath, platform);
138
+ return active === worktree || active.startsWith(worktree + '/');
139
+ }
124
140
  function checkW027(snapshot) {
125
141
  const diagnostics = [];
126
142
  const activeCwd = snapshot.cwd;
127
143
  for (const finding of snapshot.worktreeHealth.value) {
128
144
  if (finding.kind !== 'stale')
129
145
  continue;
130
- const normalizedWorktree = node_path_1.default.resolve(finding.path);
131
- const isActiveWorktree = activeCwd === normalizedWorktree || activeCwd.startsWith(normalizedWorktree + node_path_1.default.sep);
132
- if (isActiveWorktree)
146
+ if (isActiveWorktreePath(activeCwd, finding.path))
133
147
  continue;
134
148
  diagnostics.push({
135
149
  code: 'W027',
@@ -170,4 +184,4 @@ const RULES = [
170
184
  check: checkW027,
171
185
  },
172
186
  ];
173
- module.exports = { RULES };
187
+ module.exports = { RULES, isActiveWorktreePath };
@@ -103,7 +103,7 @@ const CONSISTENCY_RULES = [
103
103
  // (`verify.cts:2405-2553`) — ported verbatim, not reinvented.
104
104
  // eslint-disable-next-line @typescript-eslint/no-require-imports
105
105
  const planningWorkspaceMod = require("./planning-workspace.cjs");
106
- const { planningRoot, planningDir } = planningWorkspaceMod;
106
+ const { planningDir } = planningWorkspaceMod;
107
107
  // eslint-disable-next-line @typescript-eslint/no-require-imports
108
108
  const configLoaderMod = require("./config-loader.cjs");
109
109
  const { CONFIG_DEFAULTS } = configLoaderMod;
@@ -113,6 +113,12 @@ const { getMilestoneInfo } = roadmapParserMod;
113
113
  // eslint-disable-next-line @typescript-eslint/no-require-imports
114
114
  const stateMod = require("./state.cjs");
115
115
  const { writeStateMd } = stateMod;
116
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
117
+ const frontmatter = require("./frontmatter.cjs");
118
+ const { extractFrontmatter } = frontmatter;
119
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
120
+ const stateTransitionMod = require("./state-transition.cjs");
121
+ const { rebuildStateTransaction } = stateTransitionMod;
116
122
  const clock_cjs_1 = require("./clock.cjs");
117
123
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
118
124
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
@@ -160,7 +166,13 @@ function evaluateConsistencyRules(snapshot) {
160
166
  * document for the read side.
161
167
  */
162
168
  function repairPaths(cwd) {
163
- const rootBase = planningRoot(cwd);
169
+ // #3749: the project root is PROJECT-aware but deliberately NOT workstream-
170
+ // scoped — under GSD_PROJECT, config.json/MILESTONES.md/milestones belong to
171
+ // `.planning/<project>/` (the same base init.new-project's config_path
172
+ // names); under GSD_WORKSTREAM they stay at the workstream PARENT, keeping
173
+ // the documented root-vs-workstream split. planningDir(cwd, null) is exactly
174
+ // that base: project env honored, ws env suppressed.
175
+ const rootBase = planningDir(cwd, null);
164
176
  const wsBase = planningDir(cwd);
165
177
  return {
166
178
  rootBase,
@@ -268,7 +280,15 @@ function runRepairAction(cwd, action, paths) {
268
280
  stateContent += `**Status:** Resuming\n\n`;
269
281
  stateContent += `## Session Log\n\n`;
270
282
  stateContent += `- ${clock_cjs_1.realClock.localToday()}: STATE.md regenerated by ${slash('health')} --repair\n`;
271
- writeStateMd(statePath, stateContent, cwd);
283
+ // ADR-3473 §8.6: `/gsd-health --repair` is a factory reset, so
284
+ // preservation must not run; the snapshot of the file being replaced is
285
+ // still captured, and `{}` is the correct, legal snapshot of a
286
+ // STATE.md that had no parseable frontmatter — which is the usual
287
+ // reason this repair fires.
288
+ const priorState = node_fs_1.default.existsSync(statePath) ? ((0, shell_command_projection_cjs_1.platformReadSync)(statePath) ?? '') : '';
289
+ writeStateMd(statePath, stateContent, rebuildStateTransaction({
290
+ snapshot: extractFrontmatter(priorState, statePath),
291
+ }), cwd);
272
292
  return { success: true, path: 'STATE.md', extraDetails };
273
293
  }
274
294
  case REMEDY_ACTION.ADD_NYQUIST_KEY:
@@ -653,10 +653,25 @@ function extensionEventSurfaceFor(extensionEvents) {
653
653
  * per-host branch ADR-1239 exists to remove. Omit `prompt` entirely and the
654
654
  * resolution is byte-identical to Phase 2's (the unconsumed-resolver shape).
655
655
  *
656
- * Argv order is base args → cwd flag → prompt, so the prompt stays the final
657
- * positional token for the hosts that read it that way.
656
+ * Argv order is base args → model flag → cwd flag → prompt, so the prompt
657
+ * stays the final positional token for the hosts that read it that way. The
658
+ * model flag is placed BEFORE the cwd flag (not after, and not appended at
659
+ * the very end) purely to keep it clear of that trailing positional — the
660
+ * model value itself is never the prompt-adjacent token a host might scan
661
+ * for last.
662
+ *
663
+ * `model` (Phase 4, #3714) is optional, descriptor-gated exactly like prompt:
664
+ * a `modelFlag` string on the descriptor names the flag that pins the
665
+ * spawned executor's model (codex: `--model`); `null`/absent means the host
666
+ * exposes no such override on this exec path, and `[modelFlag, model]` is
667
+ * appended ONLY when both the descriptor's `modelFlag` and the caller's
668
+ * `model` are non-empty strings. Omitting `model` entirely (or passing it to
669
+ * a host with no `modelFlag`) is byte-identical to the resolver's behavior
670
+ * before this parameter existed. This function decides no policy about WHICH
671
+ * model to pass or what 'inherit' means — that is entirely the caller's job;
672
+ * this seam only shapes descriptor + values into argv.
658
673
  */
659
- function resolveOrchestratorExec(orchestratorExec, cwd, prompt) {
674
+ function resolveOrchestratorExec(orchestratorExec, cwd, prompt, model) {
660
675
  if (!orchestratorExec || typeof orchestratorExec !== 'object' || Array.isArray(orchestratorExec)) {
661
676
  return { ok: false, reason: 'missing_command' };
662
677
  }
@@ -676,11 +691,22 @@ function resolveOrchestratorExec(orchestratorExec, cwd, prompt) {
676
691
  if (oe.promptFlag !== undefined && oe.promptFlag !== null && typeof oe.promptFlag !== 'string') {
677
692
  return { ok: false, reason: 'invalid_prompt_flag' };
678
693
  }
694
+ if (oe.modelFlag !== undefined && oe.modelFlag !== null && typeof oe.modelFlag !== 'string') {
695
+ return { ok: false, reason: 'invalid_model_flag' };
696
+ }
679
697
  // An executor spawned with no instruction is a hang, not a degraded run —
680
698
  // fail closed rather than launching a prompt-less process.
681
699
  if (prompt !== undefined && (typeof prompt !== 'string' || prompt.length === 0)) {
682
700
  return { ok: false, reason: 'invalid_prompt' };
683
701
  }
702
+ // Unlike `prompt` — where empty is a hang, not a degraded run, hence the
703
+ // fail-closed check above — an absent/null/empty model is simply "use the
704
+ // host default", the same benign degradation `cwdFlag: null` already
705
+ // expresses. Only a present-but-non-string value (number/bool/array/object)
706
+ // is a caller error; null/undefined/'' fall through to "omit the flag".
707
+ if (model !== undefined && model !== null && typeof model !== 'string') {
708
+ return { ok: false, reason: 'invalid_model' };
709
+ }
684
710
  // Leading-dash guard, mirroring worktree-safety.cts's `unsafe_leading_dash`
685
711
  // check on git arguments. A positional prompt (or a cwd) beginning with '-'
686
712
  // is parsed by the spawned CLI as a FLAG, not a value — the same failure the
@@ -695,10 +721,17 @@ function resolveOrchestratorExec(orchestratorExec, cwd, prompt) {
695
721
  if (cwd.startsWith('-')) {
696
722
  return { ok: false, reason: 'unsafe_leading_dash_cwd' };
697
723
  }
724
+ if (typeof model === 'string' && model.startsWith('-')) {
725
+ return { ok: false, reason: 'unsafe_leading_dash_model' };
726
+ }
698
727
  const baseArgs = Array.isArray(oe.args) ? [...oe.args] : [];
699
- const args = typeof oe.cwdFlag === 'string' && oe.cwdFlag.length > 0
700
- ? [...baseArgs, oe.cwdFlag, cwd]
701
- : baseArgs;
728
+ let args = baseArgs;
729
+ if (typeof oe.modelFlag === 'string' && oe.modelFlag.length > 0 && typeof model === 'string' && model.length > 0) {
730
+ args = [...args, oe.modelFlag, model];
731
+ }
732
+ args = typeof oe.cwdFlag === 'string' && oe.cwdFlag.length > 0
733
+ ? [...args, oe.cwdFlag, cwd]
734
+ : args;
702
735
  if (typeof prompt === 'string') {
703
736
  if (typeof oe.promptFlag === 'string' && oe.promptFlag.length > 0) {
704
737
  args.push(oe.promptFlag, prompt);