@opengsd/gsd-core 1.18.0 → 1.20.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 (308) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -1
  3. package/.opencode/plugins/gsd-core.js +2 -3
  4. package/agents/gsd-code-fixer.compact.md +7 -7
  5. package/agents/gsd-code-fixer.md +13 -7
  6. package/agents/gsd-code-reviewer.compact.md +49 -8
  7. package/agents/gsd-code-reviewer.md +53 -14
  8. package/agents/gsd-executor.md +8 -4
  9. package/agents/gsd-verifier.md +1 -1
  10. package/bin/install.js +422 -103
  11. package/commands/gsd/update.md +5 -2
  12. package/gsd-core/bin/gsd-tools.cjs +179 -30
  13. package/gsd-core/bin/lib/agent-install-check.cjs +29 -3
  14. package/gsd-core/bin/lib/args-terminator.cjs +114 -0
  15. package/gsd-core/bin/lib/broken-windows.cjs +59 -1
  16. package/gsd-core/bin/lib/capability-lifecycle.cjs +1 -12
  17. package/gsd-core/bin/lib/capability-lock.cjs +13 -2
  18. package/gsd-core/bin/lib/capability-registry.cjs +376 -103
  19. package/gsd-core/bin/lib/capability-state.cjs +14 -8
  20. package/gsd-core/bin/lib/capability-validator.cjs +32 -0
  21. package/gsd-core/bin/lib/commands.cjs +27 -17
  22. package/gsd-core/bin/lib/config-loader.cjs +13 -3
  23. package/gsd-core/bin/lib/config.cjs +256 -58
  24. package/gsd-core/bin/lib/core-utils.cjs +45 -0
  25. package/gsd-core/bin/lib/decision-coverage-support.cjs +74 -0
  26. package/gsd-core/bin/lib/frontmatter.cjs +21 -3
  27. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +2 -2
  28. package/gsd-core/bin/lib/gate-context-drift.cjs +9 -4
  29. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +6 -1
  30. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +6 -1
  31. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +134 -13
  32. package/gsd-core/bin/lib/gate-phase-context.cjs +7 -8
  33. package/gsd-core/bin/lib/gate-schema-drift.cjs +1 -1
  34. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +63 -22
  35. package/gsd-core/bin/lib/gate-ui-safety.cjs +23 -5
  36. package/gsd-core/bin/lib/git-info-exclude.cjs +52 -0
  37. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +31 -12
  38. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +9 -7
  39. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +3 -3
  40. package/gsd-core/bin/lib/health-diagnostic-types.cjs +4 -0
  41. package/gsd-core/bin/lib/health-diagnostic.cjs +52 -1
  42. package/gsd-core/bin/lib/host-integration.cjs +13 -6
  43. package/gsd-core/bin/lib/init.cjs +245 -68
  44. package/gsd-core/bin/lib/install-engine.cjs +246 -32
  45. package/gsd-core/bin/lib/install-profiles.cjs +9 -4
  46. package/gsd-core/bin/lib/installer-migration-report.cjs +2 -0
  47. package/gsd-core/bin/lib/io.cjs +6 -0
  48. package/gsd-core/bin/lib/markdown-line-diff.cjs +147 -0
  49. package/gsd-core/bin/lib/markdown-sectionizer.cjs +147 -1
  50. package/gsd-core/bin/lib/markdown-table.cjs +23 -0
  51. package/gsd-core/bin/lib/mcp-server.cjs +8 -5
  52. package/gsd-core/bin/lib/milestone-lock.cjs +8 -1
  53. package/gsd-core/bin/lib/milestone-release-map.cjs +244 -0
  54. package/gsd-core/bin/lib/milestone-release-tags.cjs +440 -0
  55. package/gsd-core/bin/lib/milestone.cjs +61 -51
  56. package/gsd-core/bin/lib/model-catalog.cjs +8 -0
  57. package/gsd-core/bin/lib/normalize-test-command.cjs +67 -0
  58. package/gsd-core/bin/lib/phase-entry-grammar.cjs +119 -0
  59. package/gsd-core/bin/lib/phase-id-display.cjs +2 -0
  60. package/gsd-core/bin/lib/phase-id.cjs +629 -38
  61. package/gsd-core/bin/lib/phase-lifecycle.cjs +12 -44
  62. package/gsd-core/bin/lib/phase-locator.cjs +328 -19
  63. package/gsd-core/bin/lib/phase.cjs +3066 -653
  64. package/gsd-core/bin/lib/planning-document.cjs +26 -5
  65. package/gsd-core/bin/lib/planning-inspect.cjs +27 -35
  66. package/gsd-core/bin/lib/planning-snapshot.cjs +15 -8
  67. package/gsd-core/bin/lib/planning-workspace.cjs +21 -3
  68. package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
  69. package/gsd-core/bin/lib/read-only-git-env.cjs +79 -0
  70. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +2 -2
  71. package/gsd-core/bin/lib/review-lane-descriptor.cjs +57 -2
  72. package/gsd-core/bin/lib/review-lane-invocation.cjs +125 -18
  73. package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
  74. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +11 -9
  75. package/gsd-core/bin/lib/roadmap-parser.cjs +735 -137
  76. package/gsd-core/bin/lib/roadmap-progress.cjs +136 -0
  77. package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -7
  78. package/gsd-core/bin/lib/roadmap.cjs +383 -123
  79. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +381 -14
  80. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +8 -4
  81. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +60 -16
  82. package/gsd-core/bin/lib/shell-command-projection.cjs +151 -19
  83. package/gsd-core/bin/lib/smart-entry.cjs +35 -11
  84. package/gsd-core/bin/lib/state-contract.cjs +39 -15
  85. package/gsd-core/bin/lib/state-document.cjs +344 -27
  86. package/gsd-core/bin/lib/state-transition.cjs +156 -32
  87. package/gsd-core/bin/lib/state.cjs +527 -186
  88. package/gsd-core/bin/lib/surface.cjs +9 -0
  89. package/gsd-core/bin/lib/task-command-router.cjs +5 -25
  90. package/gsd-core/bin/lib/task-content-resolution.cjs +46 -1
  91. package/gsd-core/bin/lib/template.cjs +2 -2
  92. package/gsd-core/bin/lib/validate.cjs +3 -2
  93. package/gsd-core/bin/lib/vendor/re2js.cjs +863 -577
  94. package/gsd-core/bin/lib/vendor/re2js.d.cts +284 -219
  95. package/gsd-core/bin/lib/verification.cjs +96 -9
  96. package/gsd-core/bin/lib/verify.cjs +4 -2
  97. package/gsd-core/bin/lib/workstream-inventory.cjs +26 -20
  98. package/gsd-core/bin/lib/worktree-safety.cjs +22 -1
  99. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  100. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  101. package/gsd-core/bin/verify-reapply-patches.cjs +411 -10
  102. package/gsd-core/references/cicd-review-patterns.md +45 -0
  103. package/gsd-core/references/git-integration.md +11 -0
  104. package/gsd-core/references/loop-hook-dispatch.md +13 -6
  105. package/gsd-core/references/necessity-review.md +48 -0
  106. package/gsd-core/references/planning-config.md +2 -0
  107. package/gsd-core/references/tdd.md +2 -2
  108. package/gsd-core/templates/DEBUG.md +16 -0
  109. package/gsd-core/templates/config.json +2 -1
  110. package/gsd-core/templates/verification-report.md +1 -1
  111. package/gsd-core/workflows/audit-fix.md +2 -1
  112. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +10 -1
  113. package/gsd-core/workflows/code-review-fix.md +46 -5
  114. package/gsd-core/workflows/code-review.md +64 -7
  115. package/gsd-core/workflows/complete-milestone/steps/project-md-evolution-review.md +145 -0
  116. package/gsd-core/workflows/complete-milestone/steps/release-map.md +80 -0
  117. package/gsd-core/workflows/complete-milestone.md +37 -150
  118. package/gsd-core/workflows/diagnose-issues.md +29 -5
  119. package/gsd-core/workflows/discuss-phase/modes/power.md +1 -1
  120. package/gsd-core/workflows/discuss-phase-assumptions.md +33 -2
  121. package/gsd-core/workflows/discuss-phase-power.md +19 -2
  122. package/gsd-core/workflows/discuss-phase.md +6 -10
  123. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +13 -7
  124. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +7 -2
  125. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -1
  126. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +2 -2
  127. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +11 -2
  128. package/gsd-core/workflows/execute-phase.md +10 -6
  129. package/gsd-core/workflows/execute-plan.md +1 -1
  130. package/gsd-core/workflows/health.md +2 -1
  131. package/gsd-core/workflows/import.md +1 -1
  132. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +2 -2
  133. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +13 -9
  134. package/gsd-core/workflows/plan-phase.md +14 -14
  135. package/gsd-core/workflows/pr-branch.md +2 -0
  136. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -1
  137. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +8 -1
  138. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +2 -1
  139. package/gsd-core/workflows/quick.md +1 -1
  140. package/gsd-core/workflows/reapply-patches.md +104 -0
  141. package/gsd-core/workflows/review.md +70 -154
  142. package/gsd-core/workflows/section-manifest.json +5 -0
  143. package/gsd-core/workflows/secure-phase.md +1 -1
  144. package/gsd-core/workflows/settings-advanced.md +57 -9
  145. package/gsd-core/workflows/settings.md +1 -0
  146. package/gsd-core/workflows/ship.md +15 -4
  147. package/gsd-core/workflows/spec-phase.md +3 -6
  148. package/gsd-core/workflows/verify-work.md +4 -3
  149. package/hooks/dist/gsd-agent-isolation-guard.js +11 -1
  150. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  151. package/hooks/dist/gsd-graphify-update.sh +6 -0
  152. package/hooks/dist/gsd-path-export.js +95 -0
  153. package/hooks/dist/gsd-secret-read-guard.js +172 -39
  154. package/hooks/dist/gsd-statusline.js +37 -3
  155. package/hooks/dist/gsd-windsurf-pre-write.js +29 -2
  156. package/hooks/dist/gsd-workflow-guard.js +3 -2
  157. package/hooks/dist/gsd-worktree-path-guard.js +29 -2
  158. package/hooks/dist/lib/git-probe.js +16 -0
  159. package/hooks/dist/lib/isolation-sentinel.js +44 -0
  160. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  161. package/hooks/gsd-agent-isolation-guard.js +11 -1
  162. package/hooks/gsd-cursor-subagent-start.js +6 -2
  163. package/hooks/gsd-graphify-update.sh +6 -0
  164. package/hooks/gsd-path-export.js +95 -0
  165. package/hooks/gsd-secret-read-guard.js +172 -39
  166. package/hooks/gsd-statusline.js +37 -3
  167. package/hooks/gsd-windsurf-pre-write.js +29 -2
  168. package/hooks/gsd-workflow-guard.js +3 -2
  169. package/hooks/gsd-worktree-path-guard.js +29 -2
  170. package/hooks/hooks.json +1 -0
  171. package/hooks/lib/git-probe.js +16 -0
  172. package/hooks/lib/isolation-sentinel.js +44 -0
  173. package/hooks/managed-hooks-registry.cjs +1 -0
  174. package/package.json +15 -8
  175. package/pi/gsd.cjs +1047 -10
  176. package/pi/package-agents/gsd-advisor-researcher.md +113 -0
  177. package/pi/package-agents/gsd-ai-researcher.md +111 -0
  178. package/pi/package-agents/gsd-assumptions-analyzer.md +110 -0
  179. package/pi/package-agents/gsd-code-fixer.md +774 -0
  180. package/pi/package-agents/gsd-code-reviewer.md +441 -0
  181. package/pi/package-agents/gsd-codebase-mapper.md +850 -0
  182. package/pi/package-agents/gsd-debug-session-manager.md +416 -0
  183. package/pi/package-agents/gsd-debugger.md +1274 -0
  184. package/pi/package-agents/gsd-doc-classifier.md +269 -0
  185. package/pi/package-agents/gsd-doc-synthesizer.md +259 -0
  186. package/pi/package-agents/gsd-doc-verifier.md +212 -0
  187. package/pi/package-agents/gsd-doc-writer.md +626 -0
  188. package/pi/package-agents/gsd-dom-verifier.md +164 -0
  189. package/pi/package-agents/gsd-domain-researcher.md +144 -0
  190. package/pi/package-agents/gsd-eval-auditor.md +185 -0
  191. package/pi/package-agents/gsd-eval-planner.md +149 -0
  192. package/pi/package-agents/gsd-executor.md +898 -0
  193. package/pi/package-agents/gsd-framework-selector.md +159 -0
  194. package/pi/package-agents/gsd-integration-checker.md +476 -0
  195. package/pi/package-agents/gsd-intel-updater.md +338 -0
  196. package/pi/package-agents/gsd-mempalace-curator.md +49 -0
  197. package/pi/package-agents/gsd-nyquist-auditor.md +200 -0
  198. package/pi/package-agents/gsd-pattern-mapper.md +340 -0
  199. package/pi/package-agents/gsd-phase-researcher.md +897 -0
  200. package/pi/package-agents/gsd-plan-checker.md +1077 -0
  201. package/pi/package-agents/gsd-planner.md +1003 -0
  202. package/pi/package-agents/gsd-project-researcher.md +611 -0
  203. package/pi/package-agents/gsd-research-synthesizer.md +259 -0
  204. package/pi/package-agents/gsd-roadmapper.md +779 -0
  205. package/pi/package-agents/gsd-security-auditor.md +171 -0
  206. package/pi/package-agents/gsd-ui-auditor.md +581 -0
  207. package/pi/package-agents/gsd-ui-checker.md +417 -0
  208. package/pi/package-agents/gsd-ui-researcher.md +469 -0
  209. package/pi/package-agents/gsd-user-profiler.md +174 -0
  210. package/pi/package-agents/gsd-verifier.md +963 -0
  211. package/pi/package-agents/manifest.json +39 -0
  212. package/pi/package-skills/gsd-add-tests/SKILL.md +39 -0
  213. package/pi/package-skills/gsd-ai-integration-phase/SKILL.md +36 -0
  214. package/pi/package-skills/gsd-audit-fix/SKILL.md +34 -0
  215. package/pi/package-skills/gsd-audit-milestone/SKILL.md +40 -0
  216. package/pi/package-skills/gsd-audit-uat/SKILL.md +26 -0
  217. package/pi/package-skills/gsd-autonomous/SKILL.md +52 -0
  218. package/pi/package-skills/gsd-capture/SKILL.md +68 -0
  219. package/pi/package-skills/gsd-cleanup/SKILL.md +25 -0
  220. package/pi/package-skills/gsd-code-review/SKILL.md +71 -0
  221. package/pi/package-skills/gsd-complete-milestone/SKILL.md +147 -0
  222. package/pi/package-skills/gsd-config/SKILL.md +60 -0
  223. package/pi/package-skills/gsd-debug/SKILL.md +64 -0
  224. package/pi/package-skills/gsd-discuss-phase/SKILL.md +74 -0
  225. package/pi/package-skills/gsd-docs-update/SKILL.md +50 -0
  226. package/pi/package-skills/gsd-eval-review/SKILL.md +35 -0
  227. package/pi/package-skills/gsd-execute-phase/SKILL.md +62 -0
  228. package/pi/package-skills/gsd-explore/SKILL.md +26 -0
  229. package/pi/package-skills/gsd-extract-learnings/SKILL.md +25 -0
  230. package/pi/package-skills/gsd-fast/SKILL.md +33 -0
  231. package/pi/package-skills/gsd-forensics/SKILL.md +59 -0
  232. package/pi/package-skills/gsd-graphify/SKILL.md +210 -0
  233. package/pi/package-skills/gsd-health/SKILL.md +36 -0
  234. package/pi/package-skills/gsd-help/SKILL.md +37 -0
  235. package/pi/package-skills/gsd-import/SKILL.md +46 -0
  236. package/pi/package-skills/gsd-inbox/SKILL.md +42 -0
  237. package/pi/package-skills/gsd-ingest-docs/SKILL.md +43 -0
  238. package/pi/package-skills/gsd-manager/SKILL.md +46 -0
  239. package/pi/package-skills/gsd-map-codebase/SKILL.md +85 -0
  240. package/pi/package-skills/gsd-mempalace-capture/SKILL.md +111 -0
  241. package/pi/package-skills/gsd-mempalace-recall/SKILL.md +111 -0
  242. package/pi/package-skills/gsd-milestone-summary/SKILL.md +54 -0
  243. package/pi/package-skills/gsd-mvp-phase/SKILL.md +45 -0
  244. package/pi/package-skills/gsd-new-milestone/SKILL.md +49 -0
  245. package/pi/package-skills/gsd-new-project/SKILL.md +49 -0
  246. package/pi/package-skills/gsd-next/SKILL.md +34 -0
  247. package/pi/package-skills/gsd-ns-context/SKILL.md +31 -0
  248. package/pi/package-skills/gsd-ns-ideate/SKILL.md +30 -0
  249. package/pi/package-skills/gsd-ns-manage/SKILL.md +42 -0
  250. package/pi/package-skills/gsd-ns-project/SKILL.md +34 -0
  251. package/pi/package-skills/gsd-ns-review/SKILL.md +35 -0
  252. package/pi/package-skills/gsd-ns-workflow/SKILL.md +44 -0
  253. package/pi/package-skills/gsd-onboard/SKILL.md +46 -0
  254. package/pi/package-skills/gsd-pause-work/SKILL.md +48 -0
  255. package/pi/package-skills/gsd-phase/SKILL.md +61 -0
  256. package/pi/package-skills/gsd-plan-phase/SKILL.md +62 -0
  257. package/pi/package-skills/gsd-plan-review-convergence/SKILL.md +63 -0
  258. package/pi/package-skills/gsd-pr-branch/SKILL.md +31 -0
  259. package/pi/package-skills/gsd-profile-user/SKILL.md +48 -0
  260. package/pi/package-skills/gsd-progress/SKILL.md +51 -0
  261. package/pi/package-skills/gsd-quick/SKILL.md +179 -0
  262. package/pi/package-skills/gsd-quick-batch/SKILL.md +114 -0
  263. package/pi/package-skills/gsd-resume-work/SKILL.md +31 -0
  264. package/pi/package-skills/gsd-review/SKILL.md +50 -0
  265. package/pi/package-skills/gsd-review-backlog/SKILL.md +65 -0
  266. package/pi/package-skills/gsd-secure-phase/SKILL.md +37 -0
  267. package/pi/package-skills/gsd-settings/SKILL.md +30 -0
  268. package/pi/package-skills/gsd-ship/SKILL.md +27 -0
  269. package/pi/package-skills/gsd-sketch/SKILL.md +56 -0
  270. package/pi/package-skills/gsd-spec-phase/SKILL.md +63 -0
  271. package/pi/package-skills/gsd-spike/SKILL.md +53 -0
  272. package/pi/package-skills/gsd-stats/SKILL.md +22 -0
  273. package/pi/package-skills/gsd-surface/SKILL.md +177 -0
  274. package/pi/package-skills/gsd-thread/SKILL.md +29 -0
  275. package/pi/package-skills/gsd-ui-phase/SKILL.md +35 -0
  276. package/pi/package-skills/gsd-ui-review/SKILL.md +35 -0
  277. package/pi/package-skills/gsd-ultraplan-phase/SKILL.md +38 -0
  278. package/pi/package-skills/gsd-undo/SKILL.md +38 -0
  279. package/pi/package-skills/gsd-update/SKILL.md +54 -0
  280. package/pi/package-skills/gsd-validate-phase/SKILL.md +37 -0
  281. package/pi/package-skills/gsd-verify-work/SKILL.md +41 -0
  282. package/pi/package-skills/gsd-workspace/SKILL.md +57 -0
  283. package/pi/package-skills/gsd-workstreams/SKILL.md +73 -0
  284. package/scripts/build-hooks.js +4 -0
  285. package/scripts/ci-next-health.cjs +246 -23
  286. package/scripts/command-contract-helpers.cjs +1 -1
  287. package/scripts/docs-guard-registry.cjs +22 -7
  288. package/scripts/gen-features.cjs +14 -2
  289. package/scripts/gen-health-docs.cjs +13 -1
  290. package/scripts/gen-pi-package-skills.cjs +217 -0
  291. package/scripts/lib/drift-scan.cjs +27 -0
  292. package/scripts/lib/macos-conformance-tier.generated.cjs +17 -1
  293. package/scripts/lib/platform-conformance-tier.generated.cjs +37 -1
  294. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
  295. package/scripts/lint-phase-enumeration-drift.cjs +8 -4
  296. package/scripts/lint-retired-runtime-name.cjs +5 -0
  297. package/scripts/lint-roadmap-progress-drift.cjs +421 -0
  298. package/scripts/mutation-matrix.cjs +23 -1
  299. package/scripts/npm-audit-baseline.cjs +41 -7
  300. package/scripts/reviewed-or-maintainer-policy.cjs +181 -0
  301. package/scripts/select-docs-guards.cjs +1 -1
  302. package/scripts/self-test-report-policy.cjs +350 -0
  303. package/scripts/sync-next-version.cjs +38 -6
  304. package/skills/gsd-update/SKILL.md +5 -2
  305. package/vscode/package.json +1 -1
  306. package/scripts/diagnose-validate-commit-in-tier.cjs +0 -270
  307. package/scripts/diagnose-validate-commit-phases.cjs +0 -348
  308. package/scripts/xtrace-phase-timing.cjs +0 -205
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * "Reviewed or maintainer" policy (#5607).
6
+ *
7
+ * A pull request passes when EITHER
8
+ * 1. its author is listed in .github/MAINTAINERS (a maintainer's own PR needs
9
+ * no second reviewer), OR
10
+ * 2. someone other than the author, holding write/maintain/admin permission,
11
+ * has an APPROVED review on the CURRENT head sha as their latest standing
12
+ * review.
13
+ *
14
+ * Why this is a check and not a ruleset review count: a ruleset's
15
+ * required_approving_review_count is one number for every author, so it cannot
16
+ * say "required unless the author is a maintainer". The sole maintainer cannot
17
+ * approve their own PR, which is what pushed merges onto `--admin` (#4990).
18
+ *
19
+ * Everything that could wrongly COUNT an approval fails closed here: a stale
20
+ * sha, the author's own review, a reviewer without write access, a later
21
+ * dismissal or change request by the same reviewer, a truncated review list, an
22
+ * unreadable maintainer file. Wrongly FAILING only asks a maintainer for one
23
+ * more review.
24
+ *
25
+ * This module is pure. The workflow gathers reviews and roles with `gh api` and
26
+ * hands them in as files; the policy and the maintainer list are read from the
27
+ * BASE branch, never from the PR (.github/workflows/reviewed-or-maintainer.yml).
28
+ */
29
+
30
+ const fs = require('node:fs');
31
+ const { runMain } = require('./lib/cli-exit.cjs');
32
+
33
+ const REASON = Object.freeze({
34
+ OK_MAINTAINER_AUTHOR: 'ok_maintainer_author',
35
+ OK_APPROVED: 'ok_approved',
36
+ FAIL_NO_APPROVAL: 'fail_no_approval',
37
+ FAIL_STALE_APPROVAL_ONLY: 'fail_stale_approval_only',
38
+ FAIL_MAINTAINERS_UNREADABLE: 'fail_maintainers_unreadable',
39
+ FAIL_REVIEWS_INCOMPLETE: 'fail_reviews_incomplete',
40
+ FAIL_INPUT_INVALID: 'fail_input_invalid',
41
+ });
42
+
43
+ // Permission levels allowed to give a qualifying approval. GitHub's
44
+ // `role_name` for collaborators: read, triage, write, maintain, admin.
45
+ const APPROVER_ROLES = new Set(['write', 'maintain', 'admin']);
46
+
47
+ // A GitHub login: alphanumerics and single hyphens, 1-39 chars, optional
48
+ // `[bot]` suffix. Anything else on a MAINTAINERS line is reported, not trusted.
49
+ const LOGIN_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\[bot\])?$/;
50
+ const SHA_RE = /^[0-9a-f]{40}$/i;
51
+
52
+ // Review states that change a reviewer's standing. COMMENTED and PENDING do
53
+ // not: a later comment does not withdraw an approval on GitHub either.
54
+ const STANDING_STATES = new Set(['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED']);
55
+
56
+ /**
57
+ * @param {unknown} text - contents of .github/MAINTAINERS
58
+ * @returns {{ok: boolean, logins: Set<string>, invalid: string[]}}
59
+ */
60
+ function parseMaintainers(text) {
61
+ const logins = new Set();
62
+ const invalid = [];
63
+ if (typeof text !== 'string') return { ok: false, logins, invalid };
64
+ for (const raw of text.split(/\r?\n/)) {
65
+ const line = raw.replace(/#.*$/, '').trim();
66
+ if (line === '') continue;
67
+ if (LOGIN_RE.test(line)) logins.add(line.toLowerCase());
68
+ else invalid.push(line);
69
+ }
70
+ return { ok: logins.size > 0, logins, invalid };
71
+ }
72
+
73
+ function isBot(entry) {
74
+ const user = entry && entry.user;
75
+ return Boolean(user && (user.type === 'Bot' || /\[bot\]$/i.test(String(user.login || ''))));
76
+ }
77
+
78
+ function lower(value) {
79
+ return typeof value === 'string' ? value.toLowerCase() : '';
80
+ }
81
+
82
+ /**
83
+ * @param {object} input
84
+ * @param {string} input.authorLogin
85
+ * @param {Set<string>} input.maintainers - lowercase logins
86
+ * @param {Array<{user: {login: string}, state: string, commit_id: string}>} input.reviews - chronological
87
+ * @param {Record<string, string>} input.roles - lowercase login -> permission role name
88
+ * @param {string} input.headSha - 40 hex
89
+ * @param {boolean} input.reviewsComplete - false when the review list may be truncated
90
+ * @returns {{ok: boolean, reason: string}}
91
+ */
92
+ function evaluateReviewed({ authorLogin, maintainers, reviews, roles, headSha, reviewsComplete }) {
93
+ if (typeof authorLogin !== 'string' || authorLogin === '') {
94
+ return { ok: false, reason: REASON.FAIL_INPUT_INVALID };
95
+ }
96
+ if (typeof headSha !== 'string' || !SHA_RE.test(headSha)) {
97
+ return { ok: false, reason: REASON.FAIL_INPUT_INVALID };
98
+ }
99
+ if (!(maintainers instanceof Set) || maintainers.size === 0) {
100
+ return { ok: false, reason: REASON.FAIL_MAINTAINERS_UNREADABLE };
101
+ }
102
+
103
+ const author = lower(authorLogin);
104
+ if (maintainers.has(author)) return { ok: true, reason: REASON.OK_MAINTAINER_AUTHOR };
105
+
106
+ if (!Array.isArray(reviews)) return { ok: false, reason: REASON.FAIL_INPUT_INVALID };
107
+ if (reviewsComplete !== true) return { ok: false, reason: REASON.FAIL_REVIEWS_INCOMPLETE };
108
+
109
+ // Latest standing review per reviewer (input is chronological).
110
+ const standing = new Map();
111
+ for (const entry of reviews) {
112
+ const login = lower(entry && entry.user && entry.user.login);
113
+ if (login === '' || !STANDING_STATES.has(entry.state)) continue;
114
+ standing.set(login, entry);
115
+ }
116
+
117
+ let sawStaleApproval = false;
118
+ for (const [login, entry] of standing) {
119
+ if (login === author) continue;
120
+ if (entry.state !== 'APPROVED') continue;
121
+ // An app or bot (for example github-actions[bot] when Actions may approve
122
+ // PRs) is not a human review, whatever permission it holds.
123
+ if (isBot(entry)) continue;
124
+ const role = roles && typeof roles === 'object' ? roles[login] : undefined;
125
+ if (!APPROVER_ROLES.has(role)) continue;
126
+ if (lower(entry.commit_id) === lower(headSha)) return { ok: true, reason: REASON.OK_APPROVED };
127
+ sawStaleApproval = true;
128
+ }
129
+
130
+ return {
131
+ ok: false,
132
+ reason: sawStaleApproval ? REASON.FAIL_STALE_APPROVAL_ONLY : REASON.FAIL_NO_APPROVAL,
133
+ };
134
+ }
135
+
136
+ function readJson(file) {
137
+ try {
138
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
139
+ } catch {
140
+ return undefined;
141
+ }
142
+ }
143
+
144
+ function main() {
145
+ const maintainersText = (() => {
146
+ try {
147
+ return fs.readFileSync(process.env.MAINTAINERS_FILE || '.github/MAINTAINERS', 'utf8');
148
+ } catch {
149
+ return undefined;
150
+ }
151
+ })();
152
+ const parsed = parseMaintainers(maintainersText);
153
+ for (const line of parsed.invalid) {
154
+ process.stderr.write(`::warning::MAINTAINERS: ignored malformed line: ${JSON.stringify(line)}\n`);
155
+ }
156
+
157
+ const reviews = process.env.REVIEWS_FILE ? readJson(process.env.REVIEWS_FILE) : undefined;
158
+ const roles = process.env.ROLES_FILE ? readJson(process.env.ROLES_FILE) : undefined;
159
+
160
+ const result = evaluateReviewed({
161
+ authorLogin: process.env.PR_AUTHOR_LOGIN || '',
162
+ maintainers: parsed.ok ? parsed.logins : null,
163
+ reviews,
164
+ roles: roles && typeof roles === 'object' ? roles : {},
165
+ headSha: process.env.HEAD_SHA || '',
166
+ // The workflow's `gh api --paginate` either returns every page or exits
167
+ // non-zero before this runs; it states completeness explicitly.
168
+ reviewsComplete: process.env.REVIEWS_COMPLETE === 'true',
169
+ });
170
+
171
+ process.stdout.write(`${JSON.stringify(result)}\n`);
172
+ if (process.env.GITHUB_OUTPUT) {
173
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `ok=${result.ok ? 'true' : 'false'}\n`);
174
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `reason=${result.reason}\n`);
175
+ }
176
+ return result.ok ? 0 : 1;
177
+ }
178
+
179
+ if (require.main === module) runMain(main);
180
+
181
+ module.exports = { REASON, APPROVER_ROLES, parseMaintainers, evaluateReviewed };
@@ -22,7 +22,7 @@
22
22
  */
23
23
 
24
24
  /**
25
- * @param {string} changedPath - a single changed docs/ path (e.g. 'docs/AGENTS.md').
25
+ * @param {string} changedPath - a single changed docs/ path (e.g. 'docs/reference/agents.md').
26
26
  * @param {string} pattern - one entry from a registry value array.
27
27
  * @returns {boolean}
28
28
  */
@@ -0,0 +1,350 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Self-test report policy (#5612).
6
+ *
7
+ * A fix PR from someone who is not a listed maintainer must carry a
8
+ * `## Self-test report` section holding a fenced `json` block that records what
9
+ * the submitter ran: a failing-first test (fails on the base, passes on the
10
+ * head), the commands, the environment, and a passing full suite, bound to the
11
+ * PR's CURRENT head commit.
12
+ *
13
+ * TRUST MODEL. This validates shape and sha binding, not truth. A local run
14
+ * cannot be attested; CI on the merge-group commit stays authoritative. The
15
+ * report is a head start for the reviewer and a nudge on effort, nothing more.
16
+ * Every result here is advisory until a maintainer chooses to require it.
17
+ *
18
+ * Pure module: the workflow hands in the PR body, head ref, title, author, head
19
+ * sha and the maintainer list (read from the BASE sha, never the PR).
20
+ */
21
+
22
+ const fs = require('node:fs');
23
+ const { runMain } = require('./lib/cli-exit.cjs');
24
+ const { parseMaintainers } = require('./reviewed-or-maintainer-policy.cjs');
25
+
26
+ const REASON = Object.freeze({
27
+ OK_NOT_A_FIX: 'ok_not_a_fix',
28
+ OK_MAINTAINER_AUTHOR: 'ok_maintainer_author',
29
+ OK_BOT_AUTHOR: 'ok_bot_author',
30
+ OK_REPORT: 'ok_report',
31
+ FAIL_NO_REPORT: 'fail_no_report',
32
+ FAIL_MALFORMED_JSON: 'fail_malformed_json',
33
+ FAIL_SCHEMA: 'fail_schema',
34
+ FAIL_STALE_SHA: 'fail_stale_sha',
35
+ FAIL_NOT_FAILING_FIRST: 'fail_not_failing_first',
36
+ FAIL_SUITE_NOT_PASSED: 'fail_suite_not_passed',
37
+ FAIL_MAINTAINERS_UNREADABLE: 'fail_maintainers_unreadable',
38
+ FAIL_INPUT_INVALID: 'fail_input_invalid',
39
+ });
40
+
41
+ const SCHEMA_VERSION = 1;
42
+ const TOOLS = new Set(['gsd-test', 'npm-test', 'manual']);
43
+ const SUITE_RESULTS = new Set(['passed', 'failed', 'skipped']);
44
+ const SHA_RE = /^[0-9a-f]{40}$/i;
45
+ const PLACEHOLDER_RE = /^<[^<>]*>$/;
46
+ const EXCERPT_MAX = 2000;
47
+ const SECTION_TITLE = 'self-test report';
48
+
49
+ /** A PR is a "fix PR" when its head ref is fix/… or its title is a fix. */
50
+ function isFixPr({ headRef, title }) {
51
+ if (typeof headRef === 'string' && headRef.startsWith('fix/')) return true;
52
+ return typeof title === 'string' && /^fix(\([^)]*\))?!?:/i.test(title.trim());
53
+ }
54
+
55
+ function isBotLogin(login) {
56
+ return typeof login === 'string' && /\[bot\]$/i.test(login);
57
+ }
58
+
59
+ /**
60
+ * Find the first fenced ```json block inside the `## Self-test report` section.
61
+ *
62
+ * - Fence-aware: a `#` line inside a fence is not a heading. A fence may carry
63
+ * trailing info after the language (```json title="x"); only its first word
64
+ * names the language. A closing fence has no info and is at least as long as
65
+ * the opener, with the same character.
66
+ * - HTML comments are skipped outside fences, so a block hidden in a comment
67
+ * (invisible in the rendered PR) is not the report.
68
+ * - The section ends at the next heading of the same or a higher level.
69
+ * - The first json block wins.
70
+ *
71
+ * @returns {{found: boolean, json: string|null}}
72
+ */
73
+ // Lines longer than this are not fence or heading syntax. Capping what the
74
+ // regexes see keeps a crafted line of whitespace from costing more than a bounded
75
+ // amount of work (the body is attacker-controlled); captured JSON keeps its full text.
76
+ const SYNTAX_LINE_MAX = 1000;
77
+
78
+ /**
79
+ * ATX heading → {level, text}, parsed without a backtracking regex: a body line
80
+ * is attacker input and the obvious `^(#+)\s+(.*?)\s*#*\s*$` is cubic on a long
81
+ * run of spaces. A trailing run of `#` is a closing sequence only when preceded
82
+ * by whitespace (or the text is only hashes).
83
+ */
84
+ function parseHeading(line) {
85
+ const trimmed = line.trim();
86
+ let level = 0;
87
+ while (level < trimmed.length && trimmed[level] === '#') level += 1;
88
+ if (level < 1 || level > 6) return null;
89
+ if (level >= trimmed.length || !/\s/.test(trimmed[level])) return null;
90
+ let text = trimmed.slice(level).trim();
91
+ let end = text.length;
92
+ while (end > 0 && text[end - 1] === '#') end -= 1;
93
+ if (end < text.length && (end === 0 || /\s/.test(text[end - 1]))) text = text.slice(0, end).trimEnd();
94
+ return { level, text: text.replace(/\*\*/g, '').trim().toLowerCase() };
95
+ }
96
+
97
+ function extractReport(body) {
98
+ const lines = String(body || '').split(/\r?\n/);
99
+ let inFence = false;
100
+ let fenceChar = '';
101
+ let fenceLen = 0;
102
+ let inComment = false;
103
+ let sectionLevel = 0;
104
+ let capturing = false;
105
+ let captured = [];
106
+
107
+ for (const full of lines) {
108
+ const line = full.length > SYNTAX_LINE_MAX ? full.slice(0, SYNTAX_LINE_MAX) : full;
109
+ if (!inFence) {
110
+ if (inComment) {
111
+ if (line.includes('-->')) inComment = false;
112
+ continue;
113
+ }
114
+ const commentAt = line.indexOf('<!--');
115
+ if (commentAt !== -1 && line.indexOf('-->', commentAt + 4) === -1) {
116
+ inComment = true;
117
+ continue;
118
+ }
119
+ }
120
+
121
+ if (!inFence) {
122
+ const open = /^\s*(`{3,}|~{3,})\s*([^\s`]*)(?:\s.*)?$/.exec(line);
123
+ if (open) {
124
+ if (sectionLevel > 0 && !capturing && open[2].toLowerCase() === 'json') {
125
+ capturing = true;
126
+ captured = [];
127
+ }
128
+ inFence = true;
129
+ fenceChar = open[1][0];
130
+ fenceLen = open[1].length;
131
+ continue;
132
+ }
133
+ } else {
134
+ const close = /^\s*(`{3,}|~{3,})\s*$/.exec(line);
135
+ if (close && close[1][0] === fenceChar && close[1].length >= fenceLen) {
136
+ inFence = false;
137
+ if (capturing) return { found: true, json: captured.join('\n') };
138
+ continue;
139
+ }
140
+ if (capturing) captured.push(full);
141
+ continue;
142
+ }
143
+
144
+ const heading = parseHeading(line);
145
+ if (heading) {
146
+ if (sectionLevel === 0) {
147
+ if (heading.text === SECTION_TITLE) sectionLevel = heading.level;
148
+ } else if (heading.level <= sectionLevel) {
149
+ return { found: false, json: null };
150
+ }
151
+ }
152
+ }
153
+ return { found: false, json: null };
154
+ }
155
+
156
+ function isNonEmptyString(value) {
157
+ return typeof value === 'string' && value.trim() !== '';
158
+ }
159
+
160
+ function isCount(value) {
161
+ return Number.isInteger(value) && value >= 0;
162
+ }
163
+
164
+ /** Names of report fields whose string value is still an unfilled `<…>` placeholder. */
165
+ function placeholderFields(report) {
166
+ const found = [];
167
+ const check = (name, value) => {
168
+ if (typeof value === 'string' && PLACEHOLDER_RE.test(value.trim())) found.push(name);
169
+ };
170
+ check('head_sha', report.head_sha);
171
+ check('output_excerpt', report.output_excerpt);
172
+ if (report.failing_first && typeof report.failing_first === 'object') {
173
+ check('failing_first.test', report.failing_first.test);
174
+ check('failing_first.not_applicable_reason', report.failing_first.not_applicable_reason);
175
+ }
176
+ if (Array.isArray(report.commands)) report.commands.forEach((c) => check('commands', c));
177
+ if (report.full_suite && typeof report.full_suite === 'object') check('full_suite.command', report.full_suite.command);
178
+ if (report.environment && typeof report.environment === 'object') {
179
+ check('environment.os', report.environment.os);
180
+ check('environment.node', report.environment.node);
181
+ }
182
+ return [...new Set(found)];
183
+ }
184
+
185
+ /**
186
+ * @param {unknown} report - parsed JSON
187
+ * @param {string} headSha
188
+ * @returns {{ok: boolean, reason: string, problems: string[]}}
189
+ */
190
+ function validateReport(report, headSha) {
191
+ const problems = [];
192
+ const fail = (reason) => ({ ok: false, reason, problems });
193
+
194
+ if (!report || typeof report !== 'object' || Array.isArray(report)) {
195
+ problems.push('the report must be a JSON object');
196
+ return fail(REASON.FAIL_SCHEMA);
197
+ }
198
+ if (report.schema !== SCHEMA_VERSION) problems.push(`schema must be ${SCHEMA_VERSION}`);
199
+ if (typeof report.head_sha !== 'string' || !SHA_RE.test(report.head_sha)) {
200
+ problems.push('head_sha must be the 40-hex sha of your final commit');
201
+ }
202
+ if (!TOOLS.has(report.tool)) problems.push(`tool must be one of ${[...TOOLS].join(', ')}`);
203
+
204
+ const ff = report.failing_first;
205
+ if (!ff || typeof ff !== 'object' || Array.isArray(ff)) {
206
+ problems.push('failing_first must be an object');
207
+ } else if (ff.not_applicable_reason !== undefined) {
208
+ if (!isNonEmptyString(ff.not_applicable_reason)) {
209
+ problems.push('failing_first.not_applicable_reason must be a non-empty explanation');
210
+ }
211
+ } else {
212
+ if (!isNonEmptyString(ff.test)) problems.push('failing_first.test must name the test file');
213
+ if (typeof ff.fails_on_base !== 'boolean') problems.push('failing_first.fails_on_base must be true or false');
214
+ if (typeof ff.passes_on_head !== 'boolean') problems.push('failing_first.passes_on_head must be true or false');
215
+ }
216
+
217
+ if (!Array.isArray(report.commands) || report.commands.length === 0 || !report.commands.every(isNonEmptyString)) {
218
+ problems.push('commands must be a non-empty list of the commands you ran');
219
+ }
220
+
221
+ const suite = report.full_suite;
222
+ if (!suite || typeof suite !== 'object' || Array.isArray(suite)) {
223
+ problems.push('full_suite must be an object');
224
+ } else {
225
+ if (!isNonEmptyString(suite.command)) problems.push('full_suite.command must name the command');
226
+ if (!SUITE_RESULTS.has(suite.result)) problems.push(`full_suite.result must be one of ${[...SUITE_RESULTS].join(', ')}`);
227
+ if (!isCount(suite.passed)) problems.push('full_suite.passed must be a non-negative integer');
228
+ if (!isCount(suite.failed)) problems.push('full_suite.failed must be a non-negative integer');
229
+ }
230
+
231
+ const env = report.environment;
232
+ if (!env || typeof env !== 'object' || Array.isArray(env) || !isNonEmptyString(env.os) || !isNonEmptyString(env.node)) {
233
+ problems.push('environment must give os and node');
234
+ }
235
+
236
+ if (!isNonEmptyString(report.output_excerpt) || report.output_excerpt.length > EXCERPT_MAX) {
237
+ problems.push(`output_excerpt must be a non-empty excerpt of at most ${EXCERPT_MAX} characters`);
238
+ }
239
+
240
+ // A value still in `<…>` is the template, not something the submitter filled in.
241
+ for (const name of placeholderFields(report)) problems.push(`${name} is still the template placeholder; fill it in`);
242
+
243
+ if (problems.length > 0) return fail(REASON.FAIL_SCHEMA);
244
+
245
+ // Structurally sound. Now the substantive checks, most informative first.
246
+ if (report.head_sha.toLowerCase() !== String(headSha).toLowerCase()) {
247
+ problems.push(`head_sha ${report.head_sha} is not the PR's current head ${headSha}; update the report after your last push`);
248
+ return fail(REASON.FAIL_STALE_SHA);
249
+ }
250
+ if (ff.not_applicable_reason === undefined && !(ff.fails_on_base === true && ff.passes_on_head === true)) {
251
+ problems.push('the failing-first test must fail on the base and pass on your head');
252
+ return fail(REASON.FAIL_NOT_FAILING_FIRST);
253
+ }
254
+ if (suite.result !== 'passed' || suite.failed !== 0 || suite.passed < 1) {
255
+ problems.push('the full suite must have passed, with at least one passing test and no failures');
256
+ return fail(REASON.FAIL_SUITE_NOT_PASSED);
257
+ }
258
+ return { ok: true, reason: REASON.OK_REPORT, problems };
259
+ }
260
+
261
+ /**
262
+ * @param {object} input
263
+ * @param {string} input.body
264
+ * @param {string} input.headRef
265
+ * @param {string} input.title
266
+ * @param {string} input.authorLogin
267
+ * @param {string} input.headSha - 40 hex, read live
268
+ * @param {Set<string>|null} input.maintainers - lowercase logins
269
+ * @returns {{ok: boolean, reason: string, problems: string[]}}
270
+ */
271
+ function evaluateSelfTestReport({ body, headRef, title, authorLogin, headSha, maintainers }) {
272
+ const none = [];
273
+ if (typeof authorLogin !== 'string' || authorLogin === '' || typeof headSha !== 'string' || !SHA_RE.test(headSha)) {
274
+ return { ok: false, reason: REASON.FAIL_INPUT_INVALID, problems: none };
275
+ }
276
+ if (!isFixPr({ headRef, title })) return { ok: true, reason: REASON.OK_NOT_A_FIX, problems: none };
277
+ if (isBotLogin(authorLogin)) return { ok: true, reason: REASON.OK_BOT_AUTHOR, problems: none };
278
+ if (!(maintainers instanceof Set) || maintainers.size === 0) {
279
+ return { ok: false, reason: REASON.FAIL_MAINTAINERS_UNREADABLE, problems: none };
280
+ }
281
+ if (maintainers.has(authorLogin.toLowerCase())) {
282
+ return { ok: true, reason: REASON.OK_MAINTAINER_AUTHOR, problems: none };
283
+ }
284
+
285
+ const extracted = extractReport(body);
286
+ if (!extracted.found) {
287
+ return { ok: false, reason: REASON.FAIL_NO_REPORT, problems: ['no ```json block under a "## Self-test report" heading'] };
288
+ }
289
+ let parsed;
290
+ try {
291
+ parsed = JSON.parse(extracted.json);
292
+ } catch (error) {
293
+ return { ok: false, reason: REASON.FAIL_MALFORMED_JSON, problems: [`the report is not valid JSON: ${error.message}`] };
294
+ }
295
+ return validateReport(parsed, headSha);
296
+ }
297
+
298
+ /** A value from the environment, or from the file named by `<NAME>_FILE` (live reads are too big for env). */
299
+ function fromEnvOrFile(name) {
300
+ const file = process.env[`${name}_FILE`];
301
+ if (file) {
302
+ try {
303
+ return fs.readFileSync(file, 'utf8');
304
+ } catch {
305
+ return undefined;
306
+ }
307
+ }
308
+ return process.env[name] || '';
309
+ }
310
+
311
+ function main() {
312
+ let maintainersText;
313
+ try {
314
+ maintainersText = fs.readFileSync(process.env.MAINTAINERS_FILE || '.github/MAINTAINERS', 'utf8');
315
+ } catch {
316
+ maintainersText = undefined;
317
+ }
318
+ const parsed = parseMaintainers(maintainersText);
319
+
320
+ const body = fromEnvOrFile('PR_BODY');
321
+ const title = fromEnvOrFile('PR_TITLE');
322
+
323
+ const result = body === undefined || title === undefined
324
+ ? { ok: false, reason: REASON.FAIL_INPUT_INVALID, problems: [] }
325
+ : evaluateSelfTestReport({
326
+ body,
327
+ headRef: process.env.HEAD_REF || '',
328
+ title,
329
+ authorLogin: process.env.PR_AUTHOR_LOGIN || '',
330
+ headSha: process.env.HEAD_SHA || '',
331
+ maintainers: parsed.ok ? parsed.logins : null,
332
+ });
333
+
334
+ process.stdout.write(`${JSON.stringify(result)}\n`);
335
+ for (const problem of result.problems) {
336
+ // The annotation text is the validator's own wording plus, for a JSON syntax
337
+ // error, the parser's message (which can echo attacker text): strip newlines
338
+ // so no extra workflow command can start a line.
339
+ process.stderr.write(`::error::Self-test report: ${problem.replace(/[\r\n]+/g, ' ').slice(0, 300)}\n`);
340
+ }
341
+ if (process.env.GITHUB_OUTPUT) {
342
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `ok=${result.ok ? 'true' : 'false'}\n`);
343
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `reason=${result.reason}\n`);
344
+ }
345
+ return result.ok ? 0 : 1;
346
+ }
347
+
348
+ if (require.main === module) runMain(main);
349
+
350
+ module.exports = { REASON, SCHEMA_VERSION, EXCERPT_MAX, isFixPr, extractReport, validateReport, evaluateSelfTestReport };
@@ -9,7 +9,8 @@
9
9
  * release type (rc / hotfix / final). See issue #1104.
