@opengsd/gsd-core 1.10.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (544) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-code-fixer.md +1 -1
  4. package/agents/gsd-debug-session-manager.md +12 -1
  5. package/agents/gsd-debugger.md +1 -1
  6. package/agents/gsd-doc-synthesizer.md +2 -4
  7. package/agents/gsd-dom-verifier.md +169 -0
  8. package/agents/gsd-eval-auditor.md +1 -1
  9. package/agents/gsd-executor.md +22 -14
  10. package/agents/gsd-framework-selector.md +1 -3
  11. package/agents/gsd-intel-updater.md +1 -1
  12. package/agents/gsd-mempalace-curator.md +5 -3
  13. package/agents/gsd-pattern-mapper.md +11 -0
  14. package/agents/gsd-phase-researcher.md +23 -2
  15. package/agents/gsd-plan-checker.md +50 -53
  16. package/agents/gsd-planner.md +50 -50
  17. package/agents/gsd-project-researcher.md +1 -1
  18. package/agents/gsd-research-synthesizer.md +2 -2
  19. package/agents/gsd-roadmapper.md +15 -11
  20. package/agents/gsd-ui-checker.md +63 -4
  21. package/agents/gsd-ui-researcher.md +41 -3
  22. package/agents/gsd-user-profiler.md +3 -0
  23. package/agents/gsd-verifier.md +13 -4
  24. package/bin/install.js +1448 -1103
  25. package/commands/gsd/code-review.md +1 -1
  26. package/commands/gsd/discuss-phase.md +1 -1
  27. package/commands/gsd/execute-phase.md +1 -1
  28. package/commands/gsd/import.md +1 -1
  29. package/commands/gsd/map-codebase.md +1 -1
  30. package/commands/gsd/mempalace-capture.md +1 -1
  31. package/commands/gsd/mempalace-recall.md +1 -1
  32. package/commands/gsd/new-milestone.md +1 -1
  33. package/commands/gsd/quick.md +9 -5
  34. package/commands/gsd/review-backlog.md +2 -1
  35. package/commands/gsd/verify-work.md +1 -1
  36. package/gsd-core/bin/gsd-tools.cjs +1035 -138
  37. package/gsd-core/bin/lib/active-workstream-store.cjs +146 -22
  38. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  39. package/gsd-core/bin/lib/agent-install-check.cjs +392 -32
  40. package/gsd-core/bin/lib/api-coverage.cjs +33 -14
  41. package/gsd-core/bin/lib/artifacts.cjs +5 -0
  42. package/gsd-core/bin/lib/assumption-delta.cjs +32 -15
  43. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  44. package/gsd-core/bin/lib/audit.cjs +1026 -268
  45. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  46. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  47. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  48. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  49. package/gsd-core/bin/lib/capability-registry.cjs +845 -130
  50. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  51. package/gsd-core/bin/lib/capability-state.cjs +18 -3
  52. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  53. package/gsd-core/bin/lib/capability-validator.cjs +700 -40
  54. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  55. package/gsd-core/bin/lib/check-command-router.cjs +216 -42
  56. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  57. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  58. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  59. package/gsd-core/bin/lib/codex-agent-toml.cjs +735 -0
  60. package/gsd-core/bin/lib/command-aliases.cjs +22 -0
  61. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  62. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  63. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  64. package/gsd-core/bin/lib/commands.cjs +1172 -108
  65. package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
  66. package/gsd-core/bin/lib/complexity-trigger.cjs +1192 -0
  67. package/gsd-core/bin/lib/config-loader.cjs +187 -23
  68. package/gsd-core/bin/lib/config.cjs +102 -3
  69. package/gsd-core/bin/lib/configuration.cjs +129 -37
  70. package/gsd-core/bin/lib/core-utils.cjs +208 -33
  71. package/gsd-core/bin/lib/decisions.cjs +23 -0
  72. package/gsd-core/bin/lib/edge-probe.cjs +9 -1
  73. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  74. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  75. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  76. package/gsd-core/bin/lib/frontmatter.cjs +899 -229
  77. package/gsd-core/bin/lib/gap-checker.cjs +95 -10
  78. package/gsd-core/bin/lib/git-base-branch.cjs +276 -39
  79. package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
  80. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  81. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  82. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +149 -0
  83. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  84. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  85. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  86. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +268 -0
  87. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  88. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  89. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +187 -0
  90. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  91. package/gsd-core/bin/lib/health-diagnostic.cjs +451 -0
  92. package/gsd-core/bin/lib/host-integration.cjs +39 -6
  93. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  94. package/gsd-core/bin/lib/init-command-router.cjs +118 -21
  95. package/gsd-core/bin/lib/init.cjs +439 -168
  96. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  97. package/gsd-core/bin/lib/install-engine.cjs +811 -259
  98. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  99. package/gsd-core/bin/lib/install-model-override-resolver.cjs +235 -0
  100. package/gsd-core/bin/lib/install-profiles.cjs +212 -61
  101. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  102. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  103. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  104. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  105. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  106. package/gsd-core/bin/lib/installer-migrations.cjs +148 -38
  107. package/gsd-core/bin/lib/intel.cjs +101 -26
  108. package/gsd-core/bin/lib/io.cjs +170 -15
  109. package/gsd-core/bin/lib/learnings.cjs +85 -14
  110. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  111. package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
  112. package/gsd-core/bin/lib/markdown-table.cjs +183 -22
  113. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  114. package/gsd-core/bin/lib/milestone.cjs +842 -73
  115. package/gsd-core/bin/lib/model-catalog.cjs +232 -16
  116. package/gsd-core/bin/lib/model-resolver.cjs +193 -68
  117. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  118. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  119. package/gsd-core/bin/lib/pattern.cjs +122 -0
  120. package/gsd-core/bin/lib/phase-estimation.cjs +18 -9
  121. package/gsd-core/bin/lib/phase-id.cjs +514 -40
  122. package/gsd-core/bin/lib/phase-lifecycle.cjs +52 -19
  123. package/gsd-core/bin/lib/phase-locator.cjs +262 -34
  124. package/gsd-core/bin/lib/phase.cjs +1038 -214
  125. package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
  126. package/gsd-core/bin/lib/plan-document.cjs +263 -0
  127. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  128. package/gsd-core/bin/lib/plan-scan.cjs +98 -3
  129. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  130. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  131. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  132. package/gsd-core/bin/lib/planning-snapshot.cjs +894 -0
  133. package/gsd-core/bin/lib/planning-workspace.cjs +112 -6
  134. package/gsd-core/bin/lib/probe-core.cjs +5 -2
  135. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  136. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  137. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  138. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  139. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +766 -0
  140. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
  141. package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -13
  142. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  143. package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
  144. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  145. package/gsd-core/bin/lib/roadmap-command-router.cjs +59 -11
  146. package/gsd-core/bin/lib/roadmap-parser.cjs +1006 -184
  147. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  148. package/gsd-core/bin/lib/roadmap.cjs +442 -96
  149. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +702 -52
  150. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  151. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +459 -55
  152. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  153. package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
  154. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +402 -58
  155. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  156. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  157. package/gsd-core/bin/lib/runtime-slash.cjs +96 -8
  158. package/gsd-core/bin/lib/security.cjs +104 -5
  159. package/gsd-core/bin/lib/shell-command-projection.cjs +342 -7
  160. package/gsd-core/bin/lib/smart-entry.cjs +133 -23
  161. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  162. package/gsd-core/bin/lib/state-command-router.cjs +52 -19
  163. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  164. package/gsd-core/bin/lib/state-document.cjs +338 -8
  165. package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
  166. package/gsd-core/bin/lib/state-transition.cjs +846 -176
  167. package/gsd-core/bin/lib/state.cjs +2589 -369
  168. package/gsd-core/bin/lib/surface.cjs +33 -11
  169. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  170. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  171. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  172. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  173. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  174. package/gsd-core/bin/lib/uat-predicate.cjs +67 -23
  175. package/gsd-core/bin/lib/uat.cjs +1761 -167
  176. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  177. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  178. package/gsd-core/bin/lib/ui-safety-gate.cjs +51 -12
  179. package/gsd-core/bin/lib/unusable-input.cjs +37 -0
  180. package/gsd-core/bin/lib/update-context.cjs +8 -2
  181. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  182. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  183. package/gsd-core/bin/lib/validate.cjs +20 -6
  184. package/gsd-core/bin/lib/vendor/README.md +75 -0
  185. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  186. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  187. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  188. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  189. package/gsd-core/bin/lib/verification.cjs +272 -9
  190. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  191. package/gsd-core/bin/lib/verify.cjs +453 -918
  192. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
  193. package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
  194. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  195. package/gsd-core/bin/lib/workstream.cjs +2 -2
  196. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  197. package/gsd-core/bin/lib/worktree-safety.cjs +341 -18
  198. package/gsd-core/bin/shared/config-defaults.manifest.json +8 -1
  199. package/gsd-core/bin/shared/config-schema.manifest.json +12 -1
  200. package/gsd-core/bin/shared/exit-codes.json +8 -0
  201. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  202. package/gsd-core/bin/shared/model-catalog.json +8 -1
  203. package/gsd-core/references/agent-contracts.md +44 -26
  204. package/gsd-core/references/api-coverage.md +24 -2
  205. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  206. package/gsd-core/references/checkpoints.md +39 -21
  207. package/gsd-core/references/context-budget.md +1 -1
  208. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  209. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  210. package/gsd-core/references/doc-conflict-engine.md +1 -1
  211. package/gsd-core/references/edge-probe.md +8 -0
  212. package/gsd-core/references/execute-mvp-tdd.md +4 -6
  213. package/gsd-core/references/execute-phase-between-wave-reset.md +15 -14
  214. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  215. package/gsd-core/references/execute-phase-response-language.md +1 -1
  216. package/gsd-core/references/execute-phase-wave-guard.md +17 -11
  217. package/gsd-core/references/failing-direction.md +78 -0
  218. package/gsd-core/references/gate-prompts.md +1 -1
  219. package/gsd-core/references/git-integration.md +5 -5
  220. package/gsd-core/references/git-planning-commit.md +5 -4
  221. package/gsd-core/references/gsd-run-resolver.md +1 -1
  222. package/gsd-core/references/loop-hook-dispatch.md +61 -2
  223. package/gsd-core/references/model-profiles.md +12 -4
  224. package/gsd-core/references/mvp-concepts.md +9 -9
  225. package/gsd-core/references/nyquist-compliance.md +74 -0
  226. package/gsd-core/references/offer-next.md +3 -5
  227. package/gsd-core/references/phase-argument-parsing.md +3 -3
  228. package/gsd-core/references/planner-failing-direction.md +53 -0
  229. package/gsd-core/references/planner-guidance.md +3 -9
  230. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  231. package/gsd-core/references/planner-preconditions.md +1 -1
  232. package/gsd-core/references/planner-reviews.md +1 -1
  233. package/gsd-core/references/planner-revision.md +1 -1
  234. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  235. package/gsd-core/references/planning-config.md +44 -13
  236. package/gsd-core/references/reviewer-instances.md +31 -0
  237. package/gsd-core/references/revision-loop.md +1 -1
  238. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  239. package/gsd-core/references/specless-probe-fallback.md +1 -1
  240. package/gsd-core/references/tdd.md +1 -3
  241. package/gsd-core/references/ui-brand.md +65 -21
  242. package/gsd-core/references/ui-consideration-probe.md +1 -1
  243. package/gsd-core/references/universal-anti-patterns.md +5 -5
  244. package/gsd-core/references/verifier-phase-gates.md +192 -0
  245. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  246. package/gsd-core/references/verify-mvp-mode.md +2 -2
  247. package/gsd-core/references/workstream-flag.md +33 -17
  248. package/gsd-core/templates/README.md +1 -1
  249. package/gsd-core/templates/SECURITY.md +3 -3
  250. package/gsd-core/templates/UI-SPEC.md +25 -3
  251. package/gsd-core/templates/VALIDATION.md +3 -3
  252. package/gsd-core/templates/discussion-log.md +1 -1
  253. package/gsd-core/templates/phase-prompt.md +5 -4
  254. package/gsd-core/templates/state.md +11 -4
  255. package/gsd-core/templates/verification-report.md +9 -1
  256. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  257. package/gsd-core/workflows/add-backlog.md +1 -1
  258. package/gsd-core/workflows/add-phase.md +3 -3
  259. package/gsd-core/workflows/add-tests.md +3 -8
  260. package/gsd-core/workflows/add-todo.md +1 -1
  261. package/gsd-core/workflows/ai-integration-phase.md +13 -20
  262. package/gsd-core/workflows/audit-fix.md +12 -3
  263. package/gsd-core/workflows/audit-milestone.md +9 -9
  264. package/gsd-core/workflows/audit-uat.md +17 -2
  265. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  266. package/gsd-core/workflows/autonomous.md +11 -27
  267. package/gsd-core/workflows/check-todos.md +1 -1
  268. package/gsd-core/workflows/cleanup.md +64 -5
  269. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +14 -4
  270. package/gsd-core/workflows/code-review-fix.md +38 -11
  271. package/gsd-core/workflows/code-review.md +159 -52
  272. package/gsd-core/workflows/complete-milestone.md +151 -23
  273. package/gsd-core/workflows/debug.md +12 -8
  274. package/gsd-core/workflows/diagnose-issues.md +47 -15
  275. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  276. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -8
  277. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  278. package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
  279. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  280. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  281. package/gsd-core/workflows/discuss-phase.md +1 -1
  282. package/gsd-core/workflows/do.md +3 -6
  283. package/gsd-core/workflows/docs-update.md +5 -4
  284. package/gsd-core/workflows/edit-phase.md +27 -2
  285. package/gsd-core/workflows/eval-review.md +7 -14
  286. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  287. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +142 -15
  288. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  289. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  290. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  291. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +24 -4
  292. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  293. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  294. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
  295. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  296. package/gsd-core/workflows/execute-phase.md +72 -100
  297. package/gsd-core/workflows/execute-plan.md +52 -15
  298. package/gsd-core/workflows/explore.md +131 -4
  299. package/gsd-core/workflows/extract-learnings.md +1 -1
  300. package/gsd-core/workflows/fast.md +10 -2
  301. package/gsd-core/workflows/forensics.md +1 -1
  302. package/gsd-core/workflows/graduation.md +5 -5
  303. package/gsd-core/workflows/health.md +76 -10
  304. package/gsd-core/workflows/import.md +18 -15
  305. package/gsd-core/workflows/inbox.md +4 -5
  306. package/gsd-core/workflows/ingest-docs.md +49 -16
  307. package/gsd-core/workflows/insert-phase.md +5 -5
  308. package/gsd-core/workflows/list-seeds.md +5 -3
  309. package/gsd-core/workflows/list-workspaces.md +1 -1
  310. package/gsd-core/workflows/manager.md +12 -23
  311. package/gsd-core/workflows/map-codebase.md +1 -1
  312. package/gsd-core/workflows/milestone-summary.md +1 -1
  313. package/gsd-core/workflows/mvp-phase.md +8 -5
  314. package/gsd-core/workflows/new-milestone.md +22 -29
  315. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  316. package/gsd-core/workflows/new-project.md +26 -40
  317. package/gsd-core/workflows/new-workspace.md +1 -1
  318. package/gsd-core/workflows/next.md +14 -2
  319. package/gsd-core/workflows/pause-work.md +1 -1
  320. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  321. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  322. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
  323. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
  324. package/gsd-core/workflows/plan-phase.md +162 -59
  325. package/gsd-core/workflows/plan-review-convergence.md +96 -11
  326. package/gsd-core/workflows/plant-seed.md +2 -2
  327. package/gsd-core/workflows/pr-branch.md +187 -51
  328. package/gsd-core/workflows/profile-user.md +16 -14
  329. package/gsd-core/workflows/progress.md +61 -18
  330. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  331. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +5 -7
  332. package/gsd-core/workflows/quick/steps/quick-verification.md +28 -9
  333. package/gsd-core/workflows/quick/steps/research-phase.md +4 -6
  334. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  335. package/gsd-core/workflows/quick.md +55 -44
  336. package/gsd-core/workflows/remove-phase.md +4 -4
  337. package/gsd-core/workflows/remove-workspace.md +2 -2
  338. package/gsd-core/workflows/resume-project.md +8 -12
  339. package/gsd-core/workflows/review.md +219 -20
  340. package/gsd-core/workflows/scan.md +1 -1
  341. package/gsd-core/workflows/secure-phase.md +3 -3
  342. package/gsd-core/workflows/session-report.md +2 -1
  343. package/gsd-core/workflows/settings-advanced.md +7 -9
  344. package/gsd-core/workflows/settings-integrations.md +64 -31
  345. package/gsd-core/workflows/settings.md +69 -7
  346. package/gsd-core/workflows/ship.md +116 -50
  347. package/gsd-core/workflows/sketch-wrap-up.md +11 -17
  348. package/gsd-core/workflows/sketch.md +12 -18
  349. package/gsd-core/workflows/smart-entry.md +3 -5
  350. package/gsd-core/workflows/spec-phase.md +53 -13
  351. package/gsd-core/workflows/spike-wrap-up.md +7 -11
  352. package/gsd-core/workflows/spike.md +20 -31
  353. package/gsd-core/workflows/stats.md +2 -2
  354. package/gsd-core/workflows/sync-skills.md +64 -9
  355. package/gsd-core/workflows/thread.md +11 -7
  356. package/gsd-core/workflows/transition.md +49 -14
  357. package/gsd-core/workflows/ui-phase.md +15 -21
  358. package/gsd-core/workflows/ui-review.md +8 -12
  359. package/gsd-core/workflows/ultraplan-phase.md +5 -13
  360. package/gsd-core/workflows/undo.md +8 -16
  361. package/gsd-core/workflows/update.md +7 -11
  362. package/gsd-core/workflows/validate-phase.md +3 -3
  363. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  364. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  365. package/gsd-core/workflows/verify-work.md +66 -25
  366. package/hooks/dist/gsd-agent-isolation-guard.js +158 -30
  367. package/hooks/dist/gsd-check-update-worker.js +56 -13
  368. package/hooks/dist/gsd-check-update.js +19 -1
  369. package/hooks/dist/gsd-config-reload.js +18 -12
  370. package/hooks/dist/gsd-context-monitor.js +19 -10
  371. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  372. package/hooks/dist/gsd-cursor-pre-tool.js +2 -3
  373. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  374. package/hooks/dist/gsd-cursor-stop.js +2 -1
  375. package/hooks/dist/gsd-cursor-subagent-start.js +83 -3
  376. package/hooks/dist/gsd-cursor-subagent-stop.js +6 -3
  377. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  378. package/hooks/dist/gsd-graphify-update.sh +22 -18
  379. package/hooks/dist/gsd-node-runner.sh +76 -0
  380. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  381. package/hooks/dist/gsd-prompt-guard.js +37 -27
  382. package/hooks/dist/gsd-read-guard.js +16 -7
  383. package/hooks/dist/gsd-read-injection-scanner.js +55 -32
  384. package/hooks/dist/gsd-session-state.sh +1 -0
  385. package/hooks/dist/gsd-statusline.js +231 -24
  386. package/hooks/dist/gsd-update-banner.js +22 -1
  387. package/hooks/dist/gsd-validate-commit.sh +80 -6
  388. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  389. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  390. package/hooks/dist/gsd-workflow-guard.js +162 -46
  391. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  392. package/hooks/dist/gsd-write-guard.js +35 -25
  393. package/hooks/dist/lib/cli-exit.js +560 -0
  394. package/hooks/dist/lib/exit-code-registry.js +98 -0
  395. package/hooks/dist/lib/git-cmd.js +92 -59
  396. package/hooks/dist/lib/git-probe.js +84 -0
  397. package/hooks/dist/lib/hook-exit.js +81 -0
  398. package/hooks/dist/lib/injection-patterns.js +45 -0
  399. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  400. package/hooks/dist/lib/isolation-sentinel.js +9 -0
  401. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  402. package/hooks/gsd-agent-isolation-guard.js +158 -30
  403. package/hooks/gsd-check-update-worker.js +56 -13
  404. package/hooks/gsd-check-update.js +19 -1
  405. package/hooks/gsd-config-reload.js +18 -12
  406. package/hooks/gsd-context-monitor.js +19 -10
  407. package/hooks/gsd-cursor-post-tool.js +3 -1
  408. package/hooks/gsd-cursor-pre-tool.js +2 -3
  409. package/hooks/gsd-cursor-session-start.js +2 -1
  410. package/hooks/gsd-cursor-stop.js +2 -1
  411. package/hooks/gsd-cursor-subagent-start.js +83 -3
  412. package/hooks/gsd-cursor-subagent-stop.js +6 -3
  413. package/hooks/gsd-ensure-canonical-path.js +2 -1
  414. package/hooks/gsd-graphify-update.sh +22 -18
  415. package/hooks/gsd-node-runner.sh +76 -0
  416. package/hooks/gsd-phase-boundary.sh +1 -0
  417. package/hooks/gsd-prompt-guard.js +37 -27
  418. package/hooks/gsd-read-guard.js +16 -7
  419. package/hooks/gsd-read-injection-scanner.js +55 -32
  420. package/hooks/gsd-session-state.sh +1 -0
  421. package/hooks/gsd-statusline.js +231 -24
  422. package/hooks/gsd-update-banner.js +22 -1
  423. package/hooks/gsd-validate-commit.sh +80 -6
  424. package/hooks/gsd-windsurf-pre-command.js +16 -11
  425. package/hooks/gsd-windsurf-pre-write.js +22 -13
  426. package/hooks/gsd-workflow-guard.js +162 -46
  427. package/hooks/gsd-worktree-path-guard.js +36 -21
  428. package/hooks/gsd-write-guard.js +35 -25
  429. package/hooks/lib/cli-exit.js +560 -0
  430. package/hooks/lib/exit-code-registry.js +98 -0
  431. package/hooks/lib/git-cmd.js +92 -59
  432. package/hooks/lib/git-probe.js +84 -0
  433. package/hooks/lib/hook-exit.js +81 -0
  434. package/hooks/lib/injection-patterns.js +45 -0
  435. package/hooks/lib/isolation-deny-reason.js +39 -0
  436. package/hooks/lib/isolation-sentinel.js +9 -0
  437. package/hooks/managed-hooks-registry.cjs +3 -0
  438. package/package.json +28 -11
  439. package/pi/gsd.cjs +19 -5
  440. package/scripts/base64-scan.sh +74 -12
  441. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  442. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  443. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  444. package/scripts/build-hooks.js +5 -0
  445. package/scripts/changeset/lint.cjs +60 -5
  446. package/scripts/check-alias-drift.cjs +7 -43
  447. package/scripts/check-contract-drift.cjs +297 -0
  448. package/scripts/check-glossary-refs.cjs +77 -15
  449. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  450. package/scripts/ci-check-job-near-cap.cjs +49 -0
  451. package/scripts/ci-pr-mergeability.cjs +262 -0
  452. package/scripts/ci-test-scope.cjs +64 -14
  453. package/scripts/ci-timeout-report.cjs +230 -0
  454. package/scripts/command-contract-helpers.cjs +903 -1
  455. package/scripts/docs-guard-registry.cjs +396 -0
  456. package/scripts/gen-adr-index.cjs +728 -38
  457. package/scripts/gen-capability-registry.cjs +11 -21
  458. package/scripts/gen-context-index.cjs +2 -11
  459. package/scripts/gen-exit-code-docs.cjs +318 -0
  460. package/scripts/gen-exit-code-registry.cjs +891 -0
  461. package/scripts/gen-features.cjs +836 -0
  462. package/scripts/gen-health-docs.cjs +390 -0
  463. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  464. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  465. package/scripts/gen-inventory-manifest.cjs +50 -4
  466. package/scripts/gen-loop-host-contract.cjs +138 -25
  467. package/scripts/gen-registry.cjs +3 -14
  468. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  469. package/scripts/gen-state-md-docs.cjs +727 -0
  470. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  471. package/scripts/lib/alias-drift-families.cjs +46 -0
  472. package/scripts/lib/ci-job-timing.cjs +72 -0
  473. package/scripts/lib/cli-exit.cjs +546 -44
  474. package/scripts/lib/drift-scan.cjs +308 -0
  475. package/scripts/lib/exit-code-registry.cjs +98 -0
  476. package/scripts/lib/ndjson-reporter.cjs +119 -0
  477. package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
  478. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  479. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  480. package/scripts/lint-canary-version-leak.cjs +73 -0
  481. package/scripts/lint-command-contract.cjs +96 -13
  482. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  483. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  484. package/scripts/lint-default-flip-documentation.cjs +193 -0
  485. package/scripts/lint-docs-guard-registration.cjs +495 -0
  486. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
  487. package/scripts/lint-eslint-glob-coverage.allowlist.json +38 -0
  488. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  489. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  490. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  491. package/scripts/lint-health-diagnostic-rule-table.cjs +461 -0
  492. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  493. package/scripts/lint-milestone-window-drift.cjs +468 -0
  494. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  495. package/scripts/lint-phase-enumeration-drift.cjs +492 -0
  496. package/scripts/lint-plan-count-drift.cjs +318 -0
  497. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  498. package/scripts/lint-planning-prompt-drift.cjs +471 -0
  499. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  500. package/scripts/lint-regression-test-names.cjs +15 -13
  501. package/scripts/lint-removed-but-needed.cjs +488 -0
  502. package/scripts/lint-seam-enforcement.cjs +182 -0
  503. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  504. package/scripts/lint-source-test-name-collision.cjs +241 -0
  505. package/scripts/lint-state-field-drift.cjs +805 -0
  506. package/scripts/lint-state-write-path-drift.cjs +950 -0
  507. package/scripts/lint-test-file-count.allowlist.json +137 -8
  508. package/scripts/lint-test-file-count.cjs +25 -3
  509. package/scripts/lint-unreachable-guard-drift.cjs +830 -0
  510. package/scripts/lint-vendored-deps.cjs +297 -0
  511. package/scripts/mutation-matrix.cjs +599 -50
  512. package/scripts/pr-changed-files.cjs +63 -0
  513. package/scripts/pr-template-policy.cjs +14 -4
  514. package/scripts/prompt-injection-scan.sh +100 -14
  515. package/scripts/require-issue-link-policy.cjs +192 -0
  516. package/scripts/secret-scan.sh +75 -13
  517. package/scripts/select-docs-guards.cjs +56 -0
  518. package/scripts/sync-runtime-launcher.cjs +24 -7
  519. package/skills/gsd-autonomous/SKILL.md +0 -1
  520. package/skills/gsd-code-review/SKILL.md +1 -1
  521. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  522. package/skills/gsd-execute-phase/SKILL.md +1 -2
  523. package/skills/gsd-import/SKILL.md +1 -1
  524. package/skills/gsd-map-codebase/SKILL.md +1 -1
  525. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  526. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  527. package/skills/gsd-new-milestone/SKILL.md +1 -1
  528. package/skills/gsd-next/SKILL.md +0 -1
  529. package/skills/gsd-plan-phase/SKILL.md +0 -1
  530. package/skills/gsd-progress/SKILL.md +0 -1
  531. package/skills/gsd-quick/SKILL.md +9 -5
  532. package/skills/gsd-review-backlog/SKILL.md +2 -1
  533. package/skills/gsd-stats/SKILL.md +0 -1
  534. package/skills/gsd-verify-work/SKILL.md +1 -1
  535. package/vscode/package.json +1 -1
  536. package/bin/lib/ui-safety-gate.cjs +0 -107
  537. package/gsd-core/workflows/discovery-phase.md +0 -298
  538. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  539. package/gsd-core/workflows/verify-phase.md +0 -574
  540. package/scripts/affected-tests-lib.cjs +0 -554
  541. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  542. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  543. package/scripts/run-affected-tests.cjs +0 -7
  544. package/scripts/run-tests.cjs +0 -1051
