@opengsd/gsd-core 1.13.0 → 1.15.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 (441) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ja-JP.md +3 -3
  4. package/README.ko-KR.md +3 -3
  5. package/README.pt-BR.md +3 -3
  6. package/README.zh-CN.md +3 -3
  7. package/agents/gsd-advisor-researcher.compact.md +85 -0
  8. package/agents/gsd-ai-researcher.compact.md +96 -0
  9. package/agents/gsd-assumptions-analyzer.compact.md +81 -0
  10. package/agents/gsd-code-fixer.compact.md +459 -0
  11. package/agents/gsd-code-fixer.md +9 -8
  12. package/agents/gsd-code-reviewer.compact.md +269 -0
  13. package/agents/gsd-code-reviewer.md +15 -3
  14. package/agents/gsd-codebase-mapper.compact.md +760 -0
  15. package/agents/gsd-debug-session-manager.compact.md +360 -0
  16. package/agents/gsd-debug-session-manager.md +17 -2
  17. package/agents/gsd-debugger.md +2 -2
  18. package/agents/gsd-doc-classifier.compact.md +192 -0
  19. package/agents/gsd-doc-synthesizer.compact.md +200 -0
  20. package/agents/gsd-doc-verifier.compact.md +143 -0
  21. package/agents/gsd-doc-writer.compact.md +440 -0
  22. package/agents/gsd-dom-verifier.compact.md +138 -0
  23. package/agents/gsd-domain-researcher.compact.md +141 -0
  24. package/agents/gsd-eval-auditor.compact.md +160 -0
  25. package/agents/gsd-eval-auditor.md +1 -1
  26. package/agents/gsd-eval-planner.compact.md +137 -0
  27. package/agents/gsd-executor.md +13 -8
  28. package/agents/gsd-framework-selector.compact.md +82 -0
  29. package/agents/gsd-integration-checker.compact.md +245 -0
  30. package/agents/gsd-intel-updater.compact.md +226 -0
  31. package/agents/gsd-intel-updater.md +1 -1
  32. package/agents/gsd-mempalace-curator.compact.md +45 -0
  33. package/agents/gsd-nyquist-auditor.compact.md +179 -0
  34. package/agents/gsd-pattern-mapper.compact.md +275 -0
  35. package/agents/gsd-phase-researcher.md +19 -11
  36. package/agents/gsd-plan-checker.md +8 -7
  37. package/agents/gsd-planner.md +12 -8
  38. package/agents/gsd-project-researcher.compact.md +587 -0
  39. package/agents/gsd-project-researcher.md +1 -1
  40. package/agents/gsd-research-synthesizer.compact.md +212 -0
  41. package/agents/gsd-research-synthesizer.md +1 -1
  42. package/agents/gsd-roadmapper.compact.md +454 -0
  43. package/agents/gsd-roadmapper.md +13 -0
  44. package/agents/gsd-security-auditor.compact.md +162 -0
  45. package/agents/gsd-ui-auditor.compact.md +404 -0
  46. package/agents/gsd-ui-auditor.md +155 -17
  47. package/agents/gsd-ui-checker.compact.md +277 -0
  48. package/agents/gsd-ui-researcher.compact.md +282 -0
  49. package/agents/gsd-ui-researcher.md +1 -1
  50. package/agents/gsd-user-profiler.compact.md +108 -0
  51. package/agents/gsd-verifier.md +10 -9
  52. package/bin/install.js +848 -163
  53. package/commands/gsd/autonomous.md +2 -2
  54. package/commands/gsd/capture.md +1 -1
  55. package/commands/gsd/cleanup.md +1 -0
  56. package/commands/gsd/code-review.md +2 -1
  57. package/commands/gsd/complete-milestone.md +1 -0
  58. package/commands/gsd/config.md +1 -0
  59. package/commands/gsd/debug.md +1 -0
  60. package/commands/gsd/graphify.md +1 -0
  61. package/commands/gsd/health.md +1 -0
  62. package/commands/gsd/mempalace-capture.md +8 -3
  63. package/commands/gsd/mempalace-recall.md +1 -0
  64. package/commands/gsd/new-milestone.md +1 -0
  65. package/commands/gsd/new-project.md +1 -0
  66. package/commands/gsd/next.md +1 -0
  67. package/commands/gsd/pause-work.md +1 -0
  68. package/commands/gsd/phase.md +1 -0
  69. package/commands/gsd/plan-review-convergence.md +6 -6
  70. package/commands/gsd/pr-branch.md +1 -0
  71. package/commands/gsd/progress.md +1 -1
  72. package/commands/gsd/quick-batch.md +1 -1
  73. package/commands/gsd/resume-work.md +1 -0
  74. package/commands/gsd/review-backlog.md +1 -0
  75. package/commands/gsd/review.md +2 -3
  76. package/commands/gsd/settings.md +2 -1
  77. package/commands/gsd/stats.md +1 -0
  78. package/commands/gsd/thread.md +1 -0
  79. package/commands/gsd/workspace.md +1 -0
  80. package/commands/gsd/workstreams.md +1 -0
  81. package/gsd-core/bin/check-latest-version.cjs +8 -3
  82. package/gsd-core/bin/gsd-tools.cjs +672 -146
  83. package/gsd-core/bin/lib/adr-parser.cjs +4 -2
  84. package/gsd-core/bin/lib/artifacts.cjs +2 -1
  85. package/gsd-core/bin/lib/audit.cjs +119 -34
  86. package/gsd-core/bin/lib/broken-windows.cjs +168 -49
  87. package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
  88. package/gsd-core/bin/lib/capability-loader.cjs +135 -1
  89. package/gsd-core/bin/lib/capability-registry.cjs +96 -189
  90. package/gsd-core/bin/lib/capability-source.cjs +19 -2
  91. package/gsd-core/bin/lib/capability-validator.cjs +14 -2
  92. package/gsd-core/bin/lib/check-command-router.cjs +213 -49
  93. package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
  94. package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
  95. package/gsd-core/bin/lib/commands.cjs +823 -112
  96. package/gsd-core/bin/lib/config-loader.cjs +66 -4
  97. package/gsd-core/bin/lib/config.cjs +186 -45
  98. package/gsd-core/bin/lib/coverage.cjs +1 -1
  99. package/gsd-core/bin/lib/decisions.cjs +164 -45
  100. package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
  101. package/gsd-core/bin/lib/frontmatter.cjs +13 -0
  102. package/gsd-core/bin/lib/graphify.cjs +10 -2
  103. package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
  104. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
  105. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
  106. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  107. package/gsd-core/bin/lib/init.cjs +614 -86
  108. package/gsd-core/bin/lib/install-engine.cjs +29 -3
  109. package/gsd-core/bin/lib/install-profiles.cjs +14 -0
  110. package/gsd-core/bin/lib/installer-migrations.cjs +41 -5
  111. package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
  112. package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
  113. package/gsd-core/bin/lib/milestone.cjs +37 -13
  114. package/gsd-core/bin/lib/model-resolver.cjs +253 -53
  115. package/gsd-core/bin/lib/phase-command-router.cjs +16 -2
  116. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  117. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  118. package/gsd-core/bin/lib/phase-id.cjs +268 -27
  119. package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
  120. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  121. package/gsd-core/bin/lib/phase.cjs +393 -88
  122. package/gsd-core/bin/lib/plan-document.cjs +49 -1
  123. package/gsd-core/bin/lib/planning-document.cjs +459 -0
  124. package/gsd-core/bin/lib/planning-inspect.cjs +52 -19
  125. package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
  126. package/gsd-core/bin/lib/planning-workspace.cjs +57 -3
  127. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  128. package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
  129. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  130. package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
  131. package/gsd-core/bin/lib/project-root.cjs +41 -2
  132. package/gsd-core/bin/lib/quick-batch.cjs +1 -1
  133. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
  134. package/gsd-core/bin/lib/research-store.cjs +11 -12
  135. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  136. package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
  137. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  138. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
  139. package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
  140. package/gsd-core/bin/lib/roadmap-parser.cjs +219 -18
  141. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
  142. package/gsd-core/bin/lib/roadmap.cjs +356 -42
  143. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -41
  144. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +15 -4
  145. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
  146. package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
  147. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -37
  148. package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
  149. package/gsd-core/bin/lib/security.cjs +126 -7
  150. package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
  151. package/gsd-core/bin/lib/state-document.cjs +130 -28
  152. package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
  153. package/gsd-core/bin/lib/state-transition.cjs +181 -30
  154. package/gsd-core/bin/lib/state.cjs +265 -27
  155. package/gsd-core/bin/lib/surface.cjs +77 -3
  156. package/gsd-core/bin/lib/task-command-router.cjs +12 -6
  157. package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
  158. package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
  159. package/gsd-core/bin/lib/uat.cjs +9 -1
  160. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  161. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
  162. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  163. package/gsd-core/bin/lib/update-context.cjs +30 -24
  164. package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
  165. package/gsd-core/bin/lib/verification.cjs +315 -30
  166. package/gsd-core/bin/lib/verify-command-grounding.cjs +47 -3
  167. package/gsd-core/bin/lib/verify.cjs +320 -48
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
  169. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  170. package/gsd-core/bin/lib/worktree-safety.cjs +797 -58
  171. package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
  172. package/gsd-core/bin/shared/config-schema.manifest.json +6 -0
  173. package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
  174. package/gsd-core/references/checkpoints.md +5 -3
  175. package/gsd-core/references/compact-content-gate.md +66 -0
  176. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  177. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  178. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  179. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  180. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  181. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  182. package/gsd-core/references/edge-probe.md +195 -21
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
  184. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  185. package/gsd-core/references/gsd-run-resolver.md +1 -1
  186. package/gsd-core/references/loop-hook-dispatch.md +18 -0
  187. package/gsd-core/references/model-profiles.md +13 -4
  188. package/gsd-core/references/phase-argument-parsing.md +9 -7
  189. package/gsd-core/references/phase-id-convention.md +28 -0
  190. package/gsd-core/references/planner-gap-closure.md +2 -0
  191. package/gsd-core/references/planner-load-graph-context.md +24 -13
  192. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  193. package/gsd-core/references/planning-config.md +14 -2
  194. package/gsd-core/references/tdd.md +30 -4
  195. package/gsd-core/references/thinking-models-planning.md +18 -2
  196. package/gsd-core/references/ui-consideration-probe.md +10 -5
  197. package/gsd-core/references/verification-patterns.md +17 -4
  198. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  199. package/gsd-core/references/worktree-path-safety.md +433 -2
  200. package/gsd-core/templates/README.md +7 -1
  201. package/gsd-core/templates/state.md +6 -3
  202. package/gsd-core/templates/summary.compact.md +212 -0
  203. package/gsd-core/templates/user-setup.compact.md +199 -0
  204. package/gsd-core/templates/user-setup.md +0 -9
  205. package/gsd-core/templates/verification-report.md +1 -1
  206. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  207. package/gsd-core/workflows/add-backlog.md +1 -1
  208. package/gsd-core/workflows/add-phase.md +1 -1
  209. package/gsd-core/workflows/add-tests.md +2 -2
  210. package/gsd-core/workflows/add-todo.md +6 -5
  211. package/gsd-core/workflows/ai-integration-phase.md +11 -3
  212. package/gsd-core/workflows/audit-fix.md +1 -1
  213. package/gsd-core/workflows/audit-milestone.md +1 -1
  214. package/gsd-core/workflows/audit-uat.md +1 -1
  215. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  216. package/gsd-core/workflows/autonomous.md +29 -16
  217. package/gsd-core/workflows/check-todos.md +6 -4
  218. package/gsd-core/workflows/cleanup.md +5 -3
  219. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  220. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +8 -1
  221. package/gsd-core/workflows/code-review-fix.md +108 -22
  222. package/gsd-core/workflows/code-review.md +216 -73
  223. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
  224. package/gsd-core/workflows/complete-milestone.md +41 -264
  225. package/gsd-core/workflows/debug.md +3 -3
  226. package/gsd-core/workflows/diagnose-issues.md +1 -1
  227. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  228. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  229. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  230. package/gsd-core/workflows/discuss-phase.md +1 -1
  231. package/gsd-core/workflows/do.md +2 -2
  232. package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
  233. package/gsd-core/workflows/docs-update.md +17 -158
  234. package/gsd-core/workflows/edit-phase.md +1 -1
  235. package/gsd-core/workflows/eval-review.md +10 -3
  236. package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
  237. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
  238. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +19 -4
  239. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
  240. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +43 -4
  241. package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
  242. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  243. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  244. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  245. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  246. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  247. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  248. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  249. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  250. package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
  251. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
  252. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  253. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  254. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
  255. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  256. package/gsd-core/workflows/execute-phase.md +83 -172
  257. package/gsd-core/workflows/execute-plan.md +24 -10
  258. package/gsd-core/workflows/explore.md +3 -3
  259. package/gsd-core/workflows/extract-learnings.md +2 -1
  260. package/gsd-core/workflows/fast.md +1 -1
  261. package/gsd-core/workflows/forensics.md +1 -1
  262. package/gsd-core/workflows/graduation.md +1 -1
  263. package/gsd-core/workflows/health.md +2 -2
  264. package/gsd-core/workflows/help/modes/full.compact.md +398 -0
  265. package/gsd-core/workflows/help/modes/full.md +5 -5
  266. package/gsd-core/workflows/help/modes/topic.md +15 -5
  267. package/gsd-core/workflows/help.md +1 -1
  268. package/gsd-core/workflows/import.md +2 -2
  269. package/gsd-core/workflows/inbox.md +2 -2
  270. package/gsd-core/workflows/ingest-docs.md +3 -3
  271. package/gsd-core/workflows/insert-phase.md +1 -1
  272. package/gsd-core/workflows/list-seeds.md +1 -1
  273. package/gsd-core/workflows/list-workspaces.md +1 -1
  274. package/gsd-core/workflows/manager.md +2 -2
  275. package/gsd-core/workflows/map-codebase.md +52 -5
  276. package/gsd-core/workflows/milestone-summary.md +1 -1
  277. package/gsd-core/workflows/mvp-phase.md +1 -1
  278. package/gsd-core/workflows/new-milestone.md +56 -14
  279. package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
  280. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  281. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  282. package/gsd-core/workflows/new-project.md +39 -209
  283. package/gsd-core/workflows/new-workspace.md +2 -2
  284. package/gsd-core/workflows/next.md +1 -1
  285. package/gsd-core/workflows/note.md +1 -1
  286. package/gsd-core/workflows/onboard.md +1 -1
  287. package/gsd-core/workflows/pause-work.md +1 -1
  288. package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
  289. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  290. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  291. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  292. package/gsd-core/workflows/plan-phase.md +45 -187
  293. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  294. package/gsd-core/workflows/plant-seed.md +62 -20
  295. package/gsd-core/workflows/pr-branch.md +132 -20
  296. package/gsd-core/workflows/profile-user.md +2 -2
  297. package/gsd-core/workflows/progress.md +1 -1
  298. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
  299. package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
  300. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
  301. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  302. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  303. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  304. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  305. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  306. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  307. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
  308. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  309. package/gsd-core/workflows/quick-batch.md +1 -1
  310. package/gsd-core/workflows/quick.md +29 -10
  311. package/gsd-core/workflows/reapply-patches.md +86 -6
  312. package/gsd-core/workflows/remove-phase.md +1 -1
  313. package/gsd-core/workflows/remove-workspace.md +2 -2
  314. package/gsd-core/workflows/resume-project.md +1 -1
  315. package/gsd-core/workflows/review.md +31 -16
  316. package/gsd-core/workflows/scan.md +1 -1
  317. package/gsd-core/workflows/secure-phase.md +3 -2
  318. package/gsd-core/workflows/settings-advanced.md +30 -10
  319. package/gsd-core/workflows/settings-integrations.md +2 -3
  320. package/gsd-core/workflows/settings.md +22 -9
  321. package/gsd-core/workflows/ship.md +3 -2
  322. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  323. package/gsd-core/workflows/sketch.md +1 -1
  324. package/gsd-core/workflows/smart-entry.md +2 -2
  325. package/gsd-core/workflows/spec-phase.md +15 -5
  326. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  327. package/gsd-core/workflows/spike.md +1 -1
  328. package/gsd-core/workflows/stats.md +1 -1
  329. package/gsd-core/workflows/sync-skills.md +5 -5
  330. package/gsd-core/workflows/thread.md +1 -1
  331. package/gsd-core/workflows/transition.md +1 -1
  332. package/gsd-core/workflows/ui-phase.md +44 -8
  333. package/gsd-core/workflows/ui-review.md +18 -4
  334. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  335. package/gsd-core/workflows/undo.md +339 -20
  336. package/gsd-core/workflows/update.md +14 -12
  337. package/gsd-core/workflows/validate-phase.md +3 -2
  338. package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
  339. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  340. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  341. package/gsd-core/workflows/verify-work.md +101 -196
  342. package/hooks/dist/gsd-agent-isolation-guard.js +66 -16
  343. package/hooks/dist/gsd-context-monitor.js +88 -15
  344. package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
  345. package/hooks/dist/gsd-secret-read-guard.js +71 -19
  346. package/hooks/dist/gsd-statusline.js +81 -20
  347. package/hooks/dist/gsd-validate-commit.sh +97 -8
  348. package/hooks/dist/gsd-worktree-path-guard.js +25 -14
  349. package/hooks/dist/gsd-write-guard.js +46 -1
  350. package/hooks/dist/lib/dispatch-identity.js +187 -0
  351. package/hooks/dist/lib/filename-classification.js +64 -0
  352. package/hooks/dist/lib/isolation-deny-reason.js +53 -1
  353. package/hooks/dist/lib/isolation-sentinel.js +58 -19
  354. package/hooks/gsd-agent-isolation-guard.js +66 -16
  355. package/hooks/gsd-context-monitor.js +88 -15
  356. package/hooks/gsd-cursor-subagent-start.js +34 -14
  357. package/hooks/gsd-secret-read-guard.js +71 -19
  358. package/hooks/gsd-statusline.js +81 -20
  359. package/hooks/gsd-validate-commit.sh +97 -8
  360. package/hooks/gsd-worktree-path-guard.js +25 -14
  361. package/hooks/gsd-write-guard.js +46 -1
  362. package/hooks/lib/dispatch-identity.js +187 -0
  363. package/hooks/lib/filename-classification.js +64 -0
  364. package/hooks/lib/isolation-deny-reason.js +53 -1
  365. package/hooks/lib/isolation-sentinel.js +58 -19
  366. package/package.json +11 -6
  367. package/scripts/benchmark-compact-content-variants.cjs +298 -0
  368. package/scripts/benchmark-compact-content.cjs +368 -0
  369. package/scripts/build-hooks.js +15 -6
  370. package/scripts/check-contract-drift.cjs +131 -12
  371. package/scripts/check-env.cjs +36 -8
  372. package/scripts/check-glossary-refs.cjs +25 -21
  373. package/scripts/ci-next-health.cjs +271 -0
  374. package/scripts/ci-prepare-test-scope.cjs +7 -7
  375. package/scripts/ci-test-scope.cjs +126 -20
  376. package/scripts/ci-timeout-report.cjs +1 -1
  377. package/scripts/command-contract-helpers.cjs +3 -0
  378. package/scripts/diff-touches-shipped-paths.cjs +1 -1
  379. package/scripts/docs-guard-registry.cjs +35 -2
  380. package/scripts/gen-adr-index.cjs +8 -2
  381. package/scripts/gen-inventory-manifest.cjs +12 -0
  382. package/scripts/gen-loop-host-contract.cjs +69 -0
  383. package/scripts/gen-platform-conformance-tier.cjs +557 -0
  384. package/scripts/lib/drift-scan.cjs +1 -1
  385. package/scripts/lib/macos-conformance-tier.generated.cjs +224 -0
  386. package/scripts/lib/ndjson-reporter.cjs +3 -2
  387. package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
  388. package/scripts/lib/platform-conformance-tier.generated.cjs +287 -0
  389. package/scripts/lib/suite-detection.cjs +32 -0
  390. package/scripts/lint-allowed-tools-parity.cjs +221 -0
  391. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +47 -3
  392. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  393. package/scripts/lint-phase-id-drift.cjs +623 -13
  394. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  395. package/scripts/lint-response-language-coverage.cjs +9 -3
  396. package/scripts/lint-retired-runtime-name.cjs +619 -0
  397. package/scripts/lint-source-test-name-collision.cjs +1 -1
  398. package/scripts/lint-state-write-path-drift.cjs +93 -0
  399. package/scripts/lint-test-file-count.allowlist.json +29 -9
  400. package/scripts/lint-vendored-deps.cjs +128 -17
  401. package/scripts/lint-workflow-shellcheck-baseline.json +100 -0
  402. package/scripts/prompt-injection-scan.sh +18 -0
  403. package/scripts/release-tarball-smoke.cjs +194 -1
  404. package/scripts/workflow-size.cjs +139 -0
  405. package/skills/gsd-autonomous/SKILL.md +2 -2
  406. package/skills/gsd-capture/SKILL.md +1 -1
  407. package/skills/gsd-cleanup/SKILL.md +1 -0
  408. package/skills/gsd-code-review/SKILL.md +2 -1
  409. package/skills/gsd-complete-milestone/SKILL.md +1 -0
  410. package/skills/gsd-config/SKILL.md +1 -0
  411. package/skills/gsd-debug/SKILL.md +1 -0
  412. package/skills/gsd-graphify/SKILL.md +1 -0
  413. package/skills/gsd-health/SKILL.md +1 -0
  414. package/skills/gsd-mempalace-capture/SKILL.md +8 -3
  415. package/skills/gsd-mempalace-recall/SKILL.md +1 -0
  416. package/skills/gsd-new-milestone/SKILL.md +1 -0
  417. package/skills/gsd-new-project/SKILL.md +1 -0
  418. package/skills/gsd-next/SKILL.md +1 -0
  419. package/skills/gsd-pause-work/SKILL.md +1 -0
  420. package/skills/gsd-phase/SKILL.md +1 -0
  421. package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
  422. package/skills/gsd-pr-branch/SKILL.md +1 -0
  423. package/skills/gsd-progress/SKILL.md +1 -1
  424. package/skills/gsd-quick-batch/SKILL.md +1 -1
  425. package/skills/gsd-resume-work/SKILL.md +1 -0
  426. package/skills/gsd-review/SKILL.md +2 -3
  427. package/skills/gsd-review-backlog/SKILL.md +1 -0
  428. package/skills/gsd-settings/SKILL.md +2 -1
  429. package/skills/gsd-stats/SKILL.md +1 -0
  430. package/skills/gsd-thread/SKILL.md +1 -0
  431. package/skills/gsd-workspace/SKILL.md +1 -0
  432. package/skills/gsd-workstreams/SKILL.md +1 -0
  433. package/vscode/package.json +1 -1
  434. package/gsd-core/templates/claude-md.md +0 -145
  435. package/gsd-core/templates/codebase/concerns.md +0 -310
  436. package/gsd-core/templates/codebase/conventions.md +0 -307
  437. package/gsd-core/templates/codebase/integrations.md +0 -280
  438. package/gsd-core/templates/codebase/structure.md +0 -285
  439. package/gsd-core/templates/codebase/testing.md +0 -480
  440. package/gsd-core/templates/debug-subagent-prompt.md +0 -91
  441. package/gsd-core/templates/discovery.md +0 -146
