@opengsd/gsd-core 1.12.0 → 1.14.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 (455) 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-advisor-researcher.compact.md +85 -0
  5. package/agents/gsd-ai-researcher.compact.md +96 -0
  6. package/agents/gsd-assumptions-analyzer.compact.md +81 -0
  7. package/agents/gsd-code-fixer.compact.md +458 -0
  8. package/agents/gsd-code-fixer.md +5 -5
  9. package/agents/gsd-code-reviewer.compact.md +269 -0
  10. package/agents/gsd-code-reviewer.md +15 -3
  11. package/agents/gsd-codebase-mapper.compact.md +760 -0
  12. package/agents/gsd-debug-session-manager.compact.md +345 -0
  13. package/agents/gsd-doc-classifier.compact.md +192 -0
  14. package/agents/gsd-doc-synthesizer.compact.md +200 -0
  15. package/agents/gsd-doc-verifier.compact.md +143 -0
  16. package/agents/gsd-doc-writer.compact.md +440 -0
  17. package/agents/gsd-dom-verifier.compact.md +138 -0
  18. package/agents/gsd-domain-researcher.compact.md +141 -0
  19. package/agents/gsd-eval-auditor.compact.md +160 -0
  20. package/agents/gsd-eval-planner.compact.md +137 -0
  21. package/agents/gsd-executor.md +63 -35
  22. package/agents/gsd-framework-selector.compact.md +82 -0
  23. package/agents/gsd-integration-checker.compact.md +245 -0
  24. package/agents/gsd-intel-updater.compact.md +226 -0
  25. package/agents/gsd-mempalace-curator.compact.md +45 -0
  26. package/agents/gsd-nyquist-auditor.compact.md +179 -0
  27. package/agents/gsd-pattern-mapper.compact.md +275 -0
  28. package/agents/gsd-plan-checker.md +76 -57
  29. package/agents/gsd-planner.md +14 -0
  30. package/agents/gsd-project-researcher.compact.md +587 -0
  31. package/agents/gsd-research-synthesizer.compact.md +212 -0
  32. package/agents/gsd-roadmapper.compact.md +454 -0
  33. package/agents/gsd-roadmapper.md +13 -0
  34. package/agents/gsd-security-auditor.compact.md +162 -0
  35. package/agents/gsd-ui-auditor.compact.md +404 -0
  36. package/agents/gsd-ui-checker.compact.md +277 -0
  37. package/agents/gsd-ui-checker.md +19 -3
  38. package/agents/gsd-ui-researcher.compact.md +282 -0
  39. package/agents/gsd-ui-researcher.md +29 -0
  40. package/agents/gsd-user-profiler.compact.md +108 -0
  41. package/agents/gsd-verifier.md +23 -1
  42. package/bin/install.js +444 -134
  43. package/commands/gsd/cleanup.md +1 -0
  44. package/commands/gsd/code-review.md +2 -1
  45. package/commands/gsd/complete-milestone.md +1 -0
  46. package/commands/gsd/config.md +1 -0
  47. package/commands/gsd/debug.md +1 -0
  48. package/commands/gsd/execute-phase.md +1 -1
  49. package/commands/gsd/graphify.md +1 -0
  50. package/commands/gsd/health.md +1 -0
  51. package/commands/gsd/mempalace-capture.md +1 -0
  52. package/commands/gsd/mempalace-recall.md +1 -0
  53. package/commands/gsd/new-milestone.md +1 -0
  54. package/commands/gsd/new-project.md +1 -0
  55. package/commands/gsd/next.md +1 -0
  56. package/commands/gsd/ns-workflow.md +2 -1
  57. package/commands/gsd/pause-work.md +1 -0
  58. package/commands/gsd/phase.md +2 -1
  59. package/commands/gsd/pr-branch.md +1 -0
  60. package/commands/gsd/quick-batch.md +105 -0
  61. package/commands/gsd/resume-work.md +1 -0
  62. package/commands/gsd/review-backlog.md +1 -0
  63. package/commands/gsd/settings.md +2 -1
  64. package/commands/gsd/stats.md +1 -0
  65. package/commands/gsd/surface.md +18 -8
  66. package/commands/gsd/thread.md +1 -0
  67. package/commands/gsd/workspace.md +1 -0
  68. package/commands/gsd/workstreams.md +1 -0
  69. package/gsd-core/bin/check-latest-version.cjs +8 -3
  70. package/gsd-core/bin/gsd-tools.cjs +532 -174
  71. package/gsd-core/bin/lib/adr-parser.cjs +1 -1
  72. package/gsd-core/bin/lib/artifacts.cjs +2 -1
  73. package/gsd-core/bin/lib/audit.cjs +39 -22
  74. package/gsd-core/bin/lib/broken-windows.cjs +168 -49
  75. package/gsd-core/bin/lib/capability-activation.cjs +27 -0
  76. package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
  77. package/gsd-core/bin/lib/capability-loader.cjs +135 -1
  78. package/gsd-core/bin/lib/capability-registry.cjs +528 -116
  79. package/gsd-core/bin/lib/capability-source.cjs +19 -2
  80. package/gsd-core/bin/lib/capability-state.cjs +7 -1
  81. package/gsd-core/bin/lib/capability-validator.cjs +134 -5
  82. package/gsd-core/bin/lib/capability-writer.cjs +14 -4
  83. package/gsd-core/bin/lib/check-command-router.cjs +198 -38
  84. package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
  85. package/gsd-core/bin/lib/clusters.cjs +1 -0
  86. package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
  87. package/gsd-core/bin/lib/command-aliases.cjs +16 -0
  88. package/gsd-core/bin/lib/commands.cjs +981 -79
  89. package/gsd-core/bin/lib/config-loader.cjs +4 -0
  90. package/gsd-core/bin/lib/config.cjs +153 -38
  91. package/gsd-core/bin/lib/core-utils.cjs +34 -7
  92. package/gsd-core/bin/lib/coverage.cjs +1 -1
  93. package/gsd-core/bin/lib/decisions.cjs +343 -28
  94. package/gsd-core/bin/lib/edge-probe.cjs +14 -1
  95. package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
  96. package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
  97. package/gsd-core/bin/lib/frontmatter.cjs +137 -23
  98. package/gsd-core/bin/lib/gap-checker.cjs +22 -13
  99. package/gsd-core/bin/lib/git-base-branch.cjs +10 -2
  100. package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
  101. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
  102. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +54 -11
  103. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +87 -23
  104. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
  105. package/gsd-core/bin/lib/host-integration.cjs +57 -5
  106. package/gsd-core/bin/lib/init-command-router.cjs +14 -0
  107. package/gsd-core/bin/lib/init.cjs +539 -60
  108. package/gsd-core/bin/lib/install-engine.cjs +199 -14
  109. package/gsd-core/bin/lib/install-model-override-resolver.cjs +45 -0
  110. package/gsd-core/bin/lib/install-profiles.cjs +36 -14
  111. package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
  112. package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
  113. package/gsd-core/bin/lib/io.cjs +35 -0
  114. package/gsd-core/bin/lib/loop-resolver.cjs +64 -39
  115. package/gsd-core/bin/lib/markdown-table.cjs +123 -0
  116. package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
  117. package/gsd-core/bin/lib/milestone.cjs +41 -10
  118. package/gsd-core/bin/lib/model-resolver.cjs +101 -10
  119. package/gsd-core/bin/lib/phase-command-router.cjs +20 -7
  120. package/gsd-core/bin/lib/phase-id.cjs +412 -31
  121. package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
  122. package/gsd-core/bin/lib/phase.cjs +941 -98
  123. package/gsd-core/bin/lib/plan-document.cjs +10 -0
  124. package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
  125. package/gsd-core/bin/lib/planning-snapshot.cjs +206 -30
  126. package/gsd-core/bin/lib/planning-workspace.cjs +153 -29
  127. package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
  128. package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
  129. package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
  130. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
  131. package/gsd-core/bin/lib/quick-batch.cjs +840 -0
  132. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
  133. package/gsd-core/bin/lib/research-store.cjs +11 -12
  134. package/gsd-core/bin/lib/review-lane-descriptor.cjs +53 -5
  135. package/gsd-core/bin/lib/review-lane-invocation.cjs +96 -1
  136. package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
  137. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
  138. package/gsd-core/bin/lib/roadmap-parser.cjs +555 -41
  139. package/gsd-core/bin/lib/roadmap.cjs +292 -69
  140. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +260 -43
  141. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +28 -20
  142. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +294 -108
  143. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -47
  144. package/gsd-core/bin/lib/security.cjs +126 -7
  145. package/gsd-core/bin/lib/shell-command-projection.cjs +4 -0
  146. package/gsd-core/bin/lib/smart-entry.cjs +7 -9
  147. package/gsd-core/bin/lib/state-document.cjs +159 -32
  148. package/gsd-core/bin/lib/state-md-schema.cjs +44 -27
  149. package/gsd-core/bin/lib/state-transition.cjs +465 -62
  150. package/gsd-core/bin/lib/state.cjs +906 -151
  151. package/gsd-core/bin/lib/surface.cjs +83 -10
  152. package/gsd-core/bin/lib/task-command-router.cjs +12 -6
  153. package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
  154. package/gsd-core/bin/lib/uat.cjs +1420 -516
  155. package/gsd-core/bin/lib/update-context.cjs +36 -26
  156. package/gsd-core/bin/lib/validate.cjs +230 -12
  157. package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
  158. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  159. package/gsd-core/bin/lib/verification.cjs +316 -23
  160. package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
  161. package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
  162. package/gsd-core/bin/lib/verify.cjs +531 -36
  163. package/gsd-core/bin/lib/workstream-inventory.cjs +21 -2
  164. package/gsd-core/bin/lib/worktree-safety.cjs +21 -7
  165. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  166. package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
  167. package/gsd-core/bin/verify-reapply-patches.cjs +507 -81
  168. package/gsd-core/references/agent-contracts.md +3 -3
  169. package/gsd-core/references/compact-content-gate.md +66 -0
  170. package/gsd-core/references/edge-probe.md +17 -13
  171. package/gsd-core/references/execute-mvp-tdd.md +18 -16
  172. package/gsd-core/references/execute-phase-response-language.md +6 -0
  173. package/gsd-core/references/executor-examples.md +42 -0
  174. package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
  175. package/gsd-core/references/loop-hook-dispatch.md +18 -0
  176. package/gsd-core/references/model-profiles.md +12 -3
  177. package/gsd-core/references/mvp-concepts.md +2 -2
  178. package/gsd-core/references/plan-checker-examples.md +41 -0
  179. package/gsd-core/references/planner-antipatterns.md +25 -0
  180. package/gsd-core/references/planner-chunked.md +5 -1
  181. package/gsd-core/references/planner-coupling.md +42 -0
  182. package/gsd-core/references/planner-quick-batch.md +71 -0
  183. package/gsd-core/references/planner-reviews.md +47 -0
  184. package/gsd-core/references/planner-revision.md +75 -2
  185. package/gsd-core/references/planning-config.md +5 -1
  186. package/gsd-core/references/response-language-directive.md +9 -0
  187. package/gsd-core/references/revision-loop.md +118 -11
  188. package/gsd-core/references/tdd.md +17 -9
  189. package/gsd-core/references/thinking-models-planning.md +18 -2
  190. package/gsd-core/references/verification-patterns.md +17 -4
  191. package/gsd-core/references/verifier-evidence-gate.md +160 -0
  192. package/gsd-core/references/worktree-path-safety.md +112 -2
  193. package/gsd-core/templates/README.md +7 -1
  194. package/gsd-core/templates/phase-prompt.md +4 -0
  195. package/gsd-core/templates/state.md +6 -3
  196. package/gsd-core/templates/summary.compact.md +212 -0
  197. package/gsd-core/templates/user-setup.compact.md +199 -0
  198. package/gsd-core/templates/user-setup.md +0 -9
  199. package/gsd-core/templates/verification-report.md +5 -0
  200. package/gsd-core/workflows/add-backlog.md +2 -0
  201. package/gsd-core/workflows/add-phase.md +2 -0
  202. package/gsd-core/workflows/add-tests.md +1 -1
  203. package/gsd-core/workflows/add-todo.md +4 -3
  204. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  205. package/gsd-core/workflows/analyze-dependencies.md +2 -0
  206. package/gsd-core/workflows/audit-fix.md +2 -0
  207. package/gsd-core/workflows/audit-milestone.md +2 -0
  208. package/gsd-core/workflows/audit-uat.md +2 -0
  209. package/gsd-core/workflows/autonomous.md +15 -10
  210. package/gsd-core/workflows/check-todos.md +5 -3
  211. package/gsd-core/workflows/cleanup.md +4 -2
  212. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +22 -13
  213. package/gsd-core/workflows/code-review-fix.md +5 -3
  214. package/gsd-core/workflows/code-review.md +211 -43
  215. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
  216. package/gsd-core/workflows/complete-milestone.md +40 -254
  217. package/gsd-core/workflows/debug.md +1 -1
  218. package/gsd-core/workflows/diagnose-issues.md +5 -1
  219. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -0
  220. package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
  221. package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
  222. package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
  223. package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
  224. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -0
  225. package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
  226. package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
  227. package/gsd-core/workflows/discuss-phase/modes/text.md +2 -0
  228. package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
  229. package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
  230. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  231. package/gsd-core/workflows/discuss-phase-power.md +2 -0
  232. package/gsd-core/workflows/discuss-phase.md +1 -1
  233. package/gsd-core/workflows/do.md +43 -13
  234. package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
  235. package/gsd-core/workflows/docs-update.md +15 -156
  236. package/gsd-core/workflows/edit-phase.md +2 -0
  237. package/gsd-core/workflows/eval-review.md +1 -1
  238. package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
  239. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +20 -3
  240. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
  241. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +24 -3
  242. package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
  243. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +8 -2
  244. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -0
  245. package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
  246. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
  247. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
  248. package/gsd-core/workflows/execute-phase.md +78 -159
  249. package/gsd-core/workflows/execute-plan.md +28 -15
  250. package/gsd-core/workflows/explore.md +2 -0
  251. package/gsd-core/workflows/extract-learnings.md +2 -0
  252. package/gsd-core/workflows/fast.md +6 -0
  253. package/gsd-core/workflows/forensics.md +2 -0
  254. package/gsd-core/workflows/graduation.md +1 -1
  255. package/gsd-core/workflows/health.md +1 -1
  256. package/gsd-core/workflows/help/modes/brief.md +2 -0
  257. package/gsd-core/workflows/help/modes/default.md +2 -0
  258. package/gsd-core/workflows/help/modes/full.compact.md +398 -0
  259. package/gsd-core/workflows/help/modes/full.md +12 -0
  260. package/gsd-core/workflows/help/modes/topic.md +2 -0
  261. package/gsd-core/workflows/help.md +3 -1
  262. package/gsd-core/workflows/import.md +3 -3
  263. package/gsd-core/workflows/inbox.md +1 -1
  264. package/gsd-core/workflows/ingest-docs.md +1 -1
  265. package/gsd-core/workflows/insert-phase.md +2 -0
  266. package/gsd-core/workflows/list-phase-assumptions.md +2 -0
  267. package/gsd-core/workflows/list-seeds.md +2 -0
  268. package/gsd-core/workflows/list-workspaces.md +2 -0
  269. package/gsd-core/workflows/manager.md +3 -3
  270. package/gsd-core/workflows/map-codebase.md +52 -3
  271. package/gsd-core/workflows/milestone-summary.md +2 -0
  272. package/gsd-core/workflows/mvp-phase.md +1 -1
  273. package/gsd-core/workflows/new-milestone.md +55 -13
  274. package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
  275. package/gsd-core/workflows/new-project.md +37 -205
  276. package/gsd-core/workflows/new-workspace.md +1 -1
  277. package/gsd-core/workflows/next.md +2 -0
  278. package/gsd-core/workflows/node-repair.md +2 -0
  279. package/gsd-core/workflows/note.md +2 -0
  280. package/gsd-core/workflows/onboard.md +1 -1
  281. package/gsd-core/workflows/pause-work.md +19 -4
  282. package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
  283. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
  284. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -0
  285. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +9 -0
  286. package/gsd-core/workflows/plan-phase.md +144 -185
  287. package/gsd-core/workflows/plan-review-convergence.md +102 -10
  288. package/gsd-core/workflows/plant-seed.md +1 -1
  289. package/gsd-core/workflows/pr-branch.md +30 -10
  290. package/gsd-core/workflows/profile-user.md +1 -1
  291. package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
  292. package/gsd-core/workflows/progress.md +25 -3
  293. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +37 -2
  294. package/gsd-core/workflows/quick/steps/research-phase.md +3 -3
  295. package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
  296. package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
  297. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
  298. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
  299. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
  300. package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
  301. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
  302. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
  303. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
  304. package/gsd-core/workflows/quick-batch.md +203 -0
  305. package/gsd-core/workflows/quick.md +21 -4
  306. package/gsd-core/workflows/reapply-patches.md +79 -3
  307. package/gsd-core/workflows/remove-phase.md +2 -0
  308. package/gsd-core/workflows/remove-workspace.md +1 -1
  309. package/gsd-core/workflows/resume-project.md +6 -2
  310. package/gsd-core/workflows/review.md +215 -10
  311. package/gsd-core/workflows/scan.md +2 -0
  312. package/gsd-core/workflows/section-manifest.json +12 -0
  313. package/gsd-core/workflows/secure-phase.md +1 -1
  314. package/gsd-core/workflows/session-report.md +2 -0
  315. package/gsd-core/workflows/settings-advanced.md +2 -0
  316. package/gsd-core/workflows/settings-integrations.md +9 -8
  317. package/gsd-core/workflows/settings.md +19 -6
  318. package/gsd-core/workflows/ship.md +10 -10
  319. package/gsd-core/workflows/sketch-wrap-up.md +2 -0
  320. package/gsd-core/workflows/sketch.md +1 -1
  321. package/gsd-core/workflows/smart-entry.md +1 -1
  322. package/gsd-core/workflows/spec-phase.md +24 -19
  323. package/gsd-core/workflows/spike-wrap-up.md +2 -0
  324. package/gsd-core/workflows/spike.md +1 -1
  325. package/gsd-core/workflows/stats.md +2 -0
  326. package/gsd-core/workflows/sync-skills.md +12 -4
  327. package/gsd-core/workflows/thread.md +2 -0
  328. package/gsd-core/workflows/transition.md +2 -0
  329. package/gsd-core/workflows/ui-phase.md +26 -5
  330. package/gsd-core/workflows/ui-review.md +1 -1
  331. package/gsd-core/workflows/ultraplan-phase.md +2 -0
  332. package/gsd-core/workflows/undo.md +1 -1
  333. package/gsd-core/workflows/update.md +48 -43
  334. package/gsd-core/workflows/validate-phase.md +1 -1
  335. package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
  336. package/gsd-core/workflows/verify-work.md +68 -182
  337. package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
  338. package/hooks/dist/gsd-check-update-worker.js +19 -2
  339. package/hooks/dist/gsd-context-monitor.js +371 -27
  340. package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
  341. package/hooks/dist/gsd-node-runner.sh +1 -0
  342. package/hooks/dist/gsd-prompt-guard.js +30 -5
  343. package/hooks/dist/gsd-read-guard.js +2 -0
  344. package/hooks/dist/gsd-read-injection-scanner.js +5 -5
  345. package/hooks/dist/gsd-secret-read-guard.js +1105 -0
  346. package/hooks/dist/gsd-statusline.js +18 -10
  347. package/hooks/dist/gsd-validate-commit.sh +474 -7
  348. package/hooks/dist/gsd-workflow-guard.js +2 -1
  349. package/hooks/dist/gsd-worktree-path-guard.js +25 -14
  350. package/hooks/dist/gsd-write-guard.js +46 -1
  351. package/hooks/dist/lib/dispatch-identity.js +187 -0
  352. package/hooks/dist/lib/filename-classification.js +64 -0
  353. package/hooks/dist/lib/git-cmd.js +210 -1
  354. package/hooks/dist/lib/injection-patterns.js +36 -6
  355. package/hooks/dist/lib/isolation-deny-reason.js +53 -1
  356. package/hooks/dist/lib/isolation-sentinel.js +58 -19
  357. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  358. package/hooks/gsd-agent-isolation-guard.js +42 -16
  359. package/hooks/gsd-check-update-worker.js +19 -2
  360. package/hooks/gsd-context-monitor.js +371 -27
  361. package/hooks/gsd-cursor-subagent-start.js +34 -14
  362. package/hooks/gsd-node-runner.sh +1 -0
  363. package/hooks/gsd-prompt-guard.js +30 -5
  364. package/hooks/gsd-read-guard.js +2 -0
  365. package/hooks/gsd-read-injection-scanner.js +5 -5
  366. package/hooks/gsd-secret-read-guard.js +1105 -0
  367. package/hooks/gsd-statusline.js +18 -10
  368. package/hooks/gsd-validate-commit.sh +474 -7
  369. package/hooks/gsd-workflow-guard.js +2 -1
  370. package/hooks/gsd-worktree-path-guard.js +25 -14
  371. package/hooks/gsd-write-guard.js +46 -1
  372. package/hooks/hooks.json +6 -0
  373. package/hooks/lib/dispatch-identity.js +187 -0
  374. package/hooks/lib/filename-classification.js +64 -0
  375. package/hooks/lib/git-cmd.js +210 -1
  376. package/hooks/lib/injection-patterns.js +36 -6
  377. package/hooks/lib/isolation-deny-reason.js +53 -1
  378. package/hooks/lib/isolation-sentinel.js +58 -19
  379. package/hooks/managed-hooks-registry.cjs +1 -0
  380. package/package.json +13 -9
  381. package/scripts/benchmark-compact-content-variants.cjs +298 -0
  382. package/scripts/benchmark-compact-content.cjs +368 -0
  383. package/scripts/build-hooks.js +11 -4
  384. package/scripts/check-contract-drift.cjs +4 -1
  385. package/scripts/check-env.cjs +36 -8
  386. package/scripts/check-glossary-refs.cjs +25 -21
  387. package/scripts/ci-next-health.cjs +271 -0
  388. package/scripts/ci-prepare-test-scope.cjs +7 -7
  389. package/scripts/ci-test-scope.cjs +133 -20
  390. package/scripts/ci-timeout-report.cjs +1 -1
  391. package/scripts/diff-touches-shipped-paths.cjs +1 -1
  392. package/scripts/docs-guard-registry.cjs +17 -2
  393. package/scripts/gen-adr-index.cjs +8 -2
  394. package/scripts/gen-inventory-manifest.cjs +12 -0
  395. package/scripts/gen-loop-host-contract.cjs +67 -15
  396. package/scripts/gen-platform-conformance-tier.cjs +557 -0
  397. package/scripts/lib/drift-scan.cjs +1 -1
  398. package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
  399. package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
  400. package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
  401. package/scripts/lib/shellcheck-fetch.cjs +247 -0
  402. package/scripts/lib/suite-detection.cjs +32 -0
  403. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
  404. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  405. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  406. package/scripts/lint-allowed-tools-parity.cjs +221 -0
  407. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +24 -2
  408. package/scripts/lint-phase-enumeration-drift.cjs +24 -6
  409. package/scripts/lint-phase-id-drift.cjs +465 -15
  410. package/scripts/lint-portable-grep.cjs +176 -0
  411. package/scripts/lint-response-language-coverage.cjs +530 -0
  412. package/scripts/lint-source-test-name-collision.cjs +1 -1
  413. package/scripts/lint-test-file-count.allowlist.json +4 -1
  414. package/scripts/lint-vendored-deps.cjs +128 -17
  415. package/scripts/lint-workflow-shellcheck-baseline.json +1112 -0
  416. package/scripts/lint-workflow-shellcheck.cjs +614 -0
  417. package/scripts/npm-audit-baseline.cjs +376 -0
  418. package/scripts/prompt-injection-scan.sh +22 -0
  419. package/scripts/require-issue-link-policy.cjs +16 -1
  420. package/scripts/workflow-size.cjs +139 -0
  421. package/skills/gsd-cleanup/SKILL.md +1 -0
  422. package/skills/gsd-code-review/SKILL.md +2 -1
  423. package/skills/gsd-complete-milestone/SKILL.md +1 -0
  424. package/skills/gsd-config/SKILL.md +1 -0
  425. package/skills/gsd-debug/SKILL.md +1 -0
  426. package/skills/gsd-execute-phase/SKILL.md +1 -1
  427. package/skills/gsd-graphify/SKILL.md +1 -0
  428. package/skills/gsd-health/SKILL.md +1 -0
  429. package/skills/gsd-mempalace-capture/SKILL.md +1 -0
  430. package/skills/gsd-mempalace-recall/SKILL.md +1 -0
  431. package/skills/gsd-new-milestone/SKILL.md +1 -0
  432. package/skills/gsd-new-project/SKILL.md +1 -0
  433. package/skills/gsd-next/SKILL.md +1 -0
  434. package/skills/gsd-ns-workflow/SKILL.md +1 -0
  435. package/skills/gsd-pause-work/SKILL.md +1 -0
  436. package/skills/gsd-phase/SKILL.md +2 -1
  437. package/skills/gsd-pr-branch/SKILL.md +1 -0
  438. package/skills/gsd-quick-batch/SKILL.md +105 -0
  439. package/skills/gsd-resume-work/SKILL.md +1 -0
  440. package/skills/gsd-review-backlog/SKILL.md +1 -0
  441. package/skills/gsd-settings/SKILL.md +2 -1
  442. package/skills/gsd-stats/SKILL.md +1 -0
  443. package/skills/gsd-surface/SKILL.md +18 -8
  444. package/skills/gsd-thread/SKILL.md +1 -0
  445. package/skills/gsd-workspace/SKILL.md +1 -0
  446. package/skills/gsd-workstreams/SKILL.md +1 -0
  447. package/vscode/package.json +1 -1
  448. package/gsd-core/templates/claude-md.md +0 -145
  449. package/gsd-core/templates/codebase/concerns.md +0 -310
  450. package/gsd-core/templates/codebase/conventions.md +0 -307
  451. package/gsd-core/templates/codebase/integrations.md +0 -280
  452. package/gsd-core/templates/codebase/structure.md +0 -285
  453. package/gsd-core/templates/codebase/testing.md +0 -480
  454. package/gsd-core/templates/debug-subagent-prompt.md +0 -91
  455. package/gsd-core/templates/discovery.md +0 -146
