@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
@@ -14,8 +14,18 @@ exports.DYNAMIC_KEY_PATTERNS = exports.RUNTIME_STATE_KEYS = exports.VALID_CONFIG
14
14
  exports.normalizeLegacyKeys = normalizeLegacyKeys;
15
15
  exports.mergeDefaults = mergeDefaults;
16
16
  exports.migrateOnDisk = migrateOnDisk;
17
+ exports.isConfigSection = isConfigSection;
17
18
  const node_fs_1 = require("node:fs");
18
19
  const node_path_1 = require("node:path");
20
+ // ⚠️ DO NOT add a sibling `.cjs` import to this module. `configuration.cjs` must load
21
+ // from an install layout containing ONLY itself plus `bin/shared/*.manifest.json` —
22
+ // that is the #3571 contract, pinned by "co-located bin/shared manifests let
23
+ // configuration.cjs load without sdk/shared" in tests/install.test.cjs. A `require`
24
+ // for a sibling that the installer does not co-locate fails at load time with
25
+ // MODULE_NOT_FOUND. This is why #3760's out-of-band diagnostic is emitted by this
26
+ // module's CALLERS (`cmdMigrateConfig` in config.cts, `loadConfigResolved` in
27
+ // config-loader.cts) rather than here: `normalizeLegacyKeys` reports refusals
28
+ // in-band via `skipped[]`, which keeps it both pure AND dependency-free.
19
29
  // In .cts (CommonJS output) files, `require` is available as a global.
20
30
  const _require = require;
21
31
  // ─── Manifest requires ───────────────────────────────────────────────────────
@@ -109,42 +119,107 @@ function deepMergeConfig(base, overlay) {
109
119
  }
110
120
  return result;
111
121
  }
122
+ /** Type name for a report — `'array'` for arrays, otherwise `typeof`. */
123
+ function describeSectionType(value) {
124
+ return Array.isArray(value) ? 'array' : typeof value;
125
+ }
126
+ /**
127
+ * Is `value` usable as a config SECTION — something a nested key can be written into?
128
+ *
129
+ * Three cases, and the distinction between the second and third is the whole of #3760:
130
+ *
131
+ * - `null` / `undefined` — ABSENT. Not a section yet, but nothing is lost by creating one.
132
+ * Callers substitute `{}`; this predicate reports `false` and callers check for absence
133
+ * separately, so the two are never conflated.
134
+ * - a non-null, non-array `object` — a SECTION. Spreading it is safe and correct.
135
+ * - anything else (string, number, boolean, array) — a PRESENT NON-OBJECT, i.e. user data
136
+ * that a spread would destroy. `{...'main'}` is `{0:'m',1:'a',2:'i',3:'n'}`; `{...7}` is
137
+ * `{}`. An array is included here because `typeof [] === 'object'` and `{...['a']}` is
138
+ * `{0:'a'}` — the identical expansion a bare `typeof` guard would wave through.
139
+ *
140
+ * This is the nested-section analog of the top-level shape check ADR-227 already requires
141
+ * in `_readConfigFile` (`config-loader.cts`): valid JSON is not the same as a config object.
142
+ * It lives here, exported, rather than being copied into `config-loader.cts`, because two
143
+ * hand-rolled copies of one predicate is `DEFECT.GENERATIVE-FIX` by construction — the same
144
+ * reasoning that made `unusable-input.cts` a shared seam.
145
+ */
146
+ function isConfigSection(value) {
147
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
148
+ }
112
149
  // ─── Exported functions ───────────────────────────────────────────────────────