@@ -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) {
@@ -6,12 +6,29 @@
6
6
  # scripts/prompt-injection-scan.sh --file path/to/file # Scan a single file
7
7
  # scripts/prompt-injection-scan.sh --dir agents/ # Scan all files in a directory
8
8
  #
9
- # Exit codes:
10
- # 0 = clean
11
- # 1 = findings detected
12
- # 2 = usage error
9
+ # Exit codes (ADR-3889, #3908 — registered in gsd-core/bin/shared/exit-codes.json):
10
+ # 0 = clean
11
+ # 1 = findings detected
12
+ # $EXIT_USAGE (64) = usage error (bad argv, missing --file/--dir target)
13
+ # $EXIT_NO_INPUT (66) = ran; scope established; zero files in scope (genuinely empty)
14
+ # $EXIT_UNAVAILABLE (69) = could not establish scope (bad ref, not a repo, unreadable dir)
13
15
  set -euo pipefail
14
16
 
17
+ # ─── Exit-code registry (ADR-3889, #3908) ────────────────────────────────────
18
+ # Resolved relative to THIS script's location, not the caller's cwd. Loud,
19
+ # non-zero failure if the fragment is missing — never fall back to a guessed
20
+ # literal integer, and never let a missing registry silently degrade to
21
+ # exit 0.
22
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23
+ EXIT_CODES_SH="$SCRIPT_DIR/../gsd-core/bin/shared/exit-codes.sh"
24
+ if [[ ! -f "$EXIT_CODES_SH" ]]; then
25
+ echo "prompt-injection-scan: FATAL: exit-code registry not found at $EXIT_CODES_SH" >&2
26
+ echo " Regenerate with: node scripts/gen-exit-code-registry.cjs --write" >&2
27
+ exit 1
28
+ fi
29
+ # shellcheck disable=SC1090
30
+ . "$EXIT_CODES_SH"
31
+
15
32
  # ─── Patterns ────────────────────────────────────────────────────────────────
16
33
  # Each pattern is a POSIX extended regex. Keep alphabetized by category.
17
34
  #
@@ -72,6 +89,15 @@ PATTERNS=(
72
89
  # `eval('...')` (single-quoted) silently went undetected on macOS while
73
90
  # passing on GNU-grep CI runners. Found auditing #3175; fixed here since it
74
91
  # is the same unanchored/portability defect class as the boundary fix.
92
+ #
93
+ # `exec` stays receiver-blind on purpose. A left boundary that excludes a
94
+ # preceding `.` would drop every member-position `.exec('…')` — including
95
+ # `require('child_process').exec('…')`, the single most common Node spelling
96
+ # of the vector this pattern exists to catch — and a receiver allowlist
97
+ # cannot restore it, because the literal `child_process` is not adjacent to
98
+ # `.exec`. The cost is that `RegExp.prototype.exec`, which takes a subject
99
+ # string rather than code, also matches; files that legitimately call it are
100
+ # handled by ALLOWLIST below, never by narrowing the pattern.
75
101
  '(^|[^[:alnum:]])eval[[:space:]]*\([[:space:]]*["'"'"']'
76
102
  'exec[[:space:]]*\([[:space:]]*["'"'"']'
77
103
  '(^|[^[:alnum:]])Function[[:space:]]*\([[:space:]]*["'"'"'].*return'
@@ -129,6 +155,22 @@ ALLOWLIST=(
129
155
  # asserts nothing: it is the payload the guard is required to catch, carried
130
156
  # as test DATA. Same class as the read-injection-scanner suites above.
131
157
  'tests/kimi-payload-field-shadowing.security.test.cjs'
158
+ # Phase-ID grammar regression tests exercise `RegExp.prototype.exec` via
159
+ # `re.exec('<phase-id>')` against fixtures like 'MANIFOLD-64-auth' / 'CK-64-auth'.
160
+ # The scanner's `exec('` code-execution pattern matches that benign method call,
161
+ # not an attack vector — same DEFECT.PROMPT-INJECTION-SCAN-COLLISION class as the
162
+ # test fixtures above. Pre-existing content (16 such calls on `next`); it surfaces
163
+ # here only because #2573's W024 `state_head` assertions make the file appear in
164
+ # the changed-file set the diff-mode scan walks.
165
+ 'tests/health-validation.test.cjs'
166
+ # #2528 — same collision, same disposition: the continuation-grammar suite
167
+ # drives the tokenizer regexes directly via `re.exec('05-80-20')`, so the
168
+ # argument is the subject string, not a command. Exempted per file rather
169
+ # than by narrowing the `exec(` pattern: a left boundary excluding a preceding
170
+ # `.` would drop `require('child_process').exec('…')`, and a receiver
171
+ # allowlist cannot reach it either, because the literal `child_process` is
172
+ # not adjacent to `.exec`. See the note at the pattern itself.
173
+ 'tests/continuation-grammar-parity.test.cjs'
132
174
  )
133
175
 
134
176
  is_allowlisted() {
@@ -150,33 +192,73 @@ collect_files() {
150
192
  case "$mode" in
151
193
  --diff)
152
194
  local base="${1:-origin/main}"
153
- # Get changed files in the diff, filter to scannable extensions
154
- git diff --name-only --diff-filter=ACMR "$base"...HEAD 2>/dev/null \
155
- | grep -E '\.(md|cjs|js|json|yml|yaml|sh)$' || true
195
+ # Run git separately from the filter pipe so its OWN exit status (not
196
+ # grep's) decides whether the diff could be established. stdout and
197
+ # stderr are captured SEPARATELY (never merged with `2>&1`) so that a
198
+ # warning git writes to stderr on an otherwise successful diff can
199
+ # never be mistaken for a filename in the file list. On failure the
200
+ # captured stderr is emitted as the diagnostic; on success it is
201
+ # forwarded as a warning, never folded into the file list.
202
+ # `|| true` on the filter below is CORRECT (not gratuitous): `grep -E`
203
+ # exits 1 when nothing matches the scannable extensions (e.g. a diff
204
+ # touching only non-scannable file types), which is a legitimate empty
205
+ # result, not a failure to run.
206
+ local raw status err_file
207
+ err_file=$(mktemp)
208
+ raw=$(git diff --name-only --diff-filter=ACMR "$base"...HEAD 2>"$err_file")
209
+ status=$?
210
+ if (( status != 0 )); then
211
+ cat "$err_file" >&2
212
+ rm -f "$err_file"
213
+ exit "$EXIT_UNAVAILABLE"
214
+ fi
215
+ if [[ -s "$err_file" ]]; then
216
+ echo "Warning: git diff emitted stderr output:" >&2
217
+ cat "$err_file" >&2
218
+ fi
219
+ rm -f "$err_file"
220
+ printf '%s\n' "$raw" | grep -E '\.(md|cjs|js|json|yml|yaml|sh)$' || true
156
221
  ;;
157
222
  --file)
158
223
  if [[ -f "$1" ]]; then
159
224
  echo "$1"
160
225
  else
161
226
  echo "Error: file not found: $1" >&2
162
- exit 2
227
+ exit "$EXIT_USAGE"
163
228
  fi
164
229
  ;;
165
230
  --dir)
166
231
  local dir="$1"
167
232
  if [[ ! -d "$dir" ]]; then
168
233
  echo "Error: directory not found: $dir" >&2
169
- exit 2
234
+ exit "$EXIT_USAGE"
235
+ fi
236
+ # Same treatment as --diff: a `find` that fails (e.g. permission
237
+ # denied) must not be reported as an empty directory, and stdout/stderr
238
+ # are captured separately so a stderr warning never enters the file list.
239
+ local raw status err_file
240
+ err_file=$(mktemp)
241
+ raw=$(find "$dir" -type f \( -name '*.md' -o -name '*.cjs' -o -name '*.js' -o -name '*.json' -o -name '*.yml' -o -name '*.yaml' -o -name '*.sh' \) \
242
+ ! -path '*/node_modules/*' ! -path '*/.git/*' ! -path '*/dist/*' 2>"$err_file")
243
+ status=$?
244
+ if (( status != 0 )); then
245
+ cat "$err_file" >&2
246
+ rm -f "$err_file"
247
+ exit "$EXIT_UNAVAILABLE"
248
+ fi
249
+ if [[ -s "$err_file" ]]; then
250
+ echo "Warning: find emitted stderr output:" >&2
251
+ cat "$err_file" >&2
170
252
  fi
171
- find "$dir" -type f \( -name '*.md' -o -name '*.cjs' -o -name '*.js' -o -name '*.json' -o -name '*.yml' -o -name '*.yaml' -o -name '*.sh' \) \
172
- ! -path '*/node_modules/*' ! -path '*/.git/*' ! -path '*/dist/*' 2>/dev/null || true
253
+ rm -f "$err_file"
254
+ printf '%s\n' "$raw"
173
255
  ;;
174
256
  --stdin)