@@ -14,6 +14,9 @@
14
14
  // Thresholds:
15
15
  // WARNING (remaining <= 35%): Agent should wrap up current task
16
16
  // CRITICAL (remaining <= 25%): Agent should stop immediately and save state
17
+ // Both fire-points are overridable per project via .planning/config.json
18
+ // (hooks.context_warning_threshold / hooks.context_critical_threshold, #4285);
19
+ // the values above are the defaults used when the keys are absent or unusable.
17
20
  //
18
21
  // Debounce: 5 tool uses between warnings to avoid spam
19
22
  // Severity escalation bypasses debounce (WARNING -> CRITICAL fires immediately)
@@ -30,20 +33,180 @@ const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
30
33
  // context warning is far cheaper than stalling the agent's work (#3911).
31
34
  const ON_CRASH = HOOK_ON_CRASH.ALLOW;
32
35
 
33
- const WARNING_THRESHOLD = 35; // remaining_percentage <= 35%
34
- const CRITICAL_THRESHOLD = 25; // remaining_percentage <= 25%
36
+ const WARNING_THRESHOLD = 35; // remaining_percentage <= 35% (default, see resolveThresholds)
37
+ const CRITICAL_THRESHOLD = 25; // remaining_percentage <= 25% (default, see resolveThresholds)
35
38
  const STALE_SECONDS = 60; // ignore metrics older than 60s
36
39
  const DEBOUNCE_CALLS = 5; // min tool uses between warnings
40
+ // How long after a PreCompact readings stay suspect. The watermark records the
41
+ // compaction's START; the compaction keeps running after it, and a statusline
42
+ // render during it stamps the PRE-compaction reading with a CURRENT timestamp
43
+ // (Codex review of #3808, round 3) — so "newer than the watermark" alone still
44
+ // admits it. Everything inside this window is dropped instead. The cost is
45
+ // bounded: a healthy reading dropped here behaves identically to an accepted
46
+ // one (it would exit above-threshold anyway). A genuine exhaustion reading
47
+ // inside the window is SKIPPED, not queued — its warning and its #1974
48
+ // breadcrumb both fire on the next reading after the window, so they are
49
+ // delayed by at most this window plus the accepted skew below when a later
50
+ // reading comes, and lost when
51
+ // none does, i.e. when the session ends inside the window (review of #3808,
52
+ // round 9). That loss is accepted over the alternative, which is trusting a
53
+ // reading that may be the pre-compaction value under a fresh timestamp.
54
+ const COMPACT_GRACE_SECONDS = 60;
55
+ // How far AHEAD of this process's clock a watermark may be and still be
56
+ // honored. PreCompact stamps it from the same clock as the reader, so the
57
+ // legitimate skew is 0; this tolerance only absorbs a clock step. It is a
58
+ // THRESHOLD, so it is named rather than inlined and carries its own boundary
59
+ // trio (Codex review of #3808, round 4). Note it also extends the mute: a
60
+ // watermark this far ahead pushes first recovery from +61 to +66 (measured).
61
+ const WATERMARK_SKEW_SECONDS = 5;
62
+
63
+ // Resolve the two fire-points from the project's `.planning/config.json`
64
+ // (#4285). The constants above are the DEFAULTS; a project overrides either one
65
+ // through `hooks.context_warning_threshold` / `hooks.context_critical_threshold`,
66
+ // which is what keeps a tuned fire-point alive across updates — this file is in
67
+ // the MANAGED registry, so an edit to the constants is re-staged away by the
68
+ // next install.
69
+ //
70
+ // TOTAL and never-throwing: this hook must not block the tool call it rides in
71
+ // on, so every unusable input degrades to the default instead of raising.
72
+ // Unusable is decided by Number.isFinite, which is type-strict (the string
73
+ // "30" and true are both rejected, unlike the global isFinite), plus the 0-100
74
+ // domain of the remaining_percentage these are compared against.
75
+ //
76
+ // The PAIR is validated too, and falls back TOGETHER. `critical >= warning` has
77
+ // no coherent reading — critical fires deeper into the window than warning —
78
+ // and honouring one side of an inconsistent pair silently picks which of the
79
+ // operator's two numbers to discard. This also rejects a single override that
80
+ // contradicts the OTHER key's default (warning 20 with critical absent, i.e.
81
+ // 25); the resulting pair is the same nonsense either way. Set-time validation
82
+ // cannot stand in for this check: `config-set` writes one key per call, so
83
+ // tuning both (warning first, then critical) is transiently inconsistent on
84
+ // disk, and refusing it there would block a legitimate configuration.
85
+ function resolveThresholds(hooks) {
86
+ const defaults = { warning: WARNING_THRESHOLD, critical: CRITICAL_THRESHOLD };
87
+ if (!hooks || typeof hooks !== 'object') return defaults;
88
+
89
+ const usable = (value, fallback) =>
90
+ (Number.isFinite(value) && value >= 0 && value <= 100) ? value : fallback;
91
+
92
+ const warning = usable(hooks.context_warning_threshold, WARNING_THRESHOLD);
93
+ const critical = usable(hooks.context_critical_threshold, CRITICAL_THRESHOLD);
94
+
95
+ return critical < warning ? { warning, critical } : defaults;
96
+ }
97
+
98
+ // One DEFINITION of what counts as a lifecycle event name, shared by the #3709
99
+ // PreCompact reset and the #2289 output-envelope allowlist. Two call sites, one
100
+ // rule — so the two cannot drift into disagreeing about what "no event name" is.
101
+ // TOTAL, and STRICT about type: only an actual string is an event name. The old
102
+ // inline expression threw on a truthy non-string, and hoisting it ahead of the
103
+ // pipeline would have moved that throw ahead of the side effects #2289
104
+ // documents as always running; a String() coercion is no better — it renders
105
+ // ['PreCompact'] as 'PreCompact' and would run the reset off a malformed
106
+ // payload, and a hostile toString still throws (Codex review of #3808,
107
+ // round 3). typeof does neither: any non-string reads as "no event" — silent,
108
+ // side effects intact — on both call sites.
109
+ function readEventName(data) {
110
+ const name = data && data.hook_event_name;
111
+ if (typeof name === 'string') return name.trim();
112
+ // ABSENT vs MALFORMED are not the same event (Codex review of #3808, round 7,
113
+ // measured base-vs-head). A MISSING name is the documented pre-#2289 Gemini
114
+ // fallback: under GEMINI_API_KEY it means AfterTool and still emits. A name
115
+ // that is PRESENT but not a string is a malformed payload and must not
116
+ // inherit that fallback — at the merge-base it threw on `.trim()` after the
117
+ // side effects, so no envelope was ever produced, and collapsing both onto ''
118
+ // silently turned `42`, `{}` and `['PreCompact']` into emitting AfterTool
119
+ // events. Measured: base silent, head emitted, for both `42` and
120
+ // `['PreCompact']`. null keeps them distinguishable while staying unequal to
121
+ // every event name, so the PreCompact reset and the allowlist below are
122
+ // byte-for-byte unchanged for every well-formed payload.
123
+ return (name === undefined || name === null) ? '' : null;
124
+ }
125
+
126
+ // SENTINEL WRITE HARDENING (review of #3808, round 7). `warnPath` lives in
127
+ // os.tmpdir(), which may resolve to a shared sticky directory — not guaranteed
128
+ // per-user, and the file persists across invocations — so an object already
129
+ // sitting there may be a planted symlink. The three routine debounce-accounting writes were bare
130
+ // writeFileSync, which follows one and writes through to its target, while the
131
+ // PreCompact clear and the compaction watermark in this same file already
132
+ // refuse to. Unlink-then-O_EXCL is the watermark's own shape (the watermark
133
+ // write itself now calls this helper — review round 10): the unlink
134
+ // removes any existing object (regular file or link) and O_EXCL then refuses
135
+ // to create through one, so the write can only ever land on a fresh regular
136
+ // file this process made. Best effort by design — a lost sentinel write costs
137
+ // only debounce accounting, which is never worth breaking the hook over, so
138
+ // every failure is swallowed exactly as the watermark write's is.
139
+ // NOT an atomic read-modify-write, and not claimed to be (Codex review of
140
+ // #3808, round 7): two concurrent invocations can read the same state and race
141
+ // through unlink/create, so one invocation's accounting can be lost — the same
142
+ // lost-update race the bare writeFileSync already had, not a class this change
143
+ // introduces. What a lost write leaves behind is whatever the competing writer
144
+ // wrote, which may be a perfectly valid sentinel; it does not reliably mean
145
+ // "defaults on the next call". Advisory debounce bookkeeping is the right place
146
+ // to accept that.
147
+ // The read-side twin of writeSentinel (review of #3808, round 9). Both
148
+ // sentinel files this hook reads — the compaction watermark and the warn
149
+ // state — must be read the same way: lstat first so a planted link, FIFO or
150
+ // directory is refused before any open; O_NOFOLLOW so a link raced in between
151
+ // is refused by the kernel too (0 on Windows, where lstat already carries the
152
+ // check); a 4096-byte bound so a planted large file cannot stall a
153
+ // synchronous read. Rounds 4 and 7 each wrote that sequence inline at their
154
+ // own call site, which left two copies to keep in step by hand. One place
155
+ // now. Refusal THROWS; every caller already wraps the read in a try/catch and
156
+ // degrades to "no file", which is the same behaviour the inline copies had.
157
+ function readSentinel(target) {
158
+ const st = fs.lstatSync(target);
159
+ if (!st.isFile() || st.size > 4096) throw new Error('not a plain sentinel');
160
+ const fd = fs.openSync(target, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
161
+ try {
162
+ const buf = Buffer.alloc(st.size);
163
+ // The RETURN VALUE, not just the call (review of #3808, round 11). A file that shrinks
164
+ // between the lstat above and this read — a concurrent legitimate writer truncating
165
+ // mid-write, not the planted-object case the rest of this function guards — leaves the tail
166
+ // of `buf` zero-filled, and those NULs reach JSON.parse as garbage. Every caller already
167
+ // treats a throw here as "no file", so refusing a short read is both safer and the same
168
+ // outcome the caller would reach one line later, stated on purpose rather than by accident.
169
+ const bytesRead = fs.readSync(fd, buf, 0, st.size, 0);
170
+ if (bytesRead !== st.size) throw new Error('sentinel shrank under the read');
171
+ return buf.toString('utf8');
172
+ } finally { fs.closeSync(fd); }
173
+ }
174
+
175
+ function writeSentinel(target, payload) {
176
+ try {
177
+ try {
178
+ fs.unlinkSync(target);
179
+ } catch (e) {
180
+ if (!e || e.code !== 'ENOENT') throw e;
181
+ }
182
+ const fd = fs.openSync(
183
+ target,
184
+ fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL
185
+ );
186
+ try {
187
+ // LOOP, and check progress (Codex review of round 11). A single `fs.writeSync` is
188
+ // permitted to write fewer bytes than it was given, and the return value was discarded —
189
+ // a short write left a truncated sentinel that JSON.parse rejects, silently defeating the
190
+ // debounce accounting or the compaction watermark this write exists to record. Node's own
191
+ // `writeFileSync` loops for exactly this reason; the explicit no-progress guard keeps a
192
+ // pathological fd from spinning. Symmetric with the bytesRead check in readSentinel.
193
+ const buf = Buffer.from(payload, 'utf8');
194
+ let written = 0;
195
+ while (written < buf.length) {
196
+ const n = fs.writeSync(fd, buf, written, buf.length - written);
197
+ if (!(n > 0)) throw new Error('sentinel write made no progress');
198
+ written += n;
199
+ }
200
+ } finally { fs.closeSync(fd); }
201
+ } catch (e) { /* best effort — see above */ }
202
+ }
37
203
 
38
204
  let input = '';
39
- // Timeout guard: if stdin doesn't close within 10s (e.g. pipe issues on
40
- // Windows/Git Bash, or slow Claude Code piping during large outputs),
41
- // exit silently instead of hanging until Claude Code kills the process
42
- // and reports "hook error". See #775, #1162.
43
- const stdinTimeout = setTimeout(() => allow(undefined), 10000);
44
- process.stdin.setEncoding('utf8');
45
- process.stdin.on('data', chunk => input += chunk);
46
- process.stdin.on('end', () => {
205
+ // Assigned by main(); the handler below clears it. Declared out here rather
206
+ // than inside main() because the handler closes over it.
207
+ let stdinTimeout = null;
208
+
209
+ const handleStdinEnd = () => {
47
210
  clearTimeout(stdinTimeout);
48
211
  try {
49
212
  const data = JSON.parse(input);
@@ -60,29 +223,122 @@ process.stdin.on('end', () => {
60
223
  allow(undefined);
61
224
  }
62
225
 
63
- // Check if context warnings are disabled via config.
226
+ const tmpDir = os.tmpdir();
227
+ const warnPath = path.join(tmpDir, `claude-ctx-${sessionId}-warned.json`);
228
+ const metricsPath = path.join(tmpDir, `claude-ctx-${sessionId}.json`);
229
+ const watermarkPath = path.join(tmpDir, `claude-ctx-${sessionId}-compacted.json`);
230
+
231
+ // #3709: a compaction RESTARTS the context lifecycle, so neither the warn
232
+ // sentinel nor the pre-compaction statusline reading may survive it. Full
233
+ // rationale — what dies when the sentinel outlives a compaction, why the
234
+ // reset sits ahead of the config gate and the metrics read, and why an
235
+ // aborted compaction deliberately stays cleared — lives in ONE place:
236
+ // docs/context-monitor.md, "PreCompact reset". Constraints the code itself
237
+ // must keep are stated at their lines below.
238
+ if (readEventName(data) === 'PreCompact') {
239
+ // ORDERING ASSUMPTION, stated rather than enforced (review of #3808,
240
+ // round 9): this reset and the debounce writeSentinel(warnPath) further
241
+ // down are two writers to the same file, and nothing here serialises
242
+ // them. A debounce invocation that read the pre-compaction state and
243
+ // lands its write AFTER this unlink would resurrect exactly the stale
244
+ // sentinel this block removes. The hook relies on the host dispatching a
245
+ // session's hooks one at a time, which Claude Code does; the other
246
+ // runtimes this hook is installed for are assumed to, and that is not
247
+ // tested. A lock file would close it at the cost of a second file to
248
+ // harden on every platform; not taken here.
249
+ // BOTH files: with the sentinel gone but the bridge still holding the
250
+ // pre-compaction reading (fresh for STALE_SECONDS), the next PostToolUse
251
+ // would fire a spurious CRITICAL off a context the compaction just freed
252
+ // (review of #3709).
253
+ for (const stale of [warnPath, metricsPath]) {
254
+ try {
255
+ fs.unlinkSync(stale);
256
+ } catch (e) {
257
+ if (e && e.code === 'ENOENT') continue; // already absent — that IS the reset
258
+ // Best-effort fallback for a held handle (Windows EPERM/EBUSY):
259
+ // truncate to EMPTY — the one state both readers treat exactly like
260
+ // deletion, because JSON.parse('') throws. A well-formed "neutral"
261
+ // value is NOT equivalent: '{}' debounces the first post-compaction
262
+ // warning, '{"timestamp":0}' is never stale (falsy guard) and emits
263
+ // "undefined%" (review of #3808). Never through a LINK: lstat
264
+ // rejects non-regular files on every platform (Windows has no
265
+ // effective O_NOFOLLOW — libuv defines it as 0 — and TEMP/TMP means
266
+ // its tmpdir is not guaranteed per-user); O_NOFOLLOW additionally
267
+ // closes the lstat→open substitution race where honored. Every
268
+ // refusal lands in this give-up arm — including a Windows runner
269
+ // refusing the write-open of a freshly written file outright —
270
+ // which is why the fallback is best-effort, never asserted-on.
271
+ try {
272
+ if (fs.lstatSync(stale).isFile()) {
273
+ fs.closeSync(fs.openSync(
274
+ stale,
275
+ fs.constants.O_WRONLY | fs.constants.O_TRUNC | (fs.constants.O_NOFOLLOW || 0)
276
+ ));
277
+ }
278
+ } catch (e2) { /* give up, never throw */ }
279
+ }
280
+ }
281
+
282
+ // COMPACTION WATERMARK (review of #3808, round 3). Deleting the bridge
283
+ // only NARROWS the stale-reading window: the statusline is an
284
+ // uncoordinated process that re-writes the bridge on every render, so a
285
+ // render landing between this clear and the compaction's completion
286
+ // re-creates the PRE-compaction reading with a CURRENT timestamp — and
287
+ // it would sail past STALE_SECONDS as freshly valid. The watermark makes
288
+ // the pre-compaction reading identifiable rather than merely absent: the
289
+ // metrics read drops any reading not strictly newer than it. Written
290
+ // through writeSentinel (review of #3808, round 10 — this block was the
291
+ // shape writeSentinel was lifted from in round 7 and kept its own copy):
292
+ // unlink-then-O_EXCL so an existing file — or a planted symlink — is
293
+ // never followed or overwritten in place; failure to write degrades to
294
+ // the old narrowing, never throws.
295
+ writeSentinel(watermarkPath, JSON.stringify({ at: Math.floor(Date.now() / 1000) }));
296
+ // allow(), not raw process.exit: #3911/ADR-3889 moved this hook onto the
297
+ // declared-policy exit vocabulary while this PR was in review, and the
298
+ // PreCompact branch is new here, so it needs the same conversion.
299
+ // A compaction is never blocked by this hook — ALLOW is the policy the
300
+ // rest of the file already declares.
301
+ allow(undefined);
302
+ }
303
+
304
+ // Check if context warnings are disabled via config, and resolve the two
305
+ // fire-points from the same read (#4285 — one config read, not two).
64
306
  // Collapsed existsSync+readFileSync into a single read guarded by try/catch
65
307
  // (ENOENT or parse error → use defaults, same as old "planningDir absent" branch).
66
308
  const cwd = data.cwd || process.cwd();
309
+ let thresholds = { warning: WARNING_THRESHOLD, critical: CRITICAL_THRESHOLD };
67
310
  try {
68
311
  const configPath = path.join(cwd, '.planning', 'config.json');
69
312
  const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
70
313
  if (config.hooks?.context_warnings === false) {
71
314
  allow(undefined);
72
315
  }
316
+ // After the disable check, not before: a disabled monitor exits above and
317
+ // never reaches a threshold, so resolving first would only add work to the
318
+ // path that does nothing. allow() exits the process (it does not throw),
319
+ // so this line is unreachable when warnings are off.
320
+ thresholds = resolveThresholds(config.hooks);
73
321
  } catch (e) {
74
- // Missing or unparseable config → proceed with defaults (context warnings enabled)
322
+ // Missing or unparseable config → proceed with defaults (context warnings
323
+ // enabled, thresholds at the constants above, which `thresholds` already holds)
75
324
  }
76
325
 
77
- const tmpDir = os.tmpdir();
78
- const metricsPath = path.join(tmpDir, `claude-ctx-${sessionId}.json`);
79
-
80
326
  // If no metrics file, this is a subagent or fresh session -- exit silently.
81
327
  // Collapsed existsSync+readFileSync: ENOENT → exit 0 (identical to old !existsSync branch),
82
328
  // other errors rethrow to the outer catch (swallowed → exit 0, as before).
329
+ //
330
+ // Through readSentinel, like the other two (review of #3808, round 11). This read was the
331
+ // asymmetry left in this file: `metricsPath` is built one line away from `warnPath` and
332
+ // `watermarkPath` (same tmpdir, same predictable `claude-ctx-{sessionId}` shape), it is the
333
+ // only one of the three read on EVERY invocation, and it was the only one still reached by a
334
+ // bare readFileSync — so the symlink-to-FIFO stall the other two are hardened against was
335
+ // still reachable here, on the highest-traffic path in the file. The 4096-byte bound is
336
+ // ample: the statusline writes four fixed fields (`gsd-statusline.js`, ~140 bytes with a
337
+ // UUID session id), so no legitimate bridge approaches it. A refusal throws and lands in the
338
+ // rethrow below exactly as an unreadable or malformed bridge already did.
83
339
  let metricsRaw;
84
340
  try {
85
- metricsRaw = fs.readFileSync(metricsPath, 'utf8');
341
+ metricsRaw = readSentinel(metricsPath);
86
342
  } catch (e) {
87
343
  if (e && e.code === 'ENOENT') allow(undefined);
88
344
  throw e;
@@ -90,6 +346,46 @@ process.stdin.on('end', () => {
90
346
  const metrics = JSON.parse(metricsRaw);
91
347
  const now = Math.floor(Date.now() / 1000);
92
348
 
349
+ // #3709 (round 3): a reading not clearly PAST the compaction is suspect,
350
+ // whatever its timestamp says — the statusline re-writes the bridge on
351
+ // every render, and a render during the compaction stamps the OLD
352
+ // remaining_percentage with a current time. The watermark records the
353
+ // compaction's START, so "newer than the watermark" alone still admits a
354
+ // mid-compaction render (Codex review of #3808, round 3): the grace
355
+ // window covers the compaction's own duration. `!(>)` rather than `<=` so
356
+ // a missing/zero/garbage timestamp is also dropped once a compaction has
357
+ // happened — an unstamped reading cannot prove it is post-compaction.
358
+ //
359
+ // The watermark itself must be SANE to count: one stamped in the future
360
+ // (a clock step backwards, a stray file) would otherwise drop every
361
+ // reading indefinitely and silently self-disable monitoring — so it is
362
+ // honored only when its own timestamp is not ahead of this process's
363
+ // clock (small skew allowed). No watermark, an unreadable one, or an
364
+ // insane one all degrade to the plain STALE_SECONDS behaviour below.
365
+ //
366
+ // READ HARDENING (Codex review of #3808, round 4). The WRITE side already
367
+ // refuses to follow or overwrite a planted object (unlink-then-O_EXCL
368
+ // above), but this read was a bare readFileSync — so on any write-side
369
+ // give-up the planted object survived and every later invocation followed
370
+ // it. In a shared sticky os.tmpdir() that is a mute primitive (a planted
371
+ // recent watermark suppresses monitoring) and a stall primitive (a symlink
372
+ // to a FIFO blocks this synchronous read indefinitely; measured: such a
373
+ // read is still running after 300ms). The same lstat + O_NOFOLLOW pair the
374
+ // sentinel path uses, plus a size bound, applied to the file this PR adds.
375
+ // Every refusal degrades to "no watermark", never throws.
376
+ try {
377
+ const watermark = JSON.parse(readSentinel(watermarkPath));
378
+ if (
379
+ watermark && typeof watermark.at === 'number'
380
+ && watermark.at <= now + WATERMARK_SKEW_SECONDS
381
+ && !(metrics.timestamp > watermark.at + COMPACT_GRACE_SECONDS)
382
+ ) {
383
+ // Same #3911/ADR-3889 conversion as the PreCompact branch above: this
384
+ // gate is new in this PR, so it did not exist to be migrated.
385
+ allow(undefined);
386
+ }
387
+ } catch (e) { /* no watermark — nothing to compare against */ }
388
+
93
389
  // Ignore stale metrics
94
390
  if (metrics.timestamp && (now - metrics.timestamp) > STALE_SECONDS) {
95
391
  allow(undefined);
@@ -99,19 +395,36 @@ process.stdin.on('end', () => {
99
395
  const usedPct = metrics.used_pct;
100
396
 
101
397
  // No warning needed
102
- if (remaining > WARNING_THRESHOLD) {
398
+ if (remaining > thresholds.warning) {
103
399
  allow(undefined);
104
400
  }
105
401
 
106
- // Debounce: check if we warned recently
107
- const warnPath = path.join(tmpDir, `claude-ctx-${sessionId}-warned.json`);
402
+ // Debounce: check if we warned recently. `warnPath` is resolved above, next to
403
+ // metricsPath, because the PreCompact reset needs it before this point.
108
404
  let warnData = { callsSinceWarn: 0, lastLevel: null };
109
405
  let firstWarn = true;
110
406
 
111
407
  // Collapsed existsSync+readFileSync: ENOENT or parse error → keep default warnData
112
408
  // (same as old "file absent" branch). firstWarn tracks whether we read a valid sentinel.
409
+ //
410
+ // READ HARDENING (self-found while addressing round 7; same class, same
411
+ // file). Hardening the writes above leaves this read as a bare
412
+ // readFileSync on warnPath, which is the exact asymmetry round 7 asks be
413
+ // removed from the write side — and the watermark's read was hardened in
414
+ // round 4 for this same reason, so leaving this one recreates it. It was
415
+ // not the LAST bare read in the file: the statusline bridge kept its own
416
+ // until round 11 found it. All three go through readSentinel now. The
417
+ // exposure is real but bounded: the writes now unlink any planted object,
418
+ // so only a read reaching this line BEFORE the first write of an
419
+ // invocation can follow one, and re-planting reopens it every invocation.
420
+ // Following it is a mute primitive — attacker-chosen callsSinceWarn keeps
421
+ // the debounce arm below taken so no warning is ever emitted — and a
422
+ // symlink to a FIFO stalls this synchronous read, the same two primitives
423
+ // measured on the watermark. Same lstat + O_NOFOLLOW + size bound; every
424
+ // refusal degrades to the default warnData this catch already produces,
425
+ // so a normal regular file behaves exactly as before.
113
426
  try {
114
- warnData = JSON.parse(fs.readFileSync(warnPath, 'utf8'));
427
+ warnData = JSON.parse(readSentinel(warnPath));
115
428
  firstWarn = false;
116
429
  } catch (e) {
117
430
  // Missing or corrupted sentinel → firstWarn stays true, warnData stays at defaults
@@ -119,7 +432,7 @@ process.stdin.on('end', () => {
119
432
 
120
433
  warnData.callsSinceWarn = (warnData.callsSinceWarn || 0) + 1;
121
434
 
122
- const isCritical = remaining <= CRITICAL_THRESHOLD;
435
+ const isCritical = remaining <= thresholds.critical;
123
436
  const currentLevel = isCritical ? 'critical' : 'warning';
124
437
 
125
438
  // Emit immediately on first warning, then debounce subsequent ones
@@ -127,14 +440,14 @@ process.stdin.on('end', () => {
127
440
  const severityEscalated = currentLevel === 'critical' && warnData.lastLevel === 'warning';
128
441
  if (!firstWarn && warnData.callsSinceWarn < DEBOUNCE_CALLS && !severityEscalated) {
129
442
  // Update counter and exit without warning
130
- fs.writeFileSync(warnPath, JSON.stringify(warnData));
443
+ writeSentinel(warnPath, JSON.stringify(warnData));
131
444
  allow(undefined);
132
445
  }
133
446
 
134
447
  // Reset debounce counter
135
448
  warnData.callsSinceWarn = 0;
136
449
  warnData.lastLevel = currentLevel;
137
- fs.writeFileSync(warnPath, JSON.stringify(warnData));
450
+ writeSentinel(warnPath, JSON.stringify(warnData));
138
451
 
139
452
  // Detect if GSD is active (has .planning/STATE.md in working directory)
140
453
  const isGsdActive = fs.existsSync(path.join(cwd, '.planning', 'STATE.md'));
@@ -161,7 +474,7 @@ process.stdin.on('end', () => {
161
474
  ).unref();
162
475
  warnData.criticalRecorded = true;
163
476
  // Persist the sentinel so subsequent debounce cycles don't re-fire
164
- fs.writeFileSync(warnPath, JSON.stringify(warnData));
477
+ writeSentinel(warnPath, JSON.stringify(warnData));
165
478
  } catch { /* non-critical — don't let state recording break the hook */ }
166
479
  }
167
480
 
@@ -198,7 +511,7 @@ process.stdin.on('end', () => {
198
511
  // not enough — a missing name would still fall through to the injection path.
199
512
  // All side effects above (debounce counter, one-time critical-session
200
513
  // recording) have already run regardless of whether output is emitted.
201
- const eventName = (data.hook_event_name && data.hook_event_name.trim()) || "";
514
+ const eventName = readEventName(data);
202
515
  // Preserve the pre-#2289 Gemini fallback: a missing event name under a
203
516
  // Gemini-dialect runtime (GEMINI_API_KEY set) still means AfterTool, so its
204
517
  // advisory output is unchanged. A missing name on any other host is silent.
@@ -209,7 +522,8 @@ process.stdin.on('end', () => {
209
522
  const output = {
210
523
  hookSpecificOutput: {
211
524
  hookEventName: eventName || "AfterTool",
212
- additionalContext: message
525
+ additionalContext: message,
526
+ severity: currentLevel
213
527
  }
214
528
  };
215
529
  process.stdout.write(JSON.stringify(output));
@@ -220,4 +534,34 @@ process.stdin.on('end', () => {
220
534
  // exit(0) fail-open behavior exactly (#3911).
221
535
  crash(ON_CRASH, undefined);
222
536
  }
223
- });
537
+ };
538
+
539
+ // The stdin adapter is the only side-effecting statement in this file, so it is
540
+ // the only thing that must not run on `require()`. Gating it lets a test import
541
+ // `resolveThresholds` and drive it directly — the repo's own conclusion for a
542
+ // seam like this (CONTEXT-INDEX, on the ROADMAP Requirements parser: a closure
543
+ // reachable only by spawning the CLI is one "no fast-check property can do").
544
+ // A spawn-per-case property test is not the same test: it would exercise the
545
+ // resolver at whatever pairs survive to an observable severity, not over its
546
+ // whole numeric domain.
547
+ /* istanbul ignore next -- stdin adapter, exercised via spawnSync in tests */
548
+ function main() {
549
+ // Timeout guard: if stdin doesn't close within 10s (e.g. pipe issues on
550
+ // Windows/Git Bash, or slow Claude Code piping during large outputs),
551
+ // exit silently instead of hanging until Claude Code kills the process
552
+ // and reports "hook error". See #775, #1162.
553
+ stdinTimeout = setTimeout(() => allow(undefined), 10000);
554
+ process.stdin.setEncoding('utf8');
555
+ process.stdin.on('data', chunk => input += chunk);
556
+ process.stdin.on('end', handleStdinEnd);
557
+ }
558
+
559
+ if (require.main === module) {
560
+ main();
561
+ }
562
+
563
+ // Exported for the #4285 property test only. The two constants ride along so a
564
+ // test asserts the fallback pair against the SOURCE of truth rather than
565
+ // re-hardcoding 35/25 — a test carrying its own copy of the defaults would stay
566
+ // green if the constants were edited.
567
+ module.exports = { resolveThresholds, WARNING_THRESHOLD, CRITICAL_THRESHOLD };
@@ -62,8 +62,8 @@ const { allow } = require('./lib/hook-exit.js');
62
62
  // hooks/lib/cursor-workspace.js. Staged next to these scripts by
63
63
  // writeCursorHooksJson so the require always resolves post-install.
64
64
  const { resolveStatePath } = require('./lib/cursor-workspace.js');
65
- const { readSentinel, VALID_ISOLATION, extractDispatchIdentifiers, sentinelAppliesToDispatch } = require('./lib/isolation-sentinel.js');
66
- const { REASON_CODE } = require('./lib/isolation-deny-reason.js');
65
+ const { readSentinel, VALID_ISOLATION, extractDispatchIdentifiers, sentinelAppliesToDispatch, buildSentinelDiscard } = require('./lib/isolation-sentinel.js');
66
+ const { REASON_CODE, describeSentinelDiscard } = require('./lib/isolation-deny-reason.js');
67
67
  // #3582: gsd-core/bin/lib/*.cjs (runtime-homes.cjs, worktree-safety.cjs,
68
68
  // runtime-name-policy.cjs, capability-registry.cjs — required below, inside
69
69
  // resolveIsolationEvidence and resolveFallbackIsolation) are tsc build
@@ -491,18 +491,25 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
491
491
  `Refusing to allow this subagent to spawn until the runtime library is built — a guard ` +
492
492
  `that cannot verify must not answer "safe" (#3050).`,
493
493
  reasonCode: REASON_CODE.RUNTIME_BUILD_FAILED,
494
+ sentinelDiscarded: null,
494
495
  };
495
496
  }
496
497
 
498
+ // #3045 BLOCKER fix: a fresh sentinel is authoritative for THIS dispatch's
499
+ // actual resolved isolation — see the doc comment above.
500
+ // #3045 SECURITY F2: a fresh sentinel that names a DIFFERENT plan/phase
501
+ // than this dispatch is not applicable to it — fall through to the
502
+ // conservative fallback exactly as a stale sentinel would.
503
+ // Hoisted (readSentinel never throws) so the "present, fresh, but did not
504
+ // apply" case (#4594 row 15) can be reported on every deny path below
505
+ // instead of silently discarded.
506
+ const sentinel = readSentinel(root, { clock });
507
+ const applies = sentinelAppliesToDispatch(sentinel, dispatchIds);
508
+ const sentinelDiscarded = buildSentinelDiscard(sentinel, dispatchIds);
509
+
497
510
  let declaredIsolation;
498
511
  try {
499
- // #3045 BLOCKER fix: a fresh sentinel is authoritative for THIS
500
- // dispatch's actual resolved isolation — see the doc comment above.
501
- // #3045 SECURITY F2: a fresh sentinel that names a DIFFERENT
502
- // plan/phase than this dispatch is not applicable to it — fall through
503
- // to the conservative fallback exactly as a stale sentinel would.
504
- const sentinel = readSentinel(root, { clock });
505
- declaredIsolation = (sentinel.present && !sentinel.stale && sentinelAppliesToDispatch(sentinel, dispatchIds))
512
+ declaredIsolation = (sentinel.present && !sentinel.stale && applies)
506
513
  ? sentinel.isolation
507
514
  : resolveFallbackIsolation(root, configPath);
508
515
  } catch {
@@ -513,8 +520,10 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
513
520
  `dispatch-isolation configuration ('.planning/config.json' exists under "${root}"). ` +
514
521
  `Refusing to allow this subagent to spawn without being able to verify whether ` +
515
522
  `isolation is required — a guard that cannot verify must not answer "safe" (#3050). ` +
516
- `Retry once the project configuration is readable.`,
523
+ `Retry once the project configuration is readable.` +
524
+ (sentinelDiscarded ? describeSentinelDiscard(sentinelDiscarded) : ''),
517
525
  reasonCode: REASON_CODE.CONFIG_UNREADABLE,
526
+ sentinelDiscarded,
518
527
  };
519
528
  }
520
529
 
@@ -530,8 +539,10 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
530
539
  `GSD subagent isolation guard: this project's dispatch isolation resolves to ` +
531
540
  `"harness-worktree", but the subagentStart payload for this dispatch carries no usable ` +
532
541
  `subagent_type. Refusing to allow it to spawn without being able to confirm whether it ` +
533
- `is a GSD executor — a guard that cannot verify must not answer "safe" (#3050).`,
542
+ `is a GSD executor — a guard that cannot verify must not answer "safe" (#3050).` +
543
+ (sentinelDiscarded ? describeSentinelDiscard(sentinelDiscarded) : ''),
534
544
  reasonCode: REASON_CODE.NO_SUBAGENT_TYPE,
545
+ sentinelDiscarded,
535
546
  };
536
547
  }
537
548
 
@@ -547,8 +558,10 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
547
558
  `"harness-worktree", but whether "${root}" is running in an isolated Cursor worktree ` +
548
559
  `could not be determined (git did not respond). Refusing to allow subagent_type=` +
549
560
  `"${subagentType}" to spawn without being able to verify isolation — a guard that ` +
550
- `cannot verify must not answer "safe" (#3050). Retry once git is responsive.`,
561
+ `cannot verify must not answer "safe" (#3050). Retry once git is responsive.` +
562
+ (sentinelDiscarded ? describeSentinelDiscard(sentinelDiscarded) : ''),
551
563
  reasonCode: REASON_CODE.CANNOT_DETERMINE_ISOLATION,
564
+ sentinelDiscarded,
552
565
  };
553
566
  }
554
567
 
@@ -560,8 +573,10 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
560
573
  `which is not an isolated Cursor worktree — it would edit the user's primary checkout ` +
561
574
  `directly, with no consent and no warning. Start an isolated session first (the ` +
562
575
  `"--worktree" CLI flag or the "/worktree" chat command; Cursor manages these worktrees ` +
563
- `under "~/.cursor/worktrees/") and retry.`,
576
+ `under "~/.cursor/worktrees/") and retry.` +
577
+ (sentinelDiscarded ? describeSentinelDiscard(sentinelDiscarded) : ''),
564
578
  reasonCode: REASON_CODE.NOT_ISOLATED_WORKTREE,
579
+ sentinelDiscarded,
565
580
  };
566
581
  }
567
582
 
@@ -611,7 +626,12 @@ function main() {
611
626
  decision = { action: 'allow' };
612
627
  }
613
628
  if (decision.action === 'deny') {
614
- const out = { permission: 'deny', user_message: decision.reason, reason_code: decision.reasonCode };
629
+ const out = {
630
+ permission: 'deny',
631
+ user_message: decision.reason,
632
+ reason_code: decision.reasonCode,
633
+ sentinel_discarded: decision.sentinelDiscarded ?? null,
634
+ };
615
635
  if (additionalContext !== null) out.additional_context = additionalContext;
616
636
  process.stdout.write(JSON.stringify(out));
617
637
  return;
@@ -1,4 +1,5 @@
1
1
  #!/bin/sh
2
+ # gsd-hook-version: {{GSD_VERSION}}
2
3
  # gsd-node-runner.sh — GSD portable node resolver (#3662).
3
4
  #
4
5
  # Managed JS hook commands under --portable-hooks route through this script: