@opengsd/gsd-core 1.11.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 (395) 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 +1 -1
  5. package/agents/gsd-debugger.md +1 -1
  6. package/agents/gsd-dom-verifier.md +169 -0
  7. package/agents/gsd-eval-auditor.md +1 -1
  8. package/agents/gsd-executor.md +17 -9
  9. package/agents/gsd-framework-selector.md +1 -3
  10. package/agents/gsd-intel-updater.md +1 -1
  11. package/agents/gsd-mempalace-curator.md +0 -1
  12. package/agents/gsd-pattern-mapper.md +11 -0
  13. package/agents/gsd-phase-researcher.md +3 -1
  14. package/agents/gsd-plan-checker.md +15 -55
  15. package/agents/gsd-planner.md +6 -4
  16. package/agents/gsd-project-researcher.md +1 -1
  17. package/agents/gsd-research-synthesizer.md +2 -2
  18. package/agents/gsd-roadmapper.md +15 -11
  19. package/agents/gsd-ui-checker.md +63 -4
  20. package/agents/gsd-ui-researcher.md +41 -3
  21. package/agents/gsd-verifier.md +1 -1
  22. package/bin/install.js +609 -134
  23. package/commands/gsd/discuss-phase.md +1 -1
  24. package/commands/gsd/import.md +1 -1
  25. package/commands/gsd/quick.md +8 -4
  26. package/gsd-core/bin/gsd-tools.cjs +567 -51
  27. package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
  28. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  29. package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
  30. package/gsd-core/bin/lib/api-coverage.cjs +30 -9
  31. package/gsd-core/bin/lib/artifacts.cjs +2 -0
  32. package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
  33. package/gsd-core/bin/lib/audit.cjs +163 -41
  34. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  35. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  36. package/gsd-core/bin/lib/capability-registry.cjs +336 -95
  37. package/gsd-core/bin/lib/capability-state.cjs +18 -3
  38. package/gsd-core/bin/lib/capability-validator.cjs +205 -18
  39. package/gsd-core/bin/lib/check-command-router.cjs +145 -5
  40. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  41. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  42. package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
  43. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  44. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  45. package/gsd-core/bin/lib/commands.cjs +543 -44
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
  47. package/gsd-core/bin/lib/config-loader.cjs +118 -29
  48. package/gsd-core/bin/lib/config.cjs +92 -2
  49. package/gsd-core/bin/lib/configuration.cjs +129 -37
  50. package/gsd-core/bin/lib/core-utils.cjs +84 -7
  51. package/gsd-core/bin/lib/edge-probe.cjs +9 -1
  52. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  53. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  54. package/gsd-core/bin/lib/frontmatter.cjs +840 -305
  55. package/gsd-core/bin/lib/gap-checker.cjs +27 -3
  56. package/gsd-core/bin/lib/git-base-branch.cjs +174 -39
  57. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
  58. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +6 -3
  59. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
  60. package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
  61. package/gsd-core/bin/lib/host-integration.cjs +39 -6
  62. package/gsd-core/bin/lib/init-command-router.cjs +118 -21
  63. package/gsd-core/bin/lib/init.cjs +120 -41
  64. package/gsd-core/bin/lib/install-engine.cjs +68 -3
  65. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -1
  66. package/gsd-core/bin/lib/install-profiles.cjs +78 -4
  67. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  68. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  69. package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
  70. package/gsd-core/bin/lib/intel.cjs +101 -26
  71. package/gsd-core/bin/lib/io.cjs +160 -15
  72. package/gsd-core/bin/lib/learnings.cjs +85 -14
  73. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  74. package/gsd-core/bin/lib/markdown-table.cjs +52 -4
  75. package/gsd-core/bin/lib/milestone.cjs +90 -5
  76. package/gsd-core/bin/lib/model-catalog.cjs +177 -19
  77. package/gsd-core/bin/lib/model-resolver.cjs +10 -28
  78. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  79. package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
  80. package/gsd-core/bin/lib/phase-id.cjs +70 -4
  81. package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
  82. package/gsd-core/bin/lib/phase-locator.cjs +138 -17
  83. package/gsd-core/bin/lib/phase.cjs +405 -84
  84. package/gsd-core/bin/lib/plan-document.cjs +263 -0
  85. package/gsd-core/bin/lib/plan-scan.cjs +13 -2
  86. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  87. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  88. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -14
  89. package/gsd-core/bin/lib/planning-workspace.cjs +56 -0
  90. package/gsd-core/bin/lib/probe-core.cjs +4 -1
  91. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  92. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  93. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  94. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
  95. package/gsd-core/bin/lib/review-lane-descriptor.cjs +9 -9
  96. package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
  97. package/gsd-core/bin/lib/roadmap-parser.cjs +79 -16
  98. package/gsd-core/bin/lib/roadmap.cjs +74 -19
  99. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +96 -8
  100. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +34 -1
  101. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +287 -55
  102. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  103. package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
  104. package/gsd-core/bin/lib/shell-command-projection.cjs +71 -8
  105. package/gsd-core/bin/lib/smart-entry.cjs +12 -22
  106. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  107. package/gsd-core/bin/lib/state-command-router.cjs +47 -18
  108. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  109. package/gsd-core/bin/lib/state-document.cjs +186 -0
  110. package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
  111. package/gsd-core/bin/lib/state-transition.cjs +517 -101
  112. package/gsd-core/bin/lib/state.cjs +946 -163
  113. package/gsd-core/bin/lib/surface.cjs +10 -2
  114. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  115. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  116. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  117. package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
  118. package/gsd-core/bin/lib/uat.cjs +1376 -125
  119. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  120. package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
  121. package/gsd-core/bin/lib/unusable-input.cjs +13 -0
  122. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  123. package/gsd-core/bin/lib/vendor/README.md +43 -5
  124. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  125. package/gsd-core/bin/lib/verification.cjs +14 -1
  126. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  127. package/gsd-core/bin/lib/verify.cjs +95 -40
  128. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  129. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  130. package/gsd-core/bin/lib/worktree-safety.cjs +177 -21
  131. package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
  132. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  133. package/gsd-core/bin/shared/exit-codes.json +8 -0
  134. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  135. package/gsd-core/bin/shared/model-catalog.json +8 -1
  136. package/gsd-core/references/agent-contracts.md +3 -2
  137. package/gsd-core/references/api-coverage.md +24 -2
  138. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  139. package/gsd-core/references/checkpoints.md +37 -19
  140. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  141. package/gsd-core/references/edge-probe.md +8 -0
  142. package/gsd-core/references/execute-mvp-tdd.md +1 -3
  143. package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
  144. package/gsd-core/references/execute-phase-wave-guard.md +11 -9
  145. package/gsd-core/references/failing-direction.md +78 -0
  146. package/gsd-core/references/gate-prompts.md +1 -1
  147. package/gsd-core/references/git-integration.md +5 -5
  148. package/gsd-core/references/git-planning-commit.md +3 -3
  149. package/gsd-core/references/gsd-run-resolver.md +1 -1
  150. package/gsd-core/references/loop-hook-dispatch.md +22 -0
  151. package/gsd-core/references/model-profiles.md +1 -1
  152. package/gsd-core/references/nyquist-compliance.md +74 -0
  153. package/gsd-core/references/offer-next.md +3 -5
  154. package/gsd-core/references/phase-argument-parsing.md +3 -3
  155. package/gsd-core/references/planner-failing-direction.md +53 -0
  156. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  157. package/gsd-core/references/planner-revision.md +1 -1
  158. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  159. package/gsd-core/references/planning-config.md +37 -8
  160. package/gsd-core/references/reviewer-instances.md +31 -0
  161. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  162. package/gsd-core/references/tdd.md +1 -3
  163. package/gsd-core/references/ui-brand.md +65 -21
  164. package/gsd-core/references/ui-consideration-probe.md +1 -1
  165. package/gsd-core/references/universal-anti-patterns.md +2 -2
  166. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  167. package/gsd-core/references/verify-mvp-mode.md +1 -1
  168. package/gsd-core/references/workstream-flag.md +11 -11
  169. package/gsd-core/templates/README.md +1 -1
  170. package/gsd-core/templates/SECURITY.md +3 -3
  171. package/gsd-core/templates/UI-SPEC.md +25 -3
  172. package/gsd-core/templates/VALIDATION.md +3 -3
  173. package/gsd-core/templates/phase-prompt.md +3 -0
  174. package/gsd-core/templates/state.md +7 -0
  175. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  176. package/gsd-core/workflows/add-backlog.md +1 -1
  177. package/gsd-core/workflows/add-phase.md +3 -3
  178. package/gsd-core/workflows/add-tests.md +3 -8
  179. package/gsd-core/workflows/add-todo.md +1 -1
  180. package/gsd-core/workflows/ai-integration-phase.md +4 -9
  181. package/gsd-core/workflows/audit-fix.md +12 -3
  182. package/gsd-core/workflows/audit-milestone.md +9 -9
  183. package/gsd-core/workflows/audit-uat.md +17 -2
  184. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  185. package/gsd-core/workflows/autonomous.md +10 -26
  186. package/gsd-core/workflows/check-todos.md +1 -1
  187. package/gsd-core/workflows/cleanup.md +2 -2
  188. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
  189. package/gsd-core/workflows/code-review-fix.md +1 -1
  190. package/gsd-core/workflows/code-review.md +121 -40
  191. package/gsd-core/workflows/complete-milestone.md +15 -10
  192. package/gsd-core/workflows/debug.md +5 -3
  193. package/gsd-core/workflows/diagnose-issues.md +12 -6
  194. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  195. package/gsd-core/workflows/discuss-phase/modes/chain.md +3 -7
  196. package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
  197. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  198. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -2
  199. package/gsd-core/workflows/discuss-phase.md +1 -1
  200. package/gsd-core/workflows/do.md +3 -6
  201. package/gsd-core/workflows/docs-update.md +5 -4
  202. package/gsd-core/workflows/edit-phase.md +1 -1
  203. package/gsd-core/workflows/eval-review.md +4 -9
  204. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  205. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +113 -11
  206. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  207. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  208. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  209. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +22 -4
  210. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  211. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  212. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
  213. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  214. package/gsd-core/workflows/execute-phase.md +38 -54
  215. package/gsd-core/workflows/execute-plan.md +17 -12
  216. package/gsd-core/workflows/explore.md +1 -1
  217. package/gsd-core/workflows/extract-learnings.md +1 -1
  218. package/gsd-core/workflows/fast.md +2 -2
  219. package/gsd-core/workflows/forensics.md +1 -1
  220. package/gsd-core/workflows/graduation.md +5 -5
  221. package/gsd-core/workflows/health.md +3 -6
  222. package/gsd-core/workflows/import.md +14 -11
  223. package/gsd-core/workflows/inbox.md +4 -5
  224. package/gsd-core/workflows/ingest-docs.md +44 -11
  225. package/gsd-core/workflows/insert-phase.md +5 -5
  226. package/gsd-core/workflows/list-seeds.md +5 -3
  227. package/gsd-core/workflows/list-workspaces.md +1 -1
  228. package/gsd-core/workflows/manager.md +12 -23
  229. package/gsd-core/workflows/map-codebase.md +1 -1
  230. package/gsd-core/workflows/milestone-summary.md +1 -1
  231. package/gsd-core/workflows/mvp-phase.md +2 -2
  232. package/gsd-core/workflows/new-milestone.md +9 -21
  233. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  234. package/gsd-core/workflows/new-project.md +12 -26
  235. package/gsd-core/workflows/new-workspace.md +1 -1
  236. package/gsd-core/workflows/next.md +2 -2
  237. package/gsd-core/workflows/pause-work.md +1 -1
  238. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  239. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  240. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
  241. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
  242. package/gsd-core/workflows/plan-phase.md +121 -42
  243. package/gsd-core/workflows/plan-review-convergence.md +46 -9
  244. package/gsd-core/workflows/plant-seed.md +2 -2
  245. package/gsd-core/workflows/pr-branch.md +187 -51
  246. package/gsd-core/workflows/profile-user.md +16 -14
  247. package/gsd-core/workflows/progress.md +27 -12
  248. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  249. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -3
  250. package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
  251. package/gsd-core/workflows/quick/steps/research-phase.md +2 -4
  252. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  253. package/gsd-core/workflows/quick.md +20 -29
  254. package/gsd-core/workflows/remove-phase.md +4 -4
  255. package/gsd-core/workflows/remove-workspace.md +2 -2
  256. package/gsd-core/workflows/resume-project.md +8 -12
  257. package/gsd-core/workflows/review.md +193 -15
  258. package/gsd-core/workflows/scan.md +1 -1
  259. package/gsd-core/workflows/secure-phase.md +2 -2
  260. package/gsd-core/workflows/settings-advanced.md +7 -9
  261. package/gsd-core/workflows/settings-integrations.md +64 -31
  262. package/gsd-core/workflows/settings.md +3 -5
  263. package/gsd-core/workflows/ship.md +12 -6
  264. package/gsd-core/workflows/sketch-wrap-up.md +11 -17
  265. package/gsd-core/workflows/sketch.md +12 -18
  266. package/gsd-core/workflows/smart-entry.md +3 -5
  267. package/gsd-core/workflows/spec-phase.md +23 -1
  268. package/gsd-core/workflows/spike-wrap-up.md +7 -11
  269. package/gsd-core/workflows/spike.md +20 -31
  270. package/gsd-core/workflows/stats.md +2 -2
  271. package/gsd-core/workflows/sync-skills.md +1 -1
  272. package/gsd-core/workflows/thread.md +11 -7
  273. package/gsd-core/workflows/transition.md +5 -5
  274. package/gsd-core/workflows/ui-phase.md +10 -16
  275. package/gsd-core/workflows/ui-review.md +6 -10
  276. package/gsd-core/workflows/ultraplan-phase.md +5 -13
  277. package/gsd-core/workflows/undo.md +8 -16
  278. package/gsd-core/workflows/update.md +6 -10
  279. package/gsd-core/workflows/validate-phase.md +2 -2
  280. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  281. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  282. package/gsd-core/workflows/verify-work.md +57 -18
  283. package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
  284. package/hooks/dist/gsd-config-reload.js +18 -12
  285. package/hooks/dist/gsd-context-monitor.js +19 -10
  286. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  287. package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
  288. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  289. package/hooks/dist/gsd-cursor-stop.js +2 -1
  290. package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
  291. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
  292. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  293. package/hooks/dist/gsd-graphify-update.sh +22 -18
  294. package/hooks/dist/gsd-node-runner.sh +76 -0
  295. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  296. package/hooks/dist/gsd-prompt-guard.js +16 -7
  297. package/hooks/dist/gsd-read-guard.js +16 -7
  298. package/hooks/dist/gsd-read-injection-scanner.js +17 -8
  299. package/hooks/dist/gsd-session-state.sh +1 -0
  300. package/hooks/dist/gsd-statusline.js +215 -26
  301. package/hooks/dist/gsd-validate-commit.sh +80 -6
  302. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  303. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  304. package/hooks/dist/gsd-workflow-guard.js +34 -16
  305. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  306. package/hooks/dist/gsd-write-guard.js +35 -25
  307. package/hooks/dist/lib/cli-exit.js +560 -0
  308. package/hooks/dist/lib/exit-code-registry.js +98 -0
  309. package/hooks/dist/lib/git-probe.js +84 -0
  310. package/hooks/dist/lib/hook-exit.js +81 -0
  311. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  312. package/hooks/gsd-agent-isolation-guard.js +77 -38
  313. package/hooks/gsd-config-reload.js +18 -12
  314. package/hooks/gsd-context-monitor.js +19 -10
  315. package/hooks/gsd-cursor-post-tool.js +3 -1
  316. package/hooks/gsd-cursor-pre-tool.js +3 -1
  317. package/hooks/gsd-cursor-session-start.js +2 -1
  318. package/hooks/gsd-cursor-stop.js +2 -1
  319. package/hooks/gsd-cursor-subagent-start.js +28 -23
  320. package/hooks/gsd-cursor-subagent-stop.js +3 -1
  321. package/hooks/gsd-ensure-canonical-path.js +2 -1
  322. package/hooks/gsd-graphify-update.sh +22 -18
  323. package/hooks/gsd-node-runner.sh +76 -0
  324. package/hooks/gsd-phase-boundary.sh +1 -0
  325. package/hooks/gsd-prompt-guard.js +16 -7
  326. package/hooks/gsd-read-guard.js +16 -7
  327. package/hooks/gsd-read-injection-scanner.js +17 -8
  328. package/hooks/gsd-session-state.sh +1 -0
  329. package/hooks/gsd-statusline.js +215 -26
  330. package/hooks/gsd-validate-commit.sh +80 -6
  331. package/hooks/gsd-windsurf-pre-command.js +16 -11
  332. package/hooks/gsd-windsurf-pre-write.js +22 -13
  333. package/hooks/gsd-workflow-guard.js +34 -16
  334. package/hooks/gsd-worktree-path-guard.js +36 -21
  335. package/hooks/gsd-write-guard.js +35 -25
  336. package/hooks/lib/cli-exit.js +560 -0
  337. package/hooks/lib/exit-code-registry.js +98 -0
  338. package/hooks/lib/git-probe.js +84 -0
  339. package/hooks/lib/hook-exit.js +81 -0
  340. package/hooks/managed-hooks-registry.cjs +3 -0
  341. package/package.json +12 -7
  342. package/scripts/base64-scan.sh +74 -12
  343. package/scripts/build-hooks.js +5 -0
  344. package/scripts/check-glossary-refs.cjs +77 -15
  345. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  346. package/scripts/ci-check-job-near-cap.cjs +49 -0
  347. package/scripts/ci-pr-mergeability.cjs +262 -0
  348. package/scripts/ci-test-scope.cjs +45 -12
  349. package/scripts/ci-timeout-report.cjs +230 -0
  350. package/scripts/docs-guard-registry.cjs +396 -0
  351. package/scripts/gen-capability-registry.cjs +8 -6
  352. package/scripts/gen-exit-code-docs.cjs +318 -0
  353. package/scripts/gen-exit-code-registry.cjs +891 -0
  354. package/scripts/gen-features.cjs +836 -0
  355. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  356. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  357. package/scripts/gen-loop-host-contract.cjs +134 -1
  358. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  359. package/scripts/gen-state-md-docs.cjs +727 -0
  360. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  361. package/scripts/lib/ci-job-timing.cjs +72 -0
  362. package/scripts/lib/cli-exit.cjs +546 -44
  363. package/scripts/lib/drift-scan.cjs +32 -2
  364. package/scripts/lib/exit-code-registry.cjs +98 -0
  365. package/scripts/lib/ndjson-reporter.cjs +119 -0
  366. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  367. package/scripts/lint-docs-guard-registration.cjs +495 -0
  368. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
  369. package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
  370. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  371. package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
  372. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  373. package/scripts/lint-phase-enumeration-drift.cjs +21 -8
  374. package/scripts/lint-planning-prompt-drift.cjs +38 -1
  375. package/scripts/lint-removed-but-needed.cjs +184 -16
  376. package/scripts/lint-seam-enforcement.cjs +182 -0
  377. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  378. package/scripts/lint-source-test-name-collision.cjs +241 -0
  379. package/scripts/lint-state-write-path-drift.cjs +337 -432
  380. package/scripts/lint-test-file-count.allowlist.json +122 -4
  381. package/scripts/lint-test-file-count.cjs +25 -3
  382. package/scripts/lint-unreachable-guard-drift.cjs +51 -64
  383. package/scripts/lint-vendored-deps.cjs +208 -35
  384. package/scripts/mutation-matrix.cjs +599 -50
  385. package/scripts/prompt-injection-scan.sh +75 -14
  386. package/scripts/secret-scan.sh +75 -13
  387. package/scripts/select-docs-guards.cjs +56 -0
  388. package/scripts/sync-runtime-launcher.cjs +22 -3
  389. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  390. package/skills/gsd-import/SKILL.md +1 -1
  391. package/skills/gsd-quick/SKILL.md +8 -4
  392. package/vscode/package.json +1 -1
  393. package/bin/lib/ui-safety-gate.cjs +0 -109
  394. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  395. package/scripts/state-write-path-drift-baseline.json +0 -19
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+ // hooks/lib/git-probe.js — classify a bounded spawnSync(git, ...) result,
3
+ // distinguishing a GENUINE negative answer (git ran and said "no") from
4
+ // "could not determine" (timeout / spawn failure / signal kill) — #3911.
5
+ //
6
+ // Proven defect: hooks/gsd-worktree-path-guard.js, hooks/gsd-workflow-guard.js,
7
+ // and hooks/gsd-windsurf-pre-write.js each treat spawnSync(git, ..., {timeout})
8
+ // returning a non-zero/empty result as "not applicable" and allow silently.
9
+ // A macOS CI run showed three deny cases land at 2084ms/2112ms/2177ms — just
10
+ // past the 2000ms budget — each returning exit 0 with EMPTY stdout AND EMPTY
11
+ // stderr: a timeout is INDISTINGUISHABLE, at the call site, from git cleanly
12
+ // answering "not a repo" / "not this worktree" unless the spawnSync() result
13
+ // itself is inspected for `error`/`signal`, not just `status`/`stdout`.
14
+ //
15
+ // This module does not change any hook's decision or exit code — it only
16
+ // answers "was this probe's answer real?" so a caller can emit a diagnostic
17
+ // on the undetermined branch while keeping the existing fail-open behavior.
18
+ //
19
+ // Classification mirrors tests/helpers/process-seam.cjs's OUTCOME vocabulary
20
+ // (EXITED vs TIMED_OUT/SPAWN_FAILED/KILLED) — same evidence-over-classification
21
+ // ordering (a populated `status` outranks an attached `error`, since Node can
22
+ // report `error.code === 'ETIMEDOUT'` on a result that also carried a real
23
+ // exit that beat the timer) — so a probe here and a subprocess outcome in the
24
+ // test suite never disagree on the same raw spawnSync() shape.
25
+
26
+ /**
27
+ * @param {{error?: (Error & {code?: string})|null, status: number|null, signal: string|null}} result
28
+ * the raw return value of `spawnSync('git', ...)`.
29
+ * @returns {{determined: true} | {determined: false, reason: string}}
30
+ */
31
+ function classifyGitProbe(result) {
32
+ const error = result && result.error;
33
+ const status = result ? result.status : null;
34
+ const signal = result ? result.signal : null;
35
+
36
+ if (!error && signal === null) {
37
+ // A real exit — status 0 or non-zero is a genuine, trustworthy answer.
38
+ return { determined: true };
39
+ }
40
+ if (status !== null) {
41
+ // `status` is only ever populated by a real exit; it outranks an attached
42
+ // `error` even at the exact timeout boundary (process-seam.cjs's own
43
+ // "evidence over classification" rule).
44
+ return { determined: true };
45
+ }
46
+
47
+ const code = error ? (error.code ?? null) : null;
48
+ let reason;
49
+ if (code === 'ETIMEDOUT' || signal !== null) {
50
+ reason = signal ? `timed out (signal ${signal})` : 'timed out';
51
+ } else if (code) {
52
+ reason = `spawn failed (${code})`;
53
+ } else {
54
+ reason = 'could not run (unknown reason)';
55
+ }
56
+ return { determined: false, reason };
57
+ }
58
+
59
+ /**
60
+ * If `result` could not be determined (timeout / spawn failure / signal
61
+ * kill), write a stderr diagnostic naming the probe and why — never changes
62
+ * any exit code, never throws. A no-op when the probe genuinely ran and
63
+ * answered (status 0 or non-zero with no error/signal).
64
+ *
65
+ * @param {string} hookName - e.g. 'gsd-worktree-path-guard'
66
+ * @param {string} probeLabel - e.g. "git rev-parse --show-toplevel"
67
+ * @param {*} result - the raw spawnSync() return value
68
+ */
69
+ function reportIfUndetermined(hookName, probeLabel, result) {
70
+ const classification = classifyGitProbe(result);
71
+ if (classification.determined) return;
72
+ try {
73
+ process.stderr.write(
74
+ `${hookName}: git probe '${probeLabel}' ${classification.reason} — ` +
75
+ `allowing this call because the probe's answer is unknown, not because ` +
76
+ `git answered "no" (#3911).\n`
77
+ );
78
+ } catch {
79
+ // A stderr write failing (EPIPE, closed fd) must never itself change the
80
+ // hook's fail-open outcome.
81
+ }
82
+ }
83
+
84
+ module.exports = { classifyGitProbe, reportIfUndetermined };
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+ // hooks/lib/hook-exit.js — hand-written, NOT generated. A tiny hook-facing
3
+ // vocabulary layered over hooks/lib/cli-exit.js's terminateNow (ADR-3889 §3),
4
+ // for the 19 enforcement hooks P7/#3911 is migrating off raw process.exit().
5
+ //
6
+ // WHY onCrash is a REQUIRED argument, with no default (this is the entire
7
+ // point of this module): every hook audited for #3911 ended its outer
8
+ // try/catch in ONE of two ways — some `process.exit(0)` (fail open: a hook
9
+ // error must never block a legitimate tool call), others `process.exit(2)`
10
+ // (fail closed: a hook whose whole job is a security/safety denial must not
11
+ // let its own bug wave the denial through). Both are legitimate policies for
12
+ // DIFFERENT hooks — but neither is the "obviously correct" one, and a shared
13
+ // helper that silently defaulted to either would let a future hook inherit
14
+ // the wrong policy by omission, which is exactly the "nothing fails with
15
+ // success" defect ADR-3889 exists to close. Requiring the caller to name its
16
+ // policy at every call site — not just once at module load — makes "I forgot
17
+ // to decide" a load-time/call-time crash instead of a silent behavior. A
18
+ // caller that truly wants "allow on crash" must still write ALLOW; there is
19
+ // no path that reaches terminateNow without a declared choice.
20
+ //
21
+ // crash() itself is total: it does not throw on a bad `onCrash`, because
22
+ // throwing would unwind into the CALLER's own outer catch — the exact
23
+ // fail-open-by-accident hazard this module removes. Instead an unrecognized
24
+ // policy terminates via terminateNow('INTERNAL', ...) with a diagnostic
25
+ // payload naming the offending value, so a typo'd/misspelled policy is
26
+ // debuggable on stderr rather than silently reinterpreted as ALLOW or DENY.
27
+ const { terminateNow } = require('./cli-exit.js');
28
+
29
+ /** Frozen enum of the two declarable crash policies. No third option. */
30
+ const HOOK_ON_CRASH = Object.freeze({
31
+ ALLOW: 'allow',
32
+ DENY: 'deny',
33
+ });
34
+
35
+ /** Terminate with the hook-protocol PASS outcome (exit 0). */
36
+ function allow(payload) {
37
+ terminateNow('PASS', payload);
38
+ }
39
+
40
+ /**
41
+ * Terminate with the hook-protocol deny outcome (exit 2, stdout+stderr).
42
+ *
43
+ * @param {*} payload - JSON-serializable value written to fd 1 (and, when
44
+ * `stderrPayload` is omitted, fd 2 too).
45
+ * @param {*} [stderrPayload] - optional distinct value for fd 2, forwarded
46
+ * verbatim to terminateNow's own `stderrPayload` param — a string is
47
+ * written raw, anything else is JSON-stringified.
48
+ */
49
+ function deny(payload, stderrPayload) {
50
+ terminateNow('HOOK_DENY', payload, stderrPayload);
51
+ }
52
+
53
+ /**
54
+ * Dispatch to allow()/deny() per a DECLARED crash policy. `onCrash` is
55
+ * required — see the module header for why there is no default.
56
+ *
57
+ * @param {'allow'|'deny'} onCrash - one of HOOK_ON_CRASH's two values.
58
+ * @param {*} payload - JSON-serializable value forwarded to terminateNow.
59
+ */
60
+ function crash(onCrash, payload) {
61
+ if (onCrash === HOOK_ON_CRASH.ALLOW) {
62
+ allow(payload);
63
+ return;
64
+ }
65
+ if (onCrash === HOOK_ON_CRASH.DENY) {
66
+ deny(payload);
67
+ return;
68
+ }
69
+ // Never guess a policy and never throw (a throw would unwind into the
70
+ // caller's own outer catch — the fail-open-by-accident defect this module
71
+ // exists to remove). Terminate deterministically with INTERNAL, naming the
72
+ // offending value so a misspelled/omitted policy is diagnosable.
73
+ terminateNow('INTERNAL', {
74
+ error: 'hook-exit: crash() called with an unrecognized onCrash policy',
75
+ receivedOnCrash: onCrash,
76
+ expectedOneOf: [HOOK_ON_CRASH.ALLOW, HOOK_ON_CRASH.DENY],
77
+ originalPayload: payload,
78
+ });
79
+ }
80
+
81
+ module.exports = { HOOK_ON_CRASH, allow, deny, crash };
@@ -29,6 +29,9 @@ const MANAGED_HOOKS = [
29
29
  'gsd-cursor-subagent-stop.js',
30
30
  'gsd-ensure-canonical-path.js',
31
31
  'gsd-graphify-update.sh',
32
+ // #3662: portable node resolver (helper staged in hooks/; managed JS hook
33
+ // commands route through it under --portable-hooks).
34
+ 'gsd-node-runner.sh',
32
35
  'gsd-phase-boundary.sh',
33
36
  'gsd-prompt-guard.js',
34
37
  'gsd-read-guard.js',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/gsd-core",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
5
5
  "main": ".opencode/plugins/gsd-core.js",
6
6
  "bin": {
@@ -66,6 +66,7 @@
66
66
  "devDependencies": {
67
67
  "@eslint/js": "^9.39.4",
68
68
  "@stryker-mutator/core": "^9.6.1",
69
+ "@stryker-mutator/tap-runner": "^9.6.1",
69
70
  "@types/node": "^22.19.19",
70
71
  "c8": "^11.0.0",
71
72
  "eslint": "^9.39.4",
@@ -75,6 +76,7 @@
75
76
  "fast-check": "^4.8.0",
76
77
  "globals": "^16.5.0",
77
78
  "js-yaml": "^4.3.1",
79
+ "mutation-testing-metrics": "^3.7.3",
78
80
  "re2js": "^2.8.6",
79
81
  "typescript": "^6.0.3",
80
82
  "typescript-eslint": "^8.60.0"
@@ -106,7 +108,7 @@
106
108
  "gen:registry": "node scripts/gen-registry.cjs --write",
107
109
  "gen:install-tree": "node scripts/gen-install-tree-fixtures.cjs",
108
110
  "gen:section-manifest": "node scripts/gen-section-manifest.cjs --write",
109
- "regen:derived": "npm run build && npm run gen:registry && node scripts/gen-adr-index.cjs --write && node scripts/gen-capability-matrix.cjs --write && node scripts/gen-inventory-manifest.cjs --write && node scripts/gen-context-index.cjs --write && npm run gen:section-manifest && node scripts/sync-manifest-versions.cjs && npm run gen:install-tree",
111
+ "regen:derived": "npm run build && npm run gen:registry && node scripts/gen-adr-index.cjs --write && node scripts/gen-features.cjs --write && node scripts/gen-capability-matrix.cjs --write && node scripts/gen-inventory-manifest.cjs --write && node scripts/gen-context-index.cjs --write && node scripts/gen-state-md-docs.cjs --write && npm run gen:section-manifest && node scripts/sync-manifest-versions.cjs && npm run gen:install-tree && node scripts/gen-scripts-cli-exit.cjs --write && node scripts/gen-hooks-cli-exit.cjs --write && node scripts/gen-exit-code-registry.cjs --write && node scripts/gen-exit-code-docs.cjs --write",
110
112
  "validate:registry": "node scripts/validate-registry.cjs",
111
113
  "prepack": "npm run build:lib",
112
114
  "prepare": "npm run build:lib",
@@ -119,7 +121,7 @@
119
121
  "lint:table-schema-drift": "node scripts/lint-table-schema-drift.cjs",
120
122
  "lint:frontmatter-scalar-broad-grep": "node scripts/lint-frontmatter-scalar-broad-grep.cjs",
121
123
  "lint:removed-but-needed": "node scripts/lint-removed-but-needed.cjs",
122
- "lint:ci": "npm run lint && npm run lint:skill-deps && npm run lint:generated-sync && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-allow-test-rule-refs.cjs && node scripts/lint-resolution-provenance.cjs && node scripts/lint-emitted-drift-ack.cjs && node scripts/lint-portable-timeout.cjs && node scripts/validate-registry.cjs && node scripts/lint-table-schema-drift.cjs && node scripts/lint-fix-has-regression-test.cjs && node scripts/lint-example-parser-parity.cjs && node scripts/lint-docs-command-form.cjs && node scripts/lint-plan-count-drift.cjs && node scripts/lint-milestone-window-drift.cjs && node scripts/lint-phase-enumeration-drift.cjs && node scripts/lint-planning-prompt-drift.cjs && node scripts/lint-unreachable-guard-drift.cjs && node scripts/lint-completion-ratio-drift.cjs && node scripts/lint-state-field-drift.cjs && node scripts/lint-state-write-path-drift.cjs && node scripts/lint-completion-predicate-drift.cjs && node scripts/lint-planning-snapshot-bypass-drift.cjs && node scripts/lint-health-diagnostic-rule-table.cjs && node scripts/lint-planning-artifact-writer-drift.cjs && node scripts/lint-frontmatter-scalar-broad-grep.cjs && node scripts/lint-removed-but-needed.cjs && node scripts/lint-no-adhoc-regex-escape.cjs && node scripts/lint-vendored-deps.cjs && npm run lint:hooks-runtime-build-seam && node scripts/check-contract-drift.cjs",
124
+ "lint:ci": "npm run lint && npm run lint:skill-deps && npm run lint:generated-sync && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-allow-test-rule-refs.cjs && node scripts/lint-resolution-provenance.cjs && node scripts/lint-portable-timeout.cjs && node scripts/validate-registry.cjs && node scripts/lint-table-schema-drift.cjs && node scripts/lint-fix-has-regression-tests.cjs && node scripts/lint-example-parser-parity.cjs && node scripts/lint-docs-command-form.cjs && node scripts/lint-plan-count-drift.cjs && node scripts/lint-milestone-window-drift.cjs && node scripts/lint-phase-enumeration-drift.cjs && node scripts/lint-planning-prompt-drift.cjs && node scripts/lint-unreachable-guard-drift.cjs && node scripts/lint-completion-ratio-drift.cjs && node scripts/lint-slug-derivation-drift.cjs && node scripts/lint-state-field-drift.cjs && node scripts/lint-state-write-path-drift.cjs && node scripts/lint-completion-predicate-drift.cjs && node scripts/lint-planning-snapshot-bypass-drift.cjs && node scripts/lint-health-diagnostic-rule-table.cjs && node scripts/lint-planning-artifact-writer-drift.cjs && node scripts/lint-frontmatter-scalar-broad-grep.cjs && node scripts/lint-removed-but-needed.cjs && node scripts/lint-no-adhoc-regex-escape.cjs && node scripts/lint-vendored-deps.cjs && node scripts/lint-docs-guard-registration.cjs && node scripts/lint-source-test-name-collision.cjs && npm run lint:hooks-runtime-build-seam && node scripts/check-contract-drift.cjs && node scripts/lint-mutation-test-derivation-drift.cjs && node scripts/lint-seam-enforcement.cjs",
123
125
  "lint:allow-test-rule-refs": "node scripts/lint-allow-test-rule-refs.cjs",
124
126
  "lint:regression-names": "node scripts/lint-regression-test-names.cjs",
125
127
  "lint:descriptions": "node scripts/lint-descriptions.cjs",
@@ -127,10 +129,11 @@
127
129
  "lint:test-file-count": "node scripts/lint-test-file-count.cjs",
128
130
  "lint:pr-checks": "node scripts/lint-pr-check-project-dir.cjs",
129
131
  "lint:changeset": "node scripts/changeset/lint.cjs",
130
- "lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check && node scripts/gen-adr-index.cjs --check && node scripts/check-glossary-refs.cjs --check && node scripts/lint-compiled-artifact-sync.cjs --check && node scripts/gen-context-index.cjs --check && node scripts/gen-section-manifest.cjs --check && node scripts/gen-health-docs.cjs --check",
132
+ "lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check && node scripts/gen-adr-index.cjs --check && node scripts/gen-features.cjs --check && node scripts/check-glossary-refs.cjs --check && node scripts/lint-compiled-artifact-sync.cjs --check && node scripts/gen-context-index.cjs --check && node scripts/gen-section-manifest.cjs --check && node scripts/gen-health-docs.cjs --check && node scripts/gen-state-md-docs.cjs --check && node scripts/gen-scripts-cli-exit.cjs --check && node scripts/gen-hooks-cli-exit.cjs --check && node scripts/gen-exit-code-registry.cjs --check && node scripts/gen-exit-code-docs.cjs --check",
131
133
  "lint:docs": "node scripts/lint-docs-required.cjs",
132
134
  "lint:qa-smells": "node scripts/qa-smell-ratchet.cjs",
133
135
  "lint:legacy-name": "node scripts/lint-legacy-dir-name.cjs",
136
+ "lint:seam-enforcement": "node scripts/lint-seam-enforcement.cjs",
134
137
  "lint:docs-command-form": "node scripts/lint-docs-command-form.cjs",
135
138
  "lint:hooks-runtime-build-seam": "node scripts/lint-hooks-runtime-build-seam.cjs",
136
139
  "ci:test-scope": "node scripts/ci-test-scope.cjs",
@@ -146,12 +149,14 @@
146
149
  "test:affected": "node scripts/run-affected-tests.cjs",
147
150
  "test:coverage": "c8 --check-coverage --lines 70 --branches 60 --reporter text --include 'gsd-core/bin/lib/**/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs",
148
151
  "test:coverage:scripts-floor": "c8 check-coverage --lines 55 --include 'scripts/**/*.cjs' --exclude 'tests/**' --all",
149
- "test:coverage:unit": "c8 --reporter text --reporter json-summary --include 'gsd-core/bin/lib/**/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs --suite unit && node scripts/check-coverage-gate.cjs",
152
+ "test:coverage:unit": "c8 --reporter text --reporter json-summary --merge-async --include 'gsd-core/bin/lib/**/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs --suite unit && node scripts/check-coverage-gate.cjs",
150
153
  "test:coverage:unit:raw": "c8 --reporter none node scripts/run-tests.cjs --suite unit",
151
- "test:coverage:report": "c8 report --reporter text --reporter json-summary --include 'gsd-core/bin/lib/**/*.cjs' --exclude 'tests/**' --all && node scripts/check-coverage-gate.cjs",
154
+ "test:coverage:report": "c8 report --reporter text --reporter json-summary --merge-async --include 'gsd-core/bin/lib/**/*.cjs' --exclude 'tests/**' --all && node scripts/check-coverage-gate.cjs",
152
155
  "test:coverage:all": "npm run test:coverage",
153
156
  "test:mutation": "stryker run",
154
- "test:mutation:since": "stryker run --incremental --since origin/next"
157
+ "test:mutation:since": "stryker run --incremental --since origin/next",
158
+ "gen:features": "node scripts/gen-features.cjs",
159
+ "gen:state-md-docs": "node scripts/gen-state-md-docs.cjs"
155
160
  },
156
161
  "allowScripts": {
157
162
  "fallow@2.70.0": true
@@ -9,12 +9,29 @@
9
9
  # scripts/base64-scan.sh --file path/to/file # Scan a single file
10
10
  # scripts/base64-scan.sh --dir agents/ # Scan all files in a directory
11
11
  #
12
- # Exit codes:
13
- # 0 = clean
14
- # 1 = findings detected
15
- # 2 = usage error
12
+ # Exit codes (ADR-3889, #3908 — registered in gsd-core/bin/shared/exit-codes.json):
13
+ # 0 = clean
14
+ # 1 = findings detected
15
+ # $EXIT_USAGE (64) = usage error (bad argv, missing --file/--dir target)
16
+ # $EXIT_NO_INPUT (66) = ran; scope established; zero files in scope (genuinely empty)
17
+ # $EXIT_UNAVAILABLE (69) = could not establish scope (bad ref, not a repo, unreadable dir)
16
18
  set -euo pipefail
17
19
 
20
+ # ─── Exit-code registry (ADR-3889, #3908) ────────────────────────────────────
21
+ # Resolved relative to THIS script's location, not the caller's cwd. Loud,
22
+ # non-zero failure if the fragment is missing — never fall back to a guessed
23
+ # literal integer, and never let a missing registry silently degrade to
24
+ # exit 0.
25
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
+ EXIT_CODES_SH="$SCRIPT_DIR/../gsd-core/bin/shared/exit-codes.sh"
27
+ if [[ ! -f "$EXIT_CODES_SH" ]]; then
28
+ echo "base64-scan: FATAL: exit-code registry not found at $EXIT_CODES_SH" >&2
29
+ echo " Regenerate with: node scripts/gen-exit-code-registry.cjs --write" >&2
30
+ exit 1
31
+ fi
32
+ # shellcheck disable=SC1090
33
+ . "$EXIT_CODES_SH"
34
+
18
35
  # ── Locale hardening (#116) ───────────────────────────────────────────────────
19
36
  # BSD tr (macOS) treats input bytes as multi-byte characters under any UTF-8
20
37
  # locale. When the input to `tr -cd '[:print:]'` contains bytes that are not
@@ -180,7 +197,31 @@ collect_files() {
180
197
  case "$mode" in
181
198
  --diff)
182
199
  local base="${1:-origin/main}"
183
- git diff --name-only --diff-filter=ACMR "$base"...HEAD 2>/dev/null \
200
+ # Run git separately from the filter pipe so its OWN exit status (not
201
+ # grep's) decides whether the diff could be established. stdout and
202
+ # stderr are captured SEPARATELY (never merged with `2>&1`) so that a
203
+ # warning git writes to stderr on an otherwise successful diff can
204
+ # never be mistaken for a filename in the file list. On failure the
205
+ # captured stderr is emitted as the diagnostic; on success it is
206
+ # forwarded as a warning, never folded into the file list.
207
+ # `|| true` on the filter below is CORRECT (not gratuitous): `grep -v`
208
+ # exits 1 when every line is filtered out (e.g. an all-images diff),
209
+ # which is a legitimate empty result, not a failure to run.
210
+ local raw status err_file
211
+ err_file=$(mktemp)
212
+ raw=$(git diff --name-only --diff-filter=ACMR "$base"...HEAD 2>"$err_file")
213
+ status=$?
214
+ if (( status != 0 )); then
215
+ cat "$err_file" >&2
216
+ rm -f "$err_file"
217
+ exit "$EXIT_UNAVAILABLE"
218
+ fi
219
+ if [[ -s "$err_file" ]]; then
220
+ echo "Warning: git diff emitted stderr output:" >&2
221
+ cat "$err_file" >&2
222
+ fi
223
+ rm -f "$err_file"
224
+ printf '%s\n' "$raw" \
184
225
  | grep -vE '\.(png|jpg|jpeg|gif|ico|woff|woff2|ttf|eot|otf|zip|tar|gz|pdf)$' || true
185
226
  ;;
186
227
  --file)
@@ -188,24 +229,41 @@ collect_files() {
188
229
  echo "$1"
189
230
  else
190
231
  echo "Error: file not found: $1" >&2
191
- exit 2
232
+ exit "$EXIT_USAGE"
192
233
  fi
193
234
  ;;
194
235
  --dir)
195
236
  local dir="$1"
196
237
  if [[ ! -d "$dir" ]]; then
197
238
  echo "Error: directory not found: $dir" >&2
198
- exit 2
239
+ exit "$EXIT_USAGE"
240
+ fi
241
+ # Same treatment as --diff: a `find` that fails (e.g. permission
242
+ # denied) must not be reported as an empty directory, and stdout/stderr
243
+ # are captured separately so a stderr warning never enters the file list.
244
+ local raw status err_file
245
+ err_file=$(mktemp)
246
+ raw=$(find "$dir" -type f ! -path '*/node_modules/*' ! -path '*/.git/*' ! -path '*/dist/*' \
247
+ ! -name '*.png' ! -name '*.jpg' ! -name '*.gif' ! -name '*.woff*' 2>"$err_file")
248
+ status=$?
249
+ if (( status != 0 )); then
250
+ cat "$err_file" >&2
251
+ rm -f "$err_file"
252
+ exit "$EXIT_UNAVAILABLE"
253
+ fi
254
+ if [[ -s "$err_file" ]]; then
255
+ echo "Warning: find emitted stderr output:" >&2
256
+ cat "$err_file" >&2
199
257
  fi
200
- find "$dir" -type f ! -path '*/node_modules/*' ! -path '*/.git/*' ! -path '*/dist/*' \
201
- ! -name '*.png' ! -name '*.jpg' ! -name '*.gif' ! -name '*.woff*' 2>/dev/null || true
258
+ rm -f "$err_file"
259
+ printf '%s\n' "$raw"
202
260
  ;;
203
261
  --stdin)
204
262
  cat
205
263
  ;;
206
264
  *)
