@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
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * scripts/check-mutation-score-ratchet.cjs
6
+ *
7
+ * #3881 follow-up ("one YAML parser" mutation-matrix piece 3): makes each
8
+ * module's `minScore` floor (scripts/mutation-matrix.cjs COVERED) ratchet
9
+ * UPWARD instead of sitting wherever it was last hand-set.
10
+ *
11
+ * Two halves of "never lowerable without a reasoned marker":
12
+ * 1. `tests/mutation-matrix-ratchet.test.cjs`'s RATCHET_BASELINE already
13
+ * enforces the DOWN side: any change to a module's minScore — up or
14
+ * down — must land in the same diff as a matching RATCHET_BASELINE
15
+ * edit, making the change visible in code review (mirrors this repo's
16
+ * other shrink-only baselines, e.g. scripts/lint-unreachable-guard-drift.cjs's
17
+ * $comment convention: a number cannot move silently).
18
+ * 2. THIS script enforces the UP side: after a real CI Stryker run, if the
19
+ * module's ACHIEVED mutation score exceeds its declared floor by more
20
+ * than RATCHET_SLACK points, that is not "run-to-run variance" (this
21
+ * file's own header documents variance as 1-2 points; see
22
+ * scripts/mutation-matrix.cjs's "Floors are measured scores minus 1-2
23
+ * pts") — it is unclaimed headroom, and the floor should have been
24
+ * raised. Exits 1 telling the author exactly what to raise it to, so a
25
+ * module that improves cannot silently keep a stale, low bar forever.
26
+ *
27
+ * Consumes Stryker's `json` reporter output (mutation-testing-report-schema
28
+ * document; stryker.config.mjs's `reporters` includes `'json'`, default path
29
+ * `reports/mutation/mutation.json`) via `mutation-testing-metrics` — the same
30
+ * package Stryker itself uses internally to compute a report's score — rather
31
+ * than re-deriving killed/survived counts by hand.
32
+ *
33
+ * Wired into `.github/workflows/mutation.yml`'s `mutate` job as a step AFTER
34
+ * `npx stryker run`, gated on the shard having passed (a shard that failed
35
+ * MUTATION_BREAK is a floor problem, not a ratchet problem — Stryker's own
36
+ * exit code already reports that).
37
+ *
38
+ * CLI:
39
+ * node scripts/check-mutation-score-ratchet.cjs --module <name> [--report <path>] [--matrix <path>]
40
+ * `--report` defaults to reports/mutation/mutation.json (Stryker's own default).
41
+ * `--matrix` defaults to this repo's own scripts/mutation-matrix.cjs; it is an injectable seam
42
+ * so tests can point the CLI at a synthetic COVERED fixture instead of the real, live-ratcheting
43
+ * config (see tests/mutation-score-ratchet.test.cjs — a test asserting this script's fail/pass
44
+ * behaviour must never hardcode a real module's numeric floor, because that floor is exactly
45
+ * what this mechanism exists to move).
46
+ */
47
+
48
+ const fs = require('node:fs');
49
+ const path = require('node:path');
50
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
51
+
52
+ // Slack, in mutation-score points, above a module's declared minScore floor before this
53
+ // ratchet demands the floor be raised. Deliberately larger than the 1-2 point run-to-run
54
+ // TIMEOUT variance margin scripts/mutation-matrix.cjs's floors are already set with (its own
55
+ // header: "Floors are measured scores minus 1-2 pts for run-to-run variance") — that smaller
56
+ // margin is calibrated for the noise BETWEEN two runs of the SAME test list against the SAME
57
+ // source. RATCHET_SLACK instead has to tolerate the noise this file's own history already
58
+ // shows for the frontmatter module across genuine measurement events (63.35 -> 66.67 ->
59
+ // 60.58 -> pending, each a real CI measurement, not pure jitter, spanning >3 points on its
60
+ // own). 5 points sits comfortably above that observed band without being so wide that a
61
+ // module sitting well above TARGET_MUTATION_SCORE (80) could coast for years without its
62
+ // floor ever being asked to move.
63
+ const RATCHET_SLACK = 5;
64
+
65
+ /**
66
+ * Pure: does `achievedScore` (0-100) exceed `floor` (minScore) by more than `slack`?
67
+ * Returns `{ shouldRatchet, suggestedFloor }` — `suggestedFloor` follows the SAME formula
68
+ * scripts/mutation-matrix.cjs's own header documents for setting a floor from a measured
69
+ * score: `floor(measured) - 1`.
70
+ */
71
+ function evaluateRatchet(achievedScore, floor, slack = RATCHET_SLACK) {
72
+ const shouldRatchet = achievedScore - floor > slack;
73
+ return {
74
+ shouldRatchet,
75
+ suggestedFloor: shouldRatchet ? Math.floor(achievedScore) - 1 : floor,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Extract the achieved mutation score from a Stryker `json` reporter document via
81
+ * `mutation-testing-metrics` (the same library Stryker itself uses to compute scores).
82
+ * Throws a descriptive error if the document has no scoreable mutants at all (an empty
83
+ * `files` map, or every file having zero mutants) — that is a wiring bug in the caller
84
+ * (wrong report path, or the shard mutated nothing), never a score of 0.
85
+ */
86
+ function extractAchievedScore(report) {
87
+ // Lazy require: this dependency is only needed on the actual score-extraction path, not
88
+ // when this module is required purely for evaluateRatchet (used by unit tests without the
89
+ // mutation-testing-metrics package's schema-validation overhead).
90
+ const { calculateMutationTestMetrics } = require('mutation-testing-metrics');
91
+ const metrics = calculateMutationTestMetrics(report);
92
+ const score = metrics && metrics.systemUnderTestMetrics && metrics.systemUnderTestMetrics.metrics
93
+ ? metrics.systemUnderTestMetrics.metrics.mutationScore
94
+ : undefined;
95
+ if (typeof score !== 'number' || Number.isNaN(score)) {
96
+ throw new Error('mutation report produced no scoreable mutants — check --report points at the right shard\'s reports/mutation/mutation.json');
97
+ }
98
+ return score;
99
+ }
100
+
101
+ function parseArgs(argv) {
102
+ const out = { module: null, report: 'reports/mutation/mutation.json', matrix: null };
103
+ for (let i = 0; i < argv.length; i++) {
104
+ const arg = argv[i];
105
+ if (arg === '--module') {
106
+ out.module = argv[++i];
107
+ } else if (arg === '--report') {
108
+ out.report = argv[++i];
109
+ } else if (arg === '--matrix') {
110
+ out.matrix = argv[++i];
111
+ } else {
112
+ throw new Error(`unknown argument: ${arg}`);
113
+ }
114
+ }
115
+ if (!out.module) throw new Error('--module <name> is required');
116
+ return out;
117
+ }
118
+
119
+ function main() {
120
+ const args = parseArgs(process.argv.slice(2));
121
+ const matrixPath = args.matrix ? path.resolve(args.matrix) : path.join(__dirname, 'mutation-matrix.cjs');
122
+ const { COVERED } = require(matrixPath);
123
+ const entry = COVERED[args.module];
124
+ if (!entry) {
125
+ throw new ExitError(1, `check-mutation-score-ratchet: unknown module '${args.module}' — not in ${args.matrix ? matrixPath : 'scripts/mutation-matrix.cjs'} COVERED`);
126
+ }
127
+
128
+ const reportPath = path.resolve(args.report);
129
+ if (!fs.existsSync(reportPath)) {
130
+ throw new ExitError(1, `check-mutation-score-ratchet: report not found at ${reportPath} — run \`npx stryker run\` first (with the 'json' reporter enabled)`);
131
+ }
132
+ let report;
133
+ try {
134
+ report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
135
+ } catch (err) {
136
+ throw new ExitError(1, `check-mutation-score-ratchet: ${reportPath} is not valid JSON: ${err.message}`);
137
+ }
138
+
139
+ const achieved = extractAchievedScore(report);
140
+ const { shouldRatchet, suggestedFloor } = evaluateRatchet(achieved, entry.minScore);
141
+
142
+ if (!shouldRatchet) {
143
+ console.log(`ok mutation-score-ratchet: ${args.module} achieved ${achieved.toFixed(2)}% against floor ${entry.minScore} (within ${RATCHET_SLACK}-point slack)`);
144
+ return;
145
+ }
146
+
147
+ console.error(`mutation-score-ratchet: ${args.module} achieved ${achieved.toFixed(2)}%, more than ${RATCHET_SLACK} points above its declared floor (${entry.minScore}).`);
148
+ console.error(` This is unclaimed headroom, not run-to-run variance — raise the floor.`);
149
+ console.error(` remedy: in scripts/mutation-matrix.cjs, set COVERED['${args.module}'].minScore = ${suggestedFloor} (floor(achieved) - 1, this file's own convention),`);
150
+ console.error(` and update the matching entry in tests/mutation-matrix-ratchet.test.cjs's RATCHET_BASELINE in the same diff.`);
151
+ throw new ExitError(1);
152
+ }
153
+
154
+ module.exports = { RATCHET_SLACK, evaluateRatchet, extractAchievedScore };
155
+
156
+ if (require.main === module) runMain(main);
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const { computeElapsedPct, isNearCap, formatNearCapNotice } = require('./lib/ci-job-timing.cjs');
6
+
7
+ function run(env = process.env, nowMs = Date.now(), appendFileSync = fs.appendFileSync) {
8
+ const label = env.CI_JOB_LABEL;
9
+ const startEpochMs = Number(env.CI_JOB_START_EPOCH_MS);
10
+ const timeoutMinutes = Number(env.CI_JOB_TIMEOUT_MINUTES);
11
+
12
+ if (!label || !Number.isFinite(startEpochMs) || !Number.isFinite(timeoutMinutes) || timeoutMinutes <= 0) {
13
+ console.error(
14
+ 'ci-check-job-near-cap: missing or invalid CI_JOB_LABEL / CI_JOB_START_EPOCH_MS / '
15
+ + 'CI_JOB_TIMEOUT_MINUTES — skipping near-cap check (advisory only, not a failure).',
16
+ );
17
+ return { skipped: true };
18
+ }
19
+
20
+ const { elapsedMs, capMs, pct } = computeElapsedPct({
21
+ startedAt: new Date(startEpochMs).toISOString(),
22
+ completedAt: new Date(nowMs).toISOString(),
23
+ timeoutMinutes,
24
+ });
25
+
26
+ if (!isNearCap(pct)) {
27
+ return { skipped: false, nearCap: false, pct };
28
+ }
29
+
30
+ const { warningLine, summaryMarkdown } = formatNearCapNotice({ label, pct, elapsedMs, capMs });
31
+ console.log(warningLine);
32
+
33
+ if (env.GITHUB_STEP_SUMMARY) {
34
+ try {
35
+ appendFileSync(env.GITHUB_STEP_SUMMARY, `${summaryMarkdown}\n`);
36
+ } catch (err) {
37
+ console.error(`ci-check-job-near-cap: could not write GITHUB_STEP_SUMMARY: ${err.message}`);
38
+ }
39
+ }
40
+
41
+ return { skipped: false, nearCap: true, pct };
42
+ }
43
+
44
+ module.exports = { run };
45
+
46
+ if (require.main === module) {
47
+ run();
48
+ process.exitCode = 0;
49
+ }
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // ci-pr-mergeability.cjs — #3833 PR mergeability preflight.
5
+ //
6
+ // Risk asymmetry drives this design: a false positive (a clean PR wrongly
7
+ // classified CONFLICTED) skips every gated compute lane in all eight gated
8
+ // workflows on EVERY PR — a repo-wide outage. A false negative (a real
9
+ // conflict missed) is today's behavior; the per-job scripts/ci-rebase-check.cjs
10
+ // backstop still catches it there. So the weight is entirely on the
11
+ // false-positive surface: the single predicate `mergeable === false`, and
12
+ // every falsy-but-not-false value around it.
13
+ //
14
+ // GitHub's `mergeable` field on GET /repos/:owner/:repo/pulls/:number is a
15
+ // documented tri-state: `true`, `false`, or `null` while a background
16
+ // computation is still running. `null` is falsy — `if (!mergeable)` would
17
+ // misclassify "still computing" as "conflicted" and would have red every PR
18
+ // in the repo the moment this job started running. Only strict `=== false`
19
+ // counts as a conflict.
20
+ //
21
+ // `mergeable_state` is read only for the human-facing annotation, never for
22
+ // the verdict: its `blocked` value means "required checks have not passed",
23
+ // which is true for every PR while THIS preflight job itself is one of the
24
+ // required checks still running — classifying on it would self-deadlock the
25
+ // pipeline (the gate can never go green because it is itself ungreen).
26
+ //
27
+ // Because `mergeable` starts null and GitHub computes it asynchronously, this
28
+ // polls with linear backoff up to a small budget, then fails OPEN
29
+ // (INDETERMINATE => exit 0) rather than blocking the world on an API that
30
+ // hasn't finished thinking.
31
+
32
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
33
+
34
+ const VERDICT = Object.freeze({
35
+ MERGEABLE: 'MERGEABLE',
36
+ CONFLICTED: 'CONFLICTED',
37
+ INDETERMINATE: 'INDETERMINATE',
38
+ SKIPPED_NOT_A_PR: 'SKIPPED_NOT_A_PR',
39
+ });
40
+
41
+ const MAX_ATTEMPTS = 5;
42
+ const BASE_DELAY_MS = 2000;
43
+
44
+ /**
45
+ * Pure, total classifier: payload -> VERDICT atom. Never throws.
46
+ * Deliberately reads ONLY `mergeable`, and only with strict equality — see
47
+ * the header comment for why `mergeable_state` and truthiness are excluded.
48
+ */
49
+ function classifyMergeability(payload) {
50
+ if (payload === null || typeof payload !== 'object') return VERDICT.INDETERMINATE;
51
+ if (payload.mergeable === true) return VERDICT.MERGEABLE;
52
+ if (payload.mergeable === false) return VERDICT.CONFLICTED;
53
+ return VERDICT.INDETERMINATE;
54
+ }
55
+
56
+ /**
57
+ * Linear backoff: attempt 1..N-1 gives 1x..(N-1)x baseMs. Non-positive/NaN
58
+ * attempts clamp to one base interval rather than producing 0 or NaN delays.
59
+ */
60
+ function nextDelayMs(attempt, { baseMs = BASE_DELAY_MS } = {}) {
61
+ const n = Number.isFinite(attempt) && attempt > 0 ? Math.floor(attempt) : 1;
62
+ return baseMs * n;
63
+ }
64
+
65
+ /**
66
+ * Dependency-injected poll loop. Resolves as soon as GitHub reports a
67
+ * definite mergeable/conflicted state, or gives up after maxAttempts and
68
+ * reports INDETERMINATE (fail-open).
69
+ *
70
+ * @returns {Promise<{verdict:string, attempts:number, payload:*, reason:string}>}
71
+ */
72
+ async function resolveMergeability({
73
+ fetchPr,
74
+ sleep,
75
+ eventName,
76
+ prNumber,
77
+ maxAttempts = MAX_ATTEMPTS,
78
+ baseDelayMs = BASE_DELAY_MS,
79
+ } = {}) {
80
+ if (eventName !== 'pull_request') {
81
+ return { verdict: VERDICT.SKIPPED_NOT_A_PR, attempts: 0, payload: null, reason: 'not-a-pull-request' };
82
+ }
83
+ if (!Number.isInteger(prNumber) || prNumber <= 0) {
84
+ return { verdict: VERDICT.INDETERMINATE, attempts: 0, payload: null, reason: 'no-pr-number' };
85
+ }
86
+
87
+ let attempts = 0;
88
+ let lastPayload = null;
89
+ let lastError = null;
90
+
91
+ for (let i = 1; i <= maxAttempts; i++) {
92
+ attempts = i;
93
+ let payload;
94
+ try {
95
+ payload = await fetchPr(prNumber);
96
+ lastPayload = payload;
97
+ lastError = null;
98
+ } catch (err) {
99
+ lastError = err;
100
+ payload = null;
101
+ }
102
+
103
+ const verdict = classifyMergeability(payload);
104
+ if (verdict === VERDICT.MERGEABLE || verdict === VERDICT.CONFLICTED) {
105
+ return { verdict, attempts, payload, reason: 'resolved' };
106
+ }
107
+
108
+ if (i < maxAttempts) {
109
+ await sleep(nextDelayMs(i, { baseMs: baseDelayMs }));
110
+ }
111
+ }
112
+
113
+ return {
114
+ verdict: VERDICT.INDETERMINATE,
115
+ attempts,
116
+ payload: lastPayload,
117
+ reason: lastError ? 'fetch-failed' : 'mergeability-not-computed',
118
+ };
119
+ }
120
+
121
+ function usage() {
122
+ return [
123
+ 'Usage:',
124
+ ' node scripts/ci-pr-mergeability.cjs',
125
+ '',
126
+ 'CI-only preflight: polls the GitHub API for a PR\'s mergeable state and',
127
+ 'fails the job (exit 1) only on a definite conflict. Everything else',
128
+ '(unknown, unreadable, not a PR) fails open (exit 0).',
129
+ '',
130
+ 'Environment variables read:',
131
+ ' GITHUB_EVENT_NAME workflow trigger event; only "pull_request" is polled',
132
+ ' GITHUB_REPOSITORY owner/repo',
133
+ ' GITHUB_TOKEN optional bearer token for the API read',
134
+ ' GITHUB_BASE_REF PR base branch name (default: next)',
135
+ ' GITHUB_API_URL GitHub API base URL (default: https://api.github.com)',
136
+ ' PR_NUMBER the pull request number',
137
+ ' GITHUB_OUTPUT path to append verdict=/mergeable= step outputs to',
138
+ ' GITHUB_STEP_SUMMARY path to append a human-readable summary to',
139
+ ' GSD_PR_MERGEABILITY_BASE_DELAY_MS optional numeric override of the base backoff, for tests',
140
+ ].join('\n');
141
+ }
142
+
143
+ function parseArgs(argv) {
144
+ for (const arg of argv) {
145
+ if (arg === '--help' || arg === '-h') {
146
+ process.stdout.write(`${usage()}\n`);
147
+ throw new ExitError(0);
148
+ }
149
+ throw new Error(`unknown argument: ${arg}`);
150
+ }
151
+ }
152
+
153
+ function writeOutput(lines) {
154
+ const outputPath = process.env.GITHUB_OUTPUT;
155
+ if (typeof outputPath !== 'string' || outputPath === '') return;
156
+ try {
157
+ const fs = require('node:fs');
158
+ fs.appendFileSync(outputPath, `${lines.join('\n')}\n`);
159
+ } catch (err) {
160
+ // A failure while REPORTING the verdict must never invert the gate.
161
+ process.stderr.write(`::warning::failed to write GITHUB_OUTPUT: ${err.message}\n`);
162
+ }
163
+ }
164
+
165
+ function writeSummary(text) {
166
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY;
167
+ if (typeof summaryPath !== 'string' || summaryPath === '') return;
168
+ try {
169
+ const fs = require('node:fs');
170
+ fs.appendFileSync(summaryPath, `${text}\n`);
171
+ } catch (err) {
172
+ process.stderr.write(`::warning::failed to write GITHUB_STEP_SUMMARY: ${err.message}\n`);
173
+ }
174
+ }
175
+
176
+ // `argv` defaults to real CLI argv but is a parameter so tests can call
177
+ // main() in-process (e.g. main([])) without inheriting the test runner's own
178
+ // argv, which would otherwise trip parseArgs's "unknown argument" branch.
179
+ async function main(argv = process.argv.slice(2)) {
180
+ parseArgs(argv);
181
+
182
+ const eventName = process.env.GITHUB_EVENT_NAME;
183
+ const repo = process.env.GITHUB_REPOSITORY || '';
184
+ const token = process.env.GITHUB_TOKEN || '';
185
+ const baseBranch = process.env.GITHUB_BASE_REF || 'next';
186
+ const apiBase = process.env.GITHUB_API_URL || 'https://api.github.com';
187
+ const prNumberRaw = process.env.PR_NUMBER;
188
+ const prNumber = Number.parseInt(prNumberRaw, 10);
189
+
190
+ const overrideDelay = Number(process.env.GSD_PR_MERGEABILITY_BASE_DELAY_MS);
191
+ const baseDelayMs = Number.isFinite(overrideDelay) && overrideDelay >= 0 ? overrideDelay : BASE_DELAY_MS;
192
+
193
+ const fetchPr = async (number) => {
194
+ const url = `${apiBase}/repos/${repo}/pulls/${number}`;
195
+ const headers = {
196
+ accept: 'application/vnd.github+json',
197
+ 'x-github-api-version': '2022-11-28',
198
+ 'user-agent': 'gsd-core-ci-pr-mergeability',
199
+ };
200
+ if (token) headers.authorization = `Bearer ${token}`;
201
+ const response = await fetch(url, { headers, signal: AbortSignal.timeout(10000) });
202
+ if (!response.ok) {
203
+ throw new Error(`GitHub API responded ${response.status}`);
204
+ }
205
+ const text = await response.text();
206
+ try {
207
+ return JSON.parse(text);
208
+ } catch {
209
+ return null;
210
+ }
211
+ };
212
+
213
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
214
+
215
+ const result = await resolveMergeability({
216
+ fetchPr,
217
+ sleep,
218
+ eventName,
219
+ prNumber: Number.isNaN(prNumber) ? NaN : prNumber,
220
+ maxAttempts: MAX_ATTEMPTS,
221
+ baseDelayMs,
222
+ });
223
+
224
+ const mergeable = result.verdict === VERDICT.CONFLICTED ? 'false' : 'true';
225
+ writeOutput([`verdict=${result.verdict}`, `mergeable=${mergeable}`]);
226
+ writeSummary(`PR mergeability preflight: ${result.verdict}`);
227
+
228
+ if (result.verdict === VERDICT.CONFLICTED) {
229
+ process.stderr.write(
230
+ `::error::This pull request cannot cleanly merge ${baseBranch}. Rebase onto current ${baseBranch} and push again; no CI will run until the conflict is resolved.\n`,
231
+ );
232
+ const state = result.payload && typeof result.payload === 'object' ? result.payload.mergeable_state : undefined;
233
+ if (state) {
234
+ process.stderr.write(`::error::mergeable_state: ${state}\n`);
235
+ }
236
+ return 1;
237
+ }
238
+
239
+ if (result.verdict === VERDICT.INDETERMINATE) {
240
+ process.stderr.write(
241
+ '::warning::GitHub did not report mergeability within the polling budget; proceeding (fail-open). scripts/ci-rebase-check.cjs remains the in-job backstop.\n',
242
+ );
243
+ return 0;
244
+ }
245
+
246
+ process.stdout.write(`PR mergeability preflight: ${result.verdict}\n`);
247
+ return 0;
248
+ }
249
+
250
+ if (require.main === module) {
251
+ runMain(main);
252
+ }
253
+
254
+ module.exports = {
255
+ VERDICT,
256
+ MAX_ATTEMPTS,
257
+ BASE_DELAY_MS,
258
+ classifyMergeability,
259
+ nextDelayMs,
260
+ resolveMergeability,
261
+ main,
262
+ };
@@ -39,6 +39,10 @@ const PROTECTED_WORKFLOWS = new Set([
39
39
  'mutation.yml',
40
40
  'security-scan.yml',
41
41
  'release.yml',
42
+ // #3833: the reusable gate every `pull_request` compute lane depends on —
43
+ // marking it inert would let a change to the gate itself ship without ever
44
+ // running the full matrix it is responsible for enforcing.
45
+ 'pr-mergeable-preflight.yml',
42
46
  ]);
43
47
  for (const wf of PROTECTED_WORKFLOWS) {
44
48
  if (INERT_WORKFLOWS.has(wf)) {
@@ -341,19 +345,48 @@ function missingRuleTestFiles(rules) {
341
345
  return [...referenced].filter(f => !existsSync(path.join(__dirname, '..', f))).sort();
342
346
  }
343
347
 
344
- // Fail loudly at module load, mirroring the PROTECTED_WORKFLOWS check above —
345
- // this fires on EVERY invocation of the CLI (including the real `changes` job
346
- // in .github/workflows/test.yml), not only when a test suite happens to run.
347
- {
348
- const missing = missingRuleTestFiles(RULES);
349
- if (missing.length > 0) {
350
- throw new Error(
351
- `ci-test-scope: RULES reference test file(s) that do not exist on disk ` +
352
- `(silent coverage hole — see #2758):\n ${missing.join('\n ')}`,
353
- );
354
- }
348
+ /**
349
+ * Every PROTECTED_WORKFLOWS member that does not exist on disk.
350
+ * The name list and the real filenames are two surfaces over one fact
351
+ * (#3833): without this, renaming or deleting a gating workflow silently
352
+ * un-protects it and CI stays green while the protection is gone. Same
353
+ * shape and rationale as missingRuleTestFiles() above.
354
+ */
355
+ function missingProtectedWorkflows(names = PROTECTED_WORKFLOWS) {
356
+ return [...names]
357
+ .filter(name => !existsSync(path.join(__dirname, '..', '.github', 'workflows', name)))
358
+ .sort();
359
+ }
360
+
361
+ /**
362
+ * Shared module-load assertion for the two "this list names something that
363
+ * does not exist on disk" guards above. Both are silent-coverage-hole guards:
364
+ * a name that no longer resolves stops selecting/protecting anything while CI
365
+ * stays green, so both must fail loudly at load rather than at test time.
366
+ */
367
+ function assertNoneMissing(missing, summary, issueRef) {
368
+ if (missing.length === 0) return;
369
+ throw new Error(`ci-test-scope: ${summary} (${issueRef}):\n ${missing.join('\n ')}`);
355
370
  }
356
371
 
372
+ // Fail loudly at module load — this fires on EVERY invocation of the CLI
373
+ // (including the real `changes` job in .github/workflows/test.yml), not only
374
+ // when a test suite happens to run.
375
+ assertNoneMissing(
376
+ missingRuleTestFiles(RULES),
377
+ 'RULES reference test file(s) that do not exist on disk (silent coverage hole)',
378
+ 'see #2758',
379
+ );
380
+
381
+ // A PROTECTED_WORKFLOWS entry naming a file that does not exist means the
382
+ // workflow was renamed or deleted without updating this list, silently
383
+ // un-protecting it while CI stays green.
384
+ assertNoneMissing(
385
+ missingProtectedWorkflows(PROTECTED_WORKFLOWS),
386
+ 'PROTECTED_WORKFLOWS reference workflow file(s) that do not exist on disk (silent protection hole)',
387
+ 'see #3833',
388
+ );
389
+
357
390
  function usage() {
358
391
  return [
359
392
  'Usage:',
@@ -590,4 +623,4 @@ if (require.main === module) {
590
623
  runMain(main);
591
624
  }
592
625
 
593
- module.exports = { RULES, missingRuleTestFiles };
626
+ module.exports = { RULES, missingRuleTestFiles, PROTECTED_WORKFLOWS, INERT_WORKFLOWS, missingProtectedWorkflows };