@@ -11,16 +11,25 @@ const path = require('path');
11
11
  const vm = require('vm');
12
12
 
13
13
  const HOOKS_DIR = path.join(__dirname, '..', 'hooks');
14
+ const REPO_ROOT = path.join(HOOKS_DIR, '..');
14
15
  const DIST_DIR = path.join(HOOKS_DIR, 'dist');
15
16
  // Per-process staging directory for atomic writes. Using process.pid in the
16
17
  // name eliminates all contention between concurrent builders: each process
17
18
  // owns its own staging dir and never races with another builder's cleanup.
18
- // Lives under hooks/ so it shares a filesystem with DIST_DIR (POSIX
19
- // rename(2) is only atomic within the same filesystem) but is NOT inside
20
- // DIST_DIR — so readers that readdirSync(DIST_DIR) (e.g. bin/install.js,
21
- // install-hooks-copy tests) never observe a transient ".tmp" sibling.
22
- // The parent pattern hooks/.dist-staging-*/ is gitignored.
23
- const STAGE_DIR = path.join(HOOKS_DIR, `.dist-staging-${process.pid}`);
19
+ // A SIBLING of hooks/ (not inside it) — still on the same filesystem as
20
+ // DIST_DIR (both live under the repo checkout), so rename(2) stays atomic —
21
+ // but this way nothing that walks or copies hooks/ as a whole can ever
22
+ // observe this directory being created/populated/deleted concurrently.
23
+ // tests/gsd-validate-commit-sigpipe.test.cjs's fixture setup does exactly
24
+ // that (fs.cpSync(HOOKS_DIR, ..., {recursive: true})), and when its walk
25
+ // entered a staging dir that a concurrent build-hooks.js run deleted
26
+ // mid-iteration, Node's native recursive-copy (std::filesystem under the
27
+ // hood) threw an uncaught C++ exception and aborted the whole process with
28
+ // SIGABRT — not a catchable JS error. Moving the staging dir out of hooks/
29
+ // closes that race for every current and future hooks/-tree walker, not
30
+ // just that one call site (which also gained its own defensive filter).
31
+ // The parent pattern .dist-staging-*/ (repo root) is gitignored.
32
+ const STAGE_DIR = path.join(REPO_ROOT, `.dist-staging-${process.pid}`);
24
33
 
25
34
  // Hooks to copy (pure Node.js, no bundling needed)
26
35
  const HOOKS_TO_COPY = [
@@ -27,6 +27,10 @@
27
27
  * 7. Reverse direction: a workflow/command matching a quoted `## TOKEN`
28
28
  * no agent declares or emits is dispatch-on-phantom
29
29
  * (unmatched_consumer_token) — F9's shape from the consumer side.
30
+ * 8. Every reference pointer an agent file carries either resolves to
31
+ * exactly the regular file it names, contained under
32
+ * gsd-core/references/, or is reported (unresolved_reference_include) —
33
+ * a pointer the follower declines is never silently dropped (#4930).
30
34
  *
31
35
  * Exit 0 = clean. Exit 1 = violations (with diagnostics on stderr).
32
36
  */
@@ -35,6 +39,9 @@
35
39
 
36
40
  const fs = require('fs');
37
41
  const path = require('path');
42
+ // The repo's ONE containment decision (ADR-4650, src/security.cts) — realpath-resolved, so an
43
+ // intermediate component referring outside the root is refused before this loop reads the file.
44
+ const { tryWithinRoot } = require('../gsd-core/bin/lib/security.cjs');
38
45
 
39
46
  function resolveRoot(argv) {
40
47
  const idx = argv.indexOf('--root');
@@ -92,8 +99,9 @@ function toRepoRelative(absPath) {
92
99
  /**
93
100
  * referenceIncludes(content)
94
101
  *
95
- * Plain scan for `@~/.claude/gsd-core/references/*.md` tokens anywhere in an
96
- * agent file's content -- inside an `<execution_context>` block (already
102
+ * Plain scan for `@~/.claude/gsd-core/references/*.md` tokens (and the bare
103
+ * `@gsd-core/references/*.md` spelling, #4841) anywhere in an agent file's
104
+ * content -- inside an `<execution_context>` block (already
97
105
  * covered structurally by `executionContextRefs` in command-contract-helpers,
98
106
  * but a raw regex over the whole string picks those up too) and, just as
99
107
  * importantly, OUTSIDE one: agents frequently point at a reference doc from
@@ -105,17 +113,94 @@ function toRepoRelative(absPath) {
105
113
  * lives in `gsd-core/references/planner-guidance.md`, which the agent only
106
114
  * `@`-includes -- so the producer scan below must follow these includes to
107
115
  * see markers an agent's contract legitimately delegates to a reference doc.
108
- * Returns ROOT-relative paths (`gsd-core/references/foo.md`), de-duplicated.
116
+ * Returns `{ includes, declined }`: `includes` are ROOT-relative paths
117
+ * (`gsd-core/references/foo.md`), de-duplicated; `declined` are the pointers
118
+ * this scan refused to follow, as written, each with its reason (#4930).
109
119
  */
110
120
  function referenceIncludes(content) {
111
121
  const seen = new Set();
112
- const re = /@~\/\.claude\/gsd-core\/references\/[A-Za-z0-9._-]+\.md/g;
122
+ const declined = new Map();
123
+ // Both spellings the agent corpus has carried: the installed-path form the
124
+ // installer rewrites per profile, and the bare repo-relative `@gsd-core/…`
125
+ // form (#4841) that it does not. The bare form is now refused in agents/ by
126
+ // tests/shipped-reference-cites.test.cjs; it is followed here so a pointer
127
+ // that slips past that gate is still scanned rather than silently dropped.
128
+ //
129
+ // THE TOKEN IS CAPTURED WHOLE AND THE NAME GRAMMAR IS ANCHORED AT BOTH ENDS.
130
+ // Nested names are allowed (`few-shot-examples/verifier.md`); every segment
131
+ // starts with `[A-Za-z0-9_-]` — narrower than "non-dot", since `+x.md` and
132
+ // `é.md` do not match either — so a `.` or `..` segment is never a name.
133
+ // Anchoring is what makes that a statement about the whole pointer rather
134
+ // than about its first few segments: an earlier form of this pattern ended
135
+ // at `\.md` with no following boundary, so `…/references/tdd.md/xx/yy` was
136
+ // followed as `tdd.md`, and THIS FUNCTION'S CALLER READS THE PATH IT
137
+ // RETURNS — `fs.readFileSync` in main()'s agent loop — so a truncated prefix
138
+ // folded the wrong file's text into the scanned corpus. Matching the whole
139
+ // whitespace-delimited token and requiring it to satisfy the grammar end to
140
+ // end closes that by construction, and for any separator spelling rather
141
+ // than the ones a boundary lookahead happens to enumerate.
142
+ //
143
+ // ANCHORING DOES NOT ESTABLISH CONTAINMENT, and an earlier revision of this
144
+ // comment said it did. The grammar's refusal of `.`/`..` segments covers the
145
+ // TEXTUAL half only: an intermediate component that refers outside the tree
146
+ // is resolved transparently on the way to the file. Containment is decided
147
+ // separately, below, by `tryWithinRoot` — and it has to be, because this
148
+ // loop READS what it resolves.
149
+ //
150
+ // A token that does not parse WHOLE is declined rather than truncated — the
151
+ // conservative half of the same rule — and the decline is RETURNED, not
152
+ // dropped (#4930): main() reports it as unresolved_reference_include. A
153
+ // skipped pointer is a scan this tool did not perform, and the verdict it
154
+ // prints depends on that scan, so the skip has to be visible in the tool's
155
+ // own output. tests/shipped-reference-cites.test.cjs reports the same token
156
+ // for the shipped agents/ tree; this is the half that holds for any --root.
157
+ // FOUR spellings. `@$HOME/.claude/` is a second installed-path form the installer rewrites
158
+ // explicitly (applyAgentPathRewritesInner's `/\$HOME\/\.claude\//g` replace, beside the `~/.claude/`
159
+ // one). The fourth is a FAMILY rather than a string: `--relative-includes` (#4377) makes a local
160
+ // install emit project-relative includes whose prefix is DERIVED from the resolved config dir, so it
161
+ // is matched by SHAPE — one or MORE leading segments before `gsd-core` (a config dir nested under
162
+ // the project root emits `@config/nested/gsd-core/…`), none of them `.` or `..`. The `+` keeps the
163
+ // bare form the bare form by construction, since it needs a segment BEFORE `gsd-core`.
164
+ //
165
+ // THE SHAPE IS NARROWER THAN THE FAMILY, and saying otherwise would be the overstatement this
166
+ // function has already had to retract once. `_computePathPrefix` can emit a prefix this character
167
+ // class does not match — a config dir named `config+nested` or `ümlaut` — and widening the class to
168
+ // arbitrary directory names is what would turn every `@scope/…` token in prose into a pointer. So a
169
+ // prefix outside the class is not followed, which is where this follower was for ALL
170
+ // project-relative spellings before #4841. The gate's own comment states the same bound; the two
171
+ // must not drift apart, because between them they are the only record of it.
172
+ const re = /@(?:(?:~|\$HOME)\/\.claude\/|(?:(?!\.\.?\/)[A-Za-z0-9._-]+\/)+)?gsd-core\/references\/(\S+)/g;
173
+ const name = /^(?:[A-Za-z0-9_-][A-Za-z0-9._-]*\/)*[A-Za-z0-9_-][A-Za-z0-9._-]*\.md$/;
174
+ // Trailing prose punctuation is not part of a filename — a pointer may end a
175
+ // sentence or sit inside backticks, parentheses or bold markers. The class
176
+ // cannot eat into `.md`, which ends at `d`.
177
+ // START-ANCHORED: this tests a CUT SUFFIX, so it must be punctuation END TO END. An unanchored
178
+ // `/…$/` answers true for `/xx?`, which would let the loop cut a separator and call the remainder a
179
+ // name — `tdd.md/xx?` following as `tdd.md`, the defect this whole function was rewritten to close.
180
+ const trailingProseOnly = /^[.,;:!?)\]}>"'`*]+$/;
113
181
  let m;
114
182
  while ((m = re.exec(content)) !== null) {
115
- const relPath = 'gsd-core/references/' + m[0].slice('@~/.claude/gsd-core/references/'.length);
116
- seen.add(relPath);
183
+ const raw = m[1];
184
+ // MINIMAL strip, same rule as the gate: shortest trailing run whose removal yields a valid name.
185
+ let candidate = null;
186
+ for (let cut = 0; cut <= raw.length; cut++) {
187
+ const probe = raw.slice(0, raw.length - cut);
188
+ if (cut > 0 && !trailingProseOnly.test(raw.slice(raw.length - cut))) break;
189
+ if (name.test(probe)) { candidate = probe; break; }
190
+ }
191
+ if (candidate === null) {
192
+ declined.set(m[0], 'does not name a reference whole — the text continues past the name, so nothing is followed');
193
+ continue;
194
+ }
195
+ // AMBIGUITY, mirrored from the gate. If the UNSTRIPPED token also names something, the strip would
196
+ // pick one of two readings — and this loop READS what it picks, so it declines rather than guess.
197
+ if (candidate !== raw && fs.existsSync(path.join(ROOT, 'gsd-core', 'references', raw))) {
198
+ declined.set(m[0], `is ambiguous — \`${raw}\` itself names something on disk, so stripping it to \`${candidate}\` would pick one of two readings; neither is followed`);
199
+ continue;
200
+ }
201
+ seen.add('gsd-core/references/' + candidate);
117
202
  }
118
- return [...seen];
203
+ return { includes: [...seen], declined: [...declined].map(([pointer, reason]) => ({ pointer, reason })) };
119
204
  }
120
205
 
121
206
  function remedyFor(kind) {
@@ -158,9 +243,13 @@ function main() {
158
243
  const candidateMarkers = new Map();
159
244
  const agentTexts = new Map();
160
245
  const unclosedFenceViolations = [];
246
+ const includeViolations = [];
161
247
 
248
+ // #4407: .compact.md variant siblings are an alternate rendering of their
249
+ // canonical agent's SAME contract, not a distinct one — excluded so they
250
+ // don't need (and can't drift from) their own registry row.
162
251
  const agentFiles = fs.existsSync(AGENTS_DIR)
163
- ? fs.readdirSync(AGENTS_DIR).filter(f => f.endsWith('.md'))
252
+ ? fs.readdirSync(AGENTS_DIR).filter(f => f.endsWith('.md') && !f.endsWith('.compact.md'))
164
253
  : [];
165
254
 
166
255
  for (const file of agentFiles) {
@@ -170,12 +259,41 @@ function main() {
170
259
  // Single pass over the agent's @-included references: each file is read
171
260
  // once and feeds BOTH the read-tag fold (agentTexts) and marker
172
261
  // extraction (producer/candidate attribution).
262
+ // #4930: every include this loop does NOT fold in is reported, never
263
+ // swallowed. An unfollowed include is text the marker verdict below was
264
+ // computed without, so a decline that leaves no trace in the output makes
265
+ // that verdict unexplainable. (lint-command-contract's @-ref existence rule
266
+ // covers commands' <execution_context> only, not agents.)
173
267
  const includeTexts = [];
174
- for (const refRelPath of referenceIncludes(content)) {
268
+ const reportInclude = (pointer, reason) => includeViolations.push({
269
+ kind: 'unresolved_reference_include',
270
+ agent,
271
+ marker: null,
272
+ detail: `${toRepoRelative(abs)}: ${pointer} ${reason}`,
273
+ });
274
+ const { includes, declined } = referenceIncludes(content);
275
+ for (const { pointer, reason } of declined) reportInclude(pointer, reason);
276
+ for (const refRelPath of includes) {
277
+ // CONTAIN BEFORE READ. The name grammar refuses `.`/`..` segments, which covers the TEXTUAL
278
+ // half of containment and nothing else: an intermediate path component that refers outside
279
+ // the tree is resolved transparently on the way to the file, so a textually-clean name can
280
+ // still address something outside `references/`. This loop READS what it resolves and folds
281
+ // the text into the scanned corpus, so the real path is what has to be checked.
282
+ const refsDir = path.join(ROOT, 'gsd-core', 'references');
283
+ const contained = tryWithinRoot(refRelPath.slice('gsd-core/references/'.length), refsDir);
284
+ if (contained === null) {
285
+ reportInclude(refRelPath, 'does not resolve inside gsd-core/references/');
286
+ continue;
287
+ }
175
288
  try {
176
- includeTexts.push(fs.readFileSync(path.join(ROOT, refRelPath), 'utf-8'));
177
- } catch {
178
- // include miss — lint-command-contract rule 4 owns @-ref existence
289
+ // Read the ContainedPath the predicate returned, never a re-joined path (ADR-4650).
290
+ if (!fs.lstatSync(contained).isFile()) {
291
+ reportInclude(refRelPath, 'is not a regular file');
292
+ continue;
293
+ }
294
+ includeTexts.push(fs.readFileSync(contained, 'utf-8'));
295
+ } catch (e) {
296
+ reportInclude(refRelPath, e && e.code === 'ENOENT' ? 'does not exist' : `could not be read (${e && e.code})`);
179
297
  }
180
298
  }
181
299
  agentTexts.set(agent, [content, ...includeTexts].join('\n'));
@@ -234,6 +352,7 @@ function main() {
234
352
  const allViolations = [
235
353
  ...parseViolations,
236
354
  ...unclosedFenceViolations,
355
+ ...includeViolations,
237
356
  ...contractViolationsList,
238
357
  ...readTagViolationsList,
239
358
  ...reverseViolationsList,
@@ -30,7 +30,17 @@ const path = require('path');
30
30
  const { spawnSync } = require('child_process');
31
31
 
32
32
  const { ExitError, runMain } = require('./lib/cli-exit.cjs');
33
+ const { describeNpmVersionCheckFailure } = require('./lib/npm-version-check-diagnosis.cjs');
33
34
 
35
+ // #4460 follow-up: check-env.cjs runs as its own standalone CI step BEFORE
36
+ // `npm ci` / `npm run build:lib` (a deliberate pre-flight, run before there
37
+ // is even a node_modules to build with) -- confirmed the hard way, by a
38
+ // MODULE_NOT_FOUND crash on every real CI platform after a first attempt at
39
+ // this fix routed the npm-version check through the canonical execNpm seam
40
+ // (gsd-core/bin/lib/shell-command-projection.cjs), a tsc-compiled artifact
41
+ // that plain does not exist yet at that point in the pipeline. This file
42
+ // must stay self-contained: no requires reaching into gsd-core/bin/lib.
43
+ //
34
44
  // On Windows, npm ships as npm.cmd (a batch wrapper); spawnSync without
35
45
  // shell:true requires the exact filename including extension.
36
46
  const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
@@ -180,18 +190,36 @@ function main() {
180
190
  // Check 2: npm version vs engines.npm (skip if field absent)
181
191
  // ---------------------------------------------------------------------------
182
192
  const enginesNpm = pkgField('engines.npm', PROJECT_ROOT);
183
- let currentNpm = '';
184
- try {
185
- const res = spawnSync(npmCmd, ['--version'], { encoding: 'utf8', timeout: 10_000, shell: process.platform === 'win32' });
186
- if (res.status === 0 && res.stdout) {
187
- currentNpm = res.stdout.trim();
188
- }
189
- } catch { /* ignore */ }
193
+ // #4460: 15_000ms, not the original 10_000 -- matches the default this
194
+ // repo's canonical (but here unusable, see the file-header note above)
195
+ // execNpm seam already uses for npm subprocess calls generally, rather
196
+ // than inventing a new number. Confirmed via real Windows CI: a 10s
197
+ // window was insufficient twice under ~51-file concurrent test load.
198
+ const NPM_VERSION_TIMEOUT_MS = 15_000;
199
+ // No try/catch needed: spawnSync's documented contract routes ENOENT and a
200
+ // timeout-triggered kill through the RETURNED result's `.error` field, not
201
+ // a thrown exception -- there is nothing here for a catch to intercept.
202
+ const npmVersionSpawn = spawnSync(npmCmd, ['--version'], { encoding: 'utf8', timeout: NPM_VERSION_TIMEOUT_MS, shell: process.platform === 'win32' });
203
+ const npmVersionResult = {
204
+ exitCode: npmVersionSpawn.status ?? 1,
205
+ stdout: (npmVersionSpawn.stdout || '').toString().trim(),
206
+ signal: npmVersionSpawn.signal ?? null,
207
+ error: npmVersionSpawn.error ?? null,
208
+ // Canonical cross-platform timeout predicate (matches this repo's
209
+ // execNpm/isSpawnTimeout convention, src/shell-command-projection.cts):
210
+ // error.code === 'ETIMEDOUT', which Node's spawnSync guarantees when its
211
+ // own `timeout` option fires. Checking `signal === 'SIGTERM'` instead
212
+ // (what an earlier version of this fix did) is platform-fragile -- that
213
+ // module's own docstring flags a Windows-specific false-negative risk,
214
+ // the exact platform this bug was discovered on.
215
+ timedOut: (npmVersionSpawn.error && npmVersionSpawn.error.code === 'ETIMEDOUT') === true,
216
+ };
217
+ const currentNpm = npmVersionResult.exitCode === 0 && npmVersionResult.stdout ? npmVersionResult.stdout : '';
190
218
 
191
219
  if (!enginesNpm) {
192
220
  addCheck('npm-version', 'skip', 'engines.npm not set in package.json — skipping');
193
221
  } else if (!currentNpm) {
194
- addCheck('npm-version', 'fail', 'npm binary not found on PATH');
222
+ addCheck('npm-version', 'fail', describeNpmVersionCheckFailure(npmVersionResult));
195
223
  } else {
196
224
  if (satisfiesConstraint(currentNpm, enginesNpm)) {
197
225
  addCheck('npm-version', 'pass', `npm ${currentNpm} satisfies ${enginesNpm}`);
@@ -37,6 +37,7 @@ const fs = require('node:fs');
37
37
  const path = require('node:path');
38
38
 
39
39
  const { ExitError, runMain } = require('./lib/cli-exit.cjs');
40
+ const { tryWithinRoot, PathAcceptance } = require('../gsd-core/bin/lib/security.cjs');
40
41
 
41
42
  const ROOT = path.resolve(__dirname, '..');
42
43
  const CONTEXT_PATH = path.join(ROOT, 'CONTEXT.md');
@@ -131,20 +132,6 @@ function isTracked(token) {
131
132
  return TRACKED_EXACT.has(token) || TRACKED_PREFIXES.some((prefix) => token.startsWith(prefix));
132
133
  }
133
134
 
134
- /**
135
- * True if joining `token` to ROOT stays inside ROOT. `PATH_TOKEN_RE` admits `.`
136
- * inside a segment, so a token like `src/../../../etc/passwd` matches and (via
137
- * the `src/` prefix) reads as "tracked" — `path.join(ROOT, token)` would then
138
- * normalize to an out-of-tree absolute path and `fs.existsSync` would probe it,
139
- * turning a doc lint into a filesystem-existence oracle on the CI host. A
140
- * CONTEXT.md reference is always a plain in-repo path, so a `..` escape is never
141
- * legitimate: confine to ROOT and drop anything that climbs out.
142
- */
143
- function isWithinRoot(token) {
144
- const resolved = path.resolve(ROOT, token);
145
- return resolved === ROOT || resolved.startsWith(ROOT + path.sep);
146
- }
147
-
148
135
  /**
149
136
  * Every distinct, trackable file-path token referenced in `text`, with any
150
137
  * trailing `:<line>` suffix stripped.
@@ -167,7 +154,11 @@ function isWithinRoot(token) {
167
154
  * (CONTRIBUTING's "Do not compute a next number locally"), never a real path.
168
155
  */
169
156
  function extractTrackedRefs(text) {
170
- const tokens = new Set();
157
+ // Maps token -> the ContainedPath tryWithinRoot returned for it. ADR-4650:
158
+ // the value that was validated for containment must be the exact value
159
+ // that gets probed later — never a path re-derived (e.g. re-joined) from
160
+ // the token, which could diverge from what was actually checked.
161
+ const tokens = new Map();
171
162
  const add = (raw) => {
172
163
  if (!PATH_TOKEN_RE.test(raw)) return;
173
164
  const token = raw.replace(/:\d+$/, '');
@@ -177,8 +168,18 @@ function extractTrackedRefs(text) {
177
168
  if (!/[A-Za-z0-9_]$/.test(token)) return;
178
169
  if (token.includes('NNNN')) return;
179
170
  if (!isTracked(token)) return;
180
- if (!isWithinRoot(token)) return;
181
- tokens.add(token);
171
+ // Containment decision is the canonical predicate's, per ADR-4650. Carry
172
+ // the returned ContainedPath forward so checkFileRefs stats the SAME
173
+ // value that was validated, instead of re-joining `token` onto ROOT.
174
+ //
175
+ // The containment ANSWER is unchanged from the retired lexical-only
176
+ // `isWithinRoot`, but the canonical predicate resolves symlinks, so a
177
+ // rejected token is now realpath-resolved before being rejected rather
178
+ // than rejected by string comparison alone; the result is still never
179
+ // surfaced and the token is never stat'd unless it is contained.
180
+ const contained = tryWithinRoot(token, ROOT, PathAcceptance.AbsoluteInsideRoot);
181
+ if (contained === null) return;
182
+ tokens.set(token, contained);
182
183
  };
183
184
  const subTokenRe = /[\w.-]+(?:\/[\w.-]+)*/g;
184
185
  for (const line of text.split(/\r?\n/)) {
@@ -199,14 +200,17 @@ function extractTrackedRefs(text) {
199
200
 
200
201
  /** Check A: every tracked reference must resolve on disk. */
201
202
  function checkFileRefs(contextText) {
202
- const tokens = [...extractTrackedRefs(contextText)].sort();
203
+ const entries = [...extractTrackedRefs(contextText)].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
203
204
  const findings = [];
204
- for (const token of tokens) {
205
- if (!fs.existsSync(path.join(ROOT, token))) {
205
+ for (const [token, contained] of entries) {
206
+ // Stat the ContainedPath returned by tryWithinRoot — NOT a re-joined
207
+ // path.join(ROOT, token) — so the path that was validated for
208
+ // containment is the path that is probed (ADR-4650).
209
+ if (!fs.existsSync(contained)) {
206
210
  findings.push(`CONTEXT.md references \`${token}\` which does not exist in the repo.`);
207
211
  }
208
212
  }
209
- return { findings, checked: tokens.length };
213
+ return { findings, checked: entries.length };
210
214
  }
211
215
 
212
216
  /** The glossary's own claim: `Runtime enum: `allRuntimes` (N values: a, b, c)`. */
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // ci-next-health.cjs — #4422 base-branch health gate.
5
+ //
6
+ // Risk asymmetry drives this design, same shape as scripts/ci-pr-mergeability.cjs
7
+ // but pointed the other direction: on 2026-09-06 three unrelated PRs merged on
8
+ // top of an already-broken `next` before anyone noticed, because nothing checked
9
+ // the base branch's OWN health before letting a PR land on it. A false positive
10
+ // here (a healthy `next` wrongly reported RED) blocks every PR merge in the repo
11
+ // until a human notices and applies the `fix-next` bypass label — annoying, but
12
+ // loud and immediately actionable. A false negative (a broken `next` reported
13
+ // healthy) reproduces the #4422 incident exactly. So, unlike the mergeability
14
+ // preflight, this gate does NOT fail open on a definite red signal — it fails
15
+ // open only when the signal itself is unavailable or inapplicable (wrong event,
16
+ // no resolvable base ref, an API read that throws). Once GitHub actually answers
17
+ // with a non-success conclusion for the base branch's last push-triggered Tests
18
+ // run, that is treated as authoritative and blocks — with one explicit, visible,
19
+ // human-operated escape hatch (the `fix-next` label) for the PR that is itself
20
+ // the fix-forward.
21
+ //
22
+ // Every non-success conclusion (`failure`, `cancelled`, `timed_out`,
23
+ // `action_required`, ...) is treated as RED, not just `failure` — a cancelled or
24
+ // timed-out run on the base branch is not evidence the branch is healthy, it is
25
+ // evidence nobody knows yet.
26
+
27
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
28
+
29
+ const VERDICT = Object.freeze({
30
+ CLEAN: 'CLEAN',
31
+ RED: 'RED',
32
+ BYPASSED: 'BYPASSED',
33
+ SKIPPED_NOT_APPLICABLE: 'SKIPPED_NOT_APPLICABLE',
34
+ INDETERMINATE: 'INDETERMINATE',
35
+ });
36
+
37
+ const APPLICABLE_EVENTS = new Set(['pull_request', 'merge_group']);
38
+ const BYPASS_LABEL = 'fix-next';
39
+ const TESTS_WORKFLOW_FILE = 'test.yml';
40
+
41
+ /**
42
+ * Pure, total classifier: the GitHub "list workflow runs" response payload ->
43
+ * CLEAN | RED | INDETERMINATE. Never throws.
44
+ *
45
+ * - Not an object, or `workflow_runs` isn't an array -> INDETERMINATE: the
46
+ * shape we depend on is not present, so nothing can be concluded.
47
+ * - No completed push runs found yet (e.g. a brand-new `release/**`/`hotfix/**`
48
+ * branch with no Tests history) -> CLEAN: absence of evidence of red is not
49
+ * evidence of red, and a brand-new branch must not be permanently unmergeable.
50
+ * - The most recent run's `conclusion === 'success'` -> CLEAN.
51
+ * - Anything else (failure, cancelled, timed_out, action_required, ...) -> RED.
52
+ */
53
+ function classifyRunConclusion(payload) {
54
+ if (payload === null || typeof payload !== 'object') return VERDICT.INDETERMINATE;
55
+ if (!Array.isArray(payload.workflow_runs)) return VERDICT.INDETERMINATE;
56
+ if (payload.workflow_runs.length === 0) return VERDICT.CLEAN;
57
+ const [latest] = payload.workflow_runs;
58
+ if (latest && latest.conclusion === 'success') return VERDICT.CLEAN;
59
+ return VERDICT.RED;
60
+ }
61
+
62
+ /**
63
+ * Dependency-injected orchestrator. `fetchLatestRun` is an async function
64
+ * taking the resolved base branch name and returning the parsed API payload
65
+ * (or throwing) — injected so tests never touch the network.
66
+ *
67
+ * @returns {Promise<{verdict:string, payload:*, reason:string}>}
68
+ */
69
+ async function resolveNextHealth({ fetchLatestRun, eventName, baseRef, labels } = {}) {
70
+ if (!APPLICABLE_EVENTS.has(eventName)) {
71
+ return { verdict: VERDICT.SKIPPED_NOT_APPLICABLE, payload: null, reason: 'not-applicable-event' };
72
+ }
73
+
74
+ if (typeof baseRef !== 'string' || baseRef.trim() === '') {
75
+ return { verdict: VERDICT.INDETERMINATE, payload: null, reason: 'no-base-ref' };
76
+ }
77
+
78
+ let payload = null;
79
+ try {
80
+ payload = await fetchLatestRun(baseRef);
81
+ } catch (err) {
82
+ return { verdict: VERDICT.INDETERMINATE, payload: null, reason: `fetch-failed: ${err.message}` };
83
+ }
84
+
85
+ const classified = classifyRunConclusion(payload);
86
+ if (classified !== VERDICT.RED) {
87
+ return { verdict: classified, payload, reason: 'resolved' };
88
+ }
89
+
90
+ // Escape hatch. `labels` is empty/absent for merge_group — that event has no
91
+ // label surface today, which means a queued merge-group commit cannot use
92
+ // this bypass. Known gap, not solved here: a maintainer must land the
93
+ // fix-forward as a direct pull_request merge (where the label IS readable)
94
+ // rather than through the merge queue, until GitHub exposes an equivalent
95
+ // signal for merge_group.
96
+ const labelList = Array.isArray(labels) ? labels : [];
97
+ if (labelList.includes(BYPASS_LABEL)) {
98
+ return { verdict: VERDICT.BYPASSED, payload, reason: 'bypass-label' };
99
+ }
100
+
101
+ return { verdict: VERDICT.RED, payload, reason: 'red' };
102
+ }
103
+
104
+ function usage() {
105
+ return [
106
+ 'Usage:',
107
+ ' node scripts/ci-next-health.cjs',
108
+ '',
109
+ 'CI-only gate: checks whether the PR/merge-group\'s base branch\'s own last',
110
+ 'push-triggered Tests run is red, and fails the job (exit 1) when it is —',
111
+ 'unless the PR carries the "fix-next" bypass label. Every other case',
112
+ '(wrong event, no resolvable base ref, an unreadable API read) fails open',
113
+ '(exit 0).',
114
+ '',
115
+ 'Environment variables read:',
116
+ ' GITHUB_EVENT_NAME workflow trigger event; only "pull_request" and',
117
+ ' "merge_group" are checked',
118
+ ' GITHUB_REPOSITORY owner/repo',
119
+ ' GITHUB_TOKEN optional bearer token for the API read',
120
+ ' GITHUB_BASE_REF PR base branch name (pull_request events; set',
121
+ ' automatically by GitHub Actions)',
122
+ ' MERGE_GROUP_BASE_REF merge-group base ref (merge_group events; the',
123
+ ' caller workflow must populate this from',
124
+ ' github.event.merge_group.base_ref — a leading',
125
+ ' "refs/heads/" prefix is stripped if present)',
126
+ ' GITHUB_API_URL GitHub API base URL (default: https://api.github.com)',
127
+ ' PR_LABELS comma-separated PR label names (pull_request events;',
128
+ ' the caller workflow must populate this from',
129
+ ' github.event.pull_request.labels.*.name); checked',
130
+ ' for the literal "fix-next" bypass label',
131
+ ' GITHUB_OUTPUT path to append verdict= step output to',
132
+ ' GITHUB_STEP_SUMMARY path to append a human-readable summary to',
133
+ ].join('\n');
134
+ }
135
+
136
+ function parseArgs(argv) {
137
+ for (const arg of argv) {
138
+ if (arg === '--help' || arg === '-h') {
139
+ process.stdout.write(`${usage()}\n`);
140
+ throw new ExitError(0);
141
+ }
142
+ throw new Error(`unknown argument: ${arg}`);
143
+ }
144
+ }
145
+
146
+ function writeOutput(lines) {
147
+ const outputPath = process.env.GITHUB_OUTPUT;
148
+ if (typeof outputPath !== 'string' || outputPath === '') return;
149
+ try {
150
+ const fs = require('node:fs');
151
+ fs.appendFileSync(outputPath, `${lines.join('\n')}\n`);
152
+ } catch (err) {
153
+ // A failure while REPORTING the verdict must never invert the gate.
154
+ process.stderr.write(`::warning::failed to write GITHUB_OUTPUT: ${err.message}\n`);
155
+ }
156
+ }
157
+
158
+ function writeSummary(text) {
159
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY;
160
+ if (typeof summaryPath !== 'string' || summaryPath === '') return;
161
+ try {
162
+ const fs = require('node:fs');
163
+ fs.appendFileSync(summaryPath, `${text}\n`);
164
+ } catch (err) {
165
+ process.stderr.write(`::warning::failed to write GITHUB_STEP_SUMMARY: ${err.message}\n`);
166
+ }
167
+ }
168
+
169
+ function resolveBaseRef(eventName) {
170
+ if (eventName === 'pull_request') {
171
+ return process.env.GITHUB_BASE_REF || '';
172
+ }
173
+ if (eventName === 'merge_group') {
174
+ const raw = process.env.MERGE_GROUP_BASE_REF || '';
175
+ return raw.startsWith('refs/heads/') ? raw.slice('refs/heads/'.length) : raw;
176
+ }
177
+ return '';
178
+ }
179
+
180
+ function parseLabels(raw) {
181
+ if (typeof raw !== 'string' || raw.trim() === '') return [];
182
+ return raw.split(',').map((s) => s.trim()).filter((s) => s !== '');
183
+ }
184
+
185
+ function runUrlOf(payload) {
186
+ const run = payload && Array.isArray(payload.workflow_runs) ? payload.workflow_runs[0] : undefined;
187
+ return run && typeof run.html_url === 'string' ? run.html_url : '(unknown run URL)';
188
+ }
189
+
190
+ // `argv` defaults to real CLI argv but is a parameter so tests can call
191
+ // main() in-process (e.g. main([])) without inheriting the test runner's own
192
+ // argv, which would otherwise trip parseArgs's "unknown argument" branch.
193
+ async function main(argv = process.argv.slice(2)) {
194
+ parseArgs(argv);
195
+
196
+ const eventName = process.env.GITHUB_EVENT_NAME;
197
+ const repo = process.env.GITHUB_REPOSITORY || '';
198
+ const token = process.env.GITHUB_TOKEN || '';
199
+ const apiBase = process.env.GITHUB_API_URL || 'https://api.github.com';
200
+ const baseRef = resolveBaseRef(eventName);
201
+ const labels = parseLabels(process.env.PR_LABELS);
202
+
203
+ const fetchLatestRun = async (branch) => {
204
+ const url = `${apiBase}/repos/${repo}/actions/workflows/${TESTS_WORKFLOW_FILE}/runs`
205
+ + `?branch=${encodeURIComponent(branch)}&event=push&status=completed&per_page=1`;
206
+ const headers = {
207
+ accept: 'application/vnd.github+json',
208
+ 'x-github-api-version': '2022-11-28',
209
+ 'user-agent': 'gsd-core-ci-next-health',
210
+ };
211
+ if (token) headers.authorization = `Bearer ${token}`;
212
+ const response = await fetch(url, { headers, signal: AbortSignal.timeout(10000) });
213
+ if (!response.ok) {
214
+ throw new Error(`GitHub API responded ${response.status}`);
215
+ }
216
+ const text = await response.text();
217
+ try {
218
+ return JSON.parse(text);
219
+ } catch {
220
+ return null;
221
+ }
222
+ };
223
+
224
+ const result = await resolveNextHealth({ fetchLatestRun, eventName, baseRef, labels });
225
+
226
+ writeOutput([`verdict=${result.verdict}`]);
227
+ writeSummary(`Base branch health: ${result.verdict}`);
228
+
229
+ if (result.verdict === VERDICT.RED) {
230
+ const runUrl = runUrlOf(result.payload);
231
+ process.stderr.write(
232
+ `::error::the base branch "${baseRef}"'s own last Tests run is red: ${runUrl} — `
233
+ + 'wait for a fix-forward merge, or if THIS pull request is the fix, ask a '
234
+ + `maintainer to apply the "${BYPASS_LABEL}" label to explicitly override this gate.\n`,
235
+ );
236
+ return 1;
237
+ }
238
+
239
+ if (result.verdict === VERDICT.BYPASSED) {
240
+ const runUrl = runUrlOf(result.payload);
241
+ process.stderr.write(
242
+ `::warning::the base branch "${baseRef}"'s own last Tests run is red: ${runUrl} — `
243
+ + `a human applied the "${BYPASS_LABEL}" label to explicitly override this gate.\n`,
244
+ );
245
+ return 0;
246
+ }
247
+
248
+ if (result.verdict === VERDICT.INDETERMINATE) {
249
+ process.stderr.write(
250
+ `::warning::could not determine "${baseRef || '(no base ref)'}"'s Tests health (${result.reason}); `
251
+ + 'proceeding (fail-open).\n',
252
+ );
253
+ return 0;
254
+ }
255
+
256
+ process.stdout.write(`Base branch health: ${result.verdict}\n`);
257
+ return 0;
258
+ }
259
+
260
+ if (require.main === module) {
261
+ runMain(main);
262
+ }
263
+
264
+ module.exports = {
265
+ VERDICT,
266
+ BYPASS_LABEL,
267
+ TESTS_WORKFLOW_FILE,
268
+ classifyRunConclusion,
269
+ resolveNextHealth,
270
+ main,
271
+ };