@opengsd/gsd-core 1.9.1 → 1.11.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 (426) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +27 -3
  5. package/agents/gsd-debug-session-manager.md +11 -0
  6. package/agents/gsd-debugger.md +12 -246
  7. package/agents/gsd-doc-synthesizer.md +2 -4
  8. package/agents/gsd-executor.md +12 -10
  9. package/agents/gsd-integration-checker.md +3 -0
  10. package/agents/gsd-mempalace-curator.md +5 -2
  11. package/agents/gsd-phase-researcher.md +20 -1
  12. package/agents/gsd-plan-checker.md +46 -0
  13. package/agents/gsd-planner.md +49 -54
  14. package/agents/gsd-roadmapper.md +21 -3
  15. package/agents/gsd-user-profiler.md +3 -0
  16. package/agents/gsd-verifier.md +26 -73
  17. package/bin/install.js +1272 -1238
  18. package/bin/lib/ui-safety-gate.cjs +2 -0
  19. package/commands/gsd/code-review.md +1 -1
  20. package/commands/gsd/execute-phase.md +1 -1
  21. package/commands/gsd/map-codebase.md +1 -1
  22. package/commands/gsd/mempalace-capture.md +2 -2
  23. package/commands/gsd/mempalace-recall.md +1 -1
  24. package/commands/gsd/new-milestone.md +2 -2
  25. package/commands/gsd/plan-phase.md +1 -1
  26. package/commands/gsd/quick.md +1 -1
  27. package/commands/gsd/review-backlog.md +2 -1
  28. package/commands/gsd/verify-work.md +1 -1
  29. package/gsd-core/bin/gsd-tools.cjs +1009 -115
  30. package/gsd-core/bin/lib/active-workstream-store.cjs +153 -12
  31. package/gsd-core/bin/lib/agent-install-check.cjs +268 -38
  32. package/gsd-core/bin/lib/api-coverage.cjs +123 -5
  33. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  34. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  35. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  36. package/gsd-core/bin/lib/audit.cjs +926 -202
  37. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  38. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  39. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  40. package/gsd-core/bin/lib/capability-registry.cjs +608 -148
  41. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  42. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  43. package/gsd-core/bin/lib/capability-validator.cjs +507 -24
  44. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  45. package/gsd-core/bin/lib/check-command-router.cjs +114 -38
  46. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  47. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  48. package/gsd-core/bin/lib/command-aliases.cjs +94 -0
  49. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  50. package/gsd-core/bin/lib/commands.cjs +665 -99
  51. package/gsd-core/bin/lib/commonjs-marker.cjs +142 -0
  52. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  53. package/gsd-core/bin/lib/config-loader.cjs +76 -0
  54. package/gsd-core/bin/lib/config.cjs +22 -2
  55. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  56. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  57. package/gsd-core/bin/lib/core-utils.cjs +217 -40
  58. package/gsd-core/bin/lib/decisions.cjs +23 -0
  59. package/gsd-core/bin/lib/docs.cjs +3 -2
  60. package/gsd-core/bin/lib/external-job.cjs +19 -4
  61. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  62. package/gsd-core/bin/lib/frontmatter.cjs +239 -32
  63. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  64. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  65. package/gsd-core/bin/lib/git-base-branch.cjs +160 -15
  66. package/gsd-core/bin/lib/graphify.cjs +142 -27
  67. package/gsd-core/bin/lib/gsd2-import.cjs +37 -5
  68. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  69. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  70. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  71. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  72. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  73. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  74. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  75. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  76. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  77. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  78. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  79. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  80. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  81. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  82. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  83. package/gsd-core/bin/lib/init.cjs +1325 -169
  84. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  85. package/gsd-core/bin/lib/install-engine.cjs +805 -264
  86. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  87. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  88. package/gsd-core/bin/lib/install-profiles.cjs +160 -57
  89. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  90. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  91. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  92. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  93. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  94. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  95. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  96. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  97. package/gsd-core/bin/lib/installer-migrations.cjs +206 -13
  98. package/gsd-core/bin/lib/io.cjs +38 -3
  99. package/gsd-core/bin/lib/markdown-sectionizer.cjs +8 -1
  100. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  101. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  102. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  103. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  104. package/gsd-core/bin/lib/milestone.cjs +821 -109
  105. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  106. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  107. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  108. package/gsd-core/bin/lib/pattern.cjs +122 -0
  109. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  110. package/gsd-core/bin/lib/phase-id.cjs +507 -36
  111. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  112. package/gsd-core/bin/lib/phase-locator.cjs +258 -58
  113. package/gsd-core/bin/lib/phase.cjs +891 -156
  114. package/gsd-core/bin/lib/plan-dependency-graph.cjs +303 -0
  115. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  116. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  117. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  118. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  119. package/gsd-core/bin/lib/planning-workspace.cjs +60 -6
  120. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  121. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  122. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  123. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  124. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +85 -0
  125. package/gsd-core/bin/lib/review-lane-descriptor.cjs +108 -0
  126. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  127. package/gsd-core/bin/lib/review-lane-runner.cjs +447 -68
  128. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  129. package/gsd-core/bin/lib/roadmap-command-router.cjs +76 -9
  130. package/gsd-core/bin/lib/roadmap-parser.cjs +1035 -194
  131. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  132. package/gsd-core/bin/lib/roadmap.cjs +405 -84
  133. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +795 -100
  134. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  135. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +440 -57
  136. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  137. package/gsd-core/bin/lib/runtime-homes.cjs +220 -41
  138. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +220 -44
  139. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  140. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  141. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  142. package/gsd-core/bin/lib/security.cjs +104 -5
  143. package/gsd-core/bin/lib/shell-command-projection.cjs +388 -30
  144. package/gsd-core/bin/lib/smart-entry.cjs +154 -22
  145. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  146. package/gsd-core/bin/lib/state-document.cjs +152 -8
  147. package/gsd-core/bin/lib/state-transition.cjs +424 -105
  148. package/gsd-core/bin/lib/state.cjs +1927 -401
  149. package/gsd-core/bin/lib/surface.cjs +35 -10
  150. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  151. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  152. package/gsd-core/bin/lib/uat-predicate.cjs +20 -4
  153. package/gsd-core/bin/lib/uat.cjs +706 -64
  154. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  155. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  156. package/gsd-core/bin/lib/unusable-input.cjs +33 -0
  157. package/gsd-core/bin/lib/update-context.cjs +8 -2
  158. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  159. package/gsd-core/bin/lib/validate.cjs +20 -6
  160. package/gsd-core/bin/lib/vendor/README.md +37 -0
  161. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  162. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  163. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  164. package/gsd-core/bin/lib/verification.cjs +287 -20
  165. package/gsd-core/bin/lib/verify.cjs +368 -880
  166. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  167. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +203 -19
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +576 -31
  169. package/gsd-core/bin/lib/workstream.cjs +8 -2
  170. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  171. package/gsd-core/bin/lib/worktree-safety.cjs +450 -125
  172. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  173. package/gsd-core/bin/shared/config-schema.manifest.json +9 -1
  174. package/gsd-core/references/agent-contracts.md +43 -26
  175. package/gsd-core/references/artifact-types.md +10 -3
  176. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  177. package/gsd-core/references/checkpoints.md +2 -2
  178. package/gsd-core/references/context-budget.md +1 -1
  179. package/gsd-core/references/debugger-techniques.md +255 -0
  180. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  181. package/gsd-core/references/doc-conflict-engine.md +1 -1
  182. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  184. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  185. package/gsd-core/references/execute-phase-response-language.md +1 -1
  186. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  187. package/gsd-core/references/gate-prompts.md +1 -1
  188. package/gsd-core/references/git-planning-commit.md +2 -1
  189. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  190. package/gsd-core/references/model-profiles.md +12 -4
  191. package/gsd-core/references/mvp-concepts.md +9 -9
  192. package/gsd-core/references/planner-guidance.md +3 -9
  193. package/gsd-core/references/planner-preconditions.md +1 -1
  194. package/gsd-core/references/planner-reviews.md +1 -1
  195. package/gsd-core/references/planning-config.md +8 -6
  196. package/gsd-core/references/research-documentation-lookup.md +5 -3
  197. package/gsd-core/references/revision-loop.md +1 -1
  198. package/gsd-core/references/specless-probe-fallback.md +8 -7
  199. package/gsd-core/references/universal-anti-patterns.md +3 -3
  200. package/gsd-core/references/verifier-phase-gates.md +192 -0
  201. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  202. package/gsd-core/references/verify-mvp-mode.md +1 -1
  203. package/gsd-core/references/workstream-flag.md +22 -6
  204. package/gsd-core/references/worktree-branch-check.md +2 -2
  205. package/gsd-core/templates/discussion-log.md +1 -1
  206. package/gsd-core/templates/phase-prompt.md +2 -4
  207. package/gsd-core/templates/state.md +4 -4
  208. package/gsd-core/templates/summary-complex.md +2 -0
  209. package/gsd-core/templates/summary-minimal.md +2 -0
  210. package/gsd-core/templates/summary-standard.md +2 -0
  211. package/gsd-core/templates/summary.md +2 -0
  212. package/gsd-core/templates/verification-report.md +9 -1
  213. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  214. package/gsd-core/workflows/audit-milestone.md +3 -0
  215. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  216. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  217. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  218. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  219. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  220. package/gsd-core/workflows/autonomous.md +33 -70
  221. package/gsd-core/workflows/cleanup.md +62 -3
  222. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  223. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +93 -0
  224. package/gsd-core/workflows/code-review-fix.md +37 -10
  225. package/gsd-core/workflows/code-review.md +74 -166
  226. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  227. package/gsd-core/workflows/complete-milestone.md +160 -95
  228. package/gsd-core/workflows/debug.md +16 -17
  229. package/gsd-core/workflows/diagnose-issues.md +56 -8
  230. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  231. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  232. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  233. package/gsd-core/workflows/discuss-phase-assumptions.md +7 -17
  234. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  235. package/gsd-core/workflows/docs-update.md +8 -51
  236. package/gsd-core/workflows/edit-phase.md +26 -1
  237. package/gsd-core/workflows/eval-review.md +3 -5
  238. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +64 -7
  239. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  240. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  241. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  242. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +21 -0
  243. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  244. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  245. package/gsd-core/workflows/execute-phase.md +103 -187
  246. package/gsd-core/workflows/execute-plan.md +36 -4
  247. package/gsd-core/workflows/explore.md +131 -4
  248. package/gsd-core/workflows/fast.md +10 -2
  249. package/gsd-core/workflows/health.md +73 -4
  250. package/gsd-core/workflows/help/modes/full.md +6 -1
  251. package/gsd-core/workflows/import.md +4 -4
  252. package/gsd-core/workflows/ingest-docs.md +7 -6
  253. package/gsd-core/workflows/mvp-phase.md +6 -3
  254. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  255. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  256. package/gsd-core/workflows/new-milestone.md +35 -47
  257. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  258. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  259. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  260. package/gsd-core/workflows/new-project.md +27 -240
  261. package/gsd-core/workflows/next.md +12 -0
  262. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  263. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  264. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  265. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  266. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  267. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  268. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  269. package/gsd-core/workflows/plan-phase.md +89 -209
  270. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  271. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  272. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  273. package/gsd-core/workflows/progress.md +45 -159
  274. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  275. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  276. package/gsd-core/workflows/quick/steps/quick-verification.md +67 -0
  277. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  278. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  279. package/gsd-core/workflows/quick.md +55 -405
  280. package/gsd-core/workflows/resume-project.md +3 -0
  281. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  282. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  283. package/gsd-core/workflows/review.md +41 -13
  284. package/gsd-core/workflows/section-manifest.json +219 -0
  285. package/gsd-core/workflows/secure-phase.md +1 -1
  286. package/gsd-core/workflows/session-report.md +2 -1
  287. package/gsd-core/workflows/settings.md +66 -2
  288. package/gsd-core/workflows/ship.md +104 -44
  289. package/gsd-core/workflows/sketch.md +1 -1
  290. package/gsd-core/workflows/spec-phase.md +41 -20
  291. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  292. package/gsd-core/workflows/spike.md +50 -16
  293. package/gsd-core/workflows/sync-skills.md +106 -13
  294. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  295. package/gsd-core/workflows/transition.md +53 -31
  296. package/gsd-core/workflows/ui-phase.md +13 -12
  297. package/gsd-core/workflows/ui-review.md +2 -2
  298. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  299. package/gsd-core/workflows/update.md +19 -8
  300. package/gsd-core/workflows/validate-phase.md +1 -1
  301. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  302. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  303. package/gsd-core/workflows/verify-work.md +17 -65
  304. package/hooks/dist/gsd-agent-isolation-guard.js +517 -0
  305. package/hooks/dist/gsd-check-update-worker.js +64 -12
  306. package/hooks/dist/gsd-check-update.js +19 -1
  307. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  308. package/hooks/dist/gsd-cursor-subagent-start.js +607 -26
  309. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  310. package/hooks/dist/gsd-prompt-guard.js +21 -20
  311. package/hooks/dist/gsd-read-injection-scanner.js +45 -24
  312. package/hooks/dist/gsd-statusline.js +90 -6
  313. package/hooks/dist/gsd-update-banner.js +22 -1
  314. package/hooks/dist/gsd-workflow-guard.js +134 -36
  315. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  316. package/hooks/dist/gsd-write-guard.js +359 -0
  317. package/hooks/dist/lib/git-cmd.js +92 -59
  318. package/hooks/dist/lib/injection-patterns.js +45 -0
  319. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  320. package/hooks/dist/lib/isolation-sentinel.js +277 -0
  321. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  322. package/hooks/gsd-agent-isolation-guard.js +517 -0
  323. package/hooks/gsd-check-update-worker.js +64 -12
  324. package/hooks/gsd-check-update.js +19 -1
  325. package/hooks/gsd-cursor-pre-tool.js +0 -3
  326. package/hooks/gsd-cursor-subagent-start.js +607 -26
  327. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  328. package/hooks/gsd-prompt-guard.js +21 -20
  329. package/hooks/gsd-read-injection-scanner.js +45 -24
  330. package/hooks/gsd-statusline.js +90 -6
  331. package/hooks/gsd-update-banner.js +22 -1
  332. package/hooks/gsd-workflow-guard.js +134 -36
  333. package/hooks/gsd-worktree-path-guard.js +2 -1
  334. package/hooks/gsd-write-guard.js +359 -0
  335. package/hooks/hooks.json +12 -0
  336. package/hooks/lib/git-cmd.js +92 -59
  337. package/hooks/lib/injection-patterns.js +45 -0
  338. package/hooks/lib/isolation-deny-reason.js +39 -0
  339. package/hooks/lib/isolation-sentinel.js +277 -0
  340. package/hooks/managed-hooks-registry.cjs +2 -0
  341. package/package.json +31 -10
  342. package/pi/gsd.cjs +71 -12
  343. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  344. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  345. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  346. package/scripts/build-hooks.js +9 -0
  347. package/scripts/changeset/lint.cjs +68 -6
  348. package/scripts/changeset/serialize.cjs +5 -1
  349. package/scripts/check-alias-drift.cjs +7 -43
  350. package/scripts/check-contract-drift.cjs +297 -0
  351. package/scripts/ci-test-scope.cjs +19 -2
  352. package/scripts/command-contract-helpers.cjs +903 -1
  353. package/scripts/gen-adr-index.cjs +728 -38
  354. package/scripts/gen-capability-matrix.cjs +1 -1
  355. package/scripts/gen-capability-registry.cjs +3 -15
  356. package/scripts/gen-context-index.cjs +439 -0
  357. package/scripts/gen-health-docs.cjs +390 -0
  358. package/scripts/gen-inventory-manifest.cjs +150 -4
  359. package/scripts/gen-loop-host-contract.cjs +4 -24
  360. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  361. package/scripts/gen-registry.cjs +3 -14
  362. package/scripts/gen-section-manifest.cjs +638 -0
  363. package/scripts/generate-package-identity.cjs +4 -2
  364. package/scripts/lib/alias-drift-families.cjs +46 -0
  365. package/scripts/lib/drift-scan.cjs +278 -0
  366. package/scripts/lint-allow-test-rule-refs.allowlist.json +15 -54
  367. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  368. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  369. package/scripts/lint-canary-version-leak.cjs +73 -0
  370. package/scripts/lint-command-contract.cjs +96 -13
  371. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  372. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  373. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  374. package/scripts/lint-default-flip-documentation.cjs +193 -0
  375. package/scripts/lint-docs-command-form.cjs +195 -0
  376. package/scripts/lint-docs-required.cjs +9 -1
  377. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  378. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  379. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  380. package/scripts/lint-example-parser-parity.cjs +395 -0
  381. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  382. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  383. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  384. package/scripts/lint-milestone-window-drift.cjs +468 -0
  385. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  386. package/scripts/lint-plan-count-drift.cjs +318 -0
  387. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  388. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  389. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  390. package/scripts/lint-regression-test-names.cjs +15 -13
  391. package/scripts/lint-removed-but-needed.cjs +320 -0
  392. package/scripts/lint-state-field-drift.cjs +805 -0
  393. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  394. package/scripts/lint-test-file-count.allowlist.json +40 -3
  395. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  396. package/scripts/lint-vendored-deps.cjs +124 -0
  397. package/scripts/mutation-matrix.cjs +13 -0
  398. package/scripts/pr-changed-files.cjs +63 -0
  399. package/scripts/pr-template-policy.cjs +14 -4
  400. package/scripts/prompt-injection-scan.sh +52 -6
  401. package/scripts/require-issue-link-policy.cjs +192 -0
  402. package/scripts/state-write-path-drift-baseline.json +19 -0
  403. package/scripts/sync-runtime-launcher.cjs +2 -4
  404. package/skills/gsd-autonomous/SKILL.md +0 -1
  405. package/skills/gsd-code-review/SKILL.md +1 -1
  406. package/skills/gsd-execute-phase/SKILL.md +1 -2
  407. package/skills/gsd-map-codebase/SKILL.md +1 -1
  408. package/skills/gsd-mempalace-capture/SKILL.md +2 -2
  409. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  410. package/skills/gsd-new-milestone/SKILL.md +2 -2
  411. package/skills/gsd-next/SKILL.md +0 -1
  412. package/skills/gsd-plan-phase/SKILL.md +1 -2
  413. package/skills/gsd-progress/SKILL.md +0 -1
  414. package/skills/gsd-quick/SKILL.md +1 -1
  415. package/skills/gsd-review-backlog/SKILL.md +2 -1
  416. package/skills/gsd-stats/SKILL.md +0 -1
  417. package/skills/gsd-verify-work/SKILL.md +1 -1
  418. package/vscode/package.json +1 -1
  419. package/gsd-core/workflows/discovery-phase.md +0 -298
  420. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  421. package/gsd-core/workflows/verify-phase.md +0 -577
  422. package/scripts/affected-tests-lib.cjs +0 -554
  423. package/scripts/gen-emitted-baseline.cjs +0 -145
  424. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  425. package/scripts/run-affected-tests.cjs +0 -7
  426. package/scripts/run-tests.cjs +0 -1050