150
+ /**
151
+ * Hoist a legacy top-level key into its canonical nested section.
152
+ *
153
+ * Refuses — and reports the refusal — when the destination section is present but is not an
154
+ * object. Refusing means leaving BOTH the section and the legacy key exactly as they were and
155
+ * pushing no `Normalization`, so this block never marks the config dirty and nothing it
156
+ * touched can be written back. That is the #3760 contract: never expand, never drop, never
157
+ * persist a shape the user did not write.
158
+ */
159
+ function hoistLegacyKey(result, normalizations, skipped, legacyKey, section, field) {
160
+ const raw = result[section];
161
+ // `null`/`undefined` mean "no section yet" and have always been treated as absent here.
162
+ // They carry no user data, so creating the section loses nothing.
163
+ const absent = raw === null || raw === undefined;
164
+ if (!absent && !isConfigSection(raw)) {
165
+ skipped.push({
166
+ from: legacyKey,
167
+ to: `${section}.${field}`,
168
+ section,
169
+ reason: 'non_object_section',
170
+ value: result[legacyKey],
171
+ sectionType: describeSectionType(raw),
172
+ });
173
+ return;
174
+ }
175
+ // `raw` is already narrowed by the isConfigSection guard above.
176
+ const existing = absent ? {} : raw;
177
+ const value = result[legacyKey];
178
+ // Canonical nested wins when it is already set; otherwise the legacy value is hoisted.
179
+ result[section] = existing[field] === undefined
180
+ ? { ...existing, [field]: value }
181
+ : { ...existing };
182
+ delete result[legacyKey];
183
+ normalizations.push({ from: legacyKey, to: `${section}.${field}`, value });
184
+ }
113
185
  function normalizeLegacyKeys(parsed) {
114
186
  const result = { ...parsed };
115
187
  const normalizations = [];
188
+ const skipped = [];
116
189
  // 1. branching_strategy → git.branching_strategy
117
190
  if (Object.prototype.hasOwnProperty.call(result, 'branching_strategy')) {
118
- const value = result['branching_strategy'];
119
- const git = (result['git'] ?? {});
120
- if (git['branching_strategy'] === undefined) {
121
- result['git'] = { ...git, branching_strategy: value };
122
- }
123
- else {
124
- // canonical nested wins — just delete the stale top-level
125
- result['git'] = { ...git };
126
- }
127
- delete result['branching_strategy'];
128
- normalizations.push({ from: 'branching_strategy', to: 'git.branching_strategy', value });
191
+ hoistLegacyKey(result, normalizations, skipped, 'branching_strategy', 'git', 'branching_strategy');
129
192
  }
130
193
  // 2. top-level sub_repos → planning.sub_repos
131
194
  if (Object.prototype.hasOwnProperty.call(result, 'sub_repos')) {
132
- const value = result['sub_repos'];
133
- const planning = (result['planning'] ?? {});
134
- if (planning['sub_repos'] === undefined) {
135
- result['planning'] = { ...planning, sub_repos: value };
136
- }
137
- else {
138
- // canonical nested wins — just drop the stale top-level
139
- result['planning'] = { ...planning };
140
- }
141
- delete result['sub_repos'];
142
- normalizations.push({ from: 'sub_repos', to: 'planning.sub_repos', value });
195
+ hoistLegacyKey(result, normalizations, skipped, 'sub_repos', 'planning', 'sub_repos');
143
196
  }
144
197
  // 3. multiRepo: true → marker (filesystem detection deferred to migrateOnDisk / caller)
145
198
  if (result['multiRepo'] === true) {
146
- delete result['multiRepo'];
147
- normalizations.push({ from: 'multiRepo', to: 'planning.sub_repos', value: true, requiresFilesystem: true });
199
+ // #3760: refuse here too, for the same reason as blocks 1 and 2 — and it must be
200
+ // decided HERE, not in the caller. The caller is the one that runs filesystem
201
+ // detection, but whether `planning` can receive the result is knowable from the
202
+ // parsed config alone. Deciding it later meant `multiRepo` had already been
203
+ // deleted and a Normalization already pushed: the config was written, the
204
+ // sub_repos injection silently no-opped against the non-object section, and the
205
+ // user's `multiRepo: true` was consumed with nothing to show for it and no
206
+ // diagnostic. Refusing here keeps the marker, keeps the config clean of a
207
+ // migration that did not happen, and gives all three callers the same report.
208
+ const planning = result['planning'];
209
+ if (planning !== null && planning !== undefined && !isConfigSection(planning)) {
210
+ skipped.push({
211
+ from: 'multiRepo',
212
+ to: 'planning.sub_repos',
213
+ section: 'planning',
214
+ reason: 'non_object_section',
215
+ value: true,
216
+ sectionType: describeSectionType(planning),
217
+ });
218
+ }
219
+ else {
220
+ delete result['multiRepo'];
221
+ normalizations.push({ from: 'multiRepo', to: 'planning.sub_repos', value: true, requiresFilesystem: true });
222
+ }
148
223
  }
149
224
  // 4. top-level depth → granularity
150
225
  if (Object.prototype.hasOwnProperty.call(result, 'depth') && !Object.prototype.hasOwnProperty.call(result, 'granularity')) {
@@ -154,26 +229,36 @@ function normalizeLegacyKeys(parsed) {
154
229
  delete result['depth'];
155
230
  normalizations.push({ from: 'depth', to: 'granularity', value: mapped });
156
231
  }
157
- return { parsed: result, normalizations };
232
+ // 5. top-level base_branch → git.base_branch
233
+ if (Object.prototype.hasOwnProperty.call(result, 'base_branch')) {
234
+ hoistLegacyKey(result, normalizations, skipped, 'base_branch', 'git', 'base_branch');
235
+ }
236
+ return { parsed: result, normalizations, skipped };
158
237
  }
159
238
  function mergeDefaults(parsed) {
160
239
  // Start with a deep clone of defaults, then overlay parsed
161
240
  const defaults = structuredClone(CONFIG_DEFAULTS);
162
241
  return deepMergeConfig(defaults, parsed);
163
242
  }
164
- function migrateOnDisk(cwd, workstream) {
165
- const configPath = (0, node_path_1.join)(planningDir(cwd, workstream), 'config.json');
243
+ function migrateOnDisk(cwd, workstream, configPathOverride) {
244
+ // #3749: the caller (cmdMigrateConfig in config.cts) supplies the config
245
+ // path resolved through planning-workspace's PROJECT-aware planningDir.
246
+ // This module cannot import that sibling (#3571 install-layout contract),
247
+ // and its own local planningDir above is deliberately workstream-only —
248
+ // resolving here through the local copy made migrate-config under
249
+ // GSD_PROJECT rewrite the ROOT config instead of the scoped one.
250
+ const configPath = configPathOverride ?? (0, node_path_1.join)(planningDir(cwd, workstream), 'config.json');
166
251
  let raw;
167
252
  try {
168
253
  raw = (0, node_fs_1.readFileSync)(configPath, 'utf-8');
169
254
  }
170
255
  catch {
171
256
  // File missing — nothing to migrate
172
- return { migrated: false, normalizations: [], wrote: null };
257
+ return { migrated: false, normalizations: [], wrote: null, skipped: [] };
173
258
  }
174
259
  const trimmed = raw.trim();
175
260
  if (trimmed === '') {
176
- return { migrated: false, normalizations: [], wrote: null };
261
+ return { migrated: false, normalizations: [], wrote: null, skipped: [] };
177
262
  }
178
263
  let parsed;
179
264
  try {
@@ -181,23 +266,30 @@ function migrateOnDisk(cwd, workstream) {
181
266
  }
182
267
  catch {
183
268
  // Malformed — can't migrate
184
- return { migrated: false, normalizations: [], wrote: null };
185
- }
186
- const { parsed: normalized, normalizations } = normalizeLegacyKeys(parsed);
187
- if (normalizations.length === 0) {
188
- return { migrated: false, normalizations: [], wrote: null };
269
+ return { migrated: false, normalizations: [], wrote: null, skipped: [] };
189
270
  }
271
+ const { parsed: normalized, normalizations, skipped } = normalizeLegacyKeys(parsed);
190
272
  // Resolve multiRepo filesystem detection
191
273
  const result = { ...normalized };
192
274
  for (const norm of normalizations) {
193
275
  if (norm.requiresFilesystem) {
194
276
  const detected = detectSubRepos(cwd);
195
277
  if (detected.length > 0) {
196
- const planning = (result['planning'] ?? {});
197
- result['planning'] = { ...planning, sub_repos: detected, commit_docs: false };
278
+ // #3760: `requiresFilesystem` is pushed by block 3 only when `planning` was
279
+ // absent or an object, and nothing between there and here changes it — so
280
+ // `isConfigSection` picks between merge and create, and never has to discard.
281
+ const planning = result['planning'];
282
+ const existing = isConfigSection(planning) ? planning : {};
283
+ result['planning'] = { ...existing, sub_repos: detected, commit_docs: false };
198
284
  }
199
285
  }
200
286
  }
287
+ if (normalizations.length === 0) {
288
+ // Nothing changed — and that now includes the case where every legacy key was
289
+ // REFUSED. Returning early here is what keeps the corrupted-section input from
290
+ // reaching writeFileSync at all.
291
+ return { migrated: false, normalizations: [], wrote: null, skipped };
292
+ }
201
293
  try {
202
294
  (0, node_fs_1.writeFileSync)(configPath, JSON.stringify(result, null, 2));
203
295
  }
@@ -205,5 +297,5 @@ function migrateOnDisk(cwd, workstream) {
205
297
  const msg = err instanceof Error ? err.message : String(err);
206
298
  throw new Error(`Failed to write migrated config at ${configPath}: ${msg}`);
207
299
  }
208
- return { migrated: true, normalizations, wrote: configPath };
300
+ return { migrated: true, normalizations, wrote: configPath, skipped };
209
301
  }
@@ -15,6 +15,22 @@
15
15
  * - node:fs / node:path (stdlib)
16
16
  * - ./phase-id.cjs (comparePhaseNum, used by readSubdirectories)
17
17
  * - ./planning-workspace.cjs (findContextMdIn, used by getPhaseFileStats)
18
+ *
19
+ * #3883 (ADR-3473 §8.3): two of this module's cyclic partners require
20
+ * generateSlugInternal, the canonical slug formula:
21
+ * - phase-id.cjs requires this module directly.
22
+ * - planning-workspace.cjs is a cyclic partner via a longer path:
23
+ * core-utils.cjs -> planning-workspace.cjs -> active-workstream-store.cjs
24
+ * -> workstream-name-policy.cjs -> core-utils.cjs.
25
+ * Both are genuine circular requires. They are safe ONLY because every side
26
+ * accesses the other's exports lazily, through the live module-namespace
27
+ * object (`phaseIdModule.foo(...)` / `planningWorkspace.foo(...)`) inside a
28
+ * function body, never via a top-level destructure — a top-level
29
+ * `const { foo } = require(...)` copies the binding at import time and would
30
+ * silently capture `undefined` whichever module loses the load-order race.
31
+ * This is an absolute rule with no exception in this file: every cyclic
32
+ * partner's export is accessed through its module-namespace object, never
33
+ * destructured at the top level.
18
34
  */
19
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
20
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -23,12 +39,58 @@ const node_fs_1 = __importDefault(require("node:fs"));
23
39
  const node_path_1 = __importDefault(require("node:path"));
24
40
  // eslint-disable-next-line @typescript-eslint/no-require-imports
25
41
  const phaseIdModule = require("./phase-id.cjs");
26
- const { comparePhaseNum, scopeToPhase } = phaseIdModule;
27
42
  // eslint-disable-next-line @typescript-eslint/no-require-imports
28
43
  const planningWorkspace = require("./planning-workspace.cjs");
29
- const { findContextMdIn } = planningWorkspace;
30
44
  // eslint-disable-next-line @typescript-eslint/no-require-imports
31
45
  const shellCommandProjection = require("./shell-command-projection.cjs");
46
+ // ─── Line-ending normalization ─────────────────────────────────────────────────
47
+ /**
48
+ * Normalize every line ending in `content` to a bare `\n`, ONCE — the shared
49
+ * seam every document-READ boundary in this codebase should route through
50
+ * (#3707-CR follow-up MAJOR).
51
+ *
52
+ * CommonMark treats a lone CR (no paired LF) as a line ending — such a
53
+ * document RENDERS as separate lines to a human reader — but a parser that
54
+ * splits/tokenizes/scans on `\n` alone treats a lone-CR-separated document as
55
+ * ONE unbroken line, hiding every row boundary in it. `src/uat.cts` originally
56
+ * carried this exact fix as a PRIVATE, unexported function applied inside two
57
+ * of its own parse functions (`parseUatItemsWithStats`, `parseCurrentTest`) —
58
+ * which is why two OTHER read sites in the same module (`cmdAuditUat`'s
59
+ * VERIFICATION and deferred-items.md ingresses) were missed: normalizing
60
+ * per-parser means every new parser must remember to call it. Promoted here,
61
+ * to the shared leaf module every document consumer can reach without a new
62
+ * dependency edge, so normalization can be applied at the READ boundary
63
+ * instead — every current and future parser fed from a boundary that calls
64
+ * this gets normalized text by construction.
65
+ *
66
+ * `/\r\n?/g` is deliberately ONE alternation, not two separate replaces: a
67
+ * two-pass `replace(/\r\n/g,'\n').replace(/\r/g,'\n')` is equivalent here
68
+ * because the first pass already consumes every CRLF pair before the second
69
+ * pass ever runs, but a single regex avoids relying on pass ORDER and matches
70
+ * greedily left-to-right in one scan, so a CRLF is always consumed as ONE
71
+ * unit (never left as a stray trailing `\r` after the `\n` half is matched
72
+ * first) and a lone CR — including one immediately followed by nothing, i.e.
73
+ * at EOF, or by another lone CR — is still replaced.
74
+ *
75
+ * This is deliberately NOT a length-preserving transform (CRLF, two UTF-16
76
+ * units, becomes LF, one), so any offsets a caller computes must be compared
77
+ * only against THIS normalized string, never against the original raw text.
78
+ *
79
+ * U+2028 LINE SEPARATOR / U+2029 PARAGRAPH SEPARATOR (#3078-CR) are
80
+ * DELIBERATELY NOT folded here, unlike `\r`/`\r\n`. Folding is unnecessary:
81
+ * `String.prototype.split('\n')` never treats U+2028/U+2029 as a delimiter,
82
+ * so an exotic separator can never manufacture a fake line start for a
83
+ * consumer that scans lines produced by `split('\n')`, rather than anchoring
84
+ * a multiline (`/m`) regex directly over unsplit text. Only the latter
85
+ * pattern is vulnerable to the ECMA-262 LineTerminator set including these
86
+ * two code points. This module performs no line-anchored matching of its
87
+ * own; a caller that scans lines should split first and match per-line
88
+ * rather than anchor `/m` over unsplit text — this comment makes no claim
89
+ * about whether any particular caller currently does so.
90
+ */
91
+ function normalizeLineEndings(content) {
92
+ return content.replace(/\r\n?/g, '\n');
93
+ }
32
94
  // ─── Path helpers ────────────────────────────────────────────────────────────
33
95
  /**
34
96
  * Normalize a relative path to always use forward slashes (cross-platform).
@@ -108,7 +170,19 @@ function pathExistsInternal(cwd, targetPath) {
108
170
  return false;
109
171
  }
110
172
  }
111
- function generateSlugInternal(text) {
173
+ /**
174
+ * #3883 (ADR-3473 §8.3 remediation): `maxLen` lets a caller state its own
175
+ * truncation contract instead of being forced into this function's
176
+ * historical 60-char cap. Some call sites truncated at 60 before the #3883
177
+ * consolidation (commands.cts:cmdGenerateSlug) and some never truncated at
178
+ * all (phase-id.cts toDir/getPhaseDirFromPhaseId, the init.cts/phase-locator
179
+ * phase_slug sites, workstream-name-policy.cts toWorkstreamSlug) — collapsing
180
+ * every caller onto a single hard-coded 60 introduced two identity
181
+ * collisions (distinct >60-char names/phase-slugs truncating to the same
182
+ * value) that did not exist pre-migration. `maxLen: 60` remains the default
183
+ * so untouched callers keep prior behavior; pass `null` for no truncation.
184
+ */
185
+ function generateSlugInternal(text, maxLen = 60) {
112
186
  if (!text)
113
187
  return null;
114
188
  // #2849: strip leading/trailing hyphens AFTER truncation, not only before.
@@ -116,7 +190,9 @@ function generateSlugInternal(text) {
116
190
  // the strip step exists to prevent. Truncation cannot add a leading hyphen, so
117
191
  // running the full ^-+|-+$ pass last is equivalent for leading hyphens and
118
192
  // fixes the trailing-hyphen-after-truncation case.
119
- return transliterateForSlug(text).replace(/[^a-z0-9]+/g, '-').substring(0, 60).replace(/^-+|-+$/g, '');
193
+ const collapsed = transliterateForSlug(text).replace(/[^a-z0-9]+/g, '-');
194
+ const truncated = maxLen === null ? collapsed : collapsed.substring(0, maxLen);
195
+ return truncated.replace(/^-+|-+$/g, '');
120
196
  }
121
197
  // ─── Transliteration (#2848) ─────────────────────────────────────────────────
122
198
  //
@@ -215,12 +291,12 @@ function getPhaseFileStats(phaseDir) {
215
291
  scope: scan.scope,
216
292
  };
217
293
  }
218
- const scopedFiles = scopeToPhase(files, node_path_1.default.basename(phaseDir));
294
+ const scopedFiles = phaseIdModule.scopeToPhase(files, node_path_1.default.basename(phaseDir));
219
295
  return {
220
296
  plans: scan.planFiles,
221
297
  summaries: scan.summaryFiles,
222
298
  hasResearch: scopedFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'),
223
- hasContext: findContextMdIn(scopedFiles) !== null,
299
+ hasContext: planningWorkspace.findContextMdIn(scopedFiles) !== null,
224
300
  hasVerification: scopedFiles.some(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md'),
225
301
  hasReviews: scopedFiles.some(f => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md'),
226
302
  scope: scan.scope,
@@ -235,7 +311,7 @@ function readSubdirectories(dirPath, sort = false) {
235
311
  try {
236
312
  const entries = node_fs_1.default.readdirSync(dirPath, { withFileTypes: true });
237
313
  const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
238
- return sort ? dirs.sort((a, b) => comparePhaseNum(a, b)) : dirs;
314
+ return sort ? dirs.sort((a, b) => phaseIdModule.comparePhaseNum(a, b)) : dirs;
239
315
  }
240
316
  catch {
241
317
  return [];
@@ -415,6 +491,7 @@ function findOrphanSummaries(planFiles, summaryFiles) {
415
491
  }
416
492
  module.exports = {
417
493
  toPosixPath,
494
+ normalizeLineEndings,
418
495
  detectSubRepos,
419
496
  extractOneLinerFromBody,
420
497
  pathExistsInternal,
@@ -27,6 +27,9 @@ exports.validateResolution = validateResolution;
27
27
  exports.proposeEdges = proposeEdges;
28
28
  exports.analyzeCoverage = analyzeCoverage;
29
29
  const probe_core_cjs_1 = require("./probe-core.cjs");
30
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
31
+ const cliExitModule = require("./cli-exit.cjs");
32
+ const { runMain } = cliExitModule;
30
33
  /**
31
34
  * Word-boundary cues mapping requirement prose -> data/behavior shape.
32
35
  * Heuristic and intentionally lossy; an authored `shapes` array overrides it.
@@ -192,5 +195,10 @@ function analyzeCoverage(requirements, resolutions = []) {
192
195
  * `require.main === module` so it runs only when the compiled `.cjs` is executed directly.
193
196
  */
194
197
  if (require.main === module) {
195
- (0, probe_core_cjs_1.runProbeCli)((requirements, resolutions) => analyzeCoverage(requirements, resolutions), { usage: 'edge-probe.cjs <requirements.json> [resolutions.json]' });
198
+ // runProbeCli's default `exit` now throws ExitError (src/probe-core.cts) rather
199
+ // than calling process.exit directly, so this entry point must run under
200
+ // runMain to translate that throw into process.exitCode.
201
+ runMain(() => {
202
+ (0, probe_core_cjs_1.runProbeCli)((requirements, resolutions) => analyzeCoverage(requirements, resolutions), { usage: 'edge-probe.cjs <requirements.json> [resolutions.json]' });
203
+ });
196
204
  }
@@ -21,7 +21,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
21
21
  return (mod && mod.__esModule) ? mod : { "default": mod };
22
22
  };
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.CALIBRATION_FILENAME = void 0;
24
+ exports.PhasesUnreadableError = exports.CALIBRATION_FILENAME = void 0;
25
25
  exports.readSmartZoneBudget = readSmartZoneBudget;
26
26
  exports.readCalibrationSamples = readCalibrationSamples;
27
27
  exports.parseTokensFlag = parseTokensFlag;
@@ -39,9 +39,15 @@ const estimation = require("./phase-estimation.cjs");
39
39
  const planningWorkspace = require("./planning-workspace.cjs");
40
40
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
41
41
  const configLoader = require("./config-loader.cjs");
42
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-locator.cjs is an export= CommonJS module
43
+ const phaseLocator = require("./phase-locator.cjs");
44
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-scope.cjs is an export= CommonJS module
45
+ const planningScopeMod = require("./planning-scope.cjs");
42
46
  const { output, error, ERROR_REASON } = io;
43
47
  const { planningDir } = planningWorkspace;
44
48
  const { CONFIG_DEFAULTS } = configLoader;
49
+ const { listMilestonePhaseDirs } = phaseLocator;
50
+ const { SCOPE } = planningScopeMod;
45
51
  // WIN-1 parity (DEFECT.WINDOWS-FS-OPS): on Windows a concurrent reader, indexer,
46
52
  // or AV scanner can transiently hold the rename target open. Retry the transient
47
53
  // errnos with backoff, matching the writeLedger / writeConsentStore idiom.
@@ -192,6 +198,24 @@ function cmdEstimateCheck(cwd, args, raw) {
192
198
  sample_count: calibration.sampleCount,
193
199
  }, raw);
194
200
  }
201
+ /**
202
+ * Thrown by `collectCalibrationSamples` when the phases directory EXISTS but
203
+ * could not be read (EACCES/EIO, `scope: SCOPE.UNREADABLE`). #3882/ADR-3473
204
+ * §8.5: that is a NON-answer, not "zero completed phases" — silently
205
+ * returning `[]` here would make an unreadable directory output-identical to
206
+ * a genuinely empty one, which is exactly the defect class §8.5 exists to
207
+ * end. `cmdEstimateCalibrate` catches this and refuses the rebuild instead
208
+ * of persisting a phantom empty calibration.
209
+ */
210
+ class PhasesUnreadableError extends Error {
211
+ phasesRoot;
212
+ constructor(phasesRoot) {
213
+ super(`phases directory exists but could not be read: ${phasesRoot}`);
214
+ this.phasesRoot = phasesRoot;
215
+ this.name = 'PhasesUnreadableError';
216
+ }
217
+ }
218
+ exports.PhasesUnreadableError = PhasesUnreadableError;
195
219
  /**
196
220
  * Pair each completed phase's PLAN estimate with its SUMMARY actuals.
197
221
  *
@@ -199,18 +223,25 @@ function cmdEstimateCheck(cwd, args, raw) {
199
223
  * A plan with no `estimate` block, a summary with no `actuals`, or a malformed
200
224
  * value is skipped rather than guessed — a fabricated sample would silently
201
225
  * steer every future estimate.
226
+ *
227
+ * #3882 (ADR-3473 §8.2): this used to hand-roll a raw `readdirSync` over the
228
+ * phases directory, treating every directory (including sentinel phases —
229
+ * milestone 0 / 999, SENTINEL_RANGES) as a completed phase. A sentinel's
230
+ * PLAN/SUMMARY pair then silently contributed a phantom sample to the
231
+ * CALIBRATION FACTOR applied to every future estimate. Routed through the
232
+ * canonical owner (`listMilestonePhaseDirs`, `src/phase-locator.cts`) called
233
+ * with NO `cwd` — that combination is already "all milestone windows,
234
+ * sentinels excluded", exactly what this caller needs; no new API was
235
+ * required for this half (see the design doc's §3, corrected after
236
+ * measurement). It also inherits the `scope` discriminator for free, so an
237
+ * unreadable phases directory is no longer indistinguishable from a real
238
+ * empty one.
202
239
  */
203
240
  function collectCalibrationSamples(cwd) {
204
241
  const phasesRoot = node_path_1.default.join(planningDir(cwd), 'phases');
205
- let phases;
206
- try {
207
- phases = node_fs_1.default.readdirSync(phasesRoot, { withFileTypes: true })
208
- .filter((d) => d.isDirectory())
209
- .map((d) => d.name)
210
- .sort();
211
- }
212
- catch {
213
- return [];
242
+ const { value: phases, scope } = listMilestonePhaseDirs(phasesRoot);
243
+ if (scope === SCOPE.UNREADABLE) {
244
+ throw new PhasesUnreadableError(phasesRoot);
214
245
  }
215
246
  const readBlock = (file, key) => {
216
247
  let text;
@@ -277,7 +308,20 @@ function collectCalibrationSamples(cwd) {
277
308
  * next estimate reads the result.
278
309
  */
279
310
  function cmdEstimateCalibrate(cwd, _args, raw) {
280
- const samples = collectCalibrationSamples(cwd);
311
+ let samples;
312
+ try {
313
+ samples = collectCalibrationSamples(cwd);
314
+ }
315
+ catch (err) {
316
+ if (err instanceof PhasesUnreadableError) {
317
+ // #3882/ADR-3473 §8.5: refuse rather than silently rebuild the
318
+ // calibration document from a phantom empty sample set — an
319
+ // unreadable phases directory must never look output-identical to a
320
+ // project with genuinely zero completed phases.
321
+ error(err.message, ERROR_REASON.ESTIMATE_PHASES_UNREADABLE);
322
+ }
323
+ throw err;
324
+ }
281
325
  const calibration = estimation.computeCalibration(samples);
282
326
  const target = node_path_1.default.join(planningDir(cwd), exports.CALIBRATION_FILENAME);
283
327
  let written = true;
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ // GENERATED FILE — DO NOT EDIT BY HAND.
4
+ // Source of truth: gsd-core/bin/shared/exit-codes.json. Regenerate with:
5
+ // node scripts/gen-exit-code-registry.cjs --write
6
+ // This exact content is emitted to THREE locations — gsd-core/bin/lib/exit-code-registry.cjs,
7
+ // scripts/lib/exit-code-registry.cjs, and hooks/lib/exit-code-registry.js (the latter two
8
+ // committed so scripts/ and hooks/ consumers work on an unbuilt clone) — all byte-compared by
9
+ // `npm run lint:generated-sync` (#3905 ADR-3889 Phase 1; #3906 Phase 2 added the second copy;
10
+ // #3911 ADR-3889 Phase 7 added the hooks/lib/ copy).
11
+ //
12
+ // exitCodeFor(name) / nameForExitCode(code) are pure and total over this
13
+ // closed table — each throws for anything not registered here.
14
+
15
+ const EXIT_CODES = Object.freeze([
16
+ Object.freeze({
17
+ code: 2,
18
+ name: "HOOK_DENY",
19
+ meaning: "Hook protocol deny — the harness blocks the tool call",
20
+ owner: "hook-adapter",
21
+ authorizedBy: "ADR-3889",
22
+ }),
23
+ Object.freeze({
24
+ code: 64,
25
+ name: "USAGE",
26
+ meaning: "Caller error — bad argv, unknown subcommand, missing argument",
27
+ owner: "generic",
28
+ authorizedBy: "ADR-3889",
29
+ }),
30
+ Object.freeze({
31
+ code: 66,
32
+ name: "NO_INPUT",
33
+ meaning: "Ran; zero units were in scope, and that emptiness is known to be genuine",
34
+ owner: "generic",
35
+ authorizedBy: "ADR-3889",
36
+ }),
37
+ Object.freeze({
38
+ code: 69,
39
+ name: "UNAVAILABLE",
40
+ meaning: "Could not run — prerequisite absent, input unreadable, scope unestablished",
41
+ owner: "generic",
42
+ authorizedBy: "ADR-3889",
43
+ }),
44
+ Object.freeze({
45
+ code: 70,
46
+ name: "INTERNAL",
47
+ meaning: "Self-failure — crash, timeout, killed subprocess",
48
+ owner: "generic",
49
+ authorizedBy: "ADR-3889",
50
+ }),
51
+ Object.freeze({
52
+ code: 80,
53
+ name: "DEGRADED",
54
+ meaning: "Ran to completion and is reporting a condition through its result payload rather than as a process failure",
55
+ owner: "gsd-tools",
56
+ authorizedBy: "ADR-3889 + ADR-2980",
57
+ })
58
+ ]);
59
+
60
+ const NAME_TO_CODE = new Map(EXIT_CODES.map((entry) => [entry.name, entry.code]));
61
+ const CODE_TO_NAME = new Map(EXIT_CODES.map((entry) => [entry.code, entry.name]));
62
+
63
+ /**
64
+ * Resolve the registered exit code for a symbolic name. Pure, total: throws
65
+ * for anything not an exact, registered, exact-case key — including
66
+ * non-strings, the empty string, untrimmed strings, wrong case, and
67
+ * prototype-chain names like `__proto__`/`constructor`/`toString` (a Map
68
+ * lookup never touches the prototype chain, so these are indistinguishable
69
+ * from any other unregistered name).
70
+ *
71
+ * @param {string} name
72
+ * @returns {number}
73
+ */
74
+ function exitCodeFor(name) {
75
+ if (typeof name !== 'string' || name.length === 0) {
76
+ throw new Error(`exitCodeFor: name must be a non-empty string, received ${JSON.stringify(name)}`);
77
+ }
78
+ if (!NAME_TO_CODE.has(name)) {
79
+ throw new Error(`exitCodeFor: unregistered exit code name: ${JSON.stringify(name)}`);
80
+ }
81
+ return NAME_TO_CODE.get(name);
82
+ }
83
+
84
+ /**
85
+ * Reverse of exitCodeFor: resolve the symbolic name for a registered exit
86
+ * code. Pure, total: throws for anything not an exact, registered code.
87
+ *
88
+ * @param {number} code
89
+ * @returns {string}
90
+ */
91
+ function nameForExitCode(code) {
92
+ if (!CODE_TO_NAME.has(code)) {
93
+ throw new Error(`nameForExitCode: unregistered exit code: ${JSON.stringify(code)}`);
94
+ }
95
+ return CODE_TO_NAME.get(code);
96
+ }
97
+
98
+ module.exports = { EXIT_CODES, exitCodeFor, nameForExitCode };