@opengsd/gsd-core 1.18.0 → 1.19.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 (257) 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 +2 -2
  5. package/agents/gsd-code-fixer.md +4 -3
  6. package/agents/gsd-verifier.md +1 -1
  7. package/bin/install.js +422 -103
  8. package/gsd-core/bin/gsd-tools.cjs +106 -7
  9. package/gsd-core/bin/lib/agent-install-check.cjs +29 -3
  10. package/gsd-core/bin/lib/args-terminator.cjs +114 -0
  11. package/gsd-core/bin/lib/broken-windows.cjs +59 -1
  12. package/gsd-core/bin/lib/capability-lock.cjs +13 -2
  13. package/gsd-core/bin/lib/capability-registry.cjs +156 -74
  14. package/gsd-core/bin/lib/capability-state.cjs +14 -8
  15. package/gsd-core/bin/lib/capability-validator.cjs +17 -0
  16. package/gsd-core/bin/lib/config-loader.cjs +10 -0
  17. package/gsd-core/bin/lib/config.cjs +256 -58
  18. package/gsd-core/bin/lib/core-utils.cjs +34 -0
  19. package/gsd-core/bin/lib/decision-coverage-support.cjs +74 -0
  20. package/gsd-core/bin/lib/gate-context-drift.cjs +8 -3
  21. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +6 -1
  22. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +6 -1
  23. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +134 -13
  24. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +63 -22
  25. package/gsd-core/bin/lib/gate-ui-safety.cjs +23 -5
  26. package/gsd-core/bin/lib/git-info-exclude.cjs +57 -0
  27. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +18 -3
  28. package/gsd-core/bin/lib/health-diagnostic-types.cjs +4 -0
  29. package/gsd-core/bin/lib/health-diagnostic.cjs +52 -1
  30. package/gsd-core/bin/lib/host-integration.cjs +13 -6
  31. package/gsd-core/bin/lib/init.cjs +65 -1
  32. package/gsd-core/bin/lib/install-engine.cjs +246 -32
  33. package/gsd-core/bin/lib/install-profiles.cjs +9 -4
  34. package/gsd-core/bin/lib/installer-migration-report.cjs +2 -0
  35. package/gsd-core/bin/lib/markdown-line-diff.cjs +147 -0
  36. package/gsd-core/bin/lib/markdown-sectionizer.cjs +113 -0
  37. package/gsd-core/bin/lib/milestone-lock.cjs +8 -1
  38. package/gsd-core/bin/lib/milestone-release-map.cjs +244 -0
  39. package/gsd-core/bin/lib/milestone-release-tags.cjs +440 -0
  40. package/gsd-core/bin/lib/milestone.cjs +43 -1
  41. package/gsd-core/bin/lib/model-catalog.cjs +8 -0
  42. package/gsd-core/bin/lib/normalize-test-command.cjs +67 -0
  43. package/gsd-core/bin/lib/phase-id.cjs +21 -4
  44. package/gsd-core/bin/lib/phase-lifecycle.cjs +37 -4
  45. package/gsd-core/bin/lib/phase.cjs +115 -26
  46. package/gsd-core/bin/lib/planning-snapshot.cjs +3 -1
  47. package/gsd-core/bin/lib/planning-workspace.cjs +18 -0
  48. package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
  49. package/gsd-core/bin/lib/read-only-git-env.cjs +58 -0
  50. package/gsd-core/bin/lib/review-lane-descriptor.cjs +35 -2
  51. package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
  52. package/gsd-core/bin/lib/roadmap-parser.cjs +12 -0
  53. package/gsd-core/bin/lib/roadmap-upgrade.cjs +39 -2
  54. package/gsd-core/bin/lib/roadmap.cjs +33 -25
  55. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +381 -14
  56. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +8 -4
  57. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +60 -16
  58. package/gsd-core/bin/lib/shell-command-projection.cjs +121 -16
  59. package/gsd-core/bin/lib/smart-entry.cjs +13 -0
  60. package/gsd-core/bin/lib/state-contract.cjs +29 -9
  61. package/gsd-core/bin/lib/state-document.cjs +12 -4
  62. package/gsd-core/bin/lib/state-transition.cjs +119 -24
  63. package/gsd-core/bin/lib/state.cjs +50 -28
  64. package/gsd-core/bin/lib/surface.cjs +9 -0
  65. package/gsd-core/bin/lib/task-command-router.cjs +5 -25
  66. package/gsd-core/bin/lib/task-content-resolution.cjs +46 -1
  67. package/gsd-core/bin/lib/verification.cjs +96 -9
  68. package/gsd-core/bin/lib/verify.cjs +2 -0
  69. package/gsd-core/bin/lib/worktree-safety.cjs +22 -1
  70. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  71. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  72. package/gsd-core/references/git-integration.md +11 -0
  73. package/gsd-core/references/loop-hook-dispatch.md +13 -6
  74. package/gsd-core/references/planning-config.md +2 -0
  75. package/gsd-core/references/tdd.md +2 -2
  76. package/gsd-core/templates/DEBUG.md +16 -0
  77. package/gsd-core/templates/config.json +2 -1
  78. package/gsd-core/templates/verification-report.md +1 -1
  79. package/gsd-core/workflows/audit-fix.md +2 -1
  80. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +10 -1
  81. package/gsd-core/workflows/code-review.md +61 -7
  82. package/gsd-core/workflows/complete-milestone/steps/project-md-evolution-review.md +145 -0
  83. package/gsd-core/workflows/complete-milestone/steps/release-map.md +80 -0
  84. package/gsd-core/workflows/complete-milestone.md +37 -150
  85. package/gsd-core/workflows/diagnose-issues.md +29 -5
  86. package/gsd-core/workflows/discuss-phase/modes/power.md +1 -1
  87. package/gsd-core/workflows/discuss-phase-assumptions.md +33 -2
  88. package/gsd-core/workflows/discuss-phase-power.md +19 -2
  89. package/gsd-core/workflows/discuss-phase.md +6 -10
  90. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +13 -7
  91. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +7 -2
  92. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -1
  93. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +2 -2
  94. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +11 -2
  95. package/gsd-core/workflows/execute-phase.md +9 -5
  96. package/gsd-core/workflows/health.md +2 -1
  97. package/gsd-core/workflows/import.md +1 -1
  98. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +2 -2
  99. package/gsd-core/workflows/plan-phase.md +13 -13
  100. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -1
  101. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +8 -1
  102. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +2 -1
  103. package/gsd-core/workflows/quick.md +1 -1
  104. package/gsd-core/workflows/review.md +16 -13
  105. package/gsd-core/workflows/section-manifest.json +5 -0
  106. package/gsd-core/workflows/settings.md +1 -0
  107. package/gsd-core/workflows/ship.md +3 -4
  108. package/gsd-core/workflows/spec-phase.md +3 -6
  109. package/gsd-core/workflows/verify-work.md +4 -3
  110. package/hooks/dist/gsd-agent-isolation-guard.js +11 -1
  111. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  112. package/hooks/dist/gsd-path-export.js +95 -0
  113. package/hooks/dist/gsd-secret-read-guard.js +172 -39
  114. package/hooks/dist/gsd-statusline.js +35 -2
  115. package/hooks/dist/gsd-windsurf-pre-write.js +28 -1
  116. package/hooks/dist/gsd-workflow-guard.js +1 -1
  117. package/hooks/dist/gsd-worktree-path-guard.js +28 -1
  118. package/hooks/dist/lib/isolation-sentinel.js +44 -0
  119. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  120. package/hooks/gsd-agent-isolation-guard.js +11 -1
  121. package/hooks/gsd-cursor-subagent-start.js +6 -2
  122. package/hooks/gsd-path-export.js +95 -0
  123. package/hooks/gsd-secret-read-guard.js +172 -39
  124. package/hooks/gsd-statusline.js +35 -2
  125. package/hooks/gsd-windsurf-pre-write.js +28 -1
  126. package/hooks/gsd-workflow-guard.js +1 -1
  127. package/hooks/gsd-worktree-path-guard.js +28 -1
  128. package/hooks/hooks.json +1 -0
  129. package/hooks/lib/isolation-sentinel.js +44 -0
  130. package/hooks/managed-hooks-registry.cjs +1 -0
  131. package/package.json +13 -6
  132. package/pi/gsd.cjs +1047 -10
  133. package/pi/package-agents/gsd-advisor-researcher.md +113 -0
  134. package/pi/package-agents/gsd-ai-researcher.md +111 -0
  135. package/pi/package-agents/gsd-assumptions-analyzer.md +110 -0
  136. package/pi/package-agents/gsd-code-fixer.md +769 -0
  137. package/pi/package-agents/gsd-code-reviewer.md +402 -0
  138. package/pi/package-agents/gsd-codebase-mapper.md +850 -0
  139. package/pi/package-agents/gsd-debug-session-manager.md +416 -0
  140. package/pi/package-agents/gsd-debugger.md +1274 -0
  141. package/pi/package-agents/gsd-doc-classifier.md +269 -0
  142. package/pi/package-agents/gsd-doc-synthesizer.md +259 -0
  143. package/pi/package-agents/gsd-doc-verifier.md +212 -0
  144. package/pi/package-agents/gsd-doc-writer.md +626 -0
  145. package/pi/package-agents/gsd-dom-verifier.md +164 -0
  146. package/pi/package-agents/gsd-domain-researcher.md +144 -0
  147. package/pi/package-agents/gsd-eval-auditor.md +185 -0
  148. package/pi/package-agents/gsd-eval-planner.md +149 -0
  149. package/pi/package-agents/gsd-executor.md +894 -0
  150. package/pi/package-agents/gsd-framework-selector.md +159 -0
  151. package/pi/package-agents/gsd-integration-checker.md +476 -0
  152. package/pi/package-agents/gsd-intel-updater.md +338 -0
  153. package/pi/package-agents/gsd-mempalace-curator.md +49 -0
  154. package/pi/package-agents/gsd-nyquist-auditor.md +200 -0
  155. package/pi/package-agents/gsd-pattern-mapper.md +340 -0
  156. package/pi/package-agents/gsd-phase-researcher.md +897 -0
  157. package/pi/package-agents/gsd-plan-checker.md +1077 -0
  158. package/pi/package-agents/gsd-planner.md +1003 -0
  159. package/pi/package-agents/gsd-project-researcher.md +611 -0
  160. package/pi/package-agents/gsd-research-synthesizer.md +259 -0
  161. package/pi/package-agents/gsd-roadmapper.md +779 -0
  162. package/pi/package-agents/gsd-security-auditor.md +171 -0
  163. package/pi/package-agents/gsd-ui-auditor.md +581 -0
  164. package/pi/package-agents/gsd-ui-checker.md +417 -0
  165. package/pi/package-agents/gsd-ui-researcher.md +469 -0
  166. package/pi/package-agents/gsd-user-profiler.md +174 -0
  167. package/pi/package-agents/gsd-verifier.md +963 -0
  168. package/pi/package-agents/manifest.json +39 -0
  169. package/pi/package-skills/gsd-add-tests/SKILL.md +39 -0
  170. package/pi/package-skills/gsd-ai-integration-phase/SKILL.md +36 -0
  171. package/pi/package-skills/gsd-audit-fix/SKILL.md +34 -0
  172. package/pi/package-skills/gsd-audit-milestone/SKILL.md +40 -0
  173. package/pi/package-skills/gsd-audit-uat/SKILL.md +26 -0
  174. package/pi/package-skills/gsd-autonomous/SKILL.md +52 -0
  175. package/pi/package-skills/gsd-capture/SKILL.md +68 -0
  176. package/pi/package-skills/gsd-cleanup/SKILL.md +25 -0
  177. package/pi/package-skills/gsd-code-review/SKILL.md +71 -0
  178. package/pi/package-skills/gsd-complete-milestone/SKILL.md +147 -0
  179. package/pi/package-skills/gsd-config/SKILL.md +60 -0
  180. package/pi/package-skills/gsd-debug/SKILL.md +64 -0
  181. package/pi/package-skills/gsd-discuss-phase/SKILL.md +74 -0
  182. package/pi/package-skills/gsd-docs-update/SKILL.md +50 -0
  183. package/pi/package-skills/gsd-eval-review/SKILL.md +35 -0
  184. package/pi/package-skills/gsd-execute-phase/SKILL.md +62 -0
  185. package/pi/package-skills/gsd-explore/SKILL.md +26 -0
  186. package/pi/package-skills/gsd-extract-learnings/SKILL.md +25 -0
  187. package/pi/package-skills/gsd-fast/SKILL.md +33 -0
  188. package/pi/package-skills/gsd-forensics/SKILL.md +59 -0
  189. package/pi/package-skills/gsd-graphify/SKILL.md +210 -0
  190. package/pi/package-skills/gsd-health/SKILL.md +36 -0
  191. package/pi/package-skills/gsd-help/SKILL.md +37 -0
  192. package/pi/package-skills/gsd-import/SKILL.md +46 -0
  193. package/pi/package-skills/gsd-inbox/SKILL.md +42 -0
  194. package/pi/package-skills/gsd-ingest-docs/SKILL.md +43 -0
  195. package/pi/package-skills/gsd-manager/SKILL.md +46 -0
  196. package/pi/package-skills/gsd-map-codebase/SKILL.md +85 -0
  197. package/pi/package-skills/gsd-mempalace-capture/SKILL.md +111 -0
  198. package/pi/package-skills/gsd-mempalace-recall/SKILL.md +111 -0
  199. package/pi/package-skills/gsd-milestone-summary/SKILL.md +54 -0
  200. package/pi/package-skills/gsd-mvp-phase/SKILL.md +45 -0
  201. package/pi/package-skills/gsd-new-milestone/SKILL.md +49 -0
  202. package/pi/package-skills/gsd-new-project/SKILL.md +49 -0
  203. package/pi/package-skills/gsd-next/SKILL.md +34 -0
  204. package/pi/package-skills/gsd-ns-context/SKILL.md +31 -0
  205. package/pi/package-skills/gsd-ns-ideate/SKILL.md +30 -0
  206. package/pi/package-skills/gsd-ns-manage/SKILL.md +42 -0
  207. package/pi/package-skills/gsd-ns-project/SKILL.md +34 -0
  208. package/pi/package-skills/gsd-ns-review/SKILL.md +35 -0
  209. package/pi/package-skills/gsd-ns-workflow/SKILL.md +44 -0
  210. package/pi/package-skills/gsd-onboard/SKILL.md +46 -0
  211. package/pi/package-skills/gsd-pause-work/SKILL.md +48 -0
  212. package/pi/package-skills/gsd-phase/SKILL.md +61 -0
  213. package/pi/package-skills/gsd-plan-phase/SKILL.md +62 -0
  214. package/pi/package-skills/gsd-plan-review-convergence/SKILL.md +63 -0
  215. package/pi/package-skills/gsd-pr-branch/SKILL.md +31 -0
  216. package/pi/package-skills/gsd-profile-user/SKILL.md +48 -0
  217. package/pi/package-skills/gsd-progress/SKILL.md +51 -0
  218. package/pi/package-skills/gsd-quick/SKILL.md +179 -0
  219. package/pi/package-skills/gsd-quick-batch/SKILL.md +114 -0
  220. package/pi/package-skills/gsd-resume-work/SKILL.md +31 -0
  221. package/pi/package-skills/gsd-review/SKILL.md +50 -0
  222. package/pi/package-skills/gsd-review-backlog/SKILL.md +65 -0
  223. package/pi/package-skills/gsd-secure-phase/SKILL.md +37 -0
  224. package/pi/package-skills/gsd-settings/SKILL.md +30 -0
  225. package/pi/package-skills/gsd-ship/SKILL.md +27 -0
  226. package/pi/package-skills/gsd-sketch/SKILL.md +56 -0
  227. package/pi/package-skills/gsd-spec-phase/SKILL.md +63 -0
  228. package/pi/package-skills/gsd-spike/SKILL.md +53 -0
  229. package/pi/package-skills/gsd-stats/SKILL.md +22 -0
  230. package/pi/package-skills/gsd-surface/SKILL.md +177 -0
  231. package/pi/package-skills/gsd-thread/SKILL.md +29 -0
  232. package/pi/package-skills/gsd-ui-phase/SKILL.md +35 -0
  233. package/pi/package-skills/gsd-ui-review/SKILL.md +35 -0
  234. package/pi/package-skills/gsd-ultraplan-phase/SKILL.md +38 -0
  235. package/pi/package-skills/gsd-undo/SKILL.md +38 -0
  236. package/pi/package-skills/gsd-update/SKILL.md +51 -0
  237. package/pi/package-skills/gsd-validate-phase/SKILL.md +37 -0
  238. package/pi/package-skills/gsd-verify-work/SKILL.md +41 -0
  239. package/pi/package-skills/gsd-workspace/SKILL.md +57 -0
  240. package/pi/package-skills/gsd-workstreams/SKILL.md +73 -0
  241. package/scripts/build-hooks.js +4 -0
  242. package/scripts/command-contract-helpers.cjs +1 -1
  243. package/scripts/docs-guard-registry.cjs +20 -7
  244. package/scripts/gen-health-docs.cjs +13 -1
  245. package/scripts/gen-pi-package-skills.cjs +217 -0
  246. package/scripts/lib/macos-conformance-tier.generated.cjs +11 -1
  247. package/scripts/lib/platform-conformance-tier.generated.cjs +26 -1
  248. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
  249. package/scripts/lint-retired-runtime-name.cjs +5 -0
  250. package/scripts/npm-audit-baseline.cjs +41 -7
  251. package/scripts/reviewed-or-maintainer-policy.cjs +181 -0
  252. package/scripts/select-docs-guards.cjs +1 -1
  253. package/scripts/self-test-report-policy.cjs +350 -0
  254. package/vscode/package.json +1 -1
  255. package/scripts/diagnose-validate-commit-in-tier.cjs +0 -270
  256. package/scripts/diagnose-validate-commit-phases.cjs +0 -348
  257. package/scripts/xtrace-phase-timing.cjs +0 -205