10
10
  *
11
11
  * Modes:
12
- * default — open + admin-merge a `chore: sync next version` PR (used by
12
+ * default — open + admin-merge a `chore: sync next version` PR, or enqueue it
13
+ * when the base requires the merge queue (#5746) (used by
13
14
  * release.yml's rc job, which has no next-targeting PR of its
14
15
  * own). Idempotent: a no-op when `next` is already at the target.
15
16
  * --in-place — set the version (+ manifests, via the npm `version` hook) in
@@ -52,7 +53,19 @@ function applyVersion(version, { run = defaultRun } = {}) {
52
53
  run('npm', ['version', version, '--no-git-tag-version', '--allow-same-version'], { timeout: 60000 });
53
54
  }
54
55
 
55
- /** Full PR-based sync to the `next` branch. Returns 'noop' | 'synced'. */
56
+ // What `gh pr merge --admin` says when the base branch is behind a merge-queue ruleset.
57
+ const MERGE_QUEUE_REJECTION_RE = /merge queue/i;
58
+
59
+ // stdin ignored, stdout and stderr captured so a failed `gh` call carries its own text.
60
+ const PIPED_STDERR = ['ignore', 'pipe', 'pipe'];
61
+
62
+ /** Piped stderr is not visible on its own: write it out, then rethrow. */
63
+ function rethrowWithStderr(err) {
64
+ if (err && err.stderr) process.stderr.write(String(err.stderr));
65
+ throw err;
66
+ }
67
+
68
+ /** Full PR-based sync to the `next` branch. Returns 'noop' | 'synced' | 'queued' (enqueued, not yet merged). */
56
69
  function syncViaPr(version, { run = defaultRun } = {}) {
57
70
  if (!isReleaseVersion(version)) {
58
71
  throw new Error(`refusing to sync next to invalid/non-release version '${version}'`);
@@ -100,11 +113,30 @@ function syncViaPr(version, { run = defaultRun } = {}) {
100
113
  /* labels are best-effort; admin-merge below still lands the PR */
101
114
  }
102
115
  try {
103
- run('gh', ['pr', 'merge', '--admin', '--merge', prNum], { timeout: 60000 });
116
+ // stderr is piped, not inherited: with the default 'inherit' the GitHub text goes
117
+ // to the terminal and err.stderr is null, so the classification below would only
118
+ // ever see "Command failed: gh pr merge ..." (#5746).
119
+ run('gh', ['pr', 'merge', '--admin', '--merge', prNum], { timeout: 60000, stdio: PIPED_STDERR });
104
120
  } catch (err) {
105
- // tolerate "already merged"; rethrow anything else
106
121
  const msg = String((err && (err.stderr || err.message)) || '');
107
- if (!/already merged|not mergeable|Merged/i.test(msg)) throw err;
122
+ if (/already merged/i.test(msg)) {
123
+ // a PR that is already merged must never be enqueued
124
+ } else if (MERGE_QUEUE_REJECTION_RE.test(msg)) {
125
+ // #5746: `next` is governed by a merge-queue ruleset, which --admin cannot bypass.
126
+ // Enqueue instead; the queue chooses the merge strategy, so no --merge here. A
127
+ // failed enqueue is a real failure: its text is surfaced and it propagates.
128
+ try {
129
+ run('gh', ['pr', 'merge', '--auto', prNum], { timeout: 60000, stdio: PIPED_STDERR });
130
+ } catch (enqueueErr) {
131
+ rethrowWithStderr(enqueueErr);
132
+ }
133
+ process.stdout.write(`queued sync of next -> ${version} via PR #${prNum}\n`);
134
+ return 'queued';
135
+ } else if (!/not mergeable|Merged/i.test(msg)) {
136
+ // the older tolerance, kept after the queue rule so a queue rejection that happens
137
+ // to mention "mergeable" is not mistaken for success
138
+ rethrowWithStderr(err);
139
+ }
108
140
  }
109
141
  process.stdout.write(`synced next -> ${version} via PR #${prNum}\n`);
110
142
  return 'synced';
@@ -130,4 +162,4 @@ if (require.main === module) {
130
162
  }
131
163
  }
132
164
 
133
- module.exports = { isReleaseVersion, versionFromPackageJson, applyVersion, syncViaPr, main };
165
+ module.exports = { isReleaseVersion, versionFromPackageJson, applyVersion, syncViaPr, main, defaultRun };
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: gsd-update
3
3
  description: "Update GSD to latest version with changelog display"
4
- argument-hint: "[--sync | --reapply | --next | --rc]"
4
+ argument-hint: "[--sync | --reapply | --next | --rc] [--interactive=exceptions|all]"
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Write
@@ -36,14 +36,17 @@ Routes to the update workflow which handles:
36
36
  <flags>
37
37
  - **--sync**: Sync managed GSD skills across runtime roots so multi-runtime users stay aligned after an update. Runs the sync-skills workflow (--from, --to, --dry-run, --apply flags supported).
38
38
  - **--reapply**: Reapply local modifications after a GSD update. Uses three-way comparison (pristine baseline, user-modified backup, newly installed version) to merge user customizations back. Runs the reapply-patches workflow.
39
+ - **--interactive[=exceptions|all]** (with `--reapply` only): show each user edit next to the stock text with a recommended disposition and wait for a keep / retire / rewrite decision per edit. `all` shows every edit; `exceptions` (bare `--interactive`) shows only edits upstream adopted, partly adopted, reworded around, or orphaned, and keeps the rest. Without it, `--reapply` behaves exactly as before.
39
40
  - **--next** (alias **--rc**): Target the `@next` RC dist-tag instead of `@latest` so you can install or refresh a release candidate (e.g. `1.4.0-rc.1`) through the normal update flow — scope/runtime detection, changelog preview, custom-file backup, and cache clearing all still apply. Omitting it keeps targeting `@latest` (no change). See ADR #660 for the RC channel.
40
41
  - **(no flag)**: Standard update — check for new version, show changelog, install.
41
42
  </flags>
42
43
 
43
44
  <process>
45
+ If the arguments contain `--interactive` (bare or with a value) but not `--reapply`, STOP with: `--interactive applies only to --reapply (for example: /gsd-update --reapply --interactive=all)`. Do nothing else.
46
+
44
47
  Parse the first token of the `<arguments>` block:
45
48
  - If it is `--sync`: strip the flag, execute the sync-skills workflow (passing remaining args for --from/--to/--dry-run/--apply).
46
- - If it is `--reapply`: strip the flag, execute the reapply-patches workflow.
49
+ - If it is `--reapply`: strip the flag, execute the reapply-patches workflow, passing the remaining arguments (for example `--interactive=all`) to it.
47
50
  - Otherwise (including `--next` / `--rc`): execute the update workflow end-to-end, passing the `<arguments>` block through so the workflow's parse_update_channel step can select the release channel.
48
51
 
49
52
  </process>
@@ -2,7 +2,7 @@
2
2
  "name": "gsd-core-vscode",
3
3
  "displayName": "GSD Core",
4
4
  "description": "GSD orchestration engine embedded in VS Code (ADR-1239 IDE profile).",
5
- "version": "1.18.0",
5
+ "version": "1.20.0",
6
6
  "publisher": "opengsd",
7
7
  "engines": {
8
8
  "vscode": "^1.105.0"