175
257
  cat
176
258
  ;;
177
259
  *)
178
260
  echo "Usage: $0 --diff [base] | --file <path> | --dir <path> | --stdin" >&2
179
- exit 2
261
+ exit "$EXIT_USAGE"
180
262
  ;;
181
263
  esac
182
264
  }
@@ -215,7 +297,7 @@ scan_file() {
215
297
  main() {
216
298
  if [[ $# -eq 0 ]]; then
217
299
  echo "Usage: $0 --diff [base] | --file <path> | --dir <path>" >&2
218
- exit 2
300
+ exit "$EXIT_USAGE"
219
301
  fi
220
302
 
221
303
  local mode="$1"
@@ -225,8 +307,12 @@ main() {
225
307
  files=$(collect_files "$mode" "$@")
226
308
 
227
309
  if [[ -z "$files" ]]; then
310
+ # collect_files already exited (UNAVAILABLE/USAGE) for anything that
311
+ # could not establish scope. Reaching here with an empty result means
312
+ # scope WAS established and is genuinely empty — that is NO_INPUT, not a
313
+ # silent clean pass.
228
314
  echo "prompt-injection-scan: no files to scan"
229
- exit 0
315
+ exit "$EXIT_NO_INPUT"
230
316
  fi
231
317
 
232
318
  local total=0
@@ -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
+ };
@@ -17,10 +17,12 @@
17
17
  # This flag does not change secret-detection logic — only which
18
18
  # exclusions are applied.
19
19
  #
20
- # Exit codes:
21
- # 0 = clean
22
- # 1 = findings detected
23
- # 2 = usage error
20
+ # Exit codes (ADR-3889, #3908 — registered in gsd-core/bin/shared/exit-codes.json):
21
+ # 0 = clean
22
+ # 1 = findings detected
23
+ # $EXIT_USAGE (64) = usage error (bad argv, missing --file/--dir target)
24
+ # $EXIT_NO_INPUT (66) = ran; scope established; zero files in scope (genuinely empty)
25
+ # $EXIT_UNAVAILABLE (69) = could not establish scope (bad ref, not a repo, unreadable dir)
24
26
  #
25
27
  # Annotation format for .secretscanignore (required for --strict compliance):
26
28
  # # allow: <pattern> reason="..." owner="..." expires="YYYY-MM-DD" [rule-id="..."]
@@ -39,6 +41,21 @@
39
41
  # cannot permanently hide secrets. See SECURITY.md for the audit runbook.
40
42
  set -euo pipefail
41
43
 
44
+ # ─── Exit-code registry (ADR-3889, #3908) ────────────────────────────────────
45
+ # Resolved relative to THIS script's location, not the caller's cwd, so this
46
+ # works regardless of where the scanner is invoked from. Loud, non-zero
47
+ # failure if the fragment is missing — never fall back to a guessed literal
48
+ # integer, and never let a missing registry silently degrade to exit 0.
49
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
50
+ EXIT_CODES_SH="$SCRIPT_DIR/../gsd-core/bin/shared/exit-codes.sh"
51
+ if [[ ! -f "$EXIT_CODES_SH" ]]; then
52
+ echo "secret-scan: FATAL: exit-code registry not found at $EXIT_CODES_SH" >&2
53
+ echo " Regenerate with: node scripts/gen-exit-code-registry.cjs --write" >&2
54
+ exit 1
55
+ fi
56
+ # shellcheck disable=SC1090
57
+ . "$EXIT_CODES_SH"
58
+
42
59
  # ─── Global mode flag ─────────────────────────────────────────────────────────
43
60
  STRICT_MODE=false
44
61
 
@@ -235,7 +252,31 @@ collect_files() {
235
252
  case "$mode" in
236
253
  --diff)
237
254
  local base="${1:-origin/main}"
238
- git diff --name-only --diff-filter=ACMR "$base"...HEAD 2>/dev/null \
255
+ # Run git separately from the filter pipe so its OWN exit status (not
256
+ # grep's) decides whether the diff could be established. stdout and
257
+ # stderr are captured SEPARATELY (never merged with `2>&1`) so that a
258
+ # warning git writes to stderr on an otherwise successful diff can
259
+ # never be mistaken for a filename in the file list. On failure the
260
+ # captured stderr is emitted as the diagnostic; on success it is
261
+ # forwarded as a warning, never folded into the file list.
262
+ # `|| true` on the filter below is CORRECT (not gratuitous): `grep -v`
263
+ # exits 1 when every line is filtered out (e.g. an all-images diff),
264
+ # which is a legitimate empty result, not a failure to run.
265
+ local raw status err_file
266
+ err_file=$(mktemp)
267
+ raw=$(git diff --name-only --diff-filter=ACMR "$base"...HEAD 2>"$err_file")
268
+ status=$?
269
+ if (( status != 0 )); then
270
+ cat "$err_file" >&2
271
+ rm -f "$err_file"
272
+ exit "$EXIT_UNAVAILABLE"
273
+ fi
274
+ if [[ -s "$err_file" ]]; then
275
+ echo "Warning: git diff emitted stderr output:" >&2
276
+ cat "$err_file" >&2
277
+ fi
278
+ rm -f "$err_file"
279
+ printf '%s\n' "$raw" \
239
280
  | grep -vE '\.(png|jpg|jpeg|gif|ico|woff|woff2|ttf|eot|otf|zip|tar|gz|pdf)$' || true
240
281
  ;;
241
282
  --file)
@@ -243,24 +284,41 @@ collect_files() {
243
284
  echo "$1"
244
285
  else
245
286
  echo "Error: file not found: $1" >&2
246
- exit 2
287
+ exit "$EXIT_USAGE"
247
288
  fi
248
289
  ;;
249
290
  --dir)
250
291
  local dir="$1"
251
292
  if [[ ! -d "$dir" ]]; then
252
293
  echo "Error: directory not found: $dir" >&2
253
- exit 2
294
+ exit "$EXIT_USAGE"
295
+ fi
296
+ # Same treatment as --diff: a `find` that fails (e.g. permission
297
+ # denied) must not be reported as an empty directory, and stdout/stderr
298
+ # are captured separately so a stderr warning never enters the file list.
299
+ local raw status err_file
300
+ err_file=$(mktemp)
301
+ raw=$(find "$dir" -type f ! -path '*/node_modules/*' ! -path '*/.git/*' ! -path '*/dist/*' \
302
+ ! -name '*.png' ! -name '*.jpg' ! -name '*.gif' ! -name '*.woff*' 2>"$err_file")
303
+ status=$?
304
+ if (( status != 0 )); then
305
+ cat "$err_file" >&2
306
+ rm -f "$err_file"
307
+ exit "$EXIT_UNAVAILABLE"
308
+ fi
309
+ if [[ -s "$err_file" ]]; then
310
+ echo "Warning: find emitted stderr output:" >&2
311
+ cat "$err_file" >&2
254
312
  fi
255
- find "$dir" -type f ! -path '*/node_modules/*' ! -path '*/.git/*' ! -path '*/dist/*' \
256
- ! -name '*.png' ! -name '*.jpg' ! -name '*.gif' ! -name '*.woff*' 2>/dev/null || true
313
+ rm -f "$err_file"
314
+ printf '%s\n' "$raw"
257
315
  ;;
258
316
  --stdin)
259
317
  cat
260
318
  ;;
261
319
  *)
262
320
  echo "Usage: $0 --diff [base] | --file <path> | --dir <path> | --stdin" >&2
263
- exit 2
321
+ exit "$EXIT_USAGE"
264
322
  ;;
265
323
  esac
266
324
  }
