@opengsd/gsd-core 1.13.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 (257) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-advisor-researcher.compact.md +85 -0
  4. package/agents/gsd-ai-researcher.compact.md +96 -0
  5. package/agents/gsd-assumptions-analyzer.compact.md +81 -0
  6. package/agents/gsd-code-fixer.compact.md +458 -0
  7. package/agents/gsd-code-fixer.md +5 -5
  8. package/agents/gsd-code-reviewer.compact.md +269 -0
  9. package/agents/gsd-code-reviewer.md +15 -3
  10. package/agents/gsd-codebase-mapper.compact.md +760 -0
  11. package/agents/gsd-debug-session-manager.compact.md +345 -0
  12. package/agents/gsd-doc-classifier.compact.md +192 -0
  13. package/agents/gsd-doc-synthesizer.compact.md +200 -0
  14. package/agents/gsd-doc-verifier.compact.md +143 -0
  15. package/agents/gsd-doc-writer.compact.md +440 -0
  16. package/agents/gsd-dom-verifier.compact.md +138 -0
  17. package/agents/gsd-domain-researcher.compact.md +141 -0
  18. package/agents/gsd-eval-auditor.compact.md +160 -0
  19. package/agents/gsd-eval-planner.compact.md +137 -0
  20. package/agents/gsd-framework-selector.compact.md +82 -0
  21. package/agents/gsd-integration-checker.compact.md +245 -0
  22. package/agents/gsd-intel-updater.compact.md +226 -0
  23. package/agents/gsd-mempalace-curator.compact.md +45 -0
  24. package/agents/gsd-nyquist-auditor.compact.md +179 -0
  25. package/agents/gsd-pattern-mapper.compact.md +275 -0
  26. package/agents/gsd-project-researcher.compact.md +587 -0
  27. package/agents/gsd-research-synthesizer.compact.md +212 -0
  28. package/agents/gsd-roadmapper.compact.md +454 -0
  29. package/agents/gsd-roadmapper.md +13 -0
  30. package/agents/gsd-security-auditor.compact.md +162 -0
  31. package/agents/gsd-ui-auditor.compact.md +404 -0
  32. package/agents/gsd-ui-checker.compact.md +277 -0
  33. package/agents/gsd-ui-researcher.compact.md +282 -0
  34. package/agents/gsd-user-profiler.compact.md +108 -0
  35. package/bin/install.js +206 -68
  36. package/commands/gsd/cleanup.md +1 -0
  37. package/commands/gsd/code-review.md +2 -1
  38. package/commands/gsd/complete-milestone.md +1 -0
  39. package/commands/gsd/config.md +1 -0
  40. package/commands/gsd/debug.md +1 -0
  41. package/commands/gsd/graphify.md +1 -0
  42. package/commands/gsd/health.md +1 -0
  43. package/commands/gsd/mempalace-capture.md +1 -0
  44. package/commands/gsd/mempalace-recall.md +1 -0
  45. package/commands/gsd/new-milestone.md +1 -0
  46. package/commands/gsd/new-project.md +1 -0
  47. package/commands/gsd/next.md +1 -0
  48. package/commands/gsd/pause-work.md +1 -0
  49. package/commands/gsd/phase.md +1 -0
  50. package/commands/gsd/pr-branch.md +1 -0
  51. package/commands/gsd/resume-work.md +1 -0
  52. package/commands/gsd/review-backlog.md +1 -0
  53. package/commands/gsd/settings.md +2 -1
  54. package/commands/gsd/stats.md +1 -0
  55. package/commands/gsd/thread.md +1 -0
  56. package/commands/gsd/workspace.md +1 -0
  57. package/commands/gsd/workstreams.md +1 -0
  58. package/gsd-core/bin/check-latest-version.cjs +8 -3
  59. package/gsd-core/bin/gsd-tools.cjs +338 -125
  60. package/gsd-core/bin/lib/adr-parser.cjs +1 -1
  61. package/gsd-core/bin/lib/artifacts.cjs +2 -1
  62. package/gsd-core/bin/lib/audit.cjs +39 -22
  63. package/gsd-core/bin/lib/broken-windows.cjs +168 -49
  64. package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
  65. package/gsd-core/bin/lib/capability-loader.cjs +135 -1
  66. package/gsd-core/bin/lib/capability-registry.cjs +79 -67
  67. package/gsd-core/bin/lib/capability-source.cjs +19 -2
  68. package/gsd-core/bin/lib/capability-validator.cjs +14 -1
  69. package/gsd-core/bin/lib/check-command-router.cjs +113 -36
  70. package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
  71. package/gsd-core/bin/lib/commands.cjs +650 -72
  72. package/gsd-core/bin/lib/config-loader.cjs +1 -0
  73. package/gsd-core/bin/lib/config.cjs +153 -38
  74. package/gsd-core/bin/lib/coverage.cjs +1 -1
  75. package/gsd-core/bin/lib/decisions.cjs +137 -34
  76. package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
  77. package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
  78. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
  79. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
  80. package/gsd-core/bin/lib/init.cjs +409 -47
  81. package/gsd-core/bin/lib/install-engine.cjs +16 -3
  82. package/gsd-core/bin/lib/install-profiles.cjs +14 -0
  83. package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
  84. package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
  85. package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
  86. package/gsd-core/bin/lib/milestone.cjs +19 -8
  87. package/gsd-core/bin/lib/model-resolver.cjs +101 -10
  88. package/gsd-core/bin/lib/phase-command-router.cjs +7 -1
  89. package/gsd-core/bin/lib/phase-id.cjs +161 -22
  90. package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
  91. package/gsd-core/bin/lib/phase.cjs +167 -63
  92. package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
  93. package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
  94. package/gsd-core/bin/lib/planning-workspace.cjs +50 -1
  95. package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
  96. package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
  97. package/gsd-core/bin/lib/quick-batch.cjs +1 -1
  98. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
  99. package/gsd-core/bin/lib/research-store.cjs +11 -12
  100. package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
  101. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
  102. package/gsd-core/bin/lib/roadmap-parser.cjs +56 -15
  103. package/gsd-core/bin/lib/roadmap.cjs +108 -14
  104. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +27 -10
  105. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +12 -3
  106. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
  107. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +193 -4
  108. package/gsd-core/bin/lib/security.cjs +126 -7
  109. package/gsd-core/bin/lib/state-document.cjs +130 -28
  110. package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
  111. package/gsd-core/bin/lib/state-transition.cjs +142 -28
  112. package/gsd-core/bin/lib/state.cjs +223 -27
  113. package/gsd-core/bin/lib/surface.cjs +60 -2
  114. package/gsd-core/bin/lib/task-command-router.cjs +12 -6
  115. package/gsd-core/bin/lib/uat.cjs +1 -1
  116. package/gsd-core/bin/lib/update-context.cjs +30 -24
  117. package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
  118. package/gsd-core/bin/lib/verification.cjs +47 -15
  119. package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
  120. package/gsd-core/bin/lib/verify.cjs +188 -23
  121. package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
  122. package/gsd-core/bin/lib/worktree-safety.cjs +13 -7
  123. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  124. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  125. package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
  126. package/gsd-core/references/compact-content-gate.md +66 -0
  127. package/gsd-core/references/loop-hook-dispatch.md +18 -0
  128. package/gsd-core/references/model-profiles.md +12 -3
  129. package/gsd-core/references/planning-config.md +3 -0
  130. package/gsd-core/references/tdd.md +5 -2
  131. package/gsd-core/references/thinking-models-planning.md +18 -2
  132. package/gsd-core/references/verification-patterns.md +17 -4
  133. package/gsd-core/references/worktree-path-safety.md +112 -2
  134. package/gsd-core/templates/README.md +7 -1
  135. package/gsd-core/templates/state.md +6 -3
  136. package/gsd-core/templates/summary.compact.md +212 -0
  137. package/gsd-core/templates/user-setup.compact.md +199 -0
  138. package/gsd-core/templates/user-setup.md +0 -9
  139. package/gsd-core/workflows/add-todo.md +3 -2
  140. package/gsd-core/workflows/autonomous.md +13 -10
  141. package/gsd-core/workflows/check-todos.md +4 -2
  142. package/gsd-core/workflows/cleanup.md +3 -1
  143. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +7 -0
  144. package/gsd-core/workflows/code-review-fix.md +3 -3
  145. package/gsd-core/workflows/code-review.md +156 -30
  146. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
  147. package/gsd-core/workflows/complete-milestone.md +39 -262
  148. package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
  149. package/gsd-core/workflows/docs-update.md +14 -155
  150. package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
  151. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +18 -3
  152. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
  153. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +7 -2
  154. package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
  155. package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
  156. package/gsd-core/workflows/execute-phase.md +53 -152
  157. package/gsd-core/workflows/execute-plan.md +20 -7
  158. package/gsd-core/workflows/help/modes/full.compact.md +398 -0
  159. package/gsd-core/workflows/help.md +1 -1
  160. package/gsd-core/workflows/map-codebase.md +50 -3
  161. package/gsd-core/workflows/new-milestone.md +54 -12
  162. package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
  163. package/gsd-core/workflows/new-project.md +32 -202
  164. package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
  165. package/gsd-core/workflows/plan-phase.md +22 -181
  166. package/gsd-core/workflows/pr-branch.md +19 -7
  167. package/gsd-core/workflows/quick.md +8 -1
  168. package/gsd-core/workflows/reapply-patches.md +77 -3
  169. package/gsd-core/workflows/settings.md +18 -5
  170. package/gsd-core/workflows/update.md +7 -5
  171. package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
  172. package/gsd-core/workflows/verify-work.md +20 -180
  173. package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
  174. package/hooks/dist/gsd-context-monitor.js +88 -15
  175. package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
  176. package/hooks/dist/gsd-secret-read-guard.js +44 -18
  177. package/hooks/dist/gsd-statusline.js +11 -7
  178. package/hooks/dist/gsd-validate-commit.sh +34 -4
  179. package/hooks/dist/gsd-worktree-path-guard.js +25 -14
  180. package/hooks/dist/gsd-write-guard.js +46 -1
  181. package/hooks/dist/lib/dispatch-identity.js +187 -0
  182. package/hooks/dist/lib/filename-classification.js +64 -0
  183. package/hooks/dist/lib/isolation-deny-reason.js +53 -1
  184. package/hooks/dist/lib/isolation-sentinel.js +58 -19
  185. package/hooks/gsd-agent-isolation-guard.js +42 -16
  186. package/hooks/gsd-context-monitor.js +88 -15
  187. package/hooks/gsd-cursor-subagent-start.js +34 -14
  188. package/hooks/gsd-secret-read-guard.js +44 -18
  189. package/hooks/gsd-statusline.js +11 -7
  190. package/hooks/gsd-validate-commit.sh +34 -4
  191. package/hooks/gsd-worktree-path-guard.js +25 -14
  192. package/hooks/gsd-write-guard.js +46 -1
  193. package/hooks/lib/dispatch-identity.js +187 -0
  194. package/hooks/lib/filename-classification.js +64 -0
  195. package/hooks/lib/isolation-deny-reason.js +53 -1
  196. package/hooks/lib/isolation-sentinel.js +58 -19
  197. package/package.json +10 -6
  198. package/scripts/benchmark-compact-content-variants.cjs +298 -0
  199. package/scripts/benchmark-compact-content.cjs +368 -0
  200. package/scripts/check-contract-drift.cjs +4 -1
  201. package/scripts/check-env.cjs +36 -8
  202. package/scripts/check-glossary-refs.cjs +25 -21
  203. package/scripts/ci-next-health.cjs +271 -0
  204. package/scripts/ci-prepare-test-scope.cjs +7 -7
  205. package/scripts/ci-test-scope.cjs +126 -20
  206. package/scripts/ci-timeout-report.cjs +1 -1
  207. package/scripts/diff-touches-shipped-paths.cjs +1 -1
  208. package/scripts/docs-guard-registry.cjs +7 -2
  209. package/scripts/gen-adr-index.cjs +8 -2
  210. package/scripts/gen-inventory-manifest.cjs +12 -0
  211. package/scripts/gen-platform-conformance-tier.cjs +557 -0
  212. package/scripts/lib/drift-scan.cjs +1 -1
  213. package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
  214. package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
  215. package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
  216. package/scripts/lib/suite-detection.cjs +32 -0
  217. package/scripts/lint-allowed-tools-parity.cjs +221 -0
  218. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +19 -2
  219. package/scripts/lint-phase-id-drift.cjs +338 -13
  220. package/scripts/lint-response-language-coverage.cjs +9 -3
  221. package/scripts/lint-source-test-name-collision.cjs +1 -1
  222. package/scripts/lint-test-file-count.allowlist.json +1 -0
  223. package/scripts/lint-vendored-deps.cjs +128 -17
  224. package/scripts/lint-workflow-shellcheck-baseline.json +85 -0
  225. package/scripts/prompt-injection-scan.sh +14 -0
  226. package/scripts/workflow-size.cjs +139 -0
  227. package/skills/gsd-cleanup/SKILL.md +1 -0
  228. package/skills/gsd-code-review/SKILL.md +2 -1
  229. package/skills/gsd-complete-milestone/SKILL.md +1 -0
  230. package/skills/gsd-config/SKILL.md +1 -0
  231. package/skills/gsd-debug/SKILL.md +1 -0
  232. package/skills/gsd-graphify/SKILL.md +1 -0
  233. package/skills/gsd-health/SKILL.md +1 -0
  234. package/skills/gsd-mempalace-capture/SKILL.md +1 -0
  235. package/skills/gsd-mempalace-recall/SKILL.md +1 -0
  236. package/skills/gsd-new-milestone/SKILL.md +1 -0
  237. package/skills/gsd-new-project/SKILL.md +1 -0
  238. package/skills/gsd-next/SKILL.md +1 -0
  239. package/skills/gsd-pause-work/SKILL.md +1 -0
  240. package/skills/gsd-phase/SKILL.md +1 -0
  241. package/skills/gsd-pr-branch/SKILL.md +1 -0
  242. package/skills/gsd-resume-work/SKILL.md +1 -0
  243. package/skills/gsd-review-backlog/SKILL.md +1 -0
  244. package/skills/gsd-settings/SKILL.md +2 -1
  245. package/skills/gsd-stats/SKILL.md +1 -0
  246. package/skills/gsd-thread/SKILL.md +1 -0
  247. package/skills/gsd-workspace/SKILL.md +1 -0
  248. package/skills/gsd-workstreams/SKILL.md +1 -0
  249. package/vscode/package.json +1 -1
  250. package/gsd-core/templates/claude-md.md +0 -145
  251. package/gsd-core/templates/codebase/concerns.md +0 -310
  252. package/gsd-core/templates/codebase/conventions.md +0 -307
  253. package/gsd-core/templates/codebase/integrations.md +0 -280
  254. package/gsd-core/templates/codebase/structure.md +0 -285
  255. package/gsd-core/templates/codebase/testing.md +0 -480
  256. package/gsd-core/templates/debug-subagent-prompt.md +0 -91
  257. package/gsd-core/templates/discovery.md +0 -146