@@ -1,54 +1,635 @@
1
1
  #!/usr/bin/env node
2
2
  // gsd-hook-version: {{GSD_VERSION}}
3
- // gsd-cursor-subagent-start.js — Cursor subagentStart hook (ADR-1239 / #2089)
3
+ // gsd-cursor-subagent-start.js — Cursor subagentStart hook (ADR-1239 / #2089,
4
+ // isolation guard #3045)
4
5
  //
5
6
  // Cursor invokes this script when a subagent session starts.
6
7
  // Protocol: JSON from Cursor on stdin; JSON response on stdout.
7
8
  //
8
- // Input schema (cursor subagentStart):
9
- // { session_id, is_background_agent, conversation_id, generation_id,
10
- // model, hook_event_name, cursor_version, workspace_roots,
11
- // user_email, transcript_path }
9
+ // Input schema (cursor subagentStart) — Cursor's hooks contract is a COMMON
10
+ // envelope shared by every hook, PLUS event-specific fields layered on top
11
+ // (cursor.com/docs/hooks, "Reference > Common schema"). A prior version of
12
+ // this comment documented only the envelope and omitted the event-specific
13
+ // fields entirely — that omission is exactly what caused #3045's isolation
14
+ // guard work to stall on a false schema conflict, so every field below is
15
+ // still read defensively (assume any of them may be absent/malformed):
16
+ // Common envelope (all hooks): conversation_id, generation_id, model,
17
+ // model_id, model_params, hook_event_name, cursor_version,
18
+ // workspace_roots (array of paths), user_email, transcript_path.
19
+ // (Some fields are omitted for app-lifecycle hooks; this script's own
20
+ // prior comment listed session_id/is_background_agent instead of
21
+ // model_id/model_params — the exact set observed is not guaranteed.)
22
+ // subagentStart-specific additions: subagent_id, subagent_type, task,
23
+ // parent_conversation_id, tool_call_id, subagent_model,
24
+ // is_parallel_worker, git_branch (optional).
12
25
  //
13
26
  // Output schema (cursor subagentStart):
14
- // { additional_context?: string }
27
+ // { additional_context?: string, permission?: "allow"|"deny", user_message?: string }
28
+ // "ask" is NOT a supported permission value for subagentStart — Cursor
29
+ // treats it as "deny". This script only ever emits "allow" (by omitting
30
+ // `permission`, preserving the pre-#3045 output shape) or an explicit
31
+ // "deny" with `user_message`.
15
32
  //
16
33
  // Behaviour:
17
34
  // - Injects a brief GSD state reminder so subagents (planner, executor,
18
- // verifier) have the current phase context.
19
- // - Fails open: any error silently exits 0.
35
+ // verifier) have the current phase context (unchanged since #2587).
36
+ // - NEW (#3045): denies spawning a GSD executor subagent when this
37
+ // project's dispatch isolation resolves to "harness-worktree" but the
38
+ // session is NOT actually running isolated from the user's primary
39
+ // checkout. Cursor's `--worktree` is a SESSION-level flag (no per-call
40
+ // isolation parameter exists on `subagentStart`, unlike Claude's
41
+ // `Agent(isolation=...)` kwarg), so this guard verifies EFFECTIVE STATE
42
+ // instead of looking for a flag — see resolveIsolationDecision() below.
43
+ // - Fails open on a payload it cannot parse or that carries fields it does
44
+ // not need: never throws, never blocks a call it cannot evaluate.
45
+ // Isolation resolution itself fails CLOSED (denies) for the two cases
46
+ // that are load-bearing and are NOT the same as "cannot parse": (a) a
47
+ // GSD project resolved to harness-worktree whose isolation state cannot
48
+ // be verified, and (b) a harness-worktree GSD project dispatch with no
49
+ // usable subagent_type — a guard that cannot verify must not answer
50
+ // "safe" (#3050).
20
51
  //
21
52
  // Cursor docs: https://cursor.com/docs/hooks
22
53
 
23
54
  'use strict';
24
55
 
25
56
  const fs = require('fs');
57
+ const path = require('path');
58
+ const os = require('os');
59
+
60
+ // Workspace resolution is shared across the Cursor hooks (#2587) — see
61
+ // hooks/lib/cursor-workspace.js. Staged next to these scripts by
62
+ // writeCursorHooksJson so the require always resolves post-install.
63
+ const { resolveStatePath } = require('./lib/cursor-workspace.js');
64
+ const { readSentinel, VALID_ISOLATION, extractDispatchIdentifiers, sentinelAppliesToDispatch } = require('./lib/isolation-sentinel.js');
65
+ const { REASON_CODE } = require('./lib/isolation-deny-reason.js');
66
+ // #3582: gsd-core/bin/lib/*.cjs (runtime-homes.cjs, worktree-safety.cjs,
67
+ // runtime-name-policy.cjs, capability-registry.cjs — required below, inside
68
+ // resolveIsolationEvidence and resolveFallbackIsolation) are tsc build
69
+ // artifacts (ADR-457), gitignored and absent on a raw plugin-marketplace /
70
+ // git-clone install that never ran `npm run build:lib`. Self-heal once, in
71
+ // evaluateRootIsolation, before any of those four requires run — see the
72
+ // call site below. This module itself depends on nothing under ./lib.
73
+ const { ensureRuntimeBuild, RuntimeBuildError } = require('../gsd-core/bin/ensure-runtime-build.cjs');
26
74
 
27
75
  const MSG_PRESENT =
28
76
  'GSD: Subagent session started — review .planning/STATE.md for the current phase and any blockers before acting.';
29
77
  const MSG_ABSENT =
30
78
  'GSD: Subagent session started — no .planning/ workflow found.';
31
79
 
32
- // Workspace resolution is shared across the Cursor hooks (#2587) — see
33
- // hooks/lib/cursor-workspace.js. Staged next to these scripts by
34
- // writeCursorHooksJson so the require always resolves post-install.
35
- const { resolveStatePath } = require('./lib/cursor-workspace.js');
80
+ // GSD's Cursor agent artifacts install with `destSubpath: "agents"`,
81
+ // `prefix: "gsd-"`, flat nesting, via the `convertClaudeAgentToCursorAgent`
82
+ // converter, and `hostIntegration.dispatch.namedDispatch === true`
83
+ // (gsd-core/bin/lib/capability-registry.cjs, runtimes.cursor) — i.e. Cursor
84
+ // dispatches named subagents by their real agent name, identically to
85
+ // Claude. So GSD's executor surfaces as subagent_type === "gsd-executor" on
86
+ // Cursor too, the same identifier hooks/gsd-agent-isolation-guard.js checks
87
+ // for on Claude. A Set, not a bare string compare, so a future sibling
88
+ // executor role can be added here without touching the matching logic below.
89
+ const EXECUTOR_SUBAGENT_TYPES = new Set(['gsd-executor']);
90
+
91
+ /**
92
+ * Runs `realpathFn`, never throwing. A path that cannot be resolved (does not
93
+ * exist, dangling symlink, ELOOP, ...) yields `null` rather than an
94
+ * exception — the caller decides what "cannot resolve" means for its own
95
+ * verdict (#3045 finding 2).
96
+ *
97
+ * `realpathFn` is injectable (defaults to `fs.realpathSync`), per the repo's
98
+ * dependency-injection seam convention (mirrors the `clock` seam elsewhere in
99
+ * these hooks) — this lets tests exercise the realpath-based spoof-resistance
100
+ * logic below with a fabricated symlink-resolution mapping, without ever
101
+ * creating a real filesystem symlink (directory symlinks require elevated
102
+ * privileges on unprivileged Windows CI).
103
+ */
104
+ function realpathOrNull(p, realpathFn) {
105
+ try {
106
+ return realpathFn(p);
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Resolve whether `root` is running in a session Cursor ISOLATED FOR THIS
114
+ * DISPATCH — i.e. a worktree the harness itself created and manages, not
115
+ * merely "some linked git worktree".
116
+ *
117
+ * #3045 security review (finding 3): "is a linked git worktree" is NOT "is
118
+ * isolated from the tree the human is using". A developer who opens Cursor
119
+ * directly in a hand-made `git worktree add` checkout — routine, see
120
+ * `.claude/worktrees/` in this very repo — is not protected by anything;
121
+ * nothing stops them from also editing that same checkout by hand. The ONLY
122
+ * signal that actually proves harness isolation is that `root` resolves
123
+ * under Cursor's OWN managed worktree root (`<cursor config dir>/worktrees`,
124
+ * i.e. `~/.cursor/worktrees` by default — `getGlobalConfigDir('cursor')`
125
+ * honors the `CURSOR_CONFIG_DIR` env override and `~` expansion for free).
126
+ * That is made NECESSARY AND SUFFICIENT below. Do NOT reinstate
127
+ * `resolveWorktreeLinkage`'s `linked_worktree_root` mode as an alternative
128
+ * OR'd proof of isolation — that is precisely the bypass finding 3 closed;
129
+ * a future "simplification" that merges it back in re-opens unconsented
130
+ * writes to the human's active checkout.
131
+ *
132
+ * `resolveWorktreeLinkage` is still called, but ONLY as a diagnostic: it
133
+ * distinguishes "confidently not isolated" from "git could not answer
134
+ * (timeout) — cannot determine" so the eventual deny reason stays
135
+ * actionable. Its result never flips `isolated`.
136
+ *
137
+ * Both the workspace root and the managed root are realpath'd before
138
+ * comparison (#3045 finding 2) — lexical `path.relative` alone is spoofable
139
+ * by a symlink or bind mount at either location, plantable by any process
140
+ * running with the user's permissions (including an agent already inside a
141
+ * legitimately isolated worktree, which has shell access by design).
142
+ * `fs.realpathSync` throwing (nonexistent path) never propagates — it
143
+ * degrades to "cannot resolve", never to "isolated". realpath also resolves
144
+ * the `CURSOR_CONFIG_DIR`-derived managed root itself (not just `root`), so a
145
+ * symlinked or case-differing `CURSOR_CONFIG_DIR` (case-insensitive
146
+ * filesystems normalize to on-disk casing via realpath's dirent walk, not
147
+ * string comparison) is covered on BOTH sides of the comparison, not only
148
+ * `root`'s.
149
+ *
150
+ * Returns `{ isolated: true|false, cannotDetermine: bool, notApplicable: bool }`.
151
+ * `notApplicable` (#3045 MAJOR 3) is true only for a confidently-not-a-git-repo
152
+ * root — see the `not_git_repo` branch below.
153
+ *
154
+ * `realpath` is injectable (`(p: string) => string`, throws like
155
+ * `fs.realpathSync` on an unresolvable path; defaults to the real
156
+ * `fs.realpathSync`) per the repo's clock-seam-style dependency-injection
157
+ * convention. This lets tests drive the exact spoof-resistance logic this
158
+ * function exists for (a symlink at the managed root pointing OUTSIDE it)
159
+ * with an injected resolution mapping, in-process, on every platform —
160
+ * without creating a real directory symlink, which requires elevated
161
+ * privileges on unprivileged Windows CI.
162
+ */
163
+ function resolveIsolationEvidence(root, { realpath = fs.realpathSync } = {}) {
164
+ let managedRoot = null;
165
+ try {
166
+ // Sibling data/policy module, staged alongside this hook at install time
167
+ // (same pattern as hooks/gsd-statusline.js's requires of gsd-core/bin/lib/*).
168
+ const { getGlobalConfigDir } = require('../gsd-core/bin/lib/runtime-homes.cjs');
169
+ managedRoot = path.join(getGlobalConfigDir('cursor'), 'worktrees');
170
+ } catch {
171
+ managedRoot = null;
172
+ }
173
+
174
+ const realRoot = realpathOrNull(root, realpath);
175
+ const realManagedRoot = managedRoot === null ? null : realpathOrNull(managedRoot, realpath);
36
176
 
37
- let raw = '';
38
- const stdinTimeout = setTimeout(() => {
39
- process.exit(0);
40
- }, 10000);
177
+ if (realRoot !== null && realManagedRoot !== null) {
178
+ const rel = path.relative(realManagedRoot, realRoot);
179
+ const underManagedRoot = rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
180
+ if (underManagedRoot) return { isolated: true, cannotDetermine: false, notApplicable: false };
181
+ }
182
+
183
+ // Not proven isolated by the only signal that counts. Resolve the
184
+ // diagnostic-only linkage check purely to make the deny reason legible —
185
+ // see the doc comment above; this NEVER flips `isolated`.
186
+ let linkageReason = null;
187
+ try {
188
+ // Sibling data/policy module, staged alongside this hook at install time.
189
+ const { resolveWorktreeLinkage } = require('../gsd-core/bin/lib/worktree-safety.cjs');
190
+ linkageReason = resolveWorktreeLinkage(root).reason;
191
+ } catch {
192
+ linkageReason = null;
193
+ }
194
+
195
+ if (realRoot === null) {
196
+ // `root` itself could not be resolved on disk. In the live hook this is
197
+ // defense in depth rather than a reachable path today: the GSD-project
198
+ // existence gate in resolveIsolationDecision already requires `root` to
199
+ // resolve (it must contain a readable `.planning/config.json`) before
200
+ // evidence is ever consulted, so a workspace root that plainly does not
201
+ // exist allows earlier as "not a GSD project" — never here. Kept anyway
202
+ // per finding 2's explicit directive: an unresolvable path must never
203
+ // silently read as "isolated".
204
+ return { isolated: false, cannotDetermine: true, notApplicable: false };
205
+ }
206
+ if (linkageReason === 'git_timed_out') {
207
+ return { isolated: false, cannotDetermine: true, notApplicable: false };
208
+ }
209
+ if (linkageReason === 'not_git_repo') {
210
+ // #3045 MAJOR 3: a confidently-non-git `root` has no primary git
211
+ // checkout to protect from an isolated-worktree bypass — Cursor's
212
+ // `--worktree` / `/worktree` (the deny message's own remediation) create
213
+ // a GIT worktree, so telling the user to start one is unactionable
214
+ // advice for a directory that isn't a git repo at all. Treat as INERT
215
+ // (allow) rather than a confident negative; this is distinct from
216
+ // `cannotDetermine` (git responded definitively here, it just said "not
217
+ // a repo") and from `isolated` (nothing was proven isolated) — it is its
218
+ // own "this guard's threat model does not apply" outcome.
219
+ return { isolated: false, cannotDetermine: false, notApplicable: true };
220
+ }
221
+ return { isolated: false, cannotDetermine: false, notApplicable: false };
222
+ }
41
223
 
42
- process.stdin.setEncoding('utf8');
43
- process.stdin.on('data', (chunk) => { raw += chunk; });
44
- process.stdin.on('end', () => {
45
- clearTimeout(stdinTimeout);
224
+ /**
225
+ * Resolve every non-empty string entry of `workspace_roots` — ALL checkout
226
+ * paths Cursor is operating on for this hook invocation, not just the first.
227
+ *
228
+ * #3045 security review (finding 1): a multi-root Cursor workspace whose
229
+ * FIRST root is a non-GSD directory (or an isolated worktree) and whose
230
+ * SECOND root is the GSD project in the primary checkout must still be
231
+ * caught — every root is a directory the dispatched subagent can reach and
232
+ * write to, regardless of position. `hooks/lib/cursor-workspace.js` already
233
+ * established the "scan every root" precedent for its own (different)
234
+ * purpose; this is a parallel scan for isolation applicability, not a
235
+ * duplicate of that module's single-root-resolution job (it resolves ONE
236
+ * root to report state-file presence; this resolves the full set to decide
237
+ * whether ANY of them is an unconsented write target).
238
+ *
239
+ * Cursor runs hooks with cwd set to its own config dir (~/.cursor), NOT the
240
+ * workspace (hooks/lib/cursor-workspace.js), so `workspace_roots` is the
241
+ * only reliable source for "what directories is this dispatch actually in".
242
+ *
243
+ * #3045 MINOR: a RELATIVE entry is rejected (`path.isAbsolute`), not merely
244
+ * accepted-and-hoped — every downstream consumer (`.planning/config.json`
245
+ * existence check, `realpathOrNull`, `resolveWorktreeLinkage`) joins/resolves
246
+ * it against whatever the CURRENT PROCESS cwd happens to be, which for this
247
+ * hook is Cursor's own config dir (~/.cursor per the comment above), NOT the
248
+ * workspace. A relative root would therefore resolve against the wrong
249
+ * directory and — because a wrong/nonexistent `.planning/config.json` path
250
+ * reads as "not a GSD project" — silently ALLOW a dispatch this guard should
251
+ * have evaluated (fail OPEN). Filtering it out here instead makes it "not a
252
+ * resolvable workspace root", which degrades the SAME way (allow, step 2 of
253
+ * resolveIsolationDecision's applicability list) but for the honest reason.
254
+ */
255
+ function getWorkspaceRoots(data) {
256
+ const roots = Array.isArray(data.workspace_roots) ? data.workspace_roots : [];
257
+ return roots.filter((r) => typeof r === 'string' && r.length > 0 && path.isAbsolute(r));
258
+ }
259
+
260
+ /**
261
+ * Decide whether to deny this subagentStart. Returns
262
+ * `{ action: 'allow' } | { action: 'deny', reason: string }`.
263
+ *
264
+ * Applicability (must positively determine all of the following to deny —
265
+ * otherwise allow):
266
+ * 1. `subagent_type` is not confidently a NON-executor (a present,
267
+ * non-empty string that isn't in EXECUTOR_SUBAGENT_TYPES short-circuits
268
+ * to allow immediately, before any project/isolation resolution runs —
269
+ * mirrors hooks/gsd-agent-isolation-guard.js checking subagent_type
270
+ * first, and matters here specifically: an unreadable config must never
271
+ * deny a dispatch this guard was never going to enforce against),
272
+ * 2. a workspace root is resolvable from `workspace_roots`,
273
+ * 3. that root is a GSD project (`.planning/config.json` exists there),
274
+ * 4. the resolved dispatch isolation is `harness-worktree`,
275
+ * 5. `subagent_type` identifies a GSD executor (or is missing/malformed —
276
+ * see the cannot-determine case below),
277
+ * 6. the session is NOT actually isolated (resolveIsolationEvidence).
278
+ *
279
+ * No workspace root at all degrades to allow (step 2), mirroring
280
+ * hooks/gsd-agent-isolation-guard.js's own "not a GSD project → allow"
281
+ * branch: project-existence is the gate that makes fail-closed apply in the
282
+ * first place, so being unable to even locate a candidate project is not
283
+ * itself a fail-closed trigger — it is the same "not a GSD project" shape
284
+ * that guard already treats as inert.
285
+ *
286
+ * Two DISTINCT fail-closed ("cannot determine") reasons per #3050's lesson
287
+ * that a guard which cannot verify must not answer "safe" — both scoped to
288
+ * "GSD project resolved to harness-worktree", never to a dispatch already
289
+ * confirmed to be a non-executor:
290
+ * - this project's dispatch-isolation configuration cannot be read/resolved
291
+ * (registry require/parse failure, or config.json unreadable),
292
+ * - `subagent_type` is missing or not a usable non-empty string on a
293
+ * dispatch this guard could not rule out as an executor.
294
+ *
295
+ * Isolation resolution (#3045 BLOCKER fix, see hooks/lib/isolation-sentinel.js):
296
+ * prefers the workflow's own PERSISTED per-dispatch decision (the sentinel
297
+ * `record-dispatch-isolation` writes) over re-deriving a host CAPABILITY from
298
+ * the registry. `none`/`orchestrator-worktree` from a fresh sentinel ALLOW
299
+ * immediately — sequential/orchestrator-managed dispatch is legitimate. An
300
+ * absent/stale sentinel falls back to `resolveFallbackIsolation` (registry +
301
+ * `workflow.use_worktrees`, runtime resolved GSD_RUNTIME env >
302
+ * .planning/config.json `runtime` key > 'cursor'). The default is
303
+ * confidently "cursor" here — UNLIKE hooks/gsd-agent-isolation-guard.js's own
304
+ * fallback, which must treat "no explicit signal" as cannot-determine
305
+ * because that hook installs across every `hostIntegration.hooksSurface ===
306
+ * 'settings-json'` runtime — because THIS script only ever runs as Cursor's
307
+ * own subagentStart hook; there is no other host it could be executing
308
+ * under, so defaulting to 'cursor' is a confirmed fact of the execution
309
+ * context, not a guess (#3045 MINOR — this note replaces a prior comment
310
+ * that inaccurately claimed to "mirror" runtime-slash.cjs's resolveRuntime,
311
+ * which defaults to 'claude'; the two intentionally diverge).
312
+ *
313
+ * #3045 security review (finding 1): applicability step 2 above now means
314
+ * "a workspace root is resolvable", plural — resolveIsolationDecision
315
+ * evaluates EVERY entry of `workspace_roots` via evaluateRootIsolation() and
316
+ * denies on the first one that fails. `subagent_type` is still resolved
317
+ * exactly once, up front, before any root is touched (applicability step 1
318
+ * stays a single check, not per-root — an unreadable config on one root must
319
+ * never even be attempted for a confirmed non-executor dispatch).
320
+ */
321
+ function resolveIsolationDecision(data, { clock = Date, realpath = fs.realpathSync } = {}) {
322
+ const subagentType = data.subagent_type;
323
+ const isConfirmedNonExecutor = typeof subagentType === 'string'
324
+ && subagentType.length > 0
325
+ && !EXECUTOR_SUBAGENT_TYPES.has(subagentType);
326
+ if (isConfirmedNonExecutor) return { action: 'allow' };
327
+
328
+ const roots = getWorkspaceRoots(data);
329
+ if (roots.length === 0) return { action: 'allow' };
330
+
331
+ // #3045 SECURITY F2: best-effort plan/phase extraction from this
332
+ // dispatch's own `task` text (Cursor carries the same prompt content the
333
+ // Claude Agent() dispatch does — see extractDispatchIdentifiers), so a
334
+ // fresh sentinel that disagrees with THIS dispatch is treated as
335
+ // inapplicable rather than trusted.
336
+ const dispatchIds = extractDispatchIdentifiers(data.task);
337
+
338
+ for (const root of roots) {
339
+ const verdict = evaluateRootIsolation(root, subagentType, { clock, dispatchIds, realpath });
340
+ if (verdict.action === 'deny') return verdict;
341
+ }
342
+ return { action: 'allow' };
343
+ }
344
+
345
+ // ─── #3566: per-install runtime marker ────────────────────────────────────────
346
+ // Same contract as hooks/gsd-agent-isolation-guard.js's readInstallRuntimeMarker
347
+ // (mirroring src/model-resolver.cts #2297): bin/install.js writes
348
+ // `<install>/gsd-core/.gsd-runtime` beside VERSION for every runtime install;
349
+ // this hook ships at `<install>/hooks/`, so the marker is the `gsd-core` sibling
350
+ // of this file's own directory — the same sibling-layout assumption the
351
+ // require('../gsd-core/bin/lib/…') calls below already make. Epic #3473 B3 owns
352
+ // consolidating every marker reader into one shared seam.
353
+ let _installMarkerCache; // undefined = unread; null = known absent; string = value
354
+
355
+ function readInstallRuntimeMarker() {
356
+ if (_installMarkerCache !== undefined) return _installMarkerCache;
46
357
  try {
47
- const statePath = resolveStatePath(raw);
48
- const statePresent = fs.existsSync(statePath);
49
- const msg = statePresent ? MSG_PRESENT : MSG_ABSENT;
50
- process.stdout.write(JSON.stringify({ additional_context: msg }));
358
+ const markerPath = path.join(__dirname, '..', 'gsd-core', '.gsd-runtime');
359
+ const raw = fs.readFileSync(markerPath, 'utf-8').trim();
360
+ _installMarkerCache = raw || null;
51
361
  } catch {
52
- process.stdout.write(JSON.stringify({}));
362
+ // No marker: dev/source tree, or an install predating #2297 — "no signal
363
+ // from this rung", never a resolution failure.
364
+ _installMarkerCache = null;
53
365
  }
54
- });
366
+ return _installMarkerCache;
367
+ }
368
+
369
+ // Test seam — same contract as model-resolver.cts's #2297 seam; the dev/source
370
+ // tree has no marker file, so spawned-hook tests (fresh process, no marker)
371
+ // are unaffected.
372
+ function _setInstallRuntimeMarkerForTests(value) {
373
+ _installMarkerCache = value;
374
+ }
375
+
376
+ /**
377
+ * Conservative fallback resolution used when the #3045 sentinel is absent or
378
+ * stale for `root`: re-derive isolation from the registry CAPABILITY, gated
379
+ * by `workflow.use_worktrees` (config-schema key confirmed present in
380
+ * gsd-core/bin/shared/config-schema.manifest.json's validKeys, so it survives
381
+ * loadConfig's whitelist; read directly from the raw config.json here, same
382
+ * side-effect-free approach cmdConfigGet itself uses).
383
+ *
384
+ * #3045 MAJOR fix ("Cursor residual false-deny"): previously defaulted
385
+ * confidently to 'cursor' whenever no `GSD_RUNTIME`/config.json `runtime`
386
+ * signal existed, purely because this script only ever executes as Cursor's
387
+ * OWN `subagentStart` hook — true of the PROCESS, but not evidence the
388
+ * PROJECT itself declared an isolation requirement this guard can verify.
389
+ * Combined with a stale/absent sentinel (outside `execute-phase`, after
390
+ * `.gsd` cleanup, a phase running past the sentinel's staleness window, or a
391
+ * base-check-degraded run whose sentinel went stale before a fresh one was
392
+ * recorded), that default made every such `gsd-executor` dispatch resolve to
393
+ * "harness-worktree" and then hard-DENY unless the session happened to be
394
+ * running under Cursor's own managed worktree root — a false-deny of
395
+ * otherwise legitimate dispatches, unlike `hooks/gsd-agent-isolation-guard.js`,
396
+ * which degrades an undeterminable runtime to inert (#3045 MAJOR 2). Aligned
397
+ * here: an explicit signal is now required — `GSD_RUNTIME` > config.json
398
+ * `runtime` key > the per-install `.gsd-runtime` marker (#3566) >
399
+ * `~/.gsd/defaults.json` `runtime` (mirrors the Claude hook's
400
+ * `resolveRuntimeIdentity`; `bin/install.js`'s `writeNonClaudeDefaults`
401
+ * persists the installed runtime there for every non-Claude install,
402
+ * including Cursor, so a REAL Cursor+GSD install still resolves confidently
403
+ * — this only stops GUESSING 'cursor' for a project that never declared any
404
+ * runtime signal at all).
405
+ */
406
+ function resolveFallbackIsolation(root, configPath) {
407
+ const { resolveRuntimeNameFromCandidates } = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
408
+ const { runtimes } = require('../gsd-core/bin/lib/capability-registry.cjs');
409
+
410
+ let runtimeId = resolveRuntimeNameFromCandidates(process.env.GSD_RUNTIME);
411
+ const rawConfig = fs.readFileSync(configPath, 'utf-8');
412
+ const parsedConfig = JSON.parse(rawConfig);
413
+ if (!runtimeId && parsedConfig && typeof parsedConfig === 'object' && 'runtime' in parsedConfig) {
414
+ runtimeId = resolveRuntimeNameFromCandidates(parsedConfig.runtime) || null;
415
+ }
416
+ if (!runtimeId) {
417
+ // #3566: the per-install marker, above the host-wide defaults — same fix as
418
+ // hooks/gsd-agent-isolation-guard.js's resolveRuntimeIdentity. defaults.json
419
+ // is host-wide and names whichever runtime installed LAST (#2840's poison);
420
+ // the marker describes THIS install (written for every runtime since #2297).
421
+ runtimeId = resolveRuntimeNameFromCandidates(readInstallRuntimeMarker()) || null;
422
+ }
423
+ if (!runtimeId) {
424
+ try {
425
+ const defaultsPath = path.join(os.homedir(), '.gsd', 'defaults.json');
426
+ const defaultsParsed = JSON.parse(fs.readFileSync(defaultsPath, 'utf-8'));
427
+ if (defaultsParsed && typeof defaultsParsed === 'object' && 'runtime' in defaultsParsed) {
428
+ runtimeId = resolveRuntimeNameFromCandidates(defaultsParsed.runtime) || null;
429
+ }
430
+ } catch {
431
+ // Absent/unreadable ~/.gsd/defaults.json — no signal, fall through.
432
+ }
433
+ }
434
+ if (!runtimeId) {
435
+ // No explicit signal anywhere confirms this project resolved
436
+ // harness-worktree — degrade to inert rather than guess 'cursor'.
437
+ return 'none';
438
+ }
439
+
440
+ const runtimeEntry = runtimes != null ? runtimes[runtimeId] : null;
441
+ const declared = runtimeEntry?.runtime?.hostIntegration?.dispatch?.isolation ?? null;
442
+ let declaredIsolation = (typeof declared === 'string' && VALID_ISOLATION.has(declared)) ? declared : 'none';
443
+
444
+ if (declaredIsolation === 'harness-worktree' &&
445
+ parsedConfig && typeof parsedConfig === 'object' && parsedConfig.workflow &&
446
+ typeof parsedConfig.workflow === 'object' && parsedConfig.workflow.use_worktrees === false) {
447
+ declaredIsolation = 'none';
448
+ }
449
+
450
+ return declaredIsolation;
451
+ }
452
+
453
+ /**
454
+ * Applicability + isolation verdict for a SINGLE workspace root. Returns
455
+ * `{ action: 'allow' } | { action: 'deny', reason: string }`. Extracted from
456
+ * resolveIsolationDecision (#3045 finding 1) so every root in a multi-root
457
+ * workspace runs the identical check.
458
+ */
459
+ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds = null, realpath = fs.realpathSync } = {}) {
460
+ const configPath = path.join(root, '.planning', 'config.json');
461
+ let isGsdProject;
462
+ try {
463
+ fs.accessSync(configPath, fs.constants.F_OK);
464
+ isGsdProject = true;
465
+ } catch {
466
+ isGsdProject = false;
467
+ }
468
+ if (!isGsdProject) return { action: 'allow' };
469
+
470
+ // #3582: self-heal the compiled runtime library BEFORE any of its four
471
+ // downstream requires (resolveFallbackIsolation's two, resolveIsolationEvidence's
472
+ // two — reached only below this point). Checked separately from the
473
+ // sentinel/fallback try block below so a build failure surfaces its own
474
+ // actionable RuntimeBuildError message rather than being folded into the
475
+ // generic "could not read or resolve ... configuration" deny reason (the
476
+ // #3050 misreport this issue exists to fix). Still fails closed either way.
477
+ try {
478
+ ensureRuntimeBuild();
479
+ } catch (err) {
480
+ return {
481
+ action: 'deny',
482
+ reason:
483
+ `GSD subagent isolation guard: cannot resolve this project's dispatch-isolation ` +
484
+ `configuration because the GSD runtime library failed to self-build. ` +
485
+ `${err instanceof RuntimeBuildError ? err.message : String(err && err.message || err)} ` +
486
+ `Refusing to allow this subagent to spawn until the runtime library is built — a guard ` +
487
+ `that cannot verify must not answer "safe" (#3050).`,
488
+ reasonCode: REASON_CODE.RUNTIME_BUILD_FAILED,
489
+ };
490
+ }
491
+
492
+ let declaredIsolation;
493
+ try {
494
+ // #3045 BLOCKER fix: a fresh sentinel is authoritative for THIS
495
+ // dispatch's actual resolved isolation — see the doc comment above.
496
+ // #3045 SECURITY F2: a fresh sentinel that names a DIFFERENT
497
+ // plan/phase than this dispatch is not applicable to it — fall through
498
+ // to the conservative fallback exactly as a stale sentinel would.
499
+ const sentinel = readSentinel(root, { clock });
500
+ declaredIsolation = (sentinel.present && !sentinel.stale && sentinelAppliesToDispatch(sentinel, dispatchIds))
501
+ ? sentinel.isolation
502
+ : resolveFallbackIsolation(root, configPath);
503
+ } catch {
504
+ return {
505
+ action: 'deny',
506
+ reason:
507
+ `GSD subagent isolation guard: could not read or resolve this project's ` +
508
+ `dispatch-isolation configuration ('.planning/config.json' exists under "${root}"). ` +
509
+ `Refusing to allow this subagent to spawn without being able to verify whether ` +
510
+ `isolation is required — a guard that cannot verify must not answer "safe" (#3050). ` +
511
+ `Retry once the project configuration is readable.`,
512
+ reasonCode: REASON_CODE.CONFIG_UNREADABLE,
513
+ };
514
+ }
515
+
516
+ if (declaredIsolation !== 'harness-worktree') return { action: 'allow' };
517
+
518
+ // isConfirmedNonExecutor already excluded "present, non-empty, unrecognized
519
+ // string" above — reaching here means subagentType is either the confirmed
520
+ // executor or missing/malformed (cannot rule it out).
521
+ if (typeof subagentType !== 'string' || subagentType.length === 0) {
522
+ return {
523
+ action: 'deny',
524
+ reason:
525
+ `GSD subagent isolation guard: this project's dispatch isolation resolves to ` +
526
+ `"harness-worktree", but the subagentStart payload for this dispatch carries no usable ` +
527
+ `subagent_type. Refusing to allow it to spawn without being able to confirm whether it ` +
528
+ `is a GSD executor — a guard that cannot verify must not answer "safe" (#3050).`,
529
+ reasonCode: REASON_CODE.NO_SUBAGENT_TYPE,
530
+ };
531
+ }
532
+
533
+ const evidence = resolveIsolationEvidence(root, { realpath });
534
+ if (evidence.isolated) return { action: 'allow' };
535
+ if (evidence.notApplicable) return { action: 'allow' };
536
+
537
+ if (evidence.cannotDetermine) {
538
+ return {
539
+ action: 'deny',
540
+ reason:
541
+ `GSD subagent isolation guard: this project's dispatch isolation resolves to ` +
542
+ `"harness-worktree", but whether "${root}" is running in an isolated Cursor worktree ` +
543
+ `could not be determined (git did not respond). Refusing to allow subagent_type=` +
544
+ `"${subagentType}" to spawn without being able to verify isolation — a guard that ` +
545
+ `cannot verify must not answer "safe" (#3050). Retry once git is responsive.`,
546
+ reasonCode: REASON_CODE.CANNOT_DETERMINE_ISOLATION,
547
+ };
548
+ }
549
+
550
+ return {
551
+ action: 'deny',
552
+ reason:
553
+ `GSD subagent isolation guard: this project's dispatch isolation resolves to ` +
554
+ `"harness-worktree", but subagent_type="${subagentType}" is about to spawn in "${root}", ` +
555
+ `which is not an isolated Cursor worktree — it would edit the user's primary checkout ` +
556
+ `directly, with no consent and no warning. Start an isolated session first (the ` +
557
+ `"--worktree" CLI flag or the "/worktree" chat command; Cursor manages these worktrees ` +
558
+ `under "~/.cursor/worktrees/") and retry.`,
559
+ reasonCode: REASON_CODE.NOT_ISOLATED_WORKTREE,
560
+ };
561
+ }
562
+
563
+ /* istanbul ignore next -- stdin adapter, exercised via spawnSync in tests */
564
+ function main() {
565
+ let raw = '';
566
+ const stdinTimeout = setTimeout(() => {
567
+ process.exit(0);
568
+ }, 10000);
569
+
570
+ process.stdin.setEncoding('utf8');
571
+ process.stdin.on('data', (chunk) => { raw += chunk; });
572
+ process.stdin.on('end', () => {
573
+ clearTimeout(stdinTimeout);
574
+
575
+ let data = null;
576
+ try {
577
+ data = JSON.parse(raw);
578
+ } catch {
579
+ data = null;
580
+ }
581
+
582
+ // Resolve the state-reminder context ONCE, up front, so it can ride along
583
+ // with EITHER outcome below (#3045 MINOR: a deny previously dropped this
584
+ // reminder entirely — process.stdout.write for the deny branch returned
585
+ // before the additional_context block ever ran — instead of preserving it
586
+ // alongside the deny; the subagent still benefits from phase/blocker
587
+ // context even when its dispatch is refused).
588
+ let additionalContext = null;
589
+ try {
590
+ const statePath = resolveStatePath(raw);
591
+ const statePresent = fs.existsSync(statePath);
592
+ additionalContext = statePresent ? MSG_PRESENT : MSG_ABSENT;
593
+ } catch {
594
+ additionalContext = null;
595
+ }
596
+
597
+ if (data && typeof data === 'object') {
598
+ let decision = { action: 'allow' };
599
+ try {
600
+ decision = resolveIsolationDecision(data);
601
+ } catch {
602
+ // Defense in depth only: every verify-and-deny path above has its own
603
+ // explicit try/catch that resolves to a deny with a distinct reason.
604
+ // Anything reaching here is an unexpected failure outside those paths
605
+ // (e.g. malformed workspace_roots entries) — never crash the hook.
606
+ decision = { action: 'allow' };
607
+ }
608
+ if (decision.action === 'deny') {
609
+ const out = { permission: 'deny', user_message: decision.reason, reason_code: decision.reasonCode };
610
+ if (additionalContext !== null) out.additional_context = additionalContext;
611
+ process.stdout.write(JSON.stringify(out));
612
+ return;
613
+ }
614
+ }
615
+
616
+ process.stdout.write(JSON.stringify(additionalContext !== null ? { additional_context: additionalContext } : {}));
617
+ });
618
+ }
619
+
620
+ if (require.main === module) {
621
+ main();
622
+ }
623
+
624
+ // #3045 MAJOR ("clock seam is dead code" fix): exported so tests can
625
+ // `require()` this module and inject a `clock` (`{now(): number}`) directly
626
+ // per the repo's clock-seam convention, instead of racing real `Date.now()`
627
+ // across a spawned subprocess boundary.
628
+ module.exports = {
629
+ resolveIsolationDecision,
630
+ evaluateRootIsolation,
631
+ resolveFallbackIsolation,
632
+ resolveIsolationEvidence,
633
+ getWorkspaceRoots,
634
+ _setInstallRuntimeMarkerForTests,
635
+ };