207
265
  echo "Usage: $0 --diff [base] | --file <path> | --dir <path> | --stdin" >&2
208
- exit 2
266
+ exit "$EXIT_USAGE"
209
267
  ;;
210
268
  esac
211
269
  }
@@ -309,7 +367,7 @@ extract_and_check_blobs() {
309
367
  main() {
310
368
  if [[ $# -eq 0 ]]; then
311
369
  echo "Usage: $0 --diff [base] | --file <path> | --dir <path>" >&2
312
- exit 2
370
+ exit "$EXIT_USAGE"
313
371
  fi
314
372
 
315
373
  load_ignorelist
@@ -321,8 +379,12 @@ main() {
321
379
  files=$(collect_files "$mode" "$@")
322
380
 
323
381
  if [[ -z "$files" ]]; then
382
+ # collect_files already exited (UNAVAILABLE/USAGE) for anything that
383
+ # could not establish scope. Reaching here with an empty result means
384
+ # scope WAS established and is genuinely empty (e.g. an all-images
385
+ # diff) — that is NO_INPUT, not a silent clean pass.
324
386
  echo "base64-scan: no files to scan"
325
- exit 0
387
+ exit "$EXIT_NO_INPUT"
326
388
  fi
327
389
 
328
390
  local total=0
@@ -71,6 +71,11 @@ const HOOKS_TO_COPY = [
71
71
  'gsd-session-state.sh',
72
72
  'gsd-validate-commit.sh',
73
73
  'gsd-phase-boundary.sh',
74
+ // Portable node resolver (#3662). Not a registered hook itself: managed JS
75
+ // hook commands under --portable-hooks route through it (bash <resolver>
76
+ // <baked-node> <script>) so node resolves at hook-fire time in every
77
+ // environment sharing the config root. Staged verbatim — no templating.
78
+ 'gsd-node-runner.sh',
74
79
  // Graphify auto-update hook (#3347 / PR #3557 / #3579). Opt-in via
75
80
  // .planning/config.json graphify.auto_update; off by default.
76
81
  'gsd-graphify-update.sh'
@@ -65,7 +65,7 @@ const TRACKED_PREFIXES = [
65
65
  const TRACKED_EXACT = new Set(['bin/install.js', 'package.json']);
66
66
 
67
67
  /**
68
- * Paths CONTEXT.md documents whose ABSENCE is the healthy steady state (#2778).
68
+ * Paths CONTEXT.md documents whose ABSENCE is the healthy steady state.
69
69
  *
70
70
  * `TRACKED_PREFIXES` skips claims this gate *cannot* check. This is the narrower
71
71
  * third case: a claim it must not check, because "does not exist" is the correct
@@ -78,14 +78,43 @@ const TRACKED_EXACT = new Set(['bin/install.js', 'package.json']);
78
78
  * shipping an empty stub. So the file is absent on a healthy `next` and present
79
79
  * only inside a PR that needs it, and asserting either way is wrong.
80
80
  *
81
- * Until #2778 this passed only by accident: CONTEXT.md's `RULESET.` entries are
82
- * themselves backtick-wrapped and contain backticks, so the sequential pairing in
83
- * `extractTrackedRefs` happened to leave this token outside a code span. Any edit
84
- * that shifted the parity — such as #2778's own — exposed it. A gate that passes
85
- * by luck is not passing; naming the exemption makes the intent explicit and
86
- * survives the next edit.
81
+ * The emitted-attribution family (`tests/fixtures/golden-install-parity`,
82
+ * `tests/golden-install-parity.test.cjs`, `scripts/gen-golden-install-parity-zcode.cjs`,
83
+ * `tests/agent-size-baseline.json`, `tests/workflow-size-baseline.json`,
84
+ * `scripts/git-merge-regen-driver.cjs`, `scripts/update-size-baseline.cjs`) was RETIRED
85
+ * by the #2724 cutover — the differential attribution check replaced the committed
86
+ * baselines and their generator/bridge tooling. CONTEXT.md's RULESET.EMITTED_ATTRIBUTION
87
+ * predicate documents that retirement ("Historically …"), so the mentions are history,
88
+ * not live claims, and their absence is exactly the healthy state the retirement
89
+ * produced. The whole documented family is listed, not just the members today's tick
90
+ * parity happens to hide — the two tooling paths were invisible only because of where
91
+ * line 585's inner backticks sat, which is the #2778 luck this gate must not rely on.
92
+ *
93
+ * `scripts/eslint-rules` appears only inside a CONTRASTIVE mention ("the local
94
+ * plugin lives at `eslint-rules/` (repo root, NOT `scripts/eslint-rules/`)") — the
95
+ * predicate asserts where the directory is NOT, so non-existence is the claim
96
+ * being made, not drift away from one.
97
+ *
98
+ * Until #2778 this set's first entry passed only by accident: CONTEXT.md's
99
+ * `RULESET.` entries are themselves backtick-wrapped and contain backticks, so the
100
+ * sequential pairing in `extractTrackedRefs` happened to leave this token outside a
101
+ * code span. Any edit that shifted the parity — such as #2778's own — exposed it.
102
+ * A gate that passes by luck is not passing; naming the exemption makes the intent
103
+ * explicit and survives the next edit. #3604 removed the luck itself (pairing is
104
+ * per line), which is what surfaced the entries above: each names a path whose
105
+ * absence is deliberate, so each is exempted by name for the same reason.
87
106
  */
88
- const INTENTIONALLY_ABSENT = new Set(['tests/emitted-drift-ack.json']);
107
+ const INTENTIONALLY_ABSENT = new Set([
108
+ 'tests/emitted-drift-ack.json',
109
+ 'tests/fixtures/golden-install-parity',
110
+ 'tests/golden-install-parity.test.cjs',
111
+ 'scripts/gen-golden-install-parity-zcode.cjs',
112
+ 'tests/agent-size-baseline.json',
113
+ 'tests/workflow-size-baseline.json',
114
+ 'scripts/git-merge-regen-driver.cjs',
115
+ 'scripts/update-size-baseline.cjs',
116
+ 'scripts/eslint-rules',
117
+ ]);
89
118
 
90
119
  /**
91
120
  * Shape a backticked token must have to even be considered a path candidate:
@@ -119,18 +148,51 @@ function isWithinRoot(token) {
119
148
  /**
120
149
  * Every distinct, trackable file-path token referenced in `text`, with any
121
150
  * trailing `:<line>` suffix stripped.
151
+ *
152
+ * #3604: pairing is per LINE, not over the whole text. A single whole-text pass
153
+ * `[^`]+` crosses newlines, so one odd-backtick line (the `RULESET.*` predicate
154
+ * format is backtick-wrapped and its values sometimes contain backticks) shifted
155
+ * the pairing of every later line — a tracked token's visibility depended on
156
+ * where it sat, which is how a renamed test file stayed invisible for weeks.
157
+ *
158
+ * The fact-store predicate lines (one `CLASS.subkey=value` fact per line,
159
+ * backtick-wrapped as a whole) carry REAL paths in their values; the span itself
160
+ * is not path-shaped (spaces, `=`), so a second pass harvests path-shaped tracked
161
+ * tokens from inside any predicate-shaped span rather than letting the outer
162
+ * wrapper hide them.
163
+ *
164
+ * Fragment guards: a real path in this repo never ends in `-` or `.` — those are
165
+ * remnants of glob/template mentions (`scripts/gen-*.cjs`, `tests/foo.*.test.cjs`)
166
+ * split at the `*` — and `NNNN` is the ADR filename template token
167
+ * (CONTRIBUTING's "Do not compute a next number locally"), never a real path.
122
168
  */
123
169
  function extractTrackedRefs(text) {
124
170
  const tokens = new Set();
125
- const re = /`([^`]+)`/g;
126
- let m;
127
- while ((m = re.exec(text)) !== null) {
128
- const raw = m[1];
129
- if (!PATH_TOKEN_RE.test(raw)) continue;
171
+ const add = (raw) => {
172
+ if (!PATH_TOKEN_RE.test(raw)) return;
130
173
  const token = raw.replace(/:\d+$/, '');
131
- if (!isTracked(token)) continue;
132
- if (!isWithinRoot(token)) continue;
174
+ // Fragment guard, on the EMITTED token (after the :line strip, so
175
+ // `src/foo-:12` is judged on `src/foo-`): glob/template remnants end in `-`
176
+ // or `.`, a real path in this repo never does.
177
+ if (!/[A-Za-z0-9_]$/.test(token)) return;
178
+ if (token.includes('NNNN')) return;
179
+ if (!isTracked(token)) return;
180
+ if (!isWithinRoot(token)) return;
133
181
  tokens.add(token);
182
+ };
183
+ const subTokenRe = /[\w.-]+(?:\/[\w.-]+)*/g;
184
+ for (const line of text.split(/\r?\n/)) {
185
+ const re = /`([^`]+)`/g;
186
+ let m;
187
+ while ((m = re.exec(line)) !== null) {
188
+ const span = m[1];
189
+ if (PATH_TOKEN_RE.test(span)) {
190
+ add(span);
191
+ continue;
192
+ }
193
+ if (!/^[A-Z][A-Za-z0-9_.-]*=/.test(span)) continue;
194
+ for (const sub of span.matchAll(subTokenRe)) add(sub[0]);
195
+ }
134
196
  }
135
197
  return tokens;
136
198
  }