@@ -33,6 +33,8 @@ No Pass/Fail buttons. No severity questions. Just: "Here's what should happen. D
33
33
 
34
34
  <process>
35
35
 
36
+ **Compact Content Gate.** Read and follow `gsd-core/references/compact-content-gate.md` now — it states the `workflow.compact_content` check and the resolution rule this spine defers to. When it directs a Read, read `gsd-core/workflows/verify-work/detail/elaboration.md` in full before continuing past this point; its content elaborates on the resume/reconcile steps and the full gap-closure sub-flow below.
37
+
36
38
  <step name="initialize" priority="first">
37
39
  If $ARGUMENTS contains a phase number, load context:
38
40
 
@@ -489,50 +491,20 @@ If no more tests → Go to `complete_session`
489
491
  </step>
490
492
 
491
493
  <step name="reconcile_gaps">
492
- **Reconcile diagnosed gaps against completed gap-closure plans (#1921):**
493
-
494
- When verify-work resumes after `/gsd:execute-phase --gaps-only`, the UAT `## Gaps` entries still read `status: failed` even though their fix plans have executed. Without reconciliation verify-work re-diagnoses them as fresh blockers and spawns new gap plans — losing the verification state. This step closes the loop.
495
-
496
- Read the UAT `## Gaps` section and the phase dir `*-PLAN.md` frontmatter. For each gap with `status: failed`:
497
- 1. Find a `*-PLAN.md` whose frontmatter `gap_ids` includes the gap's `gap_id` (`G-{phase}-{N}`).
498
- 2. If such a plan exists AND has a matching `*-SUMMARY.md` in the phase dir (the plan was executed by `--gaps-only`), the gap is **resolved** — update its YAML in place:
499
- ```yaml
500
- - gap_id: G-{phase}-{N}
501
- status: resolved # was: failed
502
- resolved_by: {plan basename}
503
- resolved_at: {today}
504
- ```
505
- 3. If no plan references the `gap_id`, or the plan has no SUMMARY, leave the gap `status: failed` (still open).
494
+ **Reconcile diagnosed gaps against completed gap-closure plans (#1921):** when verify-work resumes after `/gsd:execute-phase --gaps-only`, UAT `## Gaps` entries still read `status: failed` even though their fix plans already executed — without reconciliation they'd be re-diagnosed as fresh blockers. For each `status: failed` gap with a `*-PLAN.md` whose `gap_ids` names it AND a matching `*-SUMMARY.md`, mark it `resolved` (with `resolved_by`/`resolved_at`) in place; otherwise leave it `failed`. Resolved gaps are never re-diagnosed or re-planned; a later regression gets a fresh `gap_id`, not a reopened old one.
506
495
 
507
- Read plan frontmatter directly in-context — do not pipe it through a shell parser. After reconciliation, announce:
508
- ```
509
- Reconciled gap-closure state: {resolved_count} gap(s) resolved by executed plans, {open_count} still open.
510
- ```
511
-
512
- Resolved gaps are NOT re-diagnosed and do NOT spawn new gap plans. If the user later reports the same behavior as still broken, treat it as a new issue (a regression) with a fresh `gap_id`.
496
+ Exact YAML shape and the announcement line: `gsd-core/workflows/verify-work/detail/elaboration.md` § 1.
513
497
  </step>
514
498
 
515
499
  <step name="resume_from_file">
516
- **Resume testing from UAT file:**
517
-
518
- **First run `reconcile_gaps`** (above) so gaps already fixed by `/gsd:execute-phase --gaps-only` are marked `resolved` before testing resumes (#1921).
519
-
520
- Read the full UAT file.
500
+ **Resume testing from UAT file:** first run `reconcile_gaps` (above), then read the full UAT file.
521
501
 
522
502
  Find first test with `result: [pending]`.
523
503
  If no `[pending]` test found → go to `complete_session`.
524
504
 
525
- Announce:
526
- ```
527
- Resuming: Phase {phase} UAT
528
- Progress: {passed + issues + skipped}/{total}
529
- Issues found so far: {issues count}
505
+ Otherwise announce progress and continue from that test at `present_test`.
530
506
 
531
- Continuing from Test {N}...
532
- ```
533
-
534
- Update Current Test section with the pending test.
535
- Proceed to `present_test`.
507
+ Exact resume-announcement wording: `gsd-core/workflows/verify-work/detail/elaboration.md` § 2.
536
508
  </step>
537
509
 
538
510
  <step name="complete_session">
@@ -726,138 +698,29 @@ SECURITY: File paths in output are constructed from validated path components on
726
698
  </step>
727
699
 
728
700
  <step name="diagnose_issues">
729
- **Diagnose root causes before planning fixes:**
730
-
731
- ```
732
- ---
733
-
734
- {N} issues found. Diagnosing root causes...
735
-
736
- Spawning parallel debug agents to investigate each issue.
737
- ```
738
-
739
- - Load diagnose-issues workflow
740
- - Follow @~/.claude/gsd-core/workflows/diagnose-issues.md
741
- - Spawn parallel debug agents for each issue
742
- - Collect root causes
743
- - Update UAT.md with root causes
744
- - Proceed to `plan_gap_closure`
745
-
746
- Diagnosis runs automatically - no user prompt. Parallel agents investigate simultaneously, so overhead is minimal and fixes are more accurate.
701
+ When UAT testing found issues, this sub-flow (diagnose_issues -> plan_gap_closure -> verify_gap_plans -> revision_loop) runs before present_ready; a session with zero issues never reaches it. Spawn parallel debug agents (one per issue, via diagnose-issues.md) to find root causes with no user prompt, then update UAT.md and proceed to plan_gap_closure.
747
702
  </step>
748
703
 
749
704
  <step name="plan_gap_closure">
750
- **Auto-plan fixes from diagnosed gaps:**
751
-
752
- Display:
753
- ```
754
- ### GSD ► PLANNING FIXES
755
-
756
- ◆ Spawning planner for gap closure... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)
757
- ```
758
-
759
- Spawn gsd-planner in --gaps mode:
760
-
761
- <!-- #2517 model-omit-on-inherit -->
762
-
763
- > **Model omission (#2517).** Omit the `model` parameter entirely when the value it would carry (`planner_model`, `checker_model`) is `"inherit"` or empty. An empty value 404s on runtimes without native tier aliases — the default on non-Claude runtimes. Omitting it inherits the orchestrator's model. See @gsd-core/references/model-profile-resolution.md.
764
-
765
- ````
766
- Agent(
767
- prompt="""
768
- <planning_context>
769
-
770
- **Phase:** {phase_number}
771
- **Mode:** gap_closure
772
-
773
- <required_reading>
774
- - {phase_dir}/{phase_num}-UAT.md (UAT with diagnoses)
775
- - {state_path} (Project State)
776
- - {roadmap_path} (Roadmap)
777
- </required_reading>
778
-
779
- ${AGENT_SKILLS_PLANNER}
780
-
781
- </planning_context>
782
-
783
- <downstream_consumer>
784
- Output consumed by /gsd:execute-phase
785
- Plans must be executable prompts.
786
-
787
- <!-- #2508 runtime-aware-dispatch -->
788
-
789
- > **Runtime-aware dispatch (#2508 Phase 4).** GSD workflows dispatch specialized subagents by role. Before dispatching on a built-in-only runtime (kimi-code — three built-ins only), resolve the role to a built-in via `gsd_run query resolve-dispatch-type --requested <role> --raw`. On named-dispatch runtimes (Claude/OpenCode/…) the role is returned unchanged; on kimi-code it maps to `coder`/`explore`/`plan` by role-suffix. The persona rides `${AGENT_SKILLS_<ROLE>}` (Phase 3) regardless. See @gsd-core/references/runtime-aware-dispatch.md.
790
-
791
- **Gap linkage (#1921):** each created `*-PLAN.md` MUST list the UAT gap ids it addresses in its frontmatter:
792
- ```yaml
793
- ---
794
- gap_closure: true
795
- gap_ids: [G-{phase}-{N}, ...] # the ## Gaps gap_id values this plan fixes
796
- ---
797
- ```
798
- This lets `/gsd:verify-work` reconcile resolved gaps on resume (a gap whose plan has a matching `*-SUMMARY.md` is marked `status: resolved`, not re-diagnosed as a fresh blocker).
799
- </downstream_consumer>
800
- """,
801
- subagent_type="gsd-planner",
802
- model="{planner_model}",
803
- description="Plan gap fixes for Phase {phase}"
804
- )
805
- ````
705
+ Spawn gsd-planner in --gaps mode against the UAT (with diagnoses), `{state_path}` (Project State), and `{roadmap_path}` (Roadmap). Each created PLAN.md MUST carry `gap_closure: true` and `gap_ids: [...]` in its frontmatter (#1921) so a later verify-work resume can reconcile it.
806
706
 
707
+ <!-- gsd:protected -->
807
708
  > **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.
808
709
 
809
- On return:
810
- - **PLANNING COMPLETE:** Proceed to `verify_gap_plans`
811
- - **PLANNING INCONCLUSIVE:** Report and offer manual intervention
710
+ PLANNING COMPLETE proceeds to verify_gap_plans; PLANNING INCONCLUSIVE reports and offers manual intervention.
812
711
  </step>
813
712
 
814
713
  <step name="verify_gap_plans">
815
- **Verify fix plans with checker:**
816
-
817
- Display:
818
- ```
819
- ### GSD ► VERIFYING FIX PLANS
820
-
821
- ◆ Spawning plan checker... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)
822
- ```
823
-
824
- Initialize: `iteration_count = 1`
825
-
826
- Spawn gsd-plan-checker:
827
-
828
- ```
829
- Agent(
830
- prompt="""
831
- <verification_context>
832
-
833
- **Phase:** {phase_number}
834
- **Phase Goal:** Close diagnosed gaps from UAT
835
-
836
- <required_reading>
837
- - {phase_dir}/*-PLAN.md (Plans to verify)
838
- </required_reading>
839
-
840
- ${AGENT_SKILLS_CHECKER}
841
-
842
- </verification_context>
843
-
844
- <expected_output>
845
- Return one of:
846
- - ## VERIFICATION PASSED — all checks pass
847
- - ## ISSUES FOUND — structured issue list
848
- </expected_output>
849
- """,
850
- subagent_type="gsd-plan-checker",
851
- model="{checker_model}",
852
- description="Verify Phase {phase} fix plans"
853
- )
854
- ```
714
+ Spawn gsd-plan-checker against the fix plans (iteration_count starts at 1), model="{checker_model}" (omit on inherit/empty, #2517).
855
715
 
716
+ <!-- gsd:protected -->
856
717
  > **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.
857
718
 
858
719
  On return:
859
720
  - **VERIFICATION PASSED:** Proceed to `present_ready`
860
721
  - **ISSUES FOUND:** Count BLOCKER + WARNING entries in the YAML issues block; an entry whose severity is missing or unrecognized counts as a BLOCKER (fail closed). If zero — every entry is explicitly INFO — display `ℹ advisory — {dimension}: {description}` per entry and proceed to `present_ready`; INFO is advisory and never enters the loop (#3724). Otherwise proceed to `revision_loop`
722
+
723
+ Exact Agent() prompt fields: `gsd-core/workflows/verify-work/detail/elaboration.md` § 2.
861
724
  </step>
862
725
 
863
726
  <step name="revision_loop">
@@ -869,26 +732,6 @@ Display: `Sending back to planner for revision... (iteration {N}/3)`
869
732
 
870
733
  Spawn gsd-planner with revision context:
871
734
 
872
- ```
873
- Agent(
874
- prompt="""
875
- <revision_context>
876
-
877
- **Phase:** {phase_number}
878
- **Mode:** revision
879
-
880
- <required_reading>
881
- - {phase_dir}/*-PLAN.md (Existing plans)
882
- </required_reading>
883
-
884
- ${AGENT_SKILLS_PLANNER}
885
-
886
- **Checker issues:**
887
- {structured_issues_from_checker}
888
-
889
- </revision_context>
890
-
891
- <instructions>
892
735
  Read existing PLAN.md files. Make targeted updates to address checker issues.
893
736
 
894
737
  `required_property` + evidence + severity BIND. `fix_hint` is ONE non-binding example route: a
@@ -900,14 +743,8 @@ the alternatives rather than applying or working around it. Full contract:
900
743
  `gsd-core/references/planner-revision.md`, which you load in revision mode.
901
744
 
902
745
  Do NOT replan from scratch unless issues are fundamental.
903
- </instructions>
904
- """,
905
- subagent_type="gsd-planner",
906
- model="{planner_model}",
907
- description="Revise Phase {phase} plans"
908
- )
909
- ```
910
746
 
747
+ <!-- gsd:protected -->
911
748
  > **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.
912
749
 
913
750
  **If the planner returns `## REVISION_CONFLICT`:** do NOT increment `iteration_count` and do NOT
@@ -938,9 +775,12 @@ Offer options:
938
775
  2. Provide guidance (user gives direction, retry)
939
776
  3. Abandon (exit, user runs /gsd:plan-phase manually)
940
777
 
941
- Wait for user response.
778
+ Then wait for the user to pick one.
779
+
780
+ Exact Agent() prompt fields (revision_context, required_reading): `gsd-core/workflows/verify-work/detail/elaboration.md` § 3.
942
781
  </step>
943
782
 
783
+
944
784
  <step name="present_ready">
945
785
  **Present completion and next steps:**
946
786
 
@@ -63,8 +63,8 @@
63
63
  const fs = require('fs');
64
64
  const path = require('path');
65
65
  const os = require('os');
66
- const { readSentinel, VALID_ISOLATION, extractDispatchIdentifiers, sentinelAppliesToDispatch } = require('./lib/isolation-sentinel.js');
67
- const { REASON_CODE } = require('./lib/isolation-deny-reason.js');
66
+ const { readSentinel, VALID_ISOLATION, extractDispatchIdentifiers, sentinelAppliesToDispatch, buildSentinelDiscard } = require('./lib/isolation-sentinel.js');
67
+ const { REASON_CODE, describeSentinelDiscard } = require('./lib/isolation-deny-reason.js');
68
68
  const { HOOK_ON_CRASH, allow, deny, crash } = require('./lib/hook-exit.js');
69
69
 
70
70
  // Required at module top, alongside the other ./lib requires — NOT behind
@@ -385,16 +385,20 @@ function resolveIsolationState(cwd, { clock = Date, dispatchIds = null } = {}) {
385
385
  projectExists = false;
386
386
  }
387
387
  if (!projectExists) {
388
- return { gsdProject: false, isolation: null, harnessFlag: null, error: null };
388
+ return { gsdProject: false, isolation: null, harnessFlag: null, error: null, sentinelDiscarded: null };
389
389
  }
390
390
 
391
391
  const sentinel = readSentinel(cwd, { clock });
392
- if (sentinel.present && !sentinel.stale && sentinelAppliesToDispatch(sentinel, dispatchIds)) {
392
+ // Hoisted so the "sentinel was present/fresh but did not apply" case below
393
+ // (#4594 row 15 — Postel's-Law finding) can distinguish itself from
394
+ // "absent"/"stale" without re-deriving applicability.
395
+ const applies = sentinelAppliesToDispatch(sentinel, dispatchIds);
396
+ if (sentinel.present && !sentinel.stale && applies) {
393
397
  if (sentinel.isolation !== 'harness-worktree') {
394
- return { gsdProject: true, isolation: sentinel.isolation, harnessFlag: null, error: null };
398
+ return { gsdProject: true, isolation: sentinel.isolation, harnessFlag: null, error: null, sentinelDiscarded: null };
395
399
  }
396
400
  if (sentinel.harnessFlag) {
397
- return { gsdProject: true, isolation: 'harness-worktree', harnessFlag: sentinel.harnessFlag, error: null };
401
+ return { gsdProject: true, isolation: 'harness-worktree', harnessFlag: sentinel.harnessFlag, error: null, sentinelDiscarded: null };
398
402
  }
399
403
  // #3045 BLOCKER 2 fix: the sentinel already PROVED this dispatch requires
400
404
  // isolation (it resolved harness-worktree) but carries no usable flag —
@@ -423,6 +427,7 @@ function resolveIsolationState(cwd, { clock = Date, dispatchIds = null } = {}) {
423
427
  'dispatch-isolation sentinel resolved "harness-worktree" but recorded no harness_flag — ' +
424
428
  'cannot verify what parameter the dispatch must carry.'
425
429
  ),
430
+ sentinelDiscarded: null,
426
431
  };
427
432
  }
428
433
 
@@ -430,11 +435,19 @@ function resolveIsolationState(cwd, { clock = Date, dispatchIds = null } = {}) {
430
435
  // conservative fallback (#3045 finding — must still cover fail-closed case
431
436
  // (a): a project that opted out of worktrees entirely via
432
437
  // workflow.use_worktrees).
438
+ //
439
+ // #4594 row 15: a PRESENT, FRESH sentinel that simply does not apply to
440
+ // THIS dispatch (identifiers disagree) is a distinct case from "absent" or
441
+ // "stale" — record what was discarded so evaluateDispatch can name it in a
442
+ // block reason instead of silently falling through to a registry-resolution
443
+ // message that never mentions the sentinel existed.
444
+ const sentinelDiscarded = buildSentinelDiscard(sentinel, dispatchIds);
445
+
433
446
  try {
434
447
  const { isolation, harnessFlag } = resolveRegistryIsolation(cwd, configPath);
435
- return { gsdProject: true, isolation, harnessFlag, error: null };
448
+ return { gsdProject: true, isolation, harnessFlag, error: null, sentinelDiscarded };
436
449
  } catch (err) {
437
- return { gsdProject: true, isolation: null, harnessFlag: null, error: err };
450
+ return { gsdProject: true, isolation: null, harnessFlag: null, error: err, sentinelDiscarded };
438
451
  }
439
452
  }
440
453
 
@@ -464,10 +477,16 @@ function evaluateDispatch(data, { clock = Date } = {}) {
464
477
  }
465
478
 
466
479
  const cwd = data.cwd || process.cwd();
467
- // #3045 SECURITY F2: best-effort plan/phase extraction from this
468
- // dispatch's own description text, so a fresh sentinel that disagrees
469
- // with THIS dispatch is treated as inapplicable rather than trusted.
470
- const dispatchIds = extractDispatchIdentifiers(toolInput.description);
480
+ // #3045 SECURITY F2 / #4594: best-effort plan/phase extraction from this
481
+ // dispatch's own text, so a fresh sentinel that disagrees with THIS
482
+ // dispatch is treated as inapplicable rather than trusted. PROMPT FIRST:
483
+ // `description` is short, model-authored free text that only carries usable
484
+ // identity when the model happens to reproduce the dispatch template
485
+ // verbatim, while the canonical `[gsd:dispatch phase="…" plan="…"]` marker
486
+ // (or, failing that, the prose fallback) lives in the prompt body itself —
487
+ // `description` is kept only as a fallback for a marker/prose match that
488
+ // exists solely in it.
489
+ const dispatchIds = extractDispatchIdentifiers(toolInput.prompt, toolInput.description);
471
490
  const state = resolveIsolationState(cwd, { clock, dispatchIds });
472
491
 
473
492
  if (!state.gsdProject) return { action: 'allow' };
@@ -493,7 +512,8 @@ function evaluateDispatch(data, { clock = Date } = {}) {
493
512
  `required — a guard that cannot verify must not answer "safe" (#3050). Retry once the ` +
494
513
  `project configuration is readable.`;
495
514
  const reasonCode = isBuildFailure ? REASON_CODE.RUNTIME_BUILD_FAILED : REASON_CODE.CONFIG_UNREADABLE;
496
- return { action: 'block', reason, reasonCode };
515
+ const fullReason = state.sentinelDiscarded ? reason + describeSentinelDiscard(state.sentinelDiscarded) : reason;
516
+ return { action: 'block', reason: fullReason, reasonCode, sentinelDiscarded: state.sentinelDiscarded };
497
517
  }
498
518
 
499
519
  if (state.isolation !== 'harness-worktree') return { action: 'allow' };
@@ -503,13 +523,14 @@ function evaluateDispatch(data, { clock = Date } = {}) {
503
523
 
504
524
  if (toolInput[parsed.param] === parsed.value) return { action: 'allow' };
505
525
 
506
- const reason =
526
+ let reason =
507
527
  `Agent isolation guard: this project's dispatch isolation resolves to "harness-worktree", ` +
508
528
  `but the Agent() dispatch for subagent_type="${subagentType}" is missing ` +
509
529
  `${parsed.param}="${parsed.value}". Add ${parsed.param}="${parsed.value}" to the Agent() ` +
510
530
  `call so the executor runs in an isolated worktree instead of the primary checkout ` +
511
531
  `(gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md).`;
512
- return { action: 'block', reason, reasonCode: REASON_CODE.HARNESS_FLAG_MISSING };
532
+ if (state.sentinelDiscarded) reason += describeSentinelDiscard(state.sentinelDiscarded);
533
+ return { action: 'block', reason, reasonCode: REASON_CODE.HARNESS_FLAG_MISSING, sentinelDiscarded: state.sentinelDiscarded };
513
534
  }
514
535
 
515
536
  /* istanbul ignore next -- stdin adapter, exercised via spawnSync in tests */
@@ -524,7 +545,12 @@ function main() {
524
545
  const data = JSON.parse(input);
525
546
  const decision = evaluateDispatch(data);
526
547
  if (decision.action === 'block') {
527
- const out = { decision: 'block', reason: decision.reason, reason_code: decision.reasonCode };
548
+ const out = {
549
+ decision: 'block',
550
+ reason: decision.reason,
551
+ reason_code: decision.reasonCode,
552
+ sentinel_discarded: decision.sentinelDiscarded ?? null,
553
+ };
528
554
  // Kimi feeds stderr (not stdout) back to the model on exit 2.
529
555
  deny(out, decision.reason);
530
556
  }
@@ -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,8 +33,8 @@ 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
37
40
  // How long after a PreCompact readings stay suspect. The watermark records the
@@ -57,6 +60,41 @@ const COMPACT_GRACE_SECONDS = 60;
57
60
  // watermark this far ahead pushes first recovery from +61 to +66 (measured).
58
61
  const WATERMARK_SKEW_SECONDS = 5;
59
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
+
60
98
  // One DEFINITION of what counts as a lifecycle event name, shared by the #3709
61
99
  // PreCompact reset and the #2289 output-envelope allowlist. Two call sites, one
62
100
  // rule — so the two cannot drift into disagreeing about what "no event name" is.
@@ -164,14 +202,11 @@ function writeSentinel(target, payload) {
164
202
  }
165
203
 
166
204
  let input = '';
167
- // Timeout guard: if stdin doesn't close within 10s (e.g. pipe issues on
168
- // Windows/Git Bash, or slow Claude Code piping during large outputs),
169
- // exit silently instead of hanging until Claude Code kills the process
170
- // and reports "hook error". See #775, #1162.
171
- const stdinTimeout = setTimeout(() => allow(undefined), 10000);
172
- process.stdin.setEncoding('utf8');
173
- process.stdin.on('data', chunk => input += chunk);
174
- 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 = () => {
175
210
  clearTimeout(stdinTimeout);
176
211
  try {
177
212
  const data = JSON.parse(input);
@@ -266,18 +301,26 @@ process.stdin.on('end', () => {
266
301
  allow(undefined);
267
302
  }
268
303
 
269
- // Check if context warnings are disabled via config.
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).
270
306
  // Collapsed existsSync+readFileSync into a single read guarded by try/catch
271
307
  // (ENOENT or parse error → use defaults, same as old "planningDir absent" branch).
272
308
  const cwd = data.cwd || process.cwd();
309
+ let thresholds = { warning: WARNING_THRESHOLD, critical: CRITICAL_THRESHOLD };
273
310
  try {
274
311
  const configPath = path.join(cwd, '.planning', 'config.json');
275
312
  const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
276
313
  if (config.hooks?.context_warnings === false) {
277
314
  allow(undefined);
278
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);
279
321
  } catch (e) {
280
- // 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)
281
324
  }
282
325
 
283
326
  // If no metrics file, this is a subagent or fresh session -- exit silently.
@@ -352,7 +395,7 @@ process.stdin.on('end', () => {
352
395
  const usedPct = metrics.used_pct;
353
396
 
354
397
  // No warning needed
355
- if (remaining > WARNING_THRESHOLD) {
398
+ if (remaining > thresholds.warning) {
356
399
  allow(undefined);
357
400
  }
358
401
 
@@ -389,7 +432,7 @@ process.stdin.on('end', () => {
389
432
 
390
433
  warnData.callsSinceWarn = (warnData.callsSinceWarn || 0) + 1;
391
434
 
392
- const isCritical = remaining <= CRITICAL_THRESHOLD;
435
+ const isCritical = remaining <= thresholds.critical;
393
436
  const currentLevel = isCritical ? 'critical' : 'warning';
394
437
 
395
438
  // Emit immediately on first warning, then debounce subsequent ones
@@ -491,4 +534,34 @@ process.stdin.on('end', () => {
491
534
  // exit(0) fail-open behavior exactly (#3911).
492
535
  crash(ON_CRASH, undefined);
493
536
  }
494
- });
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;