@opengsd/gsd-core 1.10.0 → 1.11.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 (328) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-debug-session-manager.md +11 -0
  4. package/agents/gsd-doc-synthesizer.md +2 -4
  5. package/agents/gsd-executor.md +5 -5
  6. package/agents/gsd-mempalace-curator.md +5 -2
  7. package/agents/gsd-phase-researcher.md +20 -1
  8. package/agents/gsd-plan-checker.md +37 -0
  9. package/agents/gsd-planner.md +44 -46
  10. package/agents/gsd-user-profiler.md +3 -0
  11. package/agents/gsd-verifier.md +12 -3
  12. package/bin/install.js +841 -971
  13. package/bin/lib/ui-safety-gate.cjs +2 -0
  14. package/commands/gsd/code-review.md +1 -1
  15. package/commands/gsd/execute-phase.md +1 -1
  16. package/commands/gsd/map-codebase.md +1 -1
  17. package/commands/gsd/mempalace-capture.md +1 -1
  18. package/commands/gsd/mempalace-recall.md +1 -1
  19. package/commands/gsd/new-milestone.md +1 -1
  20. package/commands/gsd/quick.md +1 -1
  21. package/commands/gsd/review-backlog.md +2 -1
  22. package/commands/gsd/verify-work.md +1 -1
  23. package/gsd-core/bin/gsd-tools.cjs +469 -88
  24. package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
  25. package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
  26. package/gsd-core/bin/lib/api-coverage.cjs +3 -5
  27. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  28. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  29. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  30. package/gsd-core/bin/lib/audit.cjs +876 -240
  31. package/gsd-core/bin/lib/broken-windows.cjs +1 -1
  32. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  33. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  34. package/gsd-core/bin/lib/capability-registry.cjs +575 -101
  35. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  36. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  37. package/gsd-core/bin/lib/capability-validator.cjs +495 -22
  38. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  39. package/gsd-core/bin/lib/check-command-router.cjs +71 -37
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  41. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  42. package/gsd-core/bin/lib/command-aliases.cjs +22 -0
  43. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  44. package/gsd-core/bin/lib/commands.cjs +651 -86
  45. package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  47. package/gsd-core/bin/lib/config-loader.cjs +75 -0
  48. package/gsd-core/bin/lib/config.cjs +10 -1
  49. package/gsd-core/bin/lib/core-utils.cjs +127 -29
  50. package/gsd-core/bin/lib/decisions.cjs +23 -0
  51. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  52. package/gsd-core/bin/lib/frontmatter.cjs +155 -20
  53. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  54. package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
  55. package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
  56. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  57. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  58. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  59. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  60. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  61. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  62. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  63. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  64. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  65. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  66. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  67. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  68. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  69. package/gsd-core/bin/lib/init.cjs +321 -129
  70. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  71. package/gsd-core/bin/lib/install-engine.cjs +745 -258
  72. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  73. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  74. package/gsd-core/bin/lib/install-profiles.cjs +134 -57
  75. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  76. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  77. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  78. package/gsd-core/bin/lib/installer-migrations.cjs +138 -31
  79. package/gsd-core/bin/lib/io.cjs +10 -0
  80. package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
  81. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  82. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  83. package/gsd-core/bin/lib/milestone.cjs +754 -70
  84. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  85. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  86. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  87. package/gsd-core/bin/lib/pattern.cjs +122 -0
  88. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  89. package/gsd-core/bin/lib/phase-id.cjs +444 -36
  90. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  91. package/gsd-core/bin/lib/phase-locator.cjs +125 -18
  92. package/gsd-core/bin/lib/phase.cjs +646 -143
  93. package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
  94. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  95. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  96. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  97. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  98. package/gsd-core/bin/lib/planning-workspace.cjs +56 -6
  99. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  100. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  101. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  102. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
  103. package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
  104. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  105. package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
  106. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  107. package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
  108. package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
  109. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  110. package/gsd-core/bin/lib/roadmap.cjs +385 -94
  111. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
  112. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  113. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
  114. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  115. package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
  116. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
  117. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  118. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  119. package/gsd-core/bin/lib/security.cjs +104 -5
  120. package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
  121. package/gsd-core/bin/lib/smart-entry.cjs +142 -22
  122. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  123. package/gsd-core/bin/lib/state-document.cjs +152 -8
  124. package/gsd-core/bin/lib/state-transition.cjs +371 -117
  125. package/gsd-core/bin/lib/state.cjs +1794 -357
  126. package/gsd-core/bin/lib/surface.cjs +23 -9
  127. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  128. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  129. package/gsd-core/bin/lib/uat-predicate.cjs +9 -3
  130. package/gsd-core/bin/lib/uat.cjs +399 -56
  131. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  132. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  133. package/gsd-core/bin/lib/unusable-input.cjs +24 -0
  134. package/gsd-core/bin/lib/update-context.cjs +8 -2
  135. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  136. package/gsd-core/bin/lib/validate.cjs +20 -6
  137. package/gsd-core/bin/lib/vendor/README.md +37 -0
  138. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  139. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  140. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  141. package/gsd-core/bin/lib/verification.cjs +258 -8
  142. package/gsd-core/bin/lib/verify.cjs +368 -888
  143. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
  144. package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
  145. package/gsd-core/bin/lib/workstream.cjs +2 -2
  146. package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
  147. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  148. package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
  149. package/gsd-core/references/agent-contracts.md +43 -26
  150. package/gsd-core/references/checkpoints.md +2 -2
  151. package/gsd-core/references/context-budget.md +1 -1
  152. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  153. package/gsd-core/references/doc-conflict-engine.md +1 -1
  154. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  155. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  156. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  157. package/gsd-core/references/execute-phase-response-language.md +1 -1
  158. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  159. package/gsd-core/references/gate-prompts.md +1 -1
  160. package/gsd-core/references/git-planning-commit.md +2 -1
  161. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  162. package/gsd-core/references/model-profiles.md +12 -4
  163. package/gsd-core/references/mvp-concepts.md +9 -9
  164. package/gsd-core/references/planner-guidance.md +3 -9
  165. package/gsd-core/references/planner-preconditions.md +1 -1
  166. package/gsd-core/references/planner-reviews.md +1 -1
  167. package/gsd-core/references/planning-config.md +8 -6
  168. package/gsd-core/references/revision-loop.md +1 -1
  169. package/gsd-core/references/specless-probe-fallback.md +1 -1
  170. package/gsd-core/references/universal-anti-patterns.md +3 -3
  171. package/gsd-core/references/verifier-phase-gates.md +192 -0
  172. package/gsd-core/references/verify-mvp-mode.md +1 -1
  173. package/gsd-core/references/workstream-flag.md +22 -6
  174. package/gsd-core/templates/discussion-log.md +1 -1
  175. package/gsd-core/templates/phase-prompt.md +2 -4
  176. package/gsd-core/templates/state.md +4 -4
  177. package/gsd-core/templates/verification-report.md +9 -1
  178. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  179. package/gsd-core/workflows/autonomous.md +1 -1
  180. package/gsd-core/workflows/cleanup.md +62 -3
  181. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
  182. package/gsd-core/workflows/code-review-fix.md +37 -10
  183. package/gsd-core/workflows/code-review.md +38 -12
  184. package/gsd-core/workflows/complete-milestone.md +141 -18
  185. package/gsd-core/workflows/debug.md +7 -5
  186. package/gsd-core/workflows/diagnose-issues.md +35 -9
  187. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  188. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  189. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -1
  190. package/gsd-core/workflows/edit-phase.md +26 -1
  191. package/gsd-core/workflows/eval-review.md +3 -5
  192. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +31 -6
  193. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  194. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +2 -0
  195. package/gsd-core/workflows/execute-phase.md +38 -50
  196. package/gsd-core/workflows/execute-plan.md +36 -4
  197. package/gsd-core/workflows/explore.md +131 -4
  198. package/gsd-core/workflows/fast.md +10 -2
  199. package/gsd-core/workflows/health.md +73 -4
  200. package/gsd-core/workflows/import.md +4 -4
  201. package/gsd-core/workflows/ingest-docs.md +5 -5
  202. package/gsd-core/workflows/mvp-phase.md +6 -3
  203. package/gsd-core/workflows/new-milestone.md +14 -9
  204. package/gsd-core/workflows/new-project.md +14 -14
  205. package/gsd-core/workflows/next.md +12 -0
  206. package/gsd-core/workflows/plan-phase.md +41 -17
  207. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  208. package/gsd-core/workflows/progress.md +34 -6
  209. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
  210. package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
  211. package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
  212. package/gsd-core/workflows/quick.md +35 -15
  213. package/gsd-core/workflows/review.md +26 -5
  214. package/gsd-core/workflows/secure-phase.md +1 -1
  215. package/gsd-core/workflows/session-report.md +2 -1
  216. package/gsd-core/workflows/settings.md +66 -2
  217. package/gsd-core/workflows/ship.md +104 -44
  218. package/gsd-core/workflows/spec-phase.md +30 -12
  219. package/gsd-core/workflows/sync-skills.md +63 -8
  220. package/gsd-core/workflows/transition.md +46 -11
  221. package/gsd-core/workflows/ui-phase.md +5 -5
  222. package/gsd-core/workflows/ui-review.md +2 -2
  223. package/gsd-core/workflows/update.md +1 -1
  224. package/gsd-core/workflows/validate-phase.md +1 -1
  225. package/gsd-core/workflows/verify-work.md +9 -7
  226. package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
  227. package/hooks/dist/gsd-check-update-worker.js +56 -13
  228. package/hooks/dist/gsd-check-update.js +19 -1
  229. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  230. package/hooks/dist/gsd-cursor-subagent-start.js +77 -2
  231. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  232. package/hooks/dist/gsd-prompt-guard.js +21 -20
  233. package/hooks/dist/gsd-read-injection-scanner.js +38 -24
  234. package/hooks/dist/gsd-statusline.js +18 -0
  235. package/hooks/dist/gsd-update-banner.js +22 -1
  236. package/hooks/dist/gsd-workflow-guard.js +134 -36
  237. package/hooks/dist/lib/git-cmd.js +92 -59
  238. package/hooks/dist/lib/injection-patterns.js +45 -0
  239. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  240. package/hooks/dist/lib/isolation-sentinel.js +9 -0
  241. package/hooks/gsd-agent-isolation-guard.js +103 -14
  242. package/hooks/gsd-check-update-worker.js +56 -13
  243. package/hooks/gsd-check-update.js +19 -1
  244. package/hooks/gsd-cursor-pre-tool.js +0 -3
  245. package/hooks/gsd-cursor-subagent-start.js +77 -2
  246. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  247. package/hooks/gsd-prompt-guard.js +21 -20
  248. package/hooks/gsd-read-injection-scanner.js +38 -24
  249. package/hooks/gsd-statusline.js +18 -0
  250. package/hooks/gsd-update-banner.js +22 -1
  251. package/hooks/gsd-workflow-guard.js +134 -36
  252. package/hooks/lib/git-cmd.js +92 -59
  253. package/hooks/lib/injection-patterns.js +45 -0
  254. package/hooks/lib/isolation-deny-reason.js +39 -0
  255. package/hooks/lib/isolation-sentinel.js +9 -0
  256. package/package.json +21 -9
  257. package/pi/gsd.cjs +19 -5
  258. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  259. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  260. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  261. package/scripts/changeset/lint.cjs +60 -5
  262. package/scripts/check-alias-drift.cjs +7 -43
  263. package/scripts/check-contract-drift.cjs +297 -0
  264. package/scripts/ci-test-scope.cjs +19 -2
  265. package/scripts/command-contract-helpers.cjs +903 -1
  266. package/scripts/gen-adr-index.cjs +728 -38
  267. package/scripts/gen-capability-registry.cjs +3 -15
  268. package/scripts/gen-context-index.cjs +2 -11
  269. package/scripts/gen-health-docs.cjs +390 -0
  270. package/scripts/gen-inventory-manifest.cjs +50 -4
  271. package/scripts/gen-loop-host-contract.cjs +4 -24
  272. package/scripts/gen-registry.cjs +3 -14
  273. package/scripts/lib/alias-drift-families.cjs +46 -0
  274. package/scripts/lib/drift-scan.cjs +278 -0
  275. package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
  276. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  277. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  278. package/scripts/lint-canary-version-leak.cjs +73 -0
  279. package/scripts/lint-command-contract.cjs +96 -13
  280. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  281. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  282. package/scripts/lint-default-flip-documentation.cjs +193 -0
  283. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  284. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  285. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  286. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  287. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  288. package/scripts/lint-milestone-window-drift.cjs +468 -0
  289. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  290. package/scripts/lint-plan-count-drift.cjs +318 -0
  291. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  292. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  293. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  294. package/scripts/lint-regression-test-names.cjs +15 -13
  295. package/scripts/lint-removed-but-needed.cjs +320 -0
  296. package/scripts/lint-state-field-drift.cjs +805 -0
  297. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  298. package/scripts/lint-test-file-count.allowlist.json +21 -10
  299. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  300. package/scripts/lint-vendored-deps.cjs +124 -0
  301. package/scripts/pr-changed-files.cjs +63 -0
  302. package/scripts/pr-template-policy.cjs +14 -4
  303. package/scripts/prompt-injection-scan.sh +25 -0
  304. package/scripts/require-issue-link-policy.cjs +192 -0
  305. package/scripts/state-write-path-drift-baseline.json +19 -0
  306. package/scripts/sync-runtime-launcher.cjs +2 -4
  307. package/skills/gsd-autonomous/SKILL.md +0 -1
  308. package/skills/gsd-code-review/SKILL.md +1 -1
  309. package/skills/gsd-execute-phase/SKILL.md +1 -2
  310. package/skills/gsd-map-codebase/SKILL.md +1 -1
  311. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  312. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  313. package/skills/gsd-new-milestone/SKILL.md +1 -1
  314. package/skills/gsd-next/SKILL.md +0 -1
  315. package/skills/gsd-plan-phase/SKILL.md +0 -1
  316. package/skills/gsd-progress/SKILL.md +0 -1
  317. package/skills/gsd-quick/SKILL.md +1 -1
  318. package/skills/gsd-review-backlog/SKILL.md +2 -1
  319. package/skills/gsd-stats/SKILL.md +0 -1
  320. package/skills/gsd-verify-work/SKILL.md +1 -1
  321. package/vscode/package.json +1 -1
  322. package/gsd-core/workflows/discovery-phase.md +0 -298
  323. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  324. package/gsd-core/workflows/verify-phase.md +0 -574
  325. package/scripts/affected-tests-lib.cjs +0 -554
  326. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  327. package/scripts/run-affected-tests.cjs +0 -7
  328. package/scripts/run-tests.cjs +0 -1051
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * lint-vendored-deps.cjs — freshness gate for gsd-core/bin/lib/vendor/.
6
+ *
7
+ * #3477 follow-up: gsd-core/bin/** is copied by the installer into trees
8
+ * that have NO node_modules, so it must carry zero external requires
9
+ * (local/no-external-require-in-bin, eslint-rules/no-external-require-in-bin.cjs).
10
+ * `re2js` (src/pattern.cts's RE2 engine) is vendored verbatim under
11
+ * gsd-core/bin/lib/vendor/ instead — see gsd-core/bin/lib/vendor/README.md.
12
+ *
13
+ * A vendored artifact that silently drifts from its upstream package is
14
+ * just as dangerous as never vendoring it in the first place (a stale
15
+ * copy ships a different engine than the one actually reviewed/audited).
16
+ * This guard fails CI when:
17
+ * 1. gsd-core/bin/lib/vendor/re2js.cjs no longer matches
18
+ * node_modules/re2js/build/index.cjs byte-for-byte.
19
+ * 2. gsd-core/bin/lib/vendor/re2js.d.cts no longer matches
20
+ * node_modules/re2js/build/index.d.cts byte-for-byte.
21
+ * 3. src/vendor/re2js.d.cts (the source-side twin tsc needs to resolve
22
+ * types for src/pattern.cts's relative './vendor/re2js.cjs' import —
23
+ * module resolution for a .cts source is relative to src/, not the
24
+ * output dir) no longer matches gsd-core/bin/lib/vendor/re2js.d.cts.
25
+ * 4. The `re2js` version pinned in package.json `devDependencies` no
26
+ * longer matches the version actually installed at
27
+ * node_modules/re2js/package.json (read there, per the dispatch
28
+ * brief, rather than duplicating a second pin).
29
+ *
30
+ * Usage: node scripts/lint-vendored-deps.cjs
31
+ * Exit 0 when every vendored copy is fresh; 1 otherwise.
32
+ */
33
+
34
+ const fs = require('node:fs');
35
+ const path = require('node:path');
36
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
37
+
38
+ const ROOT = path.join(__dirname, '..');
39
+
40
+ const REFRESH_COMMAND =
41
+ 'cp node_modules/re2js/build/index.cjs gsd-core/bin/lib/vendor/re2js.cjs && '
42
+ + 'cp node_modules/re2js/build/index.d.cts gsd-core/bin/lib/vendor/re2js.d.cts && '
43
+ + 'cp node_modules/re2js/build/index.d.cts src/vendor/re2js.d.cts';
44
+
45
+ /**
46
+ * Compare two files byte-for-byte. Returns null when equal, or a short
47
+ * mismatch description (missing file / byte-length delta) otherwise.
48
+ * @param {string} relA
49
+ * @param {string} relB
50
+ * @returns {string | null}
51
+ */
52
+ function compareFiles(relA, relB) {
53
+ const absA = path.join(ROOT, relA);
54
+ const absB = path.join(ROOT, relB);
55
+ if (!fs.existsSync(absA)) return `${relA} does not exist`;
56
+ if (!fs.existsSync(absB)) return `${relB} does not exist`;
57
+ const a = fs.readFileSync(absA);
58
+ const b = fs.readFileSync(absB);
59
+ if (a.equals(b)) return null;
60
+ return `${relA} (${a.length} bytes) != ${relB} (${b.length} bytes)`;
61
+ }
62
+
63
+ /**
64
+ * Strip a leading semver range operator (^, ~, >=, >, <=, <, =) from a
65
+ * package.json dependency spec, leaving a bare version.
66
+ * @param {string} spec
67
+ * @returns {string}
68
+ */
69
+ function stripRangeOperator(spec) {
70
+ return String(spec || '').trim().replace(/^[\^~]|^>=|^<=|^>|^<|^=/, '').trim();
71
+ }
72
+
73
+ function main() {
74
+ const findings = [];
75
+
76
+ const cjsDrift = compareFiles('gsd-core/bin/lib/vendor/re2js.cjs', 'node_modules/re2js/build/index.cjs');
77
+ if (cjsDrift) findings.push(cjsDrift);
78
+
79
+ const dctsDrift = compareFiles('gsd-core/bin/lib/vendor/re2js.d.cts', 'node_modules/re2js/build/index.d.cts');
80
+ if (dctsDrift) findings.push(dctsDrift);
81
+
82
+ const srcTwinDrift = compareFiles('src/vendor/re2js.d.cts', 'gsd-core/bin/lib/vendor/re2js.d.cts');
83
+ if (srcTwinDrift) findings.push(srcTwinDrift);
84
+
85
+ const pkgPath = path.join(ROOT, 'package.json');
86
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
87
+ const pinnedSpec = pkg.devDependencies && pkg.devDependencies.re2js;
88
+ if (!pinnedSpec) {
89
+ findings.push('package.json devDependencies.re2js is missing');
90
+ } else {
91
+ const installedPkgPath = path.join(ROOT, 'node_modules', 're2js', 'package.json');
92
+ if (!fs.existsSync(installedPkgPath)) {
93
+ findings.push('node_modules/re2js/package.json does not exist (run npm install)');
94
+ } else {
95
+ const installed = JSON.parse(fs.readFileSync(installedPkgPath, 'utf8'));
96
+ const pinned = stripRangeOperator(pinnedSpec);
97
+ if (pinned !== installed.version) {
98
+ findings.push(
99
+ `package.json devDependencies.re2js ("${pinnedSpec}" -> "${pinned}") != `
100
+ + `node_modules/re2js/package.json version ("${installed.version}")`,
101
+ );
102
+ }
103
+ }
104
+ }
105
+
106
+ if (findings.length > 0) {
107
+ const detail = findings.map((f) => ` ${f}`).join('\n');
108
+ throw new ExitError(
109
+ 1,
110
+ 'lint-vendored-deps: gsd-core/bin/lib/vendor/re2js.* has drifted from its\n'
111
+ + 'upstream package (or its version pin). Refresh with:\n'
112
+ + ` ${REFRESH_COMMAND}\n`
113
+ + 'Findings:\n'
114
+ + detail,
115
+ );
116
+ }
117
+
118
+ process.stdout.write('ok lint-vendored-deps: gsd-core/bin/lib/vendor/re2js.* matches node_modules/re2js and its pinned version\n');
119
+ return 0;
120
+ }
121
+
122
+ if (require.main === module) runMain(main);
123
+
124
+ module.exports = { compareFiles, stripRangeOperator };
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Shared truncation-detection helper for `gh pr view --json files` (#3211).
5
+ *
6
+ * GitHub's GraphQL `files` connection on a pull request is a paginated
7
+ * connection. `gh pr view --json files` requests it with a single page at
8
+ * `first: 100` and does NOT paginate through the rest — so any PR touching
9
+ * more than 100 files silently returns only the first 100, with no error and
10
+ * no indication in the `files` array itself that entries are missing.
11
+ * `changedFiles` (a separate, non-paginated scalar field) reports the true
12
+ * total. A policy that relaxes a gate based on "every changed file matches
13
+ * pattern X" must treat a `files` list it cannot confirm is complete as
14
+ * incomplete — i.e. fail closed — rather than silently approving a PR whose
15
+ * 101st+ file might violate the policy.
16
+ *
17
+ * This file lives at the top level of scripts/ rather than in scripts/lib/
18
+ * because scripts/lib/** is enumerated in bin/install.js and ships to users
19
+ * on install; this is CI-only tooling with no reason to be installed.
20
+ */
21
+
22
+ // GitHub's GraphQL `files` connection is requested at `first: 100` by
23
+ // `gh pr view --json files`; `gh` does not paginate this field.
24
+ const FILE_LIST_PAGE_LIMIT = 100;
25
+
26
+ /**
27
+ * Returns true iff `changedFiles` can be trusted as the COMPLETE list of
28
+ * files changed in the PR (i.e. it was not silently truncated).
29
+ *
30
+ * - An empty/non-array list cannot confirm anything about the PR — mirror
31
+ * the fail-closed stance `allPathsAreTooling` already takes on an empty
32
+ * list in scripts/pr-template-policy.cjs.
33
+ * - When the true total is known it is the AUTHORITY, at every list size —
34
+ * not just when the list has hit the page cap. The 100-entry page cap is
35
+ * only ONE way a list can be short of the truth; a `$GITHUB_OUTPUT`
36
+ * heredoc terminated early by an attacker-named file, or a path
37
+ * containing a newline, truncates or inflates the list just as
38
+ * effectively, and at any length. Comparing against the total catches all
39
+ * of them without enumerating the mechanisms.
40
+ * - Only when no total is supplied do we fall back to the page-cap
41
+ * heuristic: a list below the cap cannot have been truncated BY THE CAP,
42
+ * which is the only mechanism a caller without a total can rule out.
43
+ */
44
+ function fileListIsComplete(changedFiles, changedFilesTotal) {
45
+ if (!Array.isArray(changedFiles) || changedFiles.length === 0) return false;
46
+ if (Number.isInteger(changedFilesTotal)) return changedFilesTotal === changedFiles.length;
47
+ return changedFiles.length < FILE_LIST_PAGE_LIMIT;
48
+ }
49
+
50
+ /**
51
+ * Parses a newline-delimited env var (as produced by `git diff --name-only`
52
+ * or `gh pr view --json files -q '.files[].path'`) into a trimmed,
53
+ * empty-line-free array. Returns [] for null/undefined/empty input.
54
+ */
55
+ function parseChangedFilesEnv(value) {
56
+ if (!value) return [];
57
+ return String(value)
58
+ .split(/\r?\n/)
59
+ .map((line) => line.trim())
60
+ .filter(Boolean);
61
+ }
62
+
63
+ module.exports = { fileListIsComplete, FILE_LIST_PAGE_LIMIT, parseChangedFilesEnv };
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { matchesGlob } = require('path');
4
+ const { fileListIsComplete, parseChangedFilesEnv } = require('./pr-changed-files.cjs');
4
5
 
5
6
  const TRUSTED_AUTHOR_ASSOCIATIONS = new Set([
6
7
  'CONTRIBUTOR',
@@ -142,8 +143,12 @@ function matchingTemplate(body) {
142
143
  * pattern in the allowlist. Returns false for an empty file list (no
143
144
  * files means we cannot confirm it is a tooling-only PR).
144
145
  */
145
- function allPathsAreTooling(changedFiles, allowlist) {
146
+ function allPathsAreTooling(changedFiles, allowlist, changedFilesTotal) {
146
147
  if (!Array.isArray(changedFiles) || changedFiles.length === 0) return false;
148
+ // #3211: `gh pr view --json files` truncates at 100 entries with no
149
+ // in-band signal. A truncated list must not relax enforcement — an unseen
150
+ // 101st+ file could fall outside the tooling allowlist entirely.
151
+ if (!fileListIsComplete(changedFiles, changedFilesTotal)) return false;
147
152
  return changedFiles.every((file) =>
148
153
  allowlist.some((pattern) => matchesGlob(file, pattern)),
149
154
  );
@@ -159,13 +164,13 @@ function hasExemptMarker(body, regex) {
159
164
  return match[1].trim().length > 0;
160
165
  }
161
166
 
162
- function evaluatePrTemplate(body, authorAssociation, changedFiles) {
167
+ function evaluatePrTemplate(body, authorAssociation, changedFiles, changedFilesTotal) {
163
168
  const association = String(authorAssociation || '').toUpperCase();
164
169
  const trusted = TRUSTED_AUTHOR_ASSOCIATIONS.has(association);
165
170
  const normalizedBody = String(body || '').trim();
166
171
 
167
172
  // --- Carve-out 1: all changed files are in the tooling allowlist ---
168
- if (allPathsAreTooling(changedFiles, TOOLING_PATH_ALLOWLIST)) {
173
+ if (allPathsAreTooling(changedFiles, TOOLING_PATH_ALLOWLIST, changedFilesTotal)) {
169
174
  return {
170
175
  valid: true,
171
176
  action: 'pass',
@@ -235,13 +240,18 @@ function evaluatePrTemplate(body, authorAssociation, changedFiles) {
235
240
  }
236
241
 
237
242
  function main() {
243
+ // Preserve the existing distinction between "unset" (undefined) and
244
+ // "set but empty" ([]) — allPathsAreTooling treats them differently.
238
245
  const changedFiles = process.env.CHANGED_FILES
239
- ? process.env.CHANGED_FILES.split('\n').map((f) => f.trim()).filter(Boolean)
246
+ ? parseChangedFilesEnv(process.env.CHANGED_FILES)
240
247
  : undefined;
248
+ const parsedTotal = Number.parseInt(process.env.CHANGED_FILES_TOTAL, 10);
249
+ const changedFilesTotal = Number.isNaN(parsedTotal) ? undefined : parsedTotal;
241
250
  const result = evaluatePrTemplate(
242
251
  process.env.PR_BODY || '',
243
252
  process.env.AUTHOR_ASSOCIATION || '',
244
253
  changedFiles,
254
+ changedFilesTotal,
245
255
  );
246
256
  process.stdout.write(`${JSON.stringify(result)}\n`);
247
257
  if (process.env.GITHUB_OUTPUT) {
@@ -72,6 +72,15 @@ PATTERNS=(
72
72
  # `eval('...')` (single-quoted) silently went undetected on macOS while
73
73
  # passing on GNU-grep CI runners. Found auditing #3175; fixed here since it
74
74
  # is the same unanchored/portability defect class as the boundary fix.
75
+ #
76
+ # `exec` stays receiver-blind on purpose. A left boundary that excludes a
77
+ # preceding `.` would drop every member-position `.exec('…')` — including
78
+ # `require('child_process').exec('…')`, the single most common Node spelling
79
+ # of the vector this pattern exists to catch — and a receiver allowlist
80
+ # cannot restore it, because the literal `child_process` is not adjacent to
81
+ # `.exec`. The cost is that `RegExp.prototype.exec`, which takes a subject
82
+ # string rather than code, also matches; files that legitimately call it are
83
+ # handled by ALLOWLIST below, never by narrowing the pattern.
75
84
  '(^|[^[:alnum:]])eval[[:space:]]*\([[:space:]]*["'"'"']'
76
85
  'exec[[:space:]]*\([[:space:]]*["'"'"']'
77
86
  '(^|[^[:alnum:]])Function[[:space:]]*\([[:space:]]*["'"'"'].*return'
@@ -129,6 +138,22 @@ ALLOWLIST=(
129
138
  # asserts nothing: it is the payload the guard is required to catch, carried
130
139
  # as test DATA. Same class as the read-injection-scanner suites above.
131
140
  'tests/kimi-payload-field-shadowing.security.test.cjs'
141
+ # Phase-ID grammar regression tests exercise `RegExp.prototype.exec` via
142
+ # `re.exec('<phase-id>')` against fixtures like 'MANIFOLD-64-auth' / 'CK-64-auth'.
143
+ # The scanner's `exec('` code-execution pattern matches that benign method call,
144
+ # not an attack vector — same DEFECT.PROMPT-INJECTION-SCAN-COLLISION class as the
145
+ # test fixtures above. Pre-existing content (16 such calls on `next`); it surfaces
146
+ # here only because #2573's W024 `state_head` assertions make the file appear in
147
+ # the changed-file set the diff-mode scan walks.
148
+ 'tests/health-validation.test.cjs'
149
+ # #2528 — same collision, same disposition: the continuation-grammar suite
150
+ # drives the tokenizer regexes directly via `re.exec('05-80-20')`, so the
151
+ # argument is the subject string, not a command. Exempted per file rather
152
+ # than by narrowing the `exec(` pattern: a left boundary excluding a preceding
153
+ # `.` would drop `require('child_process').exec('…')`, and a receiver
154
+ # allowlist cannot reach it either, because the literal `child_process` is
155
+ # not adjacent to `.exec`. See the note at the pattern itself.
156
+ 'tests/continuation-grammar-parity.test.cjs'
132
157
  )
133
158
 
134
159
  is_allowlisted() {
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Require-issue-link policy (#3211, preserving #1389).
6
+ *
7
+ * Replaces the shell-only `grep -qiE '(closes|fixes|resolves)\s+#[0-9]+'`
8
+ * step with a pure, testable `evaluateIssueLink` verdict function so a PR
9
+ * that only REFERENCES an issue (rather than closing it — e.g. a follow-up
10
+ * regression-coverage PR) is not forced to fabricate a closing keyword, while
11
+ * two pre-existing constraints are preserved exactly:
12
+ *
13
+ * 1. (#1389, anti-forgery) The backmerge exemption only fires when the
14
+ * head ref carries the backmerge prefix AND the PR is same-repo (not a
15
+ * fork) — dropping the `sameRepo` conjunct would let a fork forge a
16
+ * branch name to bypass the issue-link requirement entirely.
17
+ * 2. (#3211, truncation) `gh pr view --json files` truncates the file list
18
+ * at 100 entries with no in-band signal that it did so (see
19
+ * scripts/pr-changed-files.cjs). The reference-only carve-out below
20
+ * only applies when every changed file is a test/doc file, so a PR
21
+ * whose file list may be truncated must fail closed rather than let an
22
+ * unseen 101st+ file (which could touch `src/`) slip through.
23
+ *
24
+ * Tests assert on the typed ISSUE_LINK_REASON enum, never on free text.
25
+ */
26
+
27
+ const { fileListIsComplete, parseChangedFilesEnv } = require('./pr-changed-files.cjs');
28
+ const { runMain } = require('./lib/cli-exit.cjs');
29
+
30
+ const ISSUE_LINK_REASON = Object.freeze({
31
+ OK_CLOSING_KEYWORD: 'ok_closing_keyword',
32
+ OK_BACKMERGE_EXEMPT: 'ok_backmerge_exempt',
33
+ OK_FOLLOWUP_REFERENCE: 'ok_followup_reference',
34
+ FAIL_NO_ISSUE_REFERENCE: 'fail_no_issue_reference',
35
+ FAIL_REFERENCE_NEEDS_CLOSING: 'fail_reference_needs_closing',
36
+ FAIL_FILE_LIST_INCOMPLETE: 'fail_file_list_incomplete',
37
+ });
38
+
39
+ // #1389: backmerge PRs are opened by CI against `next`, never by a human or a
40
+ // fork, so they are exempt from the issue-link requirement outright — but
41
+ // ONLY when combined with `sameRepo === true` below (see header comment).
42
+ const BACKMERGE_BRANCH_PREFIX = 'chore/backmerge-main-to-next-';
43
+
44
+ // A follow-up-only PR (references an issue without closing it) is only
45
+ // allowed to skip the closing keyword when every changed file is a test or
46
+ // doc file — i.e. it cannot be the PR that actually implements the fix.
47
+ const EXEMPT_PATH_PREFIXES = ['tests/', 'docs/'];
48
+
49
+ // Root-level markdown is documentation — this mirrors the repo's own doc-only
50
+ // classifier (.claude/hooks/pre-pr-gate.sh:111), whose `[^/]+\.md` anchor is
51
+ // deliberately root-only so runtime-loaded text under a subdirectory
52
+ // (gsd-core/workflows/*.md, agents/*.md, commands/**/*.md) stays gated.
53
+ // CHANGELOG.md is excluded: scripts/changeset/lint.cjs classes a direct edit to
54
+ // it as user-facing precisely to close a bypass, so it must not ride in on the
55
+ // docs carve-out either.
56
+ const EXCLUDED_ROOT_DOCS = new Set(['CHANGELOG.md']);
57
+
58
+ // Case-insensitive lookup set derived from EXCLUDED_ROOT_DOCS. The exclusion
59
+ // check below is case-insensitive because the `.md` extension test above it
60
+ // already is (`/\.md$/i`) — `changelog.md` or `CHANGELOG.MD` would otherwise
61
+ // slip past the exclusion while still passing the extension test. In
62
+ // practice this is defense in depth rather than a live bypass: the GitHub
63
+ // API always reports the real path with its actual, fixed casing (the
64
+ // filesystem is case-sensitive on the runners this executes on), so a PR
65
+ // cannot rename CHANGELOG.md to bypass the check by casing alone.
66
+ const EXCLUDED_ROOT_DOCS_UPPER = new Set(
67
+ Array.from(EXCLUDED_ROOT_DOCS, (name) => name.toUpperCase()),
68
+ );
69
+
70
+ function isRootLevelDoc(normalizedPath) {
71
+ if (!normalizedPath) return false;
72
+ if (normalizedPath.includes('/')) return false;
73
+ if (!/\.md$/i.test(normalizedPath)) return false;
74
+ if (EXCLUDED_ROOT_DOCS_UPPER.has(normalizedPath.toUpperCase())) return false;
75
+ return true;
76
+ }
77
+
78
+ // Mirrors the shipped `grep -qiE '(closes|fixes|resolves)\s+#[0-9]+'` exactly:
79
+ // no additional keywords, no `\b` anchors (the shell grep it replaces has
80
+ // none either) — see the corpus-parity test in
81
+ // tests/require-issue-link-policy.test.cjs.
82
+ const CLOSING_KEYWORD_REGEX = /(?:closes|fixes|resolves)\s+#[0-9]+/i;
83
+
84
+ // Accepts a soft "this PR relates to #N" reference without claiming to close
85
+ // it. The leading `\b` prevents matching inside a longer word (e.g. `xref#1`,
86
+ // `prefs #1`) because there is no word-boundary transition between the
87
+ // preceding word character and the start of the alternative — verified
88
+ // explicitly for `preferences #1` / `unreferenced #1` in the test suite.
89
+ const FOLLOWUP_REFERENCE_REGEX = /\b(?:refs?|references?|relates\s+to|related\s+to|follow[-\s]?up\s+to)\s+#[0-9]+/i;
90
+
91
+ function hasClosingKeyword(body) {
92
+ return CLOSING_KEYWORD_REGEX.test(String(body || ''));
93
+ }
94
+
95
+ function hasFollowUpReference(body) {
96
+ return FOLLOWUP_REFERENCE_REGEX.test(String(body || ''));
97
+ }
98
+
99
+ // Path separator normalization — unconditional, per repo convention (see
100
+ // CLAUDE.md "Path-sep normalization"), so a Windows-style diff entry like
101
+ // `tests\windows\a.test.cjs` is still recognized as tests/-prefixed.
102
+ function normalizePath(p) {
103
+ return String(p).replace(/\\/g, '/');
104
+ }
105
+
106
+ /**
107
+ * Returns true iff every changed file qualifies as a test/doc file, i.e. each
108
+ * one is one of:
109
+ * 1. under one of EXEMPT_PATH_PREFIXES (`tests/`, `docs/`) — the trailing
110
+ * slash on each prefix makes this directory-boundary aware, so
111
+ * lookalikes like `tests-e2e/`, `testsuite/`, or `docsite/` are
112
+ * correctly rejected (they are not `tests/` or `docs/`); or
113
+ * 2. a root-level markdown file (no `/`, `.md` extension, case-insensitive)
114
+ * that is not in EXCLUDED_ROOT_DOCS — see isRootLevelDoc.
115
+ */
116
+ function allPathsAreTestsOrDocs(changedFiles) {
117
+ if (!Array.isArray(changedFiles) || changedFiles.length === 0) return false;
118
+ return changedFiles.every((file) => {
119
+ const normalized = normalizePath(file);
120
+ if (EXEMPT_PATH_PREFIXES.some((prefix) => normalized.startsWith(prefix))) return true;
121
+ return isRootLevelDoc(normalized);
122
+ });
123
+ }
124
+
125
+ function evaluateIssueLink({ prBody, headRef, sameRepo, changedFiles, changedFilesTotal }) {
126
+ if (hasClosingKeyword(prBody)) {
127
+ return { ok: true, reason: ISSUE_LINK_REASON.OK_CLOSING_KEYWORD };
128
+ }
129
+
130
+ // #1389: `sameRepo === true` is required, not just the branch-name prefix —
131
+ // a fork PR could otherwise name its branch to forge this exemption.
132
+ if (String(headRef || '').startsWith(BACKMERGE_BRANCH_PREFIX) && sameRepo === true) {
133
+ return { ok: true, reason: ISSUE_LINK_REASON.OK_BACKMERGE_EXEMPT };
134
+ }
135
+
136
+ if (!hasFollowUpReference(prBody)) {
137
+ return { ok: false, reason: ISSUE_LINK_REASON.FAIL_NO_ISSUE_REFERENCE };
138
+ }
139
+
140
+ // #3211: a truncated file list cannot be trusted to prove "tests/docs
141
+ // only" — fail closed rather than risk approving on an unseen file.
142
+ if (!fileListIsComplete(changedFiles, changedFilesTotal)) {
143
+ return { ok: false, reason: ISSUE_LINK_REASON.FAIL_FILE_LIST_INCOMPLETE };
144
+ }
145
+
146
+ if (allPathsAreTestsOrDocs(changedFiles)) {
147
+ return { ok: true, reason: ISSUE_LINK_REASON.OK_FOLLOWUP_REFERENCE };
148
+ }
149
+
150
+ return { ok: false, reason: ISSUE_LINK_REASON.FAIL_REFERENCE_NEEDS_CLOSING };
151
+ }
152
+
153
+ function main() {
154
+ const changedFiles = parseChangedFilesEnv(process.env.CHANGED_FILES);
155
+ const parsedTotal = Number.parseInt(process.env.CHANGED_FILES_TOTAL, 10);
156
+ const changedFilesTotal = Number.isNaN(parsedTotal) ? undefined : parsedTotal;
157
+
158
+ const result = evaluateIssueLink({
159
+ prBody: process.env.PR_BODY || '',
160
+ headRef: process.env.HEAD_REF || '',
161
+ sameRepo: process.env.SAME_REPO === 'true',
162
+ changedFiles,
163
+ changedFilesTotal,
164
+ });
165
+
166
+ process.stdout.write(`${JSON.stringify(result)}\n`);
167
+ if (process.env.GITHUB_OUTPUT) {
168
+ const fs = require('node:fs');
169
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `ok=${result.ok ? 'true' : 'false'}\n`);
170
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `reason=${result.reason}\n`);
171
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `result=${JSON.stringify(result)}\n`);
172
+ }
173
+
174
+ return result.ok ? 0 : 1;
175
+ }
176
+
177
+ if (require.main === module) runMain(main);
178
+
179
+ module.exports = {
180
+ ISSUE_LINK_REASON,
181
+ BACKMERGE_BRANCH_PREFIX,
182
+ EXEMPT_PATH_PREFIXES,
183
+ EXCLUDED_ROOT_DOCS,
184
+ CLOSING_KEYWORD_REGEX,
185
+ FOLLOWUP_REFERENCE_REGEX,
186
+ hasClosingKeyword,
187
+ hasFollowUpReference,
188
+ normalizePath,
189
+ isRootLevelDoc,
190
+ allPathsAreTestsOrDocs,
191
+ evaluateIssueLink,
192
+ };
@@ -0,0 +1,19 @@
1
+ {
2
+ "_comment": "ADR-3408 Decision 5 write-seam ratchet baseline (issue #3468, Phase 1; Phase 2 / #3469 lands the single write seam and Amendment 2). Every entry here is a `writeStateMd(`/`syncStateFrontmatter(`/`applyPostSyncPreservation(` bypass this guard found by a whole-repo scan (Decision 4(a)) — it is ACKNOWLEDGED, not endorsed: acknowledgment is in writing (this file), with the issue owning its removal recorded in the entry's \"owner\" field. This baseline is SHRINK-ONLY — an entry that stops firing goes STALE and fails the plain run until `--baseline` is re-run to drop it (ADR-3180 Decision 4(e)'s \"the baseline may only shrink\", adopted verbatim by ADR-3408). Phase 2 (#3469) removed the `cmdPhaseComplete` (`src/phase.cts`) and `cmdMilestoneComplete` (`src/milestone.cts`) entries by routing both through the single write-seam composition (`syncAndPreserveStateMd`, `src/state.cts`). ADR-3408 Amendment 2: \"0 bypasses\" was never this baseline's target — TWO entries are SANCTIONED PERMANENT, not debt, and Phase 4 (#3471) does NOT drive this file to empty: `cmdStateSync` (`src/state.cts`) exists precisely to let the body win (#905 — `state sync` re-derives frontmatter FROM the body), so routing it through preservation would invert the command rather than fix a bug; `REGENERATE_STATE` (`src/health-diagnostic.cts`) is `/gsd-health --repair`'s factory reset, which rebuilds STATE.md from scratch, so preservation would restore exactly the values it was invoked to discard. Neither entry may be \"consolidated\" away — a guard reporting them is reporting correctly, and a change that removes one is a regression, not progress.",
3
+ "entries": [
4
+ {
5
+ "file": "src/health-diagnostic.cts",
6
+ "source": "writeStateMd(statePath, stateContent, cwd);",
7
+ "symbol": "writeStateMd",
8
+ "count": 1,
9
+ "owner": "sanctioned-permanent"
10
+ },
11
+ {
12
+ "file": "src/state.cts",
13
+ "source": "writeStateMd(statePath, modified, cwd);",
14
+ "symbol": "writeStateMd",
15
+ "count": 1,
16
+ "owner": "sanctioned-permanent"
17
+ }
18
+ ]
19
+ }
@@ -19,6 +19,8 @@
19
19
  const fs = require('node:fs');
20
20
  const path = require('node:path');
21
21
 
22
+ const { escapeRegex: escapeRegExp } = require('../gsd-core/bin/lib/pattern.cjs');
23
+
22
24
  const WORKFLOWS_DIR = path.join(__dirname, '..', 'gsd-core', 'workflows');
23
25
  const AGENTS_DIR = path.join(__dirname, '..', 'agents');
24
26
  const SNIPPET_FILE = path.join(WORKFLOWS_DIR, '_runtime-launcher.snippet.sh');
@@ -373,10 +375,6 @@ function transformFile(content, preamble) {
373
375
  return outputLines.join('\n');
374
376
  }
375
377
 
376
- function escapeRegExp(str) {
377
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
378
- }
379
-
380
378
  /**
381
379
  * A file "delegates to the shared resolver" when it pulls the canonical gsd_run
382
380
  * preamble in from gsd-core/references/gsd-run-resolver.md via an @-include
@@ -2,7 +2,6 @@
2
2
  name: gsd-autonomous
3
3
  description: "Run all remaining phases autonomously — discuss→plan→execute per phase"
4
4
  argument-hint: "[--from N] [--to N] [--only N] [--interactive] [--converge]"
5
- effort: high
6
5
  allowed-tools:
7
6
  - Read
8
7
  - Write
@@ -41,7 +41,7 @@ Optional flags parsed from $ARGUMENTS:
41
41
  - `--depth=VALUE` — Depth override (quick|standard|deep). If provided, overrides workflow.code_review_depth config.
42
42
  - `--files=file1,file2,...` — Explicit file list override. Has highest precedence for file scoping per D-08. When provided, workflow skips SUMMARY.md extraction and git diff fallback entirely.
43
43
 
44
- Context files (CLAUDE.md, SUMMARY.md, phase state) are resolved inside the workflow via `gsd-tools query init.phase-op` and delegated to agent via `<files_to_read>` blocks.
44
+ Context files (CLAUDE.md, SUMMARY.md, phase state) are resolved inside the workflow via `gsd-tools query init.phase-op` and delegated to agent via `<required_reading>` blocks.
45
45
  </context>
46
46
 
47
47
  <process>
@@ -2,7 +2,6 @@
2
2
  name: gsd-execute-phase
3
3
  description: "Execute all plans in a phase with wave-based parallelization"
4
4
  argument-hint: "<phase-number> [--wave N] [--gaps-only] [--interactive] [--tdd]"
5
- effort: high
6
5
  allowed-tools:
7
6
  - Read
8
7
  - Write
@@ -56,7 +55,7 @@ Phase: $ARGUMENTS
56
55
  - If none of these tokens appear, run the standard full-phase execution flow with no flag-specific filtering
57
56
  - Do not infer that a flag is active just because it is documented in this prompt
58
57
 
59
- Context files are resolved inside the workflow via `gsd-tools query init.execute-phase` and per-subagent `<files_to_read>` blocks.
58
+ Context files are resolved inside the workflow via `gsd-tools query init.execute-phase` and per-subagent `<required_reading>` blocks.
60
59
  </context>
61
60
 
62
61
  <process>
@@ -34,7 +34,7 @@ Output: .planning/codebase/ folder with 7 structured documents about the codebas
34
34
  Arguments: $ARGUMENTS
35
35
 
36
36
  Parse the first token of $ARGUMENTS:
37
- - If it is `--fast`: strip the flag, run the scan workflow (passing remaining args including optional --focus).
37
+ - If it is `--fast`: strip the flag, then read and execute `~/.claude/gsd-core/workflows/scan.md` (passing remaining args including optional --focus). Load it on demand here — it is deliberately not in `<execution_context>`, so the common full-map path does not pay for it.
38
38
  - If it is `--query`: strip the flag, run the intel workflow (passing remaining args as the subcommand).
39
39
  - Otherwise: pass all of $ARGUMENTS as focus area to the map-codebase workflow.
40
40
 
@@ -86,7 +86,7 @@ On any error or timeout, stop and let the phase continue -- capture is best-effo
86
86
  # Mine with --wing only — no --room flag; detect_room() assigns from the folder path
87
87
  mempalace mine "$STAGE" --wing <wing>
88
88
  ```
89
- 3. **Mirror KG facts** when `config.mempalace.mirror_kg` is true: extract decision/delivery facts and `mempalace_kg_add` them with `valid_from` = the phase date (e.g. `(<project>, decided, <decision>)` from CONTEXT; `(<phase>, delivered, <capability>)` from SUMMARY). Under `augment` these are an *additive* mirror of GSD's native `.planning/graphs/`. Under `kg_backend`/`replace` the palace KG is the *authoritative* fact store — GSD still produces `.planning/graphs/` through its normal graphify, so an unreachable palace never loses a fact.
89
+ 3. **Mirror KG facts** unless `config.mempalace.mirror_kg === false` (registry default is true — an absent key means enabled): extract decision/delivery facts and `mempalace_kg_add` them with `valid_from` = the phase date (e.g. `(<project>, decided, <decision>)` from CONTEXT; `(<phase>, delivered, <capability>)` from SUMMARY). Under `augment` these are an *additive* mirror of GSD's native `.planning/graphs/`. Under `kg_backend`/`replace` the palace KG is the *authoritative* fact store — GSD still produces `.planning/graphs/` through its normal graphify, so an unreachable palace never loses a fact.
90
90
  4. Re-running a phase MUST NOT create duplicate drawers (deterministic ids + `check_duplicate`).
91
91
 
92
92
  ## Step 4 -- Report
@@ -66,7 +66,7 @@ All calls in this step are side-effect-free. On any error or timeout, stop retri
66
66
  2. **Targeted search:**
67
67
  - Interactive: `mempalace_search(query=<topic>, wing=<wing>)`.
68
68
  - Headless: `mempalace search "<topic>" --wing <wing>`.
69
- 3. **Knowledge-graph facts** (when `config.mempalace.mirror_kg` is true): `mempalace_kg_query` / `mempalace_kg_timeline` for decisions relevant to the topic and their validity windows. Under `augment` the palace KG *supplements* GSD's native `.planning/graphs/` — combine both, do not treat the palace as the sole source. Under `kg_backend` or `replace` the palace KG is the *primary* graph source — query it first and use `.planning/graphs/` only as a fallback when the palace is unreachable.
69
+ 3. **Knowledge-graph facts** (unless `config.mempalace.mirror_kg !== false` — registry default is true, an absent key means enabled): `mempalace_kg_query` / `mempalace_kg_timeline` for decisions relevant to the topic and their validity windows. Under `augment` the palace KG *supplements* GSD's native `.planning/graphs/` — combine both, do not treat the palace as the sole source. Under `kg_backend` or `replace` the palace KG is the *primary* graph source — query it first and use `.planning/graphs/` only as a fallback when the palace is unreachable.
70
70
  4. **Dedup** the returned drawers/facts; keep the top results.
71
71
 
72
72
  ## Step 4 -- Write MEMORY-RECALL.md
@@ -36,7 +36,7 @@ Brownfield equivalent of new-project. Project exists, PROJECT.md has history. Ga
36
36
  <context>
37
37
  Milestone name: $ARGUMENTS (optional - will prompt if not provided)
38
38
 
39
- Project and milestone context files are resolved inside the workflow (`init new-milestone`) and delegated via `<files_to_read>` blocks where subagents are used.
39
+ Project and milestone context files are resolved inside the workflow (`init new-milestone`) and delegated via `<required_reading>` blocks where subagents are used.
40
40
  </context>
41
41
 
42
42
  <process>
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: gsd-next
3
3
  description: "Smart entry — detect project state and route to the right next GSD action."
4
- effort: low
5
4
  allowed-tools:
6
5
  - Read
7
6
  - Bash
@@ -2,7 +2,6 @@
2
2
  name: gsd-plan-phase
3
3
  description: "Create detailed phase plan (PLAN.md) with verification loop"
4
4
  argument-hint: "[phase] [--auto] [--research] [--skip-research] [--research-phase <N>] [--view] [--gaps] [--skip-verify] [--skip-ui] [--prd <file>] [--ingest <path-or-glob>] [--ingest-format <auto|nygard|madr|narrative>] [--reviews] [--text] [--bounce] [--skip-bounce] [--chunked] [--tdd] [--mvp] [--granularity <coarse|standard|fine>] [--no-tracer] [--no-reversibility-gates]"
5
- effort: high
6
5
  allowed-tools:
7
6
  - Read
8
7
  - Write
@@ -2,7 +2,6 @@
2
2
  name: gsd-progress
3
3
  description: "Check progress, advance workflow, or dispatch freeform intent — the unified GSD situational command"
4
4
  argument-hint: "[--forensic | --next [--auto] [--converge] | --do \\\"task description\\\"]"
5
- effort: low
6
5
  allowed-tools:
7
6
  - Read
8
7
  - Bash
@@ -46,7 +46,7 @@ Granular flags are composable: `--discuss --research --validate` gives the same
46
46
  <context>
47
47
  $ARGUMENTS
48
48
 
49
- Context files are resolved inside the workflow (`init quick`) and delegated via `<files_to_read>` blocks.
49
+ Context files are resolved inside the workflow (`init quick`) and delegated via `<required_reading>` blocks.
50
50
  </context>
51
51
 
52
52
  <process>
@@ -18,7 +18,8 @@ milestone sequence or remove stale entries.
18
18
 
19
19
  1. **List backlog items:**
20
20
  ```bash
21
- ls -d .planning/phases/999* 2>/dev/null || echo "No backlog items found"
21
+ _BACKLOG=( .planning/phases/999* )
22
+ if [ -e "${_BACKLOG[0]}" ]; then ls -d "${_BACKLOG[@]}"; else echo "No backlog items found"; fi
22
23
  ```
23
24
 
24
25
  2. **Read ROADMAP.md** and extract all 999.x phase entries: