@opengsd/gsd-core 1.11.0 → 1.13.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 (498) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +12 -0
  4. package/agents/gsd-code-fixer.md +1 -1
  5. package/agents/gsd-debug-session-manager.md +1 -1
  6. package/agents/gsd-debugger.md +1 -1
  7. package/agents/gsd-dom-verifier.md +169 -0
  8. package/agents/gsd-eval-auditor.md +1 -1
  9. package/agents/gsd-executor.md +78 -42
  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 +0 -1
  13. package/agents/gsd-pattern-mapper.md +11 -0
  14. package/agents/gsd-phase-researcher.md +3 -1
  15. package/agents/gsd-plan-checker.md +91 -112
  16. package/agents/gsd-planner.md +20 -4
  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 +82 -7
  21. package/agents/gsd-ui-researcher.md +70 -3
  22. package/agents/gsd-verifier.md +24 -2
  23. package/bin/install.js +847 -200
  24. package/commands/gsd/discuss-phase.md +1 -1
  25. package/commands/gsd/execute-phase.md +1 -1
  26. package/commands/gsd/import.md +1 -1
  27. package/commands/gsd/ns-workflow.md +2 -1
  28. package/commands/gsd/phase.md +1 -1
  29. package/commands/gsd/quick-batch.md +105 -0
  30. package/commands/gsd/quick.md +8 -4
  31. package/commands/gsd/surface.md +18 -8
  32. package/gsd-core/bin/gsd-tools.cjs +761 -100
  33. package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
  34. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  35. package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
  36. package/gsd-core/bin/lib/api-coverage.cjs +30 -9
  37. package/gsd-core/bin/lib/artifacts.cjs +2 -0
  38. package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
  39. package/gsd-core/bin/lib/audit.cjs +163 -41
  40. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  41. package/gsd-core/bin/lib/capability-activation.cjs +27 -0
  42. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  43. package/gsd-core/bin/lib/capability-registry.cjs +785 -144
  44. package/gsd-core/bin/lib/capability-state.cjs +25 -4
  45. package/gsd-core/bin/lib/capability-validator.cjs +321 -18
  46. package/gsd-core/bin/lib/capability-writer.cjs +14 -4
  47. package/gsd-core/bin/lib/check-command-router.cjs +229 -6
  48. package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
  49. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  50. package/gsd-core/bin/lib/clusters.cjs +1 -0
  51. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  52. package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
  53. package/gsd-core/bin/lib/command-aliases.cjs +16 -0
  54. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  55. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  56. package/gsd-core/bin/lib/commands.cjs +877 -54
  57. package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
  58. package/gsd-core/bin/lib/config-loader.cjs +121 -29
  59. package/gsd-core/bin/lib/config.cjs +92 -2
  60. package/gsd-core/bin/lib/configuration.cjs +129 -37
  61. package/gsd-core/bin/lib/core-utils.cjs +118 -14
  62. package/gsd-core/bin/lib/decisions.cjs +213 -1
  63. package/gsd-core/bin/lib/edge-probe.cjs +23 -2
  64. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  65. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  66. package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
  67. package/gsd-core/bin/lib/frontmatter.cjs +975 -326
  68. package/gsd-core/bin/lib/gap-checker.cjs +41 -8
  69. package/gsd-core/bin/lib/git-base-branch.cjs +182 -39
  70. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
  71. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
  72. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +60 -14
  73. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
  74. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
  75. package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
  76. package/gsd-core/bin/lib/host-integration.cjs +96 -11
  77. package/gsd-core/bin/lib/init-command-router.cjs +132 -21
  78. package/gsd-core/bin/lib/init.cjs +252 -56
  79. package/gsd-core/bin/lib/install-engine.cjs +252 -15
  80. package/gsd-core/bin/lib/install-model-override-resolver.cjs +78 -1
  81. package/gsd-core/bin/lib/install-profiles.cjs +100 -18
  82. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  83. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  84. package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
  85. package/gsd-core/bin/lib/intel.cjs +101 -26
  86. package/gsd-core/bin/lib/io.cjs +195 -15
  87. package/gsd-core/bin/lib/learnings.cjs +85 -14
  88. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  89. package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
  90. package/gsd-core/bin/lib/markdown-table.cjs +175 -4
  91. package/gsd-core/bin/lib/milestone.cjs +112 -7
  92. package/gsd-core/bin/lib/model-catalog.cjs +177 -19
  93. package/gsd-core/bin/lib/model-resolver.cjs +10 -28
  94. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  95. package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
  96. package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
  97. package/gsd-core/bin/lib/phase-id.cjs +321 -13
  98. package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
  99. package/gsd-core/bin/lib/phase-locator.cjs +138 -17
  100. package/gsd-core/bin/lib/phase.cjs +1175 -115
  101. package/gsd-core/bin/lib/plan-document.cjs +273 -0
  102. package/gsd-core/bin/lib/plan-scan.cjs +13 -2
  103. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  104. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  105. package/gsd-core/bin/lib/planning-snapshot.cjs +165 -34
  106. package/gsd-core/bin/lib/planning-workspace.cjs +159 -28
  107. package/gsd-core/bin/lib/probe-core.cjs +4 -1
  108. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  109. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  110. package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
  111. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
  112. package/gsd-core/bin/lib/quick-batch.cjs +840 -0
  113. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  114. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
  115. package/gsd-core/bin/lib/review-lane-descriptor.cjs +62 -14
  116. package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
  117. package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
  118. package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
  119. package/gsd-core/bin/lib/roadmap-parser.cjs +577 -41
  120. package/gsd-core/bin/lib/roadmap.cjs +248 -64
  121. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +329 -41
  122. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
  123. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +320 -109
  124. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +487 -83
  125. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  126. package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
  127. package/gsd-core/bin/lib/shell-command-projection.cjs +75 -8
  128. package/gsd-core/bin/lib/smart-entry.cjs +19 -31
  129. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  130. package/gsd-core/bin/lib/state-command-router.cjs +47 -18
  131. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  132. package/gsd-core/bin/lib/state-document.cjs +216 -5
  133. package/gsd-core/bin/lib/state-md-schema.cjs +231 -0
  134. package/gsd-core/bin/lib/state-transition.cjs +850 -145
  135. package/gsd-core/bin/lib/state.cjs +1629 -287
  136. package/gsd-core/bin/lib/surface.cjs +33 -10
  137. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  138. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  139. package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
  140. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  141. package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
  142. package/gsd-core/bin/lib/uat.cjs +2542 -387
  143. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  144. package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
  145. package/gsd-core/bin/lib/unusable-input.cjs +13 -0
  146. package/gsd-core/bin/lib/update-context.cjs +6 -2
  147. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  148. package/gsd-core/bin/lib/validate.cjs +230 -12
  149. package/gsd-core/bin/lib/vendor/README.md +43 -5
  150. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  151. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  152. package/gsd-core/bin/lib/verification.cjs +287 -13
  153. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  154. package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
  155. package/gsd-core/bin/lib/verify.cjs +441 -56
  156. package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
  157. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  158. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  159. package/gsd-core/bin/lib/worktree-safety.cjs +185 -21
  160. package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
  161. package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
  162. package/gsd-core/bin/shared/exit-codes.json +8 -0
  163. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  164. package/gsd-core/bin/shared/model-catalog.json +8 -1
  165. package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
  166. package/gsd-core/references/agent-contracts.md +6 -5
  167. package/gsd-core/references/api-coverage.md +24 -2
  168. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  169. package/gsd-core/references/checkpoints.md +37 -19
  170. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  171. package/gsd-core/references/edge-probe.md +17 -5
  172. package/gsd-core/references/execute-mvp-tdd.md +18 -18
  173. package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
  174. package/gsd-core/references/execute-phase-response-language.md +6 -0
  175. package/gsd-core/references/execute-phase-wave-guard.md +11 -9
  176. package/gsd-core/references/executor-examples.md +42 -0
  177. package/gsd-core/references/failing-direction.md +78 -0
  178. package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
  179. package/gsd-core/references/gate-prompts.md +1 -1
  180. package/gsd-core/references/git-integration.md +5 -5
  181. package/gsd-core/references/git-planning-commit.md +3 -3
  182. package/gsd-core/references/gsd-run-resolver.md +1 -1
  183. package/gsd-core/references/loop-hook-dispatch.md +22 -0
  184. package/gsd-core/references/model-profiles.md +1 -1
  185. package/gsd-core/references/mvp-concepts.md +2 -2
  186. package/gsd-core/references/nyquist-compliance.md +74 -0
  187. package/gsd-core/references/offer-next.md +3 -5
  188. package/gsd-core/references/phase-argument-parsing.md +3 -3
  189. package/gsd-core/references/plan-checker-examples.md +41 -0
  190. package/gsd-core/references/planner-antipatterns.md +25 -0
  191. package/gsd-core/references/planner-chunked.md +5 -1
  192. package/gsd-core/references/planner-coupling.md +42 -0
  193. package/gsd-core/references/planner-failing-direction.md +53 -0
  194. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  195. package/gsd-core/references/planner-quick-batch.md +71 -0
  196. package/gsd-core/references/planner-reviews.md +47 -0
  197. package/gsd-core/references/planner-revision.md +76 -3
  198. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  199. package/gsd-core/references/planning-config.md +39 -9
  200. package/gsd-core/references/response-language-directive.md +9 -0
  201. package/gsd-core/references/reviewer-instances.md +31 -0
  202. package/gsd-core/references/revision-loop.md +118 -11
  203. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  204. package/gsd-core/references/tdd.md +15 -12
  205. package/gsd-core/references/ui-brand.md +65 -21
  206. package/gsd-core/references/ui-consideration-probe.md +1 -1
  207. package/gsd-core/references/universal-anti-patterns.md +2 -2
  208. package/gsd-core/references/verifier-evidence-gate.md +160 -0
  209. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  210. package/gsd-core/references/verify-mvp-mode.md +1 -1
  211. package/gsd-core/references/workstream-flag.md +11 -11
  212. package/gsd-core/templates/README.md +1 -1
  213. package/gsd-core/templates/SECURITY.md +3 -3
  214. package/gsd-core/templates/UI-SPEC.md +25 -3
  215. package/gsd-core/templates/VALIDATION.md +3 -3
  216. package/gsd-core/templates/phase-prompt.md +7 -0
  217. package/gsd-core/templates/state.md +7 -0
  218. package/gsd-core/templates/verification-report.md +5 -0
  219. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  220. package/gsd-core/workflows/add-backlog.md +3 -1
  221. package/gsd-core/workflows/add-phase.md +5 -3
  222. package/gsd-core/workflows/add-tests.md +4 -9
  223. package/gsd-core/workflows/add-todo.md +2 -2
  224. package/gsd-core/workflows/ai-integration-phase.md +5 -10
  225. package/gsd-core/workflows/analyze-dependencies.md +2 -0
  226. package/gsd-core/workflows/audit-fix.md +14 -3
  227. package/gsd-core/workflows/audit-milestone.md +11 -9
  228. package/gsd-core/workflows/audit-uat.md +19 -2
  229. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  230. package/gsd-core/workflows/autonomous.md +12 -26
  231. package/gsd-core/workflows/check-todos.md +2 -2
  232. package/gsd-core/workflows/cleanup.md +3 -3
  233. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +16 -14
  234. package/gsd-core/workflows/code-review-fix.md +3 -1
  235. package/gsd-core/workflows/code-review.md +192 -69
  236. package/gsd-core/workflows/complete-milestone.md +28 -14
  237. package/gsd-core/workflows/debug.md +6 -4
  238. package/gsd-core/workflows/diagnose-issues.md +17 -7
  239. package/gsd-core/workflows/discuss-phase/modes/advisor.md +3 -1
  240. package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
  241. package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
  242. package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
  243. package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
  244. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -7
  245. package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
  246. package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
  247. package/gsd-core/workflows/discuss-phase/modes/text.md +3 -1
  248. package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
  249. package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
  250. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  251. package/gsd-core/workflows/discuss-phase-assumptions.md +3 -3
  252. package/gsd-core/workflows/discuss-phase-power.md +2 -0
  253. package/gsd-core/workflows/discuss-phase.md +2 -2
  254. package/gsd-core/workflows/do.md +46 -19
  255. package/gsd-core/workflows/docs-update.md +6 -5
  256. package/gsd-core/workflows/edit-phase.md +3 -1
  257. package/gsd-core/workflows/eval-review.md +5 -10
  258. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -1
  259. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +129 -11
  260. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  261. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  262. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  263. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +29 -5
  264. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  265. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  266. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +4 -2
  267. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
  268. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  269. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
  270. package/gsd-core/workflows/execute-phase.md +68 -66
  271. package/gsd-core/workflows/execute-plan.md +25 -20
  272. package/gsd-core/workflows/explore.md +3 -1
  273. package/gsd-core/workflows/extract-learnings.md +3 -1
  274. package/gsd-core/workflows/fast.md +8 -2
  275. package/gsd-core/workflows/forensics.md +3 -1
  276. package/gsd-core/workflows/graduation.md +6 -6
  277. package/gsd-core/workflows/health.md +4 -7
  278. package/gsd-core/workflows/help/modes/brief.md +2 -0
  279. package/gsd-core/workflows/help/modes/default.md +2 -0
  280. package/gsd-core/workflows/help/modes/full.md +12 -0
  281. package/gsd-core/workflows/help/modes/topic.md +2 -0
  282. package/gsd-core/workflows/help.md +2 -0
  283. package/gsd-core/workflows/import.md +17 -14
  284. package/gsd-core/workflows/inbox.md +5 -6
  285. package/gsd-core/workflows/ingest-docs.md +45 -12
  286. package/gsd-core/workflows/insert-phase.md +7 -5
  287. package/gsd-core/workflows/list-phase-assumptions.md +2 -0
  288. package/gsd-core/workflows/list-seeds.md +7 -3
  289. package/gsd-core/workflows/list-workspaces.md +3 -1
  290. package/gsd-core/workflows/manager.md +15 -26
  291. package/gsd-core/workflows/map-codebase.md +3 -1
  292. package/gsd-core/workflows/milestone-summary.md +3 -1
  293. package/gsd-core/workflows/mvp-phase.md +3 -3
  294. package/gsd-core/workflows/new-milestone.md +10 -22
  295. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  296. package/gsd-core/workflows/new-project.md +17 -29
  297. package/gsd-core/workflows/new-workspace.md +2 -2
  298. package/gsd-core/workflows/next.md +4 -2
  299. package/gsd-core/workflows/node-repair.md +2 -0
  300. package/gsd-core/workflows/note.md +2 -0
  301. package/gsd-core/workflows/onboard.md +1 -1
  302. package/gsd-core/workflows/pause-work.md +20 -5
  303. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  304. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
  305. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +4 -4
  306. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +12 -3
  307. package/gsd-core/workflows/plan-phase.md +251 -54
  308. package/gsd-core/workflows/plan-review-convergence.md +148 -19
  309. package/gsd-core/workflows/plant-seed.md +3 -3
  310. package/gsd-core/workflows/pr-branch.md +195 -51
  311. package/gsd-core/workflows/profile-user.md +17 -15
  312. package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
  313. package/gsd-core/workflows/progress.md +52 -15
  314. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  315. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +38 -5
  316. package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
  317. package/gsd-core/workflows/quick/steps/research-phase.md +5 -7
  318. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  319. package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
  320. package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
  321. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
  322. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
  323. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
  324. package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
  325. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
  326. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
  327. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
  328. package/gsd-core/workflows/quick-batch.md +203 -0
  329. package/gsd-core/workflows/quick.md +33 -32
  330. package/gsd-core/workflows/reapply-patches.md +2 -0
  331. package/gsd-core/workflows/remove-phase.md +6 -4
  332. package/gsd-core/workflows/remove-workspace.md +3 -3
  333. package/gsd-core/workflows/resume-project.md +14 -14
  334. package/gsd-core/workflows/review.md +404 -21
  335. package/gsd-core/workflows/scan.md +3 -1
  336. package/gsd-core/workflows/section-manifest.json +12 -0
  337. package/gsd-core/workflows/secure-phase.md +3 -3
  338. package/gsd-core/workflows/session-report.md +2 -0
  339. package/gsd-core/workflows/settings-advanced.md +9 -9
  340. package/gsd-core/workflows/settings-integrations.md +66 -32
  341. package/gsd-core/workflows/settings.md +4 -6
  342. package/gsd-core/workflows/ship.md +22 -16
  343. package/gsd-core/workflows/sketch-wrap-up.md +13 -17
  344. package/gsd-core/workflows/sketch.md +13 -19
  345. package/gsd-core/workflows/smart-entry.md +4 -6
  346. package/gsd-core/workflows/spec-phase.md +31 -4
  347. package/gsd-core/workflows/spike-wrap-up.md +9 -11
  348. package/gsd-core/workflows/spike.md +21 -32
  349. package/gsd-core/workflows/stats.md +4 -2
  350. package/gsd-core/workflows/sync-skills.md +13 -5
  351. package/gsd-core/workflows/thread.md +13 -7
  352. package/gsd-core/workflows/transition.md +7 -5
  353. package/gsd-core/workflows/ui-phase.md +36 -21
  354. package/gsd-core/workflows/ui-review.md +7 -11
  355. package/gsd-core/workflows/ultraplan-phase.md +7 -13
  356. package/gsd-core/workflows/undo.md +9 -17
  357. package/gsd-core/workflows/update.md +47 -48
  358. package/gsd-core/workflows/validate-phase.md +3 -3
  359. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  360. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  361. package/gsd-core/workflows/verify-work.md +106 -21
  362. package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
  363. package/hooks/dist/gsd-check-update-worker.js +19 -2
  364. package/hooks/dist/gsd-config-reload.js +18 -12
  365. package/hooks/dist/gsd-context-monitor.js +302 -22
  366. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  367. package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
  368. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  369. package/hooks/dist/gsd-cursor-stop.js +2 -1
  370. package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
  371. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
  372. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  373. package/hooks/dist/gsd-graphify-update.sh +22 -18
  374. package/hooks/dist/gsd-node-runner.sh +77 -0
  375. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  376. package/hooks/dist/gsd-prompt-guard.js +46 -12
  377. package/hooks/dist/gsd-read-guard.js +18 -7
  378. package/hooks/dist/gsd-read-injection-scanner.js +22 -13
  379. package/hooks/dist/gsd-secret-read-guard.js +1079 -0
  380. package/hooks/dist/gsd-session-state.sh +1 -0
  381. package/hooks/dist/gsd-statusline.js +222 -29
  382. package/hooks/dist/gsd-validate-commit.sh +523 -12
  383. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  384. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  385. package/hooks/dist/gsd-workflow-guard.js +36 -17
  386. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  387. package/hooks/dist/gsd-write-guard.js +35 -25
  388. package/hooks/dist/lib/cli-exit.js +560 -0
  389. package/hooks/dist/lib/exit-code-registry.js +98 -0
  390. package/hooks/dist/lib/git-cmd.js +210 -1
  391. package/hooks/dist/lib/git-probe.js +84 -0
  392. package/hooks/dist/lib/hook-exit.js +81 -0
  393. package/hooks/dist/lib/injection-patterns.js +36 -6
  394. package/hooks/dist/managed-hooks-registry.cjs +4 -0
  395. package/hooks/gsd-agent-isolation-guard.js +77 -38
  396. package/hooks/gsd-check-update-worker.js +19 -2
  397. package/hooks/gsd-config-reload.js +18 -12
  398. package/hooks/gsd-context-monitor.js +302 -22
  399. package/hooks/gsd-cursor-post-tool.js +3 -1
  400. package/hooks/gsd-cursor-pre-tool.js +3 -1
  401. package/hooks/gsd-cursor-session-start.js +2 -1
  402. package/hooks/gsd-cursor-stop.js +2 -1
  403. package/hooks/gsd-cursor-subagent-start.js +28 -23
  404. package/hooks/gsd-cursor-subagent-stop.js +3 -1
  405. package/hooks/gsd-ensure-canonical-path.js +2 -1
  406. package/hooks/gsd-graphify-update.sh +22 -18
  407. package/hooks/gsd-node-runner.sh +77 -0
  408. package/hooks/gsd-phase-boundary.sh +1 -0
  409. package/hooks/gsd-prompt-guard.js +46 -12
  410. package/hooks/gsd-read-guard.js +18 -7
  411. package/hooks/gsd-read-injection-scanner.js +22 -13
  412. package/hooks/gsd-secret-read-guard.js +1079 -0
  413. package/hooks/gsd-session-state.sh +1 -0
  414. package/hooks/gsd-statusline.js +222 -29
  415. package/hooks/gsd-validate-commit.sh +523 -12
  416. package/hooks/gsd-windsurf-pre-command.js +16 -11
  417. package/hooks/gsd-windsurf-pre-write.js +22 -13
  418. package/hooks/gsd-workflow-guard.js +36 -17
  419. package/hooks/gsd-worktree-path-guard.js +36 -21
  420. package/hooks/gsd-write-guard.js +35 -25
  421. package/hooks/hooks.json +6 -0
  422. package/hooks/lib/cli-exit.js +560 -0
  423. package/hooks/lib/exit-code-registry.js +98 -0
  424. package/hooks/lib/git-cmd.js +210 -1
  425. package/hooks/lib/git-probe.js +84 -0
  426. package/hooks/lib/hook-exit.js +81 -0
  427. package/hooks/lib/injection-patterns.js +36 -6
  428. package/hooks/managed-hooks-registry.cjs +4 -0
  429. package/package.json +14 -9
  430. package/scripts/base64-scan.sh +74 -12
  431. package/scripts/build-hooks.js +12 -0
  432. package/scripts/check-glossary-refs.cjs +77 -15
  433. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  434. package/scripts/ci-check-job-near-cap.cjs +49 -0
  435. package/scripts/ci-pr-mergeability.cjs +262 -0
  436. package/scripts/ci-test-scope.cjs +52 -12
  437. package/scripts/ci-timeout-report.cjs +230 -0
  438. package/scripts/docs-guard-registry.cjs +406 -0
  439. package/scripts/gen-capability-registry.cjs +8 -6
  440. package/scripts/gen-exit-code-docs.cjs +318 -0
  441. package/scripts/gen-exit-code-registry.cjs +891 -0
  442. package/scripts/gen-features.cjs +836 -0
  443. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  444. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  445. package/scripts/gen-loop-host-contract.cjs +189 -4
  446. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  447. package/scripts/gen-state-md-docs.cjs +727 -0
  448. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  449. package/scripts/lib/ci-job-timing.cjs +72 -0
  450. package/scripts/lib/cli-exit.cjs +546 -44
  451. package/scripts/lib/drift-scan.cjs +32 -2
  452. package/scripts/lib/exit-code-registry.cjs +98 -0
  453. package/scripts/lib/ndjson-reporter.cjs +119 -0
  454. package/scripts/lib/shellcheck-fetch.cjs +247 -0
  455. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
  456. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  457. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  458. package/scripts/lint-docs-guard-registration.cjs +495 -0
  459. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +198 -0
  460. package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
  461. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  462. package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
  463. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  464. package/scripts/lint-phase-enumeration-drift.cjs +45 -14
  465. package/scripts/lint-phase-id-drift.cjs +133 -8
  466. package/scripts/lint-planning-prompt-drift.cjs +38 -1
  467. package/scripts/lint-portable-grep.cjs +176 -0
  468. package/scripts/lint-removed-but-needed.cjs +184 -16
  469. package/scripts/lint-response-language-coverage.cjs +524 -0
  470. package/scripts/lint-seam-enforcement.cjs +182 -0
  471. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  472. package/scripts/lint-source-test-name-collision.cjs +241 -0
  473. package/scripts/lint-state-write-path-drift.cjs +337 -432
  474. package/scripts/lint-test-file-count.allowlist.json +124 -4
  475. package/scripts/lint-test-file-count.cjs +25 -3
  476. package/scripts/lint-unreachable-guard-drift.cjs +51 -64
  477. package/scripts/lint-vendored-deps.cjs +208 -35
  478. package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
  479. package/scripts/lint-workflow-shellcheck.cjs +614 -0
  480. package/scripts/mutation-matrix.cjs +599 -50
  481. package/scripts/npm-audit-baseline.cjs +376 -0
  482. package/scripts/prompt-injection-scan.sh +83 -14
  483. package/scripts/require-issue-link-policy.cjs +16 -1
  484. package/scripts/secret-scan.sh +75 -13
  485. package/scripts/select-docs-guards.cjs +56 -0
  486. package/scripts/sync-runtime-launcher.cjs +22 -3
  487. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  488. package/skills/gsd-execute-phase/SKILL.md +1 -1
  489. package/skills/gsd-import/SKILL.md +1 -1
  490. package/skills/gsd-ns-workflow/SKILL.md +1 -0
  491. package/skills/gsd-phase/SKILL.md +1 -1
  492. package/skills/gsd-quick/SKILL.md +8 -4
  493. package/skills/gsd-quick-batch/SKILL.md +105 -0
  494. package/skills/gsd-surface/SKILL.md +18 -8
  495. package/vscode/package.json +1 -1
  496. package/bin/lib/ui-safety-gate.cjs +0 -109
  497. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  498. package/scripts/state-write-path-drift-baseline.json +0 -19
@@ -22,18 +22,153 @@ const fs = require('fs');
22
22
  const os = require('os');
23
23
  const path = require('path');
24
24
  const { spawn } = require('child_process');
25
+ const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
26
+
27
+ // This hook only injects an advisory context-usage warning; it never blocks
28
+ // the tool call it rides in on. A crash here (e.g. a malformed bridge file)
29
+ // must not turn a PostToolUse advisory into a blocked tool call — losing a
30
+ // context warning is far cheaper than stalling the agent's work (#3911).
31
+ const ON_CRASH = HOOK_ON_CRASH.ALLOW;
25
32
 
26
33
  const WARNING_THRESHOLD = 35; // remaining_percentage <= 35%
27
34
  const CRITICAL_THRESHOLD = 25; // remaining_percentage <= 25%
28
35
  const STALE_SECONDS = 60; // ignore metrics older than 60s
29
36
  const DEBOUNCE_CALLS = 5; // min tool uses between warnings
37
+ // How long after a PreCompact readings stay suspect. The watermark records the
38
+ // compaction's START; the compaction keeps running after it, and a statusline
39
+ // render during it stamps the PRE-compaction reading with a CURRENT timestamp
40
+ // (Codex review of #3808, round 3) — so "newer than the watermark" alone still
41
+ // admits it. Everything inside this window is dropped instead. The cost is
42
+ // bounded: a healthy reading dropped here behaves identically to an accepted
43
+ // one (it would exit above-threshold anyway). A genuine exhaustion reading
44
+ // inside the window is SKIPPED, not queued — its warning and its #1974
45
+ // breadcrumb both fire on the next reading after the window, so they are
46
+ // delayed by at most this window plus the accepted skew below when a later
47
+ // reading comes, and lost when
48
+ // none does, i.e. when the session ends inside the window (review of #3808,
49
+ // round 9). That loss is accepted over the alternative, which is trusting a
50
+ // reading that may be the pre-compaction value under a fresh timestamp.
51
+ const COMPACT_GRACE_SECONDS = 60;
52
+ // How far AHEAD of this process's clock a watermark may be and still be
53
+ // honored. PreCompact stamps it from the same clock as the reader, so the
54
+ // legitimate skew is 0; this tolerance only absorbs a clock step. It is a
55
+ // THRESHOLD, so it is named rather than inlined and carries its own boundary
56
+ // trio (Codex review of #3808, round 4). Note it also extends the mute: a
57
+ // watermark this far ahead pushes first recovery from +61 to +66 (measured).
58
+ const WATERMARK_SKEW_SECONDS = 5;
59
+
60
+ // One DEFINITION of what counts as a lifecycle event name, shared by the #3709
61
+ // PreCompact reset and the #2289 output-envelope allowlist. Two call sites, one
62
+ // rule — so the two cannot drift into disagreeing about what "no event name" is.
63
+ // TOTAL, and STRICT about type: only an actual string is an event name. The old
64
+ // inline expression threw on a truthy non-string, and hoisting it ahead of the
65
+ // pipeline would have moved that throw ahead of the side effects #2289
66
+ // documents as always running; a String() coercion is no better — it renders
67
+ // ['PreCompact'] as 'PreCompact' and would run the reset off a malformed
68
+ // payload, and a hostile toString still throws (Codex review of #3808,
69
+ // round 3). typeof does neither: any non-string reads as "no event" — silent,
70
+ // side effects intact — on both call sites.
71
+ function readEventName(data) {
72
+ const name = data && data.hook_event_name;
73
+ if (typeof name === 'string') return name.trim();
74
+ // ABSENT vs MALFORMED are not the same event (Codex review of #3808, round 7,
75
+ // measured base-vs-head). A MISSING name is the documented pre-#2289 Gemini
76
+ // fallback: under GEMINI_API_KEY it means AfterTool and still emits. A name
77
+ // that is PRESENT but not a string is a malformed payload and must not
78
+ // inherit that fallback — at the merge-base it threw on `.trim()` after the
79
+ // side effects, so no envelope was ever produced, and collapsing both onto ''
80
+ // silently turned `42`, `{}` and `['PreCompact']` into emitting AfterTool
81
+ // events. Measured: base silent, head emitted, for both `42` and
82
+ // `['PreCompact']`. null keeps them distinguishable while staying unequal to
83
+ // every event name, so the PreCompact reset and the allowlist below are
84
+ // byte-for-byte unchanged for every well-formed payload.
85
+ return (name === undefined || name === null) ? '' : null;
86
+ }
87
+
88
+ // SENTINEL WRITE HARDENING (review of #3808, round 7). `warnPath` lives in
89
+ // os.tmpdir(), which may resolve to a shared sticky directory — not guaranteed
90
+ // per-user, and the file persists across invocations — so an object already
91
+ // sitting there may be a planted symlink. The three routine debounce-accounting writes were bare
92
+ // writeFileSync, which follows one and writes through to its target, while the
93
+ // PreCompact clear and the compaction watermark in this same file already
94
+ // refuse to. Unlink-then-O_EXCL is the watermark's own shape (the watermark
95
+ // write itself now calls this helper — review round 10): the unlink
96
+ // removes any existing object (regular file or link) and O_EXCL then refuses
97
+ // to create through one, so the write can only ever land on a fresh regular
98
+ // file this process made. Best effort by design — a lost sentinel write costs
99
+ // only debounce accounting, which is never worth breaking the hook over, so
100
+ // every failure is swallowed exactly as the watermark write's is.
101
+ // NOT an atomic read-modify-write, and not claimed to be (Codex review of
102
+ // #3808, round 7): two concurrent invocations can read the same state and race
103
+ // through unlink/create, so one invocation's accounting can be lost — the same
104
+ // lost-update race the bare writeFileSync already had, not a class this change
105
+ // introduces. What a lost write leaves behind is whatever the competing writer
106
+ // wrote, which may be a perfectly valid sentinel; it does not reliably mean
107
+ // "defaults on the next call". Advisory debounce bookkeeping is the right place
108
+ // to accept that.
109
+ // The read-side twin of writeSentinel (review of #3808, round 9). Both
110
+ // sentinel files this hook reads — the compaction watermark and the warn
111
+ // state — must be read the same way: lstat first so a planted link, FIFO or
112
+ // directory is refused before any open; O_NOFOLLOW so a link raced in between
113
+ // is refused by the kernel too (0 on Windows, where lstat already carries the
114
+ // check); a 4096-byte bound so a planted large file cannot stall a
115
+ // synchronous read. Rounds 4 and 7 each wrote that sequence inline at their
116
+ // own call site, which left two copies to keep in step by hand. One place
117
+ // now. Refusal THROWS; every caller already wraps the read in a try/catch and
118
+ // degrades to "no file", which is the same behaviour the inline copies had.
119
+ function readSentinel(target) {
120
+ const st = fs.lstatSync(target);
121
+ if (!st.isFile() || st.size > 4096) throw new Error('not a plain sentinel');
122
+ const fd = fs.openSync(target, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
123
+ try {
124
+ const buf = Buffer.alloc(st.size);
125
+ // The RETURN VALUE, not just the call (review of #3808, round 11). A file that shrinks
126
+ // between the lstat above and this read — a concurrent legitimate writer truncating
127
+ // mid-write, not the planted-object case the rest of this function guards — leaves the tail
128
+ // of `buf` zero-filled, and those NULs reach JSON.parse as garbage. Every caller already
129
+ // treats a throw here as "no file", so refusing a short read is both safer and the same
130
+ // outcome the caller would reach one line later, stated on purpose rather than by accident.
131
+ const bytesRead = fs.readSync(fd, buf, 0, st.size, 0);
132
+ if (bytesRead !== st.size) throw new Error('sentinel shrank under the read');
133
+ return buf.toString('utf8');
134
+ } finally { fs.closeSync(fd); }
135
+ }
136
+
137
+ function writeSentinel(target, payload) {
138
+ try {
139
+ try {
140
+ fs.unlinkSync(target);
141
+ } catch (e) {
142
+ if (!e || e.code !== 'ENOENT') throw e;
143
+ }
144
+ const fd = fs.openSync(
145
+ target,
146
+ fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL
147
+ );
148
+ try {
149
+ // LOOP, and check progress (Codex review of round 11). A single `fs.writeSync` is
150
+ // permitted to write fewer bytes than it was given, and the return value was discarded —
151
+ // a short write left a truncated sentinel that JSON.parse rejects, silently defeating the
152
+ // debounce accounting or the compaction watermark this write exists to record. Node's own
153
+ // `writeFileSync` loops for exactly this reason; the explicit no-progress guard keeps a
154
+ // pathological fd from spinning. Symmetric with the bytesRead check in readSentinel.
155
+ const buf = Buffer.from(payload, 'utf8');
156
+ let written = 0;
157
+ while (written < buf.length) {
158
+ const n = fs.writeSync(fd, buf, written, buf.length - written);
159
+ if (!(n > 0)) throw new Error('sentinel write made no progress');
160
+ written += n;
161
+ }
162
+ } finally { fs.closeSync(fd); }
163
+ } catch (e) { /* best effort — see above */ }
164
+ }
30
165
 
31
166
  let input = '';
32
167
  // Timeout guard: if stdin doesn't close within 10s (e.g. pipe issues on
33
168
  // Windows/Git Bash, or slow Claude Code piping during large outputs),
34
169
  // exit silently instead of hanging until Claude Code kills the process
35
170
  // and reports "hook error". See #775, #1162.
36
- const stdinTimeout = setTimeout(() => process.exit(0), 10000);
171
+ const stdinTimeout = setTimeout(() => allow(undefined), 10000);
37
172
  process.stdin.setEncoding('utf8');
38
173
  process.stdin.on('data', chunk => input += chunk);
39
174
  process.stdin.on('end', () => {
@@ -43,14 +178,92 @@ process.stdin.on('end', () => {
43
178
  const sessionId = data.session_id;
44
179
 
45
180
  if (!sessionId) {
46
- process.exit(0);
181
+ allow(undefined);
47
182
  }
48
183
 
49
184
  // Reject session IDs that contain path traversal sequences or path separators.
50
185
  // session_id is used to construct file paths in /tmp — an unsanitized value
51
186
  // could escape the temp directory and read or write arbitrary files.
52
187
  if (/[/\\]|\.\./.test(sessionId)) {
53
- process.exit(0);
188
+ allow(undefined);
189
+ }
190
+
191
+ const tmpDir = os.tmpdir();
192
+ const warnPath = path.join(tmpDir, `claude-ctx-${sessionId}-warned.json`);
193
+ const metricsPath = path.join(tmpDir, `claude-ctx-${sessionId}.json`);
194
+ const watermarkPath = path.join(tmpDir, `claude-ctx-${sessionId}-compacted.json`);
195
+
196
+ // #3709: a compaction RESTARTS the context lifecycle, so neither the warn
197
+ // sentinel nor the pre-compaction statusline reading may survive it. Full
198
+ // rationale — what dies when the sentinel outlives a compaction, why the
199
+ // reset sits ahead of the config gate and the metrics read, and why an
200
+ // aborted compaction deliberately stays cleared — lives in ONE place:
201
+ // docs/context-monitor.md, "PreCompact reset". Constraints the code itself
202
+ // must keep are stated at their lines below.
203
+ if (readEventName(data) === 'PreCompact') {
204
+ // ORDERING ASSUMPTION, stated rather than enforced (review of #3808,
205
+ // round 9): this reset and the debounce writeSentinel(warnPath) further
206
+ // down are two writers to the same file, and nothing here serialises
207
+ // them. A debounce invocation that read the pre-compaction state and
208
+ // lands its write AFTER this unlink would resurrect exactly the stale
209
+ // sentinel this block removes. The hook relies on the host dispatching a
210
+ // session's hooks one at a time, which Claude Code does; the other
211
+ // runtimes this hook is installed for are assumed to, and that is not
212
+ // tested. A lock file would close it at the cost of a second file to
213
+ // harden on every platform; not taken here.
214
+ // BOTH files: with the sentinel gone but the bridge still holding the
215
+ // pre-compaction reading (fresh for STALE_SECONDS), the next PostToolUse
216
+ // would fire a spurious CRITICAL off a context the compaction just freed
217
+ // (review of #3709).
218
+ for (const stale of [warnPath, metricsPath]) {
219
+ try {
220
+ fs.unlinkSync(stale);
221
+ } catch (e) {
222
+ if (e && e.code === 'ENOENT') continue; // already absent — that IS the reset
223
+ // Best-effort fallback for a held handle (Windows EPERM/EBUSY):
224
+ // truncate to EMPTY — the one state both readers treat exactly like
225
+ // deletion, because JSON.parse('') throws. A well-formed "neutral"
226
+ // value is NOT equivalent: '{}' debounces the first post-compaction
227
+ // warning, '{"timestamp":0}' is never stale (falsy guard) and emits
228
+ // "undefined%" (review of #3808). Never through a LINK: lstat
229
+ // rejects non-regular files on every platform (Windows has no
230
+ // effective O_NOFOLLOW — libuv defines it as 0 — and TEMP/TMP means
231
+ // its tmpdir is not guaranteed per-user); O_NOFOLLOW additionally
232
+ // closes the lstat→open substitution race where honored. Every
233
+ // refusal lands in this give-up arm — including a Windows runner
234
+ // refusing the write-open of a freshly written file outright —
235
+ // which is why the fallback is best-effort, never asserted-on.
236
+ try {
237
+ if (fs.lstatSync(stale).isFile()) {
238
+ fs.closeSync(fs.openSync(
239
+ stale,
240
+ fs.constants.O_WRONLY | fs.constants.O_TRUNC | (fs.constants.O_NOFOLLOW || 0)
241
+ ));
242
+ }
243
+ } catch (e2) { /* give up, never throw */ }
244
+ }
245
+ }
246
+
247
+ // COMPACTION WATERMARK (review of #3808, round 3). Deleting the bridge
248
+ // only NARROWS the stale-reading window: the statusline is an
249
+ // uncoordinated process that re-writes the bridge on every render, so a
250
+ // render landing between this clear and the compaction's completion
251
+ // re-creates the PRE-compaction reading with a CURRENT timestamp — and
252
+ // it would sail past STALE_SECONDS as freshly valid. The watermark makes
253
+ // the pre-compaction reading identifiable rather than merely absent: the
254
+ // metrics read drops any reading not strictly newer than it. Written
255
+ // through writeSentinel (review of #3808, round 10 — this block was the
256
+ // shape writeSentinel was lifted from in round 7 and kept its own copy):
257
+ // unlink-then-O_EXCL so an existing file — or a planted symlink — is
258
+ // never followed or overwritten in place; failure to write degrades to
259
+ // the old narrowing, never throws.
260
+ writeSentinel(watermarkPath, JSON.stringify({ at: Math.floor(Date.now() / 1000) }));
261
+ // allow(), not raw process.exit: #3911/ADR-3889 moved this hook onto the
262
+ // declared-policy exit vocabulary while this PR was in review, and the
263
+ // PreCompact branch is new here, so it needs the same conversion.
264
+ // A compaction is never blocked by this hook — ALLOW is the policy the
265
+ // rest of the file already declares.
266
+ allow(undefined);
54
267
  }
55
268
 
56
269
  // Check if context warnings are disabled via config.
@@ -61,31 +274,78 @@ process.stdin.on('end', () => {
61
274
  const configPath = path.join(cwd, '.planning', 'config.json');
62
275
  const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
63
276
  if (config.hooks?.context_warnings === false) {
64
- process.exit(0);
277
+ allow(undefined);
65
278
  }
66
279
  } catch (e) {
67
280
  // Missing or unparseable config → proceed with defaults (context warnings enabled)
68
281
  }
69
282
 
70
- const tmpDir = os.tmpdir();
71
- const metricsPath = path.join(tmpDir, `claude-ctx-${sessionId}.json`);
72
-
73
283
  // If no metrics file, this is a subagent or fresh session -- exit silently.
74
284
  // Collapsed existsSync+readFileSync: ENOENT → exit 0 (identical to old !existsSync branch),
75
285
  // other errors rethrow to the outer catch (swallowed → exit 0, as before).
286
+ //
287
+ // Through readSentinel, like the other two (review of #3808, round 11). This read was the
288
+ // asymmetry left in this file: `metricsPath` is built one line away from `warnPath` and
289
+ // `watermarkPath` (same tmpdir, same predictable `claude-ctx-{sessionId}` shape), it is the
290
+ // only one of the three read on EVERY invocation, and it was the only one still reached by a
291
+ // bare readFileSync — so the symlink-to-FIFO stall the other two are hardened against was
292
+ // still reachable here, on the highest-traffic path in the file. The 4096-byte bound is
293
+ // ample: the statusline writes four fixed fields (`gsd-statusline.js`, ~140 bytes with a
294
+ // UUID session id), so no legitimate bridge approaches it. A refusal throws and lands in the
295
+ // rethrow below exactly as an unreadable or malformed bridge already did.
76
296
  let metricsRaw;
77
297
  try {
78
- metricsRaw = fs.readFileSync(metricsPath, 'utf8');
298
+ metricsRaw = readSentinel(metricsPath);
79
299
  } catch (e) {
80
- if (e && e.code === 'ENOENT') process.exit(0);
300
+ if (e && e.code === 'ENOENT') allow(undefined);
81
301
  throw e;
82
302
  }
83
303
  const metrics = JSON.parse(metricsRaw);
84
304
  const now = Math.floor(Date.now() / 1000);
85
305
 
306
+ // #3709 (round 3): a reading not clearly PAST the compaction is suspect,
307
+ // whatever its timestamp says — the statusline re-writes the bridge on
308
+ // every render, and a render during the compaction stamps the OLD
309
+ // remaining_percentage with a current time. The watermark records the
310
+ // compaction's START, so "newer than the watermark" alone still admits a
311
+ // mid-compaction render (Codex review of #3808, round 3): the grace
312
+ // window covers the compaction's own duration. `!(>)` rather than `<=` so
313
+ // a missing/zero/garbage timestamp is also dropped once a compaction has
314
+ // happened — an unstamped reading cannot prove it is post-compaction.
315
+ //
316
+ // The watermark itself must be SANE to count: one stamped in the future
317
+ // (a clock step backwards, a stray file) would otherwise drop every
318
+ // reading indefinitely and silently self-disable monitoring — so it is
319
+ // honored only when its own timestamp is not ahead of this process's
320
+ // clock (small skew allowed). No watermark, an unreadable one, or an
321
+ // insane one all degrade to the plain STALE_SECONDS behaviour below.
322
+ //
323
+ // READ HARDENING (Codex review of #3808, round 4). The WRITE side already
324
+ // refuses to follow or overwrite a planted object (unlink-then-O_EXCL
325
+ // above), but this read was a bare readFileSync — so on any write-side
326
+ // give-up the planted object survived and every later invocation followed
327
+ // it. In a shared sticky os.tmpdir() that is a mute primitive (a planted
328
+ // recent watermark suppresses monitoring) and a stall primitive (a symlink
329
+ // to a FIFO blocks this synchronous read indefinitely; measured: such a
330
+ // read is still running after 300ms). The same lstat + O_NOFOLLOW pair the
331
+ // sentinel path uses, plus a size bound, applied to the file this PR adds.
332
+ // Every refusal degrades to "no watermark", never throws.
333
+ try {
334
+ const watermark = JSON.parse(readSentinel(watermarkPath));
335
+ if (
336
+ watermark && typeof watermark.at === 'number'
337
+ && watermark.at <= now + WATERMARK_SKEW_SECONDS
338
+ && !(metrics.timestamp > watermark.at + COMPACT_GRACE_SECONDS)
339
+ ) {
340
+ // Same #3911/ADR-3889 conversion as the PreCompact branch above: this
341
+ // gate is new in this PR, so it did not exist to be migrated.
342
+ allow(undefined);
343
+ }
344
+ } catch (e) { /* no watermark — nothing to compare against */ }
345
+
86
346
  // Ignore stale metrics
87
347
  if (metrics.timestamp && (now - metrics.timestamp) > STALE_SECONDS) {
88
- process.exit(0);
348
+ allow(undefined);
89
349
  }
90
350
 
91
351
  const remaining = metrics.remaining_percentage;
@@ -93,18 +353,35 @@ process.stdin.on('end', () => {
93
353
 
94
354
  // No warning needed
95
355
  if (remaining > WARNING_THRESHOLD) {
96
- process.exit(0);
356
+ allow(undefined);
97
357
  }
98
358
 
99
- // Debounce: check if we warned recently
100
- const warnPath = path.join(tmpDir, `claude-ctx-${sessionId}-warned.json`);
359
+ // Debounce: check if we warned recently. `warnPath` is resolved above, next to
360
+ // metricsPath, because the PreCompact reset needs it before this point.
101
361
  let warnData = { callsSinceWarn: 0, lastLevel: null };
102
362
  let firstWarn = true;
103
363
 
104
364
  // Collapsed existsSync+readFileSync: ENOENT or parse error → keep default warnData
105
365
  // (same as old "file absent" branch). firstWarn tracks whether we read a valid sentinel.
366
+ //
367
+ // READ HARDENING (self-found while addressing round 7; same class, same
368
+ // file). Hardening the writes above leaves this read as a bare
369
+ // readFileSync on warnPath, which is the exact asymmetry round 7 asks be
370
+ // removed from the write side — and the watermark's read was hardened in
371
+ // round 4 for this same reason, so leaving this one recreates it. It was
372
+ // not the LAST bare read in the file: the statusline bridge kept its own
373
+ // until round 11 found it. All three go through readSentinel now. The
374
+ // exposure is real but bounded: the writes now unlink any planted object,
375
+ // so only a read reaching this line BEFORE the first write of an
376
+ // invocation can follow one, and re-planting reopens it every invocation.
377
+ // Following it is a mute primitive — attacker-chosen callsSinceWarn keeps
378
+ // the debounce arm below taken so no warning is ever emitted — and a
379
+ // symlink to a FIFO stalls this synchronous read, the same two primitives
380
+ // measured on the watermark. Same lstat + O_NOFOLLOW + size bound; every
381
+ // refusal degrades to the default warnData this catch already produces,
382
+ // so a normal regular file behaves exactly as before.
106
383
  try {
107
- warnData = JSON.parse(fs.readFileSync(warnPath, 'utf8'));
384
+ warnData = JSON.parse(readSentinel(warnPath));
108
385
  firstWarn = false;
109
386
  } catch (e) {
110
387
  // Missing or corrupted sentinel → firstWarn stays true, warnData stays at defaults
@@ -120,14 +397,14 @@ process.stdin.on('end', () => {
120
397
  const severityEscalated = currentLevel === 'critical' && warnData.lastLevel === 'warning';
121
398
  if (!firstWarn && warnData.callsSinceWarn < DEBOUNCE_CALLS && !severityEscalated) {
122
399
  // Update counter and exit without warning
123
- fs.writeFileSync(warnPath, JSON.stringify(warnData));
124
- process.exit(0);
400
+ writeSentinel(warnPath, JSON.stringify(warnData));
401
+ allow(undefined);
125
402
  }
126
403
 
127
404
  // Reset debounce counter
128
405
  warnData.callsSinceWarn = 0;
129
406
  warnData.lastLevel = currentLevel;
130
- fs.writeFileSync(warnPath, JSON.stringify(warnData));
407
+ writeSentinel(warnPath, JSON.stringify(warnData));
131
408
 
132
409
  // Detect if GSD is active (has .planning/STATE.md in working directory)
133
410
  const isGsdActive = fs.existsSync(path.join(cwd, '.planning', 'STATE.md'));
@@ -154,7 +431,7 @@ process.stdin.on('end', () => {
154
431
  ).unref();
155
432
  warnData.criticalRecorded = true;
156
433
  // Persist the sentinel so subsequent debounce cycles don't re-fire
157
- fs.writeFileSync(warnPath, JSON.stringify(warnData));
434
+ writeSentinel(warnPath, JSON.stringify(warnData));
158
435
  } catch { /* non-critical — don't let state recording break the hook */ }
159
436
  }
160
437
 
@@ -191,7 +468,7 @@ process.stdin.on('end', () => {
191
468
  // not enough — a missing name would still fall through to the injection path.
192
469
  // All side effects above (debounce counter, one-time critical-session
193
470
  // recording) have already run regardless of whether output is emitted.
194
- const eventName = (data.hook_event_name && data.hook_event_name.trim()) || "";
471
+ const eventName = readEventName(data);
195
472
  // Preserve the pre-#2289 Gemini fallback: a missing event name under a
196
473
  // Gemini-dialect runtime (GEMINI_API_KEY set) still means AfterTool, so its
197
474
  // advisory output is unchanged. A missing name on any other host is silent.
@@ -202,13 +479,16 @@ process.stdin.on('end', () => {
202
479
  const output = {
203
480
  hookSpecificOutput: {
204
481
  hookEventName: eventName || "AfterTool",
205
- additionalContext: message
482
+ additionalContext: message,
483
+ severity: currentLevel
206
484
  }
207
485
  };
208
486
  process.stdout.write(JSON.stringify(output));
209
487
  }
210
488
  } catch (e) {
211
- // Silent fail -- never block tool execution
212
- process.exit(0);
489
+ // Silent fail -- never block tool execution.
490
+ // ON_CRASH is declared ALLOW at module top: this preserves today's
491
+ // exit(0) fail-open behavior exactly (#3911).
492
+ crash(ON_CRASH, undefined);
213
493
  }
214
494
  });
@@ -22,6 +22,8 @@
22
22
 
23
23
  'use strict';
24
24
 
25
+ const { allow } = require('./lib/hook-exit.js');
26
+
25
27
  const WRITE_TOOL_RE = /write|edit|replace|create|delete|remove|append|apply|patch|insert|mkdir/i;
26
28
  const PATH_KEY_RE = /^(path|file|file_?path|filepath|target_?path|target|dir|directory|uri|filename)$/i;
27
29
  const PLANNING_PATH_RE = /(^|[\\/])\.planning([\\/]|$)/;
@@ -29,7 +31,7 @@ const PLANNING_PATH_RE = /(^|[\\/])\.planning([\\/]|$)/;
29
31
  let raw = '';
30
32
  const stdinTimeout = setTimeout(() => {
31
33
  // Timeout guard: exit silently rather than hanging.
32
- process.exit(0);
34
+ allow(undefined);
33
35
  }, 10000);
34
36
 
35
37
  process.stdin.setEncoding('utf8');
@@ -22,13 +22,15 @@
22
22
 
23
23
  'use strict';
24
24
 
25
+ const { allow } = require('./lib/hook-exit.js');
26
+
25
27
  const WRITE_TOOL_RE = /write|edit|replace|create|delete|remove|append|apply|patch|insert|mkdir/i;
26
28
  const PATH_KEY_RE = /^(path|file|file_?path|filepath|target_?path|target|dir|directory|uri|filename)$/i;
27
29
  const PLANNING_PATH_RE = /(^|[\\/])\.planning([\\/]|$)/;
28
30
 
29
31
  let raw = '';
30
32
  const stdinTimeout = setTimeout(() => {
31
- process.exit(0);
33
+ allow(undefined);
32
34
  }, 10000);
33
35
 
34
36
  process.stdin.setEncoding('utf8');
@@ -23,6 +23,7 @@
23
23
  'use strict';
24
24
 
25
25
  const fs = require('fs');
26
+ const { allow } = require('./lib/hook-exit.js');
26
27
 
27
28
  const MSG_PRESENT =
28
29
  'GSD: .planning/STATE.md is present — review the current phase and any blockers before acting.';
@@ -37,7 +38,7 @@ const { resolveStatePath } = require('./lib/cursor-workspace.js');
37
38
  let raw = '';
38
39
  const stdinTimeout = setTimeout(() => {
39
40
  // Timeout guard: exit silently rather than hanging.
40
- process.exit(0);
41
+ allow(undefined);
41
42
  }, 10000);
42
43
 
43
44
  process.stdin.setEncoding('utf8');
@@ -21,6 +21,7 @@
21
21
  'use strict';
22
22
 
23
23
  const fs = require('fs');
24
+ const { allow } = require('./lib/hook-exit.js');
24
25
 
25
26
  // Workspace resolution is shared across the Cursor hooks (#2587) — see
26
27
  // hooks/lib/cursor-workspace.js. Staged next to these scripts by
@@ -29,7 +30,7 @@ const { resolveStatePath } = require('./lib/cursor-workspace.js');
29
30
 
30
31
  let raw = '';
31
32
  const stdinTimeout = setTimeout(() => {
32
- process.exit(0);
33
+ allow(undefined);
33
34
  }, 10000);
34
35
 
35
36
  process.stdin.setEncoding('utf8');
@@ -56,6 +56,7 @@
56
56
  const fs = require('fs');
57
57
  const path = require('path');
58
58
  const os = require('os');
59
+ const { allow } = require('./lib/hook-exit.js');
59
60
 
60
61
  // Workspace resolution is shared across the Cursor hooks (#2587) — see
61
62
  // hooks/lib/cursor-workspace.js. Staged next to these scripts by
@@ -342,35 +343,39 @@ function resolveIsolationDecision(data, { clock = Date, realpath = fs.realpathSy
342
343
  return { action: 'allow' };
343
344
  }
344
345
 
345
- // ─── #3566: per-install runtime marker ────────────────────────────────────────
346
- // Same contract as hooks/gsd-agent-isolation-guard.js's readInstallRuntimeMarker
347
- // (mirroring src/model-resolver.cts #2297): bin/install.js writes
348
- // `<install>/gsd-core/.gsd-runtime` beside VERSION for every runtime install;
349
- // this hook ships at `<install>/hooks/`, so the marker is the `gsd-core` sibling
350
- // of this file's own directory — the same sibling-layout assumption the
351
- // require('../gsd-core/bin/lib/…') calls below already make. Epic #3473 B3 owns
352
- // consolidating every marker reader into one shared seam.
353
- let _installMarkerCache; // undefined = unread; null = known absent; string = value
354
-
346
+ // ─── #3897 rung 2: per-install runtime marker, single canonical owner ────────
347
+ // bin/install.js writes `<install>/gsd-core/.gsd-runtime` beside VERSION for
348
+ // every runtime install (#2297); this hook ships at `<install>/hooks/`, so the
349
+ // marker is the `gsd-core` sibling of this file's own directory. Previously
350
+ // this hook held its own private reader/cache (one of four #3897 found); it
351
+ // now delegates to the single canonical owner, `src/runtime-slash.cts`
352
+ // (compiled to gsd-core/bin/lib/runtime-slash.cjs), reached through
353
+ // `ensureRuntimeBuild()` like the other compiled-lib requires in this file
354
+ // (`scripts/lint-hooks-runtime-build-seam.cjs`).
355
355
  function readInstallRuntimeMarker() {
356
- if (_installMarkerCache !== undefined) return _installMarkerCache;
357
356
  try {
358
- const markerPath = path.join(__dirname, '..', 'gsd-core', '.gsd-runtime');
359
- const raw = fs.readFileSync(markerPath, 'utf-8').trim();
360
- _installMarkerCache = raw || null;
357
+ ensureRuntimeBuild();
358
+ const runtimeSlash = require('../gsd-core/bin/lib/runtime-slash.cjs');
359
+ return runtimeSlash.readInstallRuntimeMarker();
361
360
  } catch {
362
- // No marker: dev/source tree, or an install predating #2297 — "no signal
363
- // from this rung", never a resolution failure.
364
- _installMarkerCache = null;
361
+ // Unbuilt runtime library, or any other failure reaching the canonical
362
+ // owner — "no signal from this rung", never a resolution failure.
363
+ return null;
365
364
  }
366
- return _installMarkerCache;
367
365
  }
368
366
 
369
- // Test seam — same contract as model-resolver.cts's #2297 seam; the dev/source
370
- // tree has no marker file, so spawned-hook tests (fresh process, no marker)
371
- // are unaffected.
367
+ // Test seam — forwards to the canonical owner's seam so this hook and
368
+ // runtime-slash.cjs always share one cache (#3897 rung 2). Spawned-hook tests
369
+ // (fresh process, no marker) are unaffected.
372
370
  function _setInstallRuntimeMarkerForTests(value) {
373
- _installMarkerCache = value;
371
+ try {
372
+ ensureRuntimeBuild();
373
+ const runtimeSlash = require('../gsd-core/bin/lib/runtime-slash.cjs');
374
+ runtimeSlash._setInstallRuntimeMarkerForTests(value);
375
+ } catch {
376
+ // Test-only seam; an unbuilt library here means the test itself will fail
377
+ // downstream, which is a louder and more actionable signal than throwing here.
378
+ }
374
379
  }
375
380
 
376
381
  /**
@@ -564,7 +569,7 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
564
569
  function main() {
565
570
  let raw = '';
566
571
  const stdinTimeout = setTimeout(() => {
567
- process.exit(0);
572
+ allow(undefined);
568
573
  }, 10000);
569
574
 
570
575
  process.stdin.setEncoding('utf8');
@@ -20,8 +20,10 @@
20
20
 
21
21
  'use strict';
22
22
 
23
+ const { allow } = require('./lib/hook-exit.js');
24
+
23
25
  const stdinTimeout = setTimeout(() => {
24
- process.exit(0);
26
+ allow(undefined);
25
27
  }, 10000);
26
28
 
27
29
  process.stdin.setEncoding('utf8');
@@ -34,6 +34,7 @@
34
34
  const fs = require('fs');
35
35
  const path = require('path');
36
36
  const os = require('os');
37
+ const { allow } = require('./lib/hook-exit.js');
37
38
 
38
39
  // Immutable, bundled subdirectories that the canonical path must expose. These
39
40
  // are the directories `@~/.claude/gsd-core/<subdir>/...` includes point into.
@@ -301,5 +302,5 @@ if (require.main === module) {
301
302
  } catch (_) {
302
303
  // Best-effort: a canonical-path failure must never abort a session.
303
304
  }
304
- process.exit(0);
305
+ allow(undefined);
305
306
  }