@@ -252,6 +252,39 @@ function runInstalledTreeAudit(cwd, opts = {}) {
252
252
  return runNpmAuditWithRetry(cwd, ['audit', '--omit=dev', '--json'], opts);
253
253
  }
254
254
 
255
+ /**
256
+ * `-c safe.directory=<repoRoot>` for the baseline's git calls (#5659). On the gsd-test bench the
257
+ * checkout is owned by a different uid than the one running the tests, so git refuses every command
258
+ * ("detected dubious ownership") and the baseline silently resolved to nothing, which sent the
259
+ * integrity tests to their zero-tolerance fallback. git honors `safe.directory` from the command line
260
+ * only; scoping it to the audited repo root (never `*`, never global config) trusts exactly the
261
+ * checkout being audited. Same precedent as scripts/lint-compiled-artifact-sync.cjs.
262
+ */
263
+ function safeDirectoryArgs(repoRoot) {
264
+ // git compares safe.directory against its forward-slash path, also on Windows.
265
+ return ['-c', `safe.directory=${String(repoRoot).replace(/\\/g, '/')}`];
266
+ }
267
+
268
+ /**
269
+ * One line saying why no baseline resolved (#5659), for the integrity gate's zero-tolerance message.
270
+ * Without it a git refusal (ownership, a missing git, a broken checkout) is indistinguishable from a
271
+ * checkout that simply has no `origin/next`, which is how the ownership problem hid. Only the first
272
+ * line git printed is kept.
273
+ */
274
+ function explainNoBaseline(repoRoot, { execFileSyncImpl = execFileSync } = {}) {
275
+ try {
276
+ execFileSyncImpl('git', [...safeDirectoryArgs(repoRoot), 'rev-parse', '--git-dir'], {
277
+ cwd: repoRoot,
278
+ encoding: 'utf-8',
279
+ stdio: ['ignore', 'pipe', 'pipe'],
280
+ });
281
+ } catch (error) {
282
+ const detail = String((error && (error.stderr || error.message)) || '').trim().split(/\r?\n/)[0];
283
+ return `git could not read ${repoRoot}${detail ? `: ${detail}` : ''}`;
284
+ }
285
+ return 'no baseline ref (origin/next, next) resolved, or it lacks package.json / package-lock.json';
286
+ }
287
+
255
288
  /**
256
289
  * Extracts package.json + package-lock.json (optionally under `subdir`,
257
290
  * e.g. 'sdk') from `ref` at git object level into a fresh temp directory --
@@ -260,17 +293,17 @@ function runInstalledTreeAudit(cwd, opts = {}) {
260
293
  * clone that never fetched it) or either file is absent at that ref --
261
294
  * callers treat `null` as "no baseline available", not an error.
262
295
  */
263
- function extractBaselineTree(ref, repoRoot, subdir = '') {
296
+ function extractBaselineTree(ref, repoRoot, subdir = '', { execFileSyncImpl = execFileSync } = {}) {
264
297
  const rel = (name) => (subdir ? path.posix.join(subdir, name) : name);
265
298
  let pkgJson;
266
299
  let lockJson;
267
300
  try {
268
- pkgJson = execFileSync('git', ['show', `${ref}:${rel('package.json')}`], {
301
+ pkgJson = execFileSyncImpl('git', [...safeDirectoryArgs(repoRoot), 'show', `${ref}:${rel('package.json')}`], {
269
302
  cwd: repoRoot,
270
303
  encoding: 'utf-8',
271
304
  stdio: ['ignore', 'pipe', 'ignore'],
272
305
  });
273
- lockJson = execFileSync('git', ['show', `${ref}:${rel('package-lock.json')}`], {
306
+ lockJson = execFileSyncImpl('git', [...safeDirectoryArgs(repoRoot), 'show', `${ref}:${rel('package-lock.json')}`], {
274
307
  cwd: repoRoot,
275
308
  encoding: 'utf-8',
276
309
  stdio: ['ignore', 'pipe', 'ignore'],
@@ -322,12 +355,12 @@ const NULL_SHA = '0000000000000000000000000000000000000000';
322
355
  * Returns '' if none resolve -- callers fall back to strict zero-tolerance
323
356
  * rather than silently skipping the gate.
324
357
  */
325
- function resolveBaselineRef(repoRoot) {
358
+ function resolveBaselineRef(repoRoot, { execFileSyncImpl = execFileSync } = {}) {
326
359
  if (process.env.AUDIT_BASELINE_REF) return process.env.AUDIT_BASELINE_REF;
327
360
  if (process.env.GITHUB_BASE_REF) return `origin/${process.env.GITHUB_BASE_REF}`;
328
361
  if (process.env.GITHUB_EVENT_NAME === 'push') {
329
362
  try {
330
- const parent = execFileSync('git', ['rev-parse', '--verify', 'HEAD~1'], {
363
+ const parent = execFileSyncImpl('git', [...safeDirectoryArgs(repoRoot), 'rev-parse', '--verify', 'HEAD~1'], {
331
364
  cwd: repoRoot,
332
365
  encoding: 'utf-8',
333
366
  stdio: ['ignore', 'pipe', 'ignore'],
@@ -338,7 +371,7 @@ function resolveBaselineRef(repoRoot) {
338
371
  }
339
372
  }
340
373
  try {
341
- execFileSync('git', ['rev-parse', '--verify', 'origin/next'], {
374
+ execFileSyncImpl('git', [...safeDirectoryArgs(repoRoot), 'rev-parse', '--verify', 'origin/next'], {
342
375
  cwd: repoRoot,
343
376
  stdio: 'ignore',
344
377
  });
@@ -347,7 +380,7 @@ function resolveBaselineRef(repoRoot) {
347
380
  // fall through to a plain local branch
348
381
  }
349
382
  try {
350
- execFileSync('git', ['rev-parse', '--verify', 'next'], {
383
+ execFileSyncImpl('git', [...safeDirectoryArgs(repoRoot), 'rev-parse', '--verify', 'next'], {
351
384
  cwd: repoRoot,
352
385
  stdio: 'ignore',
353
386
  });
@@ -366,6 +399,7 @@ module.exports = {
366
399
  runNpmAuditWithRetry,
367
400
  extractBaselineTree,
368
401
  resolveBaselineRef,
402
+ explainNoBaseline,
369
403
  isTimeoutKill,
370
404
  buildTimeoutKillError,
371
405
  sleepSyncMs,
@@ -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 };
@@ -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.19.0",
6
6
  "publisher": "opengsd",
7
7
  "engines": {
8
8
  "vscode": "^1.105.0"