@@ -300,7 +358,7 @@ scan_file() {
300
358
  main() {
301
359
  if [[ $# -eq 0 ]]; then
302
360
  echo "Usage: $0 --diff [base] | --file <path> | --dir <path> [--strict]" >&2
303
- exit 2
361
+ exit "$EXIT_USAGE"
304
362
  fi
305
363
 
306
364
  # Parse --strict flag first (may appear anywhere in argv)
@@ -316,7 +374,7 @@ main() {
316
374
 
317
375
  if [[ $# -eq 0 ]]; then
318
376
  echo "Usage: $0 --diff [base] | --file <path> | --dir <path> [--strict]" >&2
319
- exit 2
377
+ exit "$EXIT_USAGE"
320
378
  fi
321
379
 
322
380
  load_ignorelist
@@ -328,8 +386,12 @@ main() {
328
386
  files=$(collect_files "$mode" "$@")
329
387
 
330
388
  if [[ -z "$files" ]]; then
389
+ # collect_files already exited (UNAVAILABLE/USAGE) for anything that
390
+ # could not establish scope. Reaching here with an empty result means
391
+ # scope WAS established and is genuinely empty (e.g. an all-images
392
+ # diff) — that is NO_INPUT, not a silent clean pass.
331
393
  echo "secret-scan: no files to scan"
332
- exit 0
394
+ exit "$EXIT_NO_INPUT"
333
395
  fi
334
396
 
335
397
  local total=0