@opengsd/gsd-core 1.13.0 → 1.15.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 (441) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ja-JP.md +3 -3
  4. package/README.ko-KR.md +3 -3
  5. package/README.pt-BR.md +3 -3
  6. package/README.zh-CN.md +3 -3
  7. package/agents/gsd-advisor-researcher.compact.md +85 -0
  8. package/agents/gsd-ai-researcher.compact.md +96 -0
  9. package/agents/gsd-assumptions-analyzer.compact.md +81 -0
  10. package/agents/gsd-code-fixer.compact.md +459 -0
  11. package/agents/gsd-code-fixer.md +9 -8
  12. package/agents/gsd-code-reviewer.compact.md +269 -0
  13. package/agents/gsd-code-reviewer.md +15 -3
  14. package/agents/gsd-codebase-mapper.compact.md +760 -0
  15. package/agents/gsd-debug-session-manager.compact.md +360 -0
  16. package/agents/gsd-debug-session-manager.md +17 -2
  17. package/agents/gsd-debugger.md +2 -2
  18. package/agents/gsd-doc-classifier.compact.md +192 -0
  19. package/agents/gsd-doc-synthesizer.compact.md +200 -0
  20. package/agents/gsd-doc-verifier.compact.md +143 -0
  21. package/agents/gsd-doc-writer.compact.md +440 -0
  22. package/agents/gsd-dom-verifier.compact.md +138 -0
  23. package/agents/gsd-domain-researcher.compact.md +141 -0
  24. package/agents/gsd-eval-auditor.compact.md +160 -0
  25. package/agents/gsd-eval-auditor.md +1 -1
  26. package/agents/gsd-eval-planner.compact.md +137 -0
  27. package/agents/gsd-executor.md +13 -8
  28. package/agents/gsd-framework-selector.compact.md +82 -0
  29. package/agents/gsd-integration-checker.compact.md +245 -0
  30. package/agents/gsd-intel-updater.compact.md +226 -0
  31. package/agents/gsd-intel-updater.md +1 -1
  32. package/agents/gsd-mempalace-curator.compact.md +45 -0
  33. package/agents/gsd-nyquist-auditor.compact.md +179 -0
  34. package/agents/gsd-pattern-mapper.compact.md +275 -0
  35. package/agents/gsd-phase-researcher.md +19 -11
  36. package/agents/gsd-plan-checker.md +8 -7
  37. package/agents/gsd-planner.md +12 -8
  38. package/agents/gsd-project-researcher.compact.md +587 -0
  39. package/agents/gsd-project-researcher.md +1 -1
  40. package/agents/gsd-research-synthesizer.compact.md +212 -0
  41. package/agents/gsd-research-synthesizer.md +1 -1
  42. package/agents/gsd-roadmapper.compact.md +454 -0
  43. package/agents/gsd-roadmapper.md +13 -0
  44. package/agents/gsd-security-auditor.compact.md +162 -0
  45. package/agents/gsd-ui-auditor.compact.md +404 -0
  46. package/agents/gsd-ui-auditor.md +155 -17
  47. package/agents/gsd-ui-checker.compact.md +277 -0
  48. package/agents/gsd-ui-researcher.compact.md +282 -0
  49. package/agents/gsd-ui-researcher.md +1 -1
  50. package/agents/gsd-user-profiler.compact.md +108 -0
  51. package/agents/gsd-verifier.md +10 -9
  52. package/bin/install.js +848 -163
  53. package/commands/gsd/autonomous.md +2 -2
  54. package/commands/gsd/capture.md +1 -1
  55. package/commands/gsd/cleanup.md +1 -0
  56. package/commands/gsd/code-review.md +2 -1
  57. package/commands/gsd/complete-milestone.md +1 -0
  58. package/commands/gsd/config.md +1 -0
  59. package/commands/gsd/debug.md +1 -0
  60. package/commands/gsd/graphify.md +1 -0
  61. package/commands/gsd/health.md +1 -0
  62. package/commands/gsd/mempalace-capture.md +8 -3
  63. package/commands/gsd/mempalace-recall.md +1 -0
  64. package/commands/gsd/new-milestone.md +1 -0
  65. package/commands/gsd/new-project.md +1 -0
  66. package/commands/gsd/next.md +1 -0
  67. package/commands/gsd/pause-work.md +1 -0
  68. package/commands/gsd/phase.md +1 -0
  69. package/commands/gsd/plan-review-convergence.md +6 -6
  70. package/commands/gsd/pr-branch.md +1 -0
  71. package/commands/gsd/progress.md +1 -1
  72. package/commands/gsd/quick-batch.md +1 -1
  73. package/commands/gsd/resume-work.md +1 -0
  74. package/commands/gsd/review-backlog.md +1 -0
  75. package/commands/gsd/review.md +2 -3
  76. package/commands/gsd/settings.md +2 -1
  77. package/commands/gsd/stats.md +1 -0
  78. package/commands/gsd/thread.md +1 -0
  79. package/commands/gsd/workspace.md +1 -0
  80. package/commands/gsd/workstreams.md +1 -0
  81. package/gsd-core/bin/check-latest-version.cjs +8 -3
  82. package/gsd-core/bin/gsd-tools.cjs +672 -146
  83. package/gsd-core/bin/lib/adr-parser.cjs +4 -2
  84. package/gsd-core/bin/lib/artifacts.cjs +2 -1
  85. package/gsd-core/bin/lib/audit.cjs +119 -34
  86. package/gsd-core/bin/lib/broken-windows.cjs +168 -49
  87. package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
  88. package/gsd-core/bin/lib/capability-loader.cjs +135 -1
  89. package/gsd-core/bin/lib/capability-registry.cjs +96 -189
  90. package/gsd-core/bin/lib/capability-source.cjs +19 -2
  91. package/gsd-core/bin/lib/capability-validator.cjs +14 -2
  92. package/gsd-core/bin/lib/check-command-router.cjs +213 -49
  93. package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
  94. package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
  95. package/gsd-core/bin/lib/commands.cjs +823 -112
  96. package/gsd-core/bin/lib/config-loader.cjs +66 -4
  97. package/gsd-core/bin/lib/config.cjs +186 -45
  98. package/gsd-core/bin/lib/coverage.cjs +1 -1
  99. package/gsd-core/bin/lib/decisions.cjs +164 -45
  100. package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
  101. package/gsd-core/bin/lib/frontmatter.cjs +13 -0
  102. package/gsd-core/bin/lib/graphify.cjs +10 -2
  103. package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
  104. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
  105. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
  106. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  107. package/gsd-core/bin/lib/init.cjs +614 -86
  108. package/gsd-core/bin/lib/install-engine.cjs +29 -3
  109. package/gsd-core/bin/lib/install-profiles.cjs +14 -0
  110. package/gsd-core/bin/lib/installer-migrations.cjs +41 -5
  111. package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
  112. package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
  113. package/gsd-core/bin/lib/milestone.cjs +37 -13
  114. package/gsd-core/bin/lib/model-resolver.cjs +253 -53
  115. package/gsd-core/bin/lib/phase-command-router.cjs +16 -2
  116. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  117. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  118. package/gsd-core/bin/lib/phase-id.cjs +268 -27
  119. package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
  120. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  121. package/gsd-core/bin/lib/phase.cjs +393 -88
  122. package/gsd-core/bin/lib/plan-document.cjs +49 -1
  123. package/gsd-core/bin/lib/planning-document.cjs +459 -0
  124. package/gsd-core/bin/lib/planning-inspect.cjs +52 -19
  125. package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
  126. package/gsd-core/bin/lib/planning-workspace.cjs +57 -3
  127. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  128. package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
  129. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  130. package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
  131. package/gsd-core/bin/lib/project-root.cjs +41 -2
  132. package/gsd-core/bin/lib/quick-batch.cjs +1 -1
  133. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
  134. package/gsd-core/bin/lib/research-store.cjs +11 -12
  135. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  136. package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
  137. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  138. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
  139. package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
  140. package/gsd-core/bin/lib/roadmap-parser.cjs +219 -18
  141. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
  142. package/gsd-core/bin/lib/roadmap.cjs +356 -42
  143. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -41
  144. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +15 -4
  145. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
  146. package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
  147. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -37
  148. package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
  149. package/gsd-core/bin/lib/security.cjs +126 -7
  150. package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
  151. package/gsd-core/bin/lib/state-document.cjs +130 -28
  152. package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
  153. package/gsd-core/bin/lib/state-transition.cjs +181 -30
  154. package/gsd-core/bin/lib/state.cjs +265 -27
  155. package/gsd-core/bin/lib/surface.cjs +77 -3
  156. package/gsd-core/bin/lib/task-command-router.cjs +12 -6
  157. package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
  158. package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
  159. package/gsd-core/bin/lib/uat.cjs +9 -1
  160. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  161. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
  162. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  163. package/gsd-core/bin/lib/update-context.cjs +30 -24
  164. package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
  165. package/gsd-core/bin/lib/verification.cjs +315 -30
  166. package/gsd-core/bin/lib/verify-command-grounding.cjs +47 -3
  167. package/gsd-core/bin/lib/verify.cjs +320 -48
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
  169. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  170. package/gsd-core/bin/lib/worktree-safety.cjs +797 -58
  171. package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
  172. package/gsd-core/bin/shared/config-schema.manifest.json +6 -0
  173. package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
  174. package/gsd-core/references/checkpoints.md +5 -3
  175. package/gsd-core/references/compact-content-gate.md +66 -0
  176. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  177. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  178. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  179. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  180. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  181. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  182. package/gsd-core/references/edge-probe.md +195 -21
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
  184. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  185. package/gsd-core/references/gsd-run-resolver.md +1 -1
  186. package/gsd-core/references/loop-hook-dispatch.md +18 -0
  187. package/gsd-core/references/model-profiles.md +13 -4
  188. package/gsd-core/references/phase-argument-parsing.md +9 -7
  189. package/gsd-core/references/phase-id-convention.md +28 -0
  190. package/gsd-core/references/planner-gap-closure.md +2 -0
  191. package/gsd-core/references/planner-load-graph-context.md +24 -13
  192. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  193. package/gsd-core/references/planning-config.md +14 -2
  194. package/gsd-core/references/tdd.md +30 -4
  195. package/gsd-core/references/thinking-models-planning.md +18 -2
  196. package/gsd-core/references/ui-consideration-probe.md +10 -5
  197. package/gsd-core/references/verification-patterns.md +17 -4
  198. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  199. package/gsd-core/references/worktree-path-safety.md +433 -2
  200. package/gsd-core/templates/README.md +7 -1
  201. package/gsd-core/templates/state.md +6 -3
  202. package/gsd-core/templates/summary.compact.md +212 -0
  203. package/gsd-core/templates/user-setup.compact.md +199 -0
  204. package/gsd-core/templates/user-setup.md +0 -9
  205. package/gsd-core/templates/verification-report.md +1 -1
  206. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  207. package/gsd-core/workflows/add-backlog.md +1 -1
  208. package/gsd-core/workflows/add-phase.md +1 -1
  209. package/gsd-core/workflows/add-tests.md +2 -2
  210. package/gsd-core/workflows/add-todo.md +6 -5
  211. package/gsd-core/workflows/ai-integration-phase.md +11 -3
  212. package/gsd-core/workflows/audit-fix.md +1 -1
  213. package/gsd-core/workflows/audit-milestone.md +1 -1
  214. package/gsd-core/workflows/audit-uat.md +1 -1
  215. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  216. package/gsd-core/workflows/autonomous.md +29 -16
  217. package/gsd-core/workflows/check-todos.md +6 -4
  218. package/gsd-core/workflows/cleanup.md +5 -3
  219. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  220. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +8 -1
  221. package/gsd-core/workflows/code-review-fix.md +108 -22
  222. package/gsd-core/workflows/code-review.md +216 -73
  223. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
  224. package/gsd-core/workflows/complete-milestone.md +41 -264
  225. package/gsd-core/workflows/debug.md +3 -3
  226. package/gsd-core/workflows/diagnose-issues.md +1 -1
  227. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  228. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  229. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  230. package/gsd-core/workflows/discuss-phase.md +1 -1
  231. package/gsd-core/workflows/do.md +2 -2
  232. package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
  233. package/gsd-core/workflows/docs-update.md +17 -158
  234. package/gsd-core/workflows/edit-phase.md +1 -1
  235. package/gsd-core/workflows/eval-review.md +10 -3
  236. package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
  237. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
  238. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +19 -4
  239. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
  240. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +43 -4
  241. package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
  242. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  243. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  244. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  245. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  246. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  247. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  248. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  249. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  250. package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
  251. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
  252. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  253. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  254. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
  255. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  256. package/gsd-core/workflows/execute-phase.md +83 -172
  257. package/gsd-core/workflows/execute-plan.md +24 -10
  258. package/gsd-core/workflows/explore.md +3 -3
  259. package/gsd-core/workflows/extract-learnings.md +2 -1
  260. package/gsd-core/workflows/fast.md +1 -1
  261. package/gsd-core/workflows/forensics.md +1 -1
  262. package/gsd-core/workflows/graduation.md +1 -1
  263. package/gsd-core/workflows/health.md +2 -2
  264. package/gsd-core/workflows/help/modes/full.compact.md +398 -0
  265. package/gsd-core/workflows/help/modes/full.md +5 -5
  266. package/gsd-core/workflows/help/modes/topic.md +15 -5
  267. package/gsd-core/workflows/help.md +1 -1
  268. package/gsd-core/workflows/import.md +2 -2
  269. package/gsd-core/workflows/inbox.md +2 -2
  270. package/gsd-core/workflows/ingest-docs.md +3 -3
  271. package/gsd-core/workflows/insert-phase.md +1 -1
  272. package/gsd-core/workflows/list-seeds.md +1 -1
  273. package/gsd-core/workflows/list-workspaces.md +1 -1
  274. package/gsd-core/workflows/manager.md +2 -2
  275. package/gsd-core/workflows/map-codebase.md +52 -5
  276. package/gsd-core/workflows/milestone-summary.md +1 -1
  277. package/gsd-core/workflows/mvp-phase.md +1 -1
  278. package/gsd-core/workflows/new-milestone.md +56 -14
  279. package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
  280. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  281. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  282. package/gsd-core/workflows/new-project.md +39 -209
  283. package/gsd-core/workflows/new-workspace.md +2 -2
  284. package/gsd-core/workflows/next.md +1 -1
  285. package/gsd-core/workflows/note.md +1 -1
  286. package/gsd-core/workflows/onboard.md +1 -1
  287. package/gsd-core/workflows/pause-work.md +1 -1
  288. package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
  289. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  290. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  291. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  292. package/gsd-core/workflows/plan-phase.md +45 -187
  293. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  294. package/gsd-core/workflows/plant-seed.md +62 -20
  295. package/gsd-core/workflows/pr-branch.md +132 -20
  296. package/gsd-core/workflows/profile-user.md +2 -2
  297. package/gsd-core/workflows/progress.md +1 -1
  298. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
  299. package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
  300. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
  301. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  302. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  303. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  304. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  305. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  306. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  307. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
  308. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  309. package/gsd-core/workflows/quick-batch.md +1 -1
  310. package/gsd-core/workflows/quick.md +29 -10
  311. package/gsd-core/workflows/reapply-patches.md +86 -6
  312. package/gsd-core/workflows/remove-phase.md +1 -1
  313. package/gsd-core/workflows/remove-workspace.md +2 -2
  314. package/gsd-core/workflows/resume-project.md +1 -1
  315. package/gsd-core/workflows/review.md +31 -16
  316. package/gsd-core/workflows/scan.md +1 -1
  317. package/gsd-core/workflows/secure-phase.md +3 -2
  318. package/gsd-core/workflows/settings-advanced.md +30 -10
  319. package/gsd-core/workflows/settings-integrations.md +2 -3
  320. package/gsd-core/workflows/settings.md +22 -9
  321. package/gsd-core/workflows/ship.md +3 -2
  322. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  323. package/gsd-core/workflows/sketch.md +1 -1
  324. package/gsd-core/workflows/smart-entry.md +2 -2
  325. package/gsd-core/workflows/spec-phase.md +15 -5
  326. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  327. package/gsd-core/workflows/spike.md +1 -1
  328. package/gsd-core/workflows/stats.md +1 -1
  329. package/gsd-core/workflows/sync-skills.md +5 -5
  330. package/gsd-core/workflows/thread.md +1 -1
  331. package/gsd-core/workflows/transition.md +1 -1
  332. package/gsd-core/workflows/ui-phase.md +44 -8
  333. package/gsd-core/workflows/ui-review.md +18 -4
  334. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  335. package/gsd-core/workflows/undo.md +339 -20
  336. package/gsd-core/workflows/update.md +14 -12
  337. package/gsd-core/workflows/validate-phase.md +3 -2
  338. package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
  339. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  340. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  341. package/gsd-core/workflows/verify-work.md +101 -196
  342. package/hooks/dist/gsd-agent-isolation-guard.js +66 -16
  343. package/hooks/dist/gsd-context-monitor.js +88 -15
  344. package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
  345. package/hooks/dist/gsd-secret-read-guard.js +71 -19
  346. package/hooks/dist/gsd-statusline.js +81 -20
  347. package/hooks/dist/gsd-validate-commit.sh +97 -8
  348. package/hooks/dist/gsd-worktree-path-guard.js +25 -14
  349. package/hooks/dist/gsd-write-guard.js +46 -1
  350. package/hooks/dist/lib/dispatch-identity.js +187 -0
  351. package/hooks/dist/lib/filename-classification.js +64 -0
  352. package/hooks/dist/lib/isolation-deny-reason.js +53 -1
  353. package/hooks/dist/lib/isolation-sentinel.js +58 -19
  354. package/hooks/gsd-agent-isolation-guard.js +66 -16
  355. package/hooks/gsd-context-monitor.js +88 -15
  356. package/hooks/gsd-cursor-subagent-start.js +34 -14
  357. package/hooks/gsd-secret-read-guard.js +71 -19
  358. package/hooks/gsd-statusline.js +81 -20
  359. package/hooks/gsd-validate-commit.sh +97 -8
  360. package/hooks/gsd-worktree-path-guard.js +25 -14
  361. package/hooks/gsd-write-guard.js +46 -1
  362. package/hooks/lib/dispatch-identity.js +187 -0
  363. package/hooks/lib/filename-classification.js +64 -0
  364. package/hooks/lib/isolation-deny-reason.js +53 -1
  365. package/hooks/lib/isolation-sentinel.js +58 -19
  366. package/package.json +11 -6
  367. package/scripts/benchmark-compact-content-variants.cjs +298 -0
  368. package/scripts/benchmark-compact-content.cjs +368 -0
  369. package/scripts/build-hooks.js +15 -6
  370. package/scripts/check-contract-drift.cjs +131 -12
  371. package/scripts/check-env.cjs +36 -8
  372. package/scripts/check-glossary-refs.cjs +25 -21
  373. package/scripts/ci-next-health.cjs +271 -0
  374. package/scripts/ci-prepare-test-scope.cjs +7 -7
  375. package/scripts/ci-test-scope.cjs +126 -20
  376. package/scripts/ci-timeout-report.cjs +1 -1
  377. package/scripts/command-contract-helpers.cjs +3 -0
  378. package/scripts/diff-touches-shipped-paths.cjs +1 -1
  379. package/scripts/docs-guard-registry.cjs +35 -2
  380. package/scripts/gen-adr-index.cjs +8 -2
  381. package/scripts/gen-inventory-manifest.cjs +12 -0
  382. package/scripts/gen-loop-host-contract.cjs +69 -0
  383. package/scripts/gen-platform-conformance-tier.cjs +557 -0
  384. package/scripts/lib/drift-scan.cjs +1 -1
  385. package/scripts/lib/macos-conformance-tier.generated.cjs +224 -0
  386. package/scripts/lib/ndjson-reporter.cjs +3 -2
  387. package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
  388. package/scripts/lib/platform-conformance-tier.generated.cjs +287 -0
  389. package/scripts/lib/suite-detection.cjs +32 -0
  390. package/scripts/lint-allowed-tools-parity.cjs +221 -0
  391. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +47 -3
  392. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  393. package/scripts/lint-phase-id-drift.cjs +623 -13
  394. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  395. package/scripts/lint-response-language-coverage.cjs +9 -3
  396. package/scripts/lint-retired-runtime-name.cjs +619 -0
  397. package/scripts/lint-source-test-name-collision.cjs +1 -1
  398. package/scripts/lint-state-write-path-drift.cjs +93 -0
  399. package/scripts/lint-test-file-count.allowlist.json +29 -9
  400. package/scripts/lint-vendored-deps.cjs +128 -17
  401. package/scripts/lint-workflow-shellcheck-baseline.json +100 -0
  402. package/scripts/prompt-injection-scan.sh +18 -0
  403. package/scripts/release-tarball-smoke.cjs +194 -1
  404. package/scripts/workflow-size.cjs +139 -0
  405. package/skills/gsd-autonomous/SKILL.md +2 -2
  406. package/skills/gsd-capture/SKILL.md +1 -1
  407. package/skills/gsd-cleanup/SKILL.md +1 -0
  408. package/skills/gsd-code-review/SKILL.md +2 -1
  409. package/skills/gsd-complete-milestone/SKILL.md +1 -0
  410. package/skills/gsd-config/SKILL.md +1 -0
  411. package/skills/gsd-debug/SKILL.md +1 -0
  412. package/skills/gsd-graphify/SKILL.md +1 -0
  413. package/skills/gsd-health/SKILL.md +1 -0
  414. package/skills/gsd-mempalace-capture/SKILL.md +8 -3
  415. package/skills/gsd-mempalace-recall/SKILL.md +1 -0
  416. package/skills/gsd-new-milestone/SKILL.md +1 -0
  417. package/skills/gsd-new-project/SKILL.md +1 -0
  418. package/skills/gsd-next/SKILL.md +1 -0
  419. package/skills/gsd-pause-work/SKILL.md +1 -0
  420. package/skills/gsd-phase/SKILL.md +1 -0
  421. package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
  422. package/skills/gsd-pr-branch/SKILL.md +1 -0
  423. package/skills/gsd-progress/SKILL.md +1 -1
  424. package/skills/gsd-quick-batch/SKILL.md +1 -1
  425. package/skills/gsd-resume-work/SKILL.md +1 -0
  426. package/skills/gsd-review/SKILL.md +2 -3
  427. package/skills/gsd-review-backlog/SKILL.md +1 -0
  428. package/skills/gsd-settings/SKILL.md +2 -1
  429. package/skills/gsd-stats/SKILL.md +1 -0
  430. package/skills/gsd-thread/SKILL.md +1 -0
  431. package/skills/gsd-workspace/SKILL.md +1 -0
  432. package/skills/gsd-workstreams/SKILL.md +1 -0
  433. package/vscode/package.json +1 -1
  434. package/gsd-core/templates/claude-md.md +0 -145
  435. package/gsd-core/templates/codebase/concerns.md +0 -310
  436. package/gsd-core/templates/codebase/conventions.md +0 -307
  437. package/gsd-core/templates/codebase/integrations.md +0 -280
  438. package/gsd-core/templates/codebase/structure.md +0 -285
  439. package/gsd-core/templates/codebase/testing.md +0 -480
  440. package/gsd-core/templates/debug-subagent-prompt.md +0 -91
  441. package/gsd-core/templates/discovery.md +0 -146
@@ -16,6 +16,21 @@ set -euo pipefail
16
16
  # Idempotent and failure-proof by construction: unset vars expand to "" (a
17
17
  # no-op rm -f target), and `|| true` guarantees the trap itself never changes
18
18
  # the script's exit status.
19
+ # Subprocess exit statuses, pre-initialised so they can never be inherited from
20
+ # the ambient environment. Each is captured as `... || VAR=$?`, which assigns
21
+ # ONLY on the failure branch; on success the variable keeps whatever it already
22
+ # held, and `${VAR:-0}` defaults only when unset or empty. So an EXPORTED
23
+ # CONFIG_STATUS / CMD_STATUS / CLASSIFY_STATUS — from a CI wrapper, a .envrc, or
24
+ # another hook — survived into the success path and was read as "the subprocess
25
+ # failed". Measured: `CLASSIFY_STATUS=3 git commit -m "nope: bad"` printed
26
+ # "validator disabled for this call" and exited 0, silently accepting a
27
+ # non-conforming commit. Same for CONFIG_STATUS and CMD_STATUS. Found by the
28
+ # security review of #4429; the gate is fail-open by design on a genuine
29
+ # subprocess failure (#3838), which is exactly what made this bypass quiet.
30
+ CONFIG_STATUS=0
31
+ CMD_STATUS=0
32
+ CLASSIFY_STATUS=0
33
+
19
34
  ENABLED_ERR=""
20
35
  CMD_ERR=""
21
36
  CLASSIFY_ERR=""
@@ -61,6 +76,12 @@ if [ -f .planning/config.json ]; then
61
76
  process.exit(3);
62
77
  }
63
78
  " 2>"$ENABLED_ERR") || CONFIG_STATUS=$?
79
+ # Pre-initialised, NOT left to `${...:-0}` alone: the capture below only
80
+ # assigns on the `||` branch, so on SUCCESS the variable keeps whatever it
81
+ # already held — and an EXPORTED variable of this name is inherited from the
82
+ # ambient environment. `${VAR:-0}` defaults only when unset/empty, so
83
+ # `CONFIG_STATUS=3 git commit …` made this hook print "validator disabled" and exit 0,
84
+ # silently accepting a non-conforming commit. Found by review of #4429.
64
85
  CONFIG_STATUS=${CONFIG_STATUS:-0}
65
86
  if [ "$CONFIG_STATUS" != "0" ]; then
66
87
  # Could not determine the opt-in flag at all (node missing, JSON parse
@@ -69,7 +90,11 @@ if [ -f .planning/config.json ]; then
69
90
  echo "gsd-validate-commit.sh: could not read .planning/config.json (opt-in check) — validator disabled for this call. $(cat "$ENABLED_ERR")" >&2
70
91
  exit 0
71
92
  fi
72
- ENABLED=$(printf '%s\n' "$CONFIG_OUT" | head -1)
93
+ # Pure parameter expansion, not `printf ... | head -1`: same SIGPIPE race
94
+ # class as the SUBJECT extraction below (`echo "$MSG" | head -1`) — CONFIG_OUT
95
+ # is multi-line whenever extra commit types are configured, and `head -1`
96
+ # closing early can SIGPIPE `printf` under `set -euo pipefail`.
97
+ ENABLED="${CONFIG_OUT%%$'\n'*}"
73
98
  if [ "$ENABLED" != "1" ]; then exit 0; fi
74
99
  # Remaining lines (if any) are the sanitized, deduped configured commit
75
100
  # types beyond the 10 built-ins (#3811). Read into a bash-3.2-safe array —
@@ -99,6 +124,12 @@ CMD=$(echo "$INPUT" | node -e "
99
124
  }
100
125
  });
101
126
  " 2>"$CMD_ERR") || CMD_STATUS=$?
127
+ # Pre-initialised, NOT left to `${...:-0}` alone: the capture below only
128
+ # assigns on the `||` branch, so on SUCCESS the variable keeps whatever it
129
+ # already held — and an EXPORTED variable of this name is inherited from the
130
+ # ambient environment. `${VAR:-0}` defaults only when unset/empty, so
131
+ # `CMD_STATUS=3 git commit …` made this hook print "validator disabled" and exit 0,
132
+ # silently accepting a non-conforming commit. Found by review of #4429.
102
133
  CMD_STATUS=${CMD_STATUS:-0}
103
134
  if [ "$CMD_STATUS" != "0" ]; then
104
135
  # Could not extract tool_input.command at all (node missing, malformed
@@ -123,6 +154,12 @@ GIT_CMD_LIB="$HOOK_DIR/lib/git-cmd.js" node -e "
123
154
  process.exit(3);
124
155
  }
125
156
  " "$CMD" 2>"$CLASSIFY_ERR" || CLASSIFY_STATUS=$?
157
+ # Pre-initialised, NOT left to `${...:-0}` alone: the capture below only
158
+ # assigns on the `||` branch, so on SUCCESS the variable keeps whatever it
159
+ # already held — and an EXPORTED variable of this name is inherited from the
160
+ # ambient environment. `${VAR:-0}` defaults only when unset/empty, so
161
+ # `CLASSIFY_STATUS=3 git commit …` made this hook print "validator disabled" and exit 0,
162
+ # silently accepting a non-conforming commit. Found by review of #4429.
126
163
  CLASSIFY_STATUS=${CLASSIFY_STATUS:-0}
127
164
  if [ "$CLASSIFY_STATUS" != "0" ] && [ "$CLASSIFY_STATUS" != "1" ]; then
128
165
  # 0 = is a git commit (validate below); 1 = genuinely not a git commit
@@ -214,7 +251,24 @@ if [ "$CLASSIFY_STATUS" = "0" ]; then
214
251
  # the message — the window a guard must use when the token it scans for
215
252
  # is also legal English inside a commit message, but may legally appear
216
253
  # on EITHER side of the message on the command line.
217
- MSG_SUFFIX="${CMD#*"$MSG_MATCH"}"
254
+ # Indexed, not searched (#4492). `${CMD#*"$MSG_MATCH"}` is quadratic in
255
+ # the message: bash walks every prefix length and compares the whole
256
+ # matched literal at each one, and MSG_MATCH is BASH_REMATCH[0] — the
257
+ # entire `-m "..."` — so the cost grows with the thing being scanned.
258
+ # Measured on the path EVERY commit takes (conforming and non-conforming
259
+ # cost the same): 10.0 s at a 64 KB message, 22.0 s at 96 KB, 30.2 s at
260
+ # 112 KB. Sizes stop there deliberately — a single argument above Linux's
261
+ # MAX_ARG_STRLEN (131072 on a 4 KB-page kernel) never reaches this code
262
+ # at all, because execve fails and the hook fails open, so a larger
263
+ # "measurement" would be timing the wrong thing.
264
+ #
265
+ # MSG_PREFIX above has already located the match, so the suffix is
266
+ # arithmetic rather than a search: skip the prefix and the match. This
267
+ # removes the quadratic SEARCH; the expansion still counts characters and
268
+ # materialises a substring, so it is linear in the command, not O(1).
269
+ # Same first-occurrence assumption both expansions here always made —
270
+ # MSG_MATCH is a literal substring of CMD by construction.
271
+ MSG_SUFFIX="${CMD:$(( ${#MSG_PREFIX} + ${#MSG_MATCH} ))}"
218
272
  # LINE CONTINUATIONS ARE NOT SEPARATORS (review of #3816, rounds 8 and 9).
219
273
  # `git commit \` newline ` -m "$(cat <<'EOF' …` is an ordinary way to
220
274
  # spread an invocation over lines, and every guard below reads a newline in
@@ -521,13 +575,22 @@ if [ "$CLASSIFY_STATUS" = "0" ]; then
521
575
  SUBJECT=$(GIT_CMD_LIB="$HOOK_DIR/lib/git-cmd.js" MSG="$MSG" node -e "
522
576
  const {resolveCommitSubject}=require(process.env.GIT_CMD_LIB);
523
577
  process.stdout.write(resolveCommitSubject(process.env.MSG));
524
- " 2>/dev/null) || SUBJECT=$(echo "$MSG" | head -1)
578
+ " 2>/dev/null) || SUBJECT="${MSG%%$'\n'*}"
525
579
  else
526
- SUBJECT=$(echo "$MSG" | head -1)
580
+ # Pure parameter expansion, not `echo "$MSG" | head -1`: that pipeline
581
+ # raced a SIGPIPE under `set -euo pipefail` whenever $MSG had a body
582
+ # (the common case) — `head -1` can close its read end as soon as it
583
+ # has the first line, and if `echo`'s write lands after that close,
584
+ # `echo` dies with signal 13 (exit 141), which is NOT suppressed by
585
+ # `set -e` and aborted the whole hook intermittently (observed in
586
+ # tests/hooks-opt-in.test.cjs's --fixup=HEAD "round 7" case). Zero
587
+ # subprocesses here means zero pipe/race surface. Equivalent to
588
+ # `head -1` for single-line, multi-line, and trailing-newline input.
589
+ SUBJECT="${MSG%%$'\n'*}"
527
590
  fi
528
591
  # Single source of truth for the accepted commit-type list (#3811): the
529
592
  # 10 built-ins plus whatever passed the safe-token filter above. Both the
530
- # regex alternation and the human-readable error text below are derived
593
+ # membership test and the human-readable error text below are derived
531
594
  # from this ONE array — no hand-synced second copy.
532
595
  #
533
596
  # The `"${EXTRA_COMMIT_TYPES[@]+"${EXTRA_COMMIT_TYPES[@]}"}"` form (not
@@ -538,7 +601,6 @@ if [ "$CLASSIFY_STATUS" = "0" ]; then
538
601
  # /bin/bash 3.2.57 on macOS. The `${arr[@]+word}` form is the
539
602
  # nounset-safe idiom for "expand if set, empty otherwise" on empty arrays.
540
603
  COMMIT_TYPES=("${BUILTIN_COMMIT_TYPES[@]}" "${EXTRA_COMMIT_TYPES[@]+"${EXTRA_COMMIT_TYPES[@]}"}")
541
- COMMIT_TYPE_ALT=$(IFS='|'; echo "${COMMIT_TYPES[*]}")
542
604
  COMMIT_TYPE_LIST=$(printf '%s, ' "${COMMIT_TYPES[@]}")
543
605
  COMMIT_TYPE_LIST="${COMMIT_TYPE_LIST%, }"
544
606
  # Typed `valid_types` array (#3811 review finding): CONTRIBUTING.md bans
@@ -550,8 +612,35 @@ if [ "$CLASSIFY_STATUS" = "0" ]; then
550
612
  # or `\`.
551
613
  COMMIT_TYPES_JSON=$(printf '"%s",' "${COMMIT_TYPES[@]}")
552
614
  COMMIT_TYPES_JSON="[${COMMIT_TYPES_JSON%,}]"
553
- # Validate Conventional Commits format
554
- if ! [[ "$SUBJECT" =~ ^($COMMIT_TYPE_ALT)(\(.+\))?:[[:space:]].+ ]]; then
615
+ # Validate Conventional Commits format.
616
+ #
617
+ # #4429: do NOT build `^(type1|type2|...)` out of COMMIT_TYPES. That
618
+ # alternation grows with the CONFIGURED list, and how large a pattern can be
619
+ # compiled is a property of the platform's regex engine. bash 3.2.57 / BSD
620
+ # libc (macOS, this file's stated target) caps it at 64 KiB - bisected: a
621
+ # 65504-byte alternation compiles, 65515 fails. bash 5.2 / glibc has no
622
+ # reachable cap, so this half never bit Linux. Past a cap `[[ =~ ]]`
623
+ # returns 2, and `if !` cannot tell a COMPILE ERROR from "the subject does
624
+ # not conform" - so a valid `feat(auth): ...` was blocked with
625
+ # CONVENTIONAL_COMMITS_VIOLATION while `feat` sat in its own valid_types.
626
+ #
627
+ # Match the SHAPE with a fixed-size pattern, then test membership against
628
+ # the array. The regex no longer depends on how many types are configured,
629
+ # and the loop adds no subprocess or pipe (the #4429 hazard this file
630
+ # already avoids elsewhere). The character class is exactly the safe-token
631
+ # filter `^[a-z][a-z0-9-]*$` applied above, so it captures every type that
632
+ # can legally reach COMMIT_TYPES and no token that cannot.
633
+ SUBJECT_TYPE=''
634
+ if [[ "$SUBJECT" =~ ^([a-z][a-z0-9-]*)(\(.+\))?:[[:space:]].+ ]]; then
635
+ SUBJECT_TYPE="${BASH_REMATCH[1]}"
636
+ fi
637
+ COMMIT_TYPE_OK=0
638
+ if [ -n "$SUBJECT_TYPE" ]; then
639
+ for _known_type in "${COMMIT_TYPES[@]}"; do
640
+ if [ "$_known_type" = "$SUBJECT_TYPE" ]; then COMMIT_TYPE_OK=1; break; fi
641
+ done
642
+ fi
643
+ if [ "$COMMIT_TYPE_OK" -ne 1 ]; then
555
644
  # Emit typed `code` and `valid_types` fields alongside `reason` (#2974,
556
645
  # #3811). Tests assert on the stable code string and the typed array;
557
646
  # the reason is the human-readable copy, never grepped by tests.
@@ -163,17 +163,37 @@ process.stdin.on('end', () => {
163
163
  // returns a path containing .git/worktrees/ as a component.
164
164
  // In the main repo or a submodule it returns .git (or a path without /worktrees/).
165
165
  // This approach works even when cwd is a subdirectory of the worktree.
166
- const gitDirResult = git(['rev-parse', '--git-dir'], cwd);
166
+ // Combined into one spawn — git rev-parse accepts multiple query flags in
167
+ // one invocation and prints one line of output per flag, in the exact
168
+ // order given, reducing this guard's worst-case subprocess count under
169
+ // CI/load contention (three spawns collapse into one). `--abbrev-ref HEAD`
170
+ // is used instead of `symbolic-ref --short HEAD` because it is combinable
171
+ // (a single `rev-parse` call) and behaviorally equivalent for this guard's
172
+ // branch-acceptance check, including on detached HEAD: `--abbrev-ref`
173
+ // returns the literal string `HEAD` there (exit 0), which the acceptance
174
+ // regex below also rejects — the same guard outcome as symbolic-ref's
175
+ // exit-128/empty-stdout failure. Do not change any timeout value as part
176
+ // of this change, only the spawn count.
177
+ const combinedResult = git(['rev-parse', '--git-dir', '--abbrev-ref', 'HEAD', '--show-toplevel'], cwd);
167
178
  // #3911: a timeout/spawn-failure result is indistinguishable from a clean
168
179
  // "not a git repo" answer by status/stdout alone — reportIfUndetermined
169
180
  // is a no-op on a genuine negative and only fires the diagnostic when the
170
181
  // probe itself could not run. The allow() below is UNCHANGED either way.
171
- reportIfUndetermined('gsd-worktree-path-guard', 'git rev-parse --git-dir', gitDirResult);
172
- if (gitDirResult.status !== 0 || !gitDirResult.stdout) {
182
+ reportIfUndetermined(
183
+ 'gsd-worktree-path-guard',
184
+ 'git rev-parse --git-dir --abbrev-ref HEAD --show-toplevel',
185
+ combinedResult
186
+ );
187
+ if (combinedResult.status !== 0 || !combinedResult.stdout) {
173
188
  allow(undefined); // not a git repo — pass through
174
189
  }
175
190
 
176
- const gitDir = gitDirResult.stdout.trim();
191
+ const combinedLines = combinedResult.stdout.split('\n').map((l) => l.trim()).filter((l) => l.length > 0);
192
+ if (combinedLines.length < 3) {
193
+ allow(undefined); // malformed/short output — can't determine root, fail open
194
+ }
195
+ const [gitDir, branch, wtTopRaw] = combinedLines;
196
+
177
197
  // A linked worktree's --git-dir contains .git/worktrees/ as a path component
178
198
  const isLinkedWorktree = /[/\\]\.git[/\\]worktrees[/\\]/.test(gitDir);
179
199
  if (!isLinkedWorktree) {
@@ -186,23 +206,14 @@ process.stdin.on('end', () => {
186
206
  // created linked worktree (plain non-GSD work, e.g. Claude Code plan-mode) is
187
207
  // on the user's own branch, so the guard must be a no-op there. Detached HEAD
188
208
  // / error → not GSD-managed → no-op.
189
- const branchResult = git(['symbolic-ref', '--short', 'HEAD'], cwd);
190
- reportIfUndetermined('gsd-worktree-path-guard', 'git symbolic-ref --short HEAD', branchResult);
191
- const branch = branchResult.status === 0 && branchResult.stdout ? branchResult.stdout.trim() : '';
192
209
  // #3021: accept worktree-wf_<runid>-<n> branches (Workflow backend's naming).
193
210
  if (!/^((worktree-)?agent-|worktree-wf_)[A-Za-z0-9._/-]+$/.test(branch)) {
194
211
  allow(undefined); // not a GSD-managed executor worktree — no-op
195
212
  }
196
213
 
197
- // Get the raw --show-toplevel output for the worktree (cwd).
214
+ // wtTopRaw: the raw --show-toplevel output for the worktree (cwd).
198
215
  // We keep it raw (not path.resolve'd) to compare directly with the
199
216
  // file's toplevel — same git binary, same format, no normalization needed.
200
- const wtTopResult = git(['rev-parse', '--show-toplevel'], cwd);
201
- reportIfUndetermined('gsd-worktree-path-guard', 'git rev-parse --show-toplevel (worktree cwd)', wtTopResult);
202
- if (wtTopResult.status !== 0 || !wtTopResult.stdout) {
203
- allow(undefined); // can't determine root — fail open
204
- }
205
- const wtTopRaw = wtTopResult.stdout.trim();
206
217
 
207
218
  // #2595 (review Major 3): read the field TYPED. `?.file_path || ''` let a
208
219
  // non-string through — `[]` and `{}` are truthy, so they survived the
@@ -105,10 +105,37 @@ const FLOOR_LINES = 40;
105
105
  // default to, a differently-cased path is the SAME real file — a Write to
106
106
  // '.planning/roadmap.md' clobbers ROADMAP.md while a case-sensitive match
107
107
  // waves it through.
108
+ // #4455: workstream-scoped (and optionally project-scoped) variants —
109
+ // planningDir(cwd) (src/planning-workspace.cts) resolves to
110
+ // `.planning/[<project>/]workstreams/<ws>/...` whenever GSD_WORKSTREAM is
111
+ // set. Before this, none of these three root-only patterns matched a
112
+ // workstream-scoped target at all, so the ENTIRE guard (not just the
113
+ // sentinel step — the shrink-ratio check too) silently never engaged for a
114
+ // workstream-scoped ROADMAP.md/STATE.md/milestone-archive Write: exactly
115
+ // the catastrophic-shrink scenario this file exists to stop, unguarded
116
+ // under an active workstream. consumeSentinelFor's own `.planning`
117
+ // derivation below is unaffected by this addition — it locates the single
118
+ // outer `.planning` segment regardless of what's nested inside it, which is
119
+ // also where the workflow's sentinel `printf` already writes, so no change
120
+ // was needed there.
121
+ //
122
+ // Same gap exists one level up: planningDir(cwd) ALSO resolves to
123
+ // `.planning/<project>/...` when GSD_PROJECT is set with NO GSD_WORKSTREAM
124
+ // (project-only mode — the two env vars are independent; see planningDir's
125
+ // own body). None of the patterns above cover that shape either. Found
126
+ // during #4455's own review pass (same root cause, one more path variant)
127
+ // — fixed in the same change rather than deferred, since it is the
128
+ // identical defect class this PR already exists to close.
108
129
  const CURATED_PATTERNS = [
109
130
  /(?:^|\/)\.planning\/ROADMAP\.md$/i,
110
131
  /(?:^|\/)\.planning\/STATE\.md$/i,
111
132
  /(?:^|\/)\.planning\/milestones\/[^/]+-ROADMAP\.md$/i,
133
+ /(?:^|\/)\.planning\/(?:[^/]+\/)?workstreams\/[^/]+\/ROADMAP\.md$/i,
134
+ /(?:^|\/)\.planning\/(?:[^/]+\/)?workstreams\/[^/]+\/STATE\.md$/i,
135
+ /(?:^|\/)\.planning\/(?:[^/]+\/)?workstreams\/[^/]+\/milestones\/[^/]+-ROADMAP\.md$/i,
136
+ /(?:^|\/)\.planning\/[^/]+\/ROADMAP\.md$/i,
137
+ /(?:^|\/)\.planning\/[^/]+\/STATE\.md$/i,
138
+ /(?:^|\/)\.planning\/[^/]+\/milestones\/[^/]+-ROADMAP\.md$/i,
112
139
  ];
113
140
 
114
141
  // Count logical lines, ignoring a single trailing newline so that
@@ -156,7 +183,25 @@ function consumeSentinelFor(filePath, normalized) {
156
183
  // Path-bound: the token names exactly one file, resolved against the
157
184
  // .planning/ dir's parent (repo root) — same case-insensitive stance as
158
185
  // the curated match itself.
159
- const namedNorm = path.resolve(path.join(planningDir, '..'), token).replace(/\\/g, '/').toLowerCase();
186
+ let namedPath = path.resolve(path.join(planningDir, '..'), token);
187
+ // Symmetry with the caller's own resolution (#4455 CI finding, macOS
188
+ // full-test shard): `filePath`/`normalized` were already realpath-resolved
189
+ // before this function was called (round 9 Minor 1's symlink-before-match
190
+ // fix), but `token` — typically an already-absolute path composed by the
191
+ // workflow's own init.* fields — was compared WITHOUT that same
192
+ // resolution. Wherever cwd sits under a symlink (macOS's /var ->
193
+ // /private/var is the common case, since that's exactly what os.tmpdir()
194
+ // resolves through, but any symlinked project/worktree checkout hits the
195
+ // same asymmetry), the token names the lexical path while `normalized`
196
+ // names the realpath — a validly-armed sentinel then never matches, and a
197
+ // legitimate milestone-reset Write stays incorrectly blocked. The named
198
+ // file is already known to exist (the caller only reaches this function
199
+ // after successfully reading it), so realpath is expected to succeed;
200
+ // keep the lexical path on failure, matching the caller's own fallback.
201
+ try {
202
+ namedPath = fs.realpathSync(namedPath);
203
+ } catch { /* keep the lexical path */ }
204
+ const namedNorm = namedPath.replace(/\\/g, '/').toLowerCase();
160
205
  if (namedNorm !== normalized.toLowerCase()) {
161
206
  return false; // armed for a different file — leave it for that write
162
207
  }
@@ -0,0 +1,187 @@
1
+ 'use strict';
2
+ // hooks/lib/dispatch-identity.js — the ONE canonical owner of the
3
+ // `[gsd:dispatch phase="…" plan="…"]` marker format and its prose fallback
4
+ // (#4594, epic #4630 Phase 1). See
5
+ // `.gsd/phase/fix-4594-dispatch-identity-seam/40-design.md` for the full
6
+ // rationale (behavior table, negative space, rejected alternatives).
7
+ //
8
+ // COLD-LOAD CONSTRAINT (load-bearing, not a style choice): this module MUST
9
+ // require NOTHING — no `fs`, no `path`, and above all nothing under
10
+ // `gsd-core/bin/lib/` or `ensure-runtime-build`. The guard hooks that consume
11
+ // this module must load on a raw plugin-marketplace install where the
12
+ // compiled lib is absent and the self-healing build seam has not run — a
13
+ // hook that dies at module load is worse than one carrying a mirror. See
14
+ // `tests/dispatch-identity.test.cjs`'s "cold tree load" test, which asserts
15
+ // this by monkeypatching `Module._load` to throw on exactly those paths.
16
+
17
+ // DISPATCH_PHASE_TOKEN_SOURCE is a DELIBERATE MIRROR of
18
+ // `CASE_FLEXIBLE_PHASE_NUMBER_TOKEN_SOURCE` in `src/phase-id.cts`
19
+ // (ADR-2121 owns the phase-token grammar; `gsd-core/bin/lib/phase-id.cjs` is
20
+ // its compiled form). It cannot be an `require()`-based import: importing the
21
+ // compiled lib here would violate the cold-load constraint above (either a
22
+ // direct dependency on `gsd-core/bin/lib/` or a forced self-heal via
23
+ // `ensure-runtime-build`), which is exactly the failure mode this module
24
+ // exists to avoid for the guard hooks that consume it.
25
+ //
26
+ // Because this is a hand-copied mirror and not a shared reference, a
27
+ // hand-edited grammar change on one side that is not mirrored on the other
28
+ // does NOT fail loudly — both sides remain independently valid regex
29
+ // sources, so the failure mode is a silent, invisible non-match (a dispatch
30
+ // whose phase token the two owners now parse differently), never a thrown
31
+ // error. `tests/dispatch-identity.test.cjs`'s "templates: prose token source
32
+ // matches the case-flexible phase-id grammar" test pins this string equal to
33
+ // the compiled source at test time specifically to turn that silent drift
34
+ // into a loud, in-CI failure.
35
+ const DISPATCH_PHASE_TOKEN_SOURCE = '\\d+[A-Za-z]?(?:\\.\\d+)*';
36
+
37
+ // Bounded marker grammar: `[gsd:dispatch key="value" key2="value2"]`.
38
+ // - Key names bounded to {1,31} — no key we emit or expect is anywhere near
39
+ // that long; this is purely a backstop against pathological input.
40
+ // - Values bounded to {0,200} and forbidden from containing `"`, `]`, or any
41
+ // control character that could either close the marker early or forge a
42
+ // sibling key — enforced structurally by the negated character class, not
43
+ // by a separate validation pass.
44
+ // Global flag so `findMarker` can scan forward through a large prompt.
45
+ const MARKER_RE = /\[gsd:dispatch((?:\s+[A-Za-z][A-Za-z0-9_-]{0,31}="[^"\]\r\n]{0,200}")*)\s*\]/g;
46
+ const MARKER_KV_RE = /([A-Za-z][A-Za-z0-9_-]{0,31})="([^"\]\r\n]{0,200})"/g;
47
+
48
+ // Prose fallback frame: `execute plan <token> of phase <PHASE TOKEN>`.
49
+ // - Case-insensitive, `\s+` throughout so CRLF (and any run of whitespace)
50
+ // parses identically to LF.
51
+ // - The plan token is bounded (`\S{1,80}`) but its VALUE is deliberately
52
+ // discarded by the caller (see `parseDispatchIdentity` below) — captured
53
+ // only so the phase token can be anchored correctly after it.
54
+ // - The phase token is bounded by `DISPATCH_PHASE_TOKEN_SOURCE`, replacing
55
+ // the old greedy `(\S+)`, so a directory-name suffix (`-auth`) or a
56
+ // sentence-terminating period is never swept into the token.
57
+ const PROSE_RE = new RegExp(
58
+ `execute\\s+plan\\s+(\\S{1,80})\\s+of\\s+phase\\s+(${DISPATCH_PHASE_TOKEN_SOURCE})`,
59
+ 'i',
60
+ );
61
+
62
+ /**
63
+ * Render the `[gsd:dispatch phase="…" plan="…"]` marker for a producer to
64
+ * embed verbatim in a dispatch description/prompt. Never throws.
65
+ *
66
+ * Emits only keys whose value is a non-empty string not containing `"`, `]`,
67
+ * `\r`, or `\n` — such a value is UNUSABLE and that key is omitted entirely,
68
+ * because embedding it could close the marker early and forge a second,
69
+ * attacker-controlled field. Key order is always `phase` then `plan`.
70
+ * Returns `''` when neither key is usable (nothing worth emitting).
71
+ */
72
+ function renderDispatchIdentityMarker(input) {
73
+ const value = input && typeof input === 'object' ? input : {};
74
+ const parts = [];
75
+ for (const key of ['phase', 'plan']) {
76
+ const v = value[key];
77
+ if (typeof v === 'string' && v.length > 0 && !/["\]\r\n]/.test(v)) {
78
+ parts.push(`${key}="${v}"`);
79
+ }
80
+ }
81
+ if (parts.length === 0) return '';
82
+ return `[gsd:dispatch ${parts.join(' ')}]`;
83
+ }
84
+
85
+ function emptyResult() {
86
+ return { phase: null, plan: null, source: null };
87
+ }
88
+
89
+ /**
90
+ * Scan `texts` in order for the first well-formed marker that yields at
91
+ * least one recognized key (`phase` and/or `plan`). Returns `{ phase, plan }`
92
+ * (one of the two possibly null, never both) or `null` if no QUALIFYING
93
+ * marker was found in any text. Unrecognized keys inside a marker are
94
+ * ignored (forward compatibility — a later `run=`/`wave=` key must not break
95
+ * a deployed parser).
96
+ *
97
+ * #4594 F1 fix: a marker that matches `MARKER_RE` but carries neither
98
+ * `phase=` nor `plan=` (e.g. only unrecognized keys, or an empty kv block)
99
+ * is NOT treated as "found" — it is skipped and scanning continues (later
100
+ * markers in the same text, then subsequent texts), falling through to the
101
+ * prose fallback if nothing qualifying turns up. Without this, prompt text
102
+ * that merely CONTAINS the literal marker syntax with no usable identifiers
103
+ * silently suppressed the prose fallback entirely, since the old
104
+ * implementation returned on the first syntactic match regardless of
105
+ * content.
106
+ */
107
+ function findMarker(texts) {
108
+ for (const text of texts) {
109
+ if (typeof text !== 'string' || text.length === 0) continue;
110
+ MARKER_RE.lastIndex = 0;
111
+ let match;
112
+ while ((match = MARKER_RE.exec(text)) !== null) {
113
+ const kvBlock = match[1] || '';
114
+ let phase = null;
115
+ let plan = null;
116
+ MARKER_KV_RE.lastIndex = 0;
117
+ let kv;
118
+ while ((kv = MARKER_KV_RE.exec(kvBlock)) !== null) {
119
+ const [, key, val] = kv;
120
+ if (key === 'phase' && phase === null) phase = val;
121
+ else if (key === 'plan' && plan === null) plan = val;
122
+ }
123
+ if (phase === null && plan === null) continue;
124
+ return { phase, plan };
125
+ }
126
+ }
127
+ return null;
128
+ }
129
+
130
+ /**
131
+ * Scan `texts` in order for the first `execute plan <token> of phase
132
+ * <PHASE TOKEN>` frame. Returns `{ phase }` or `null`.
133
+ *
134
+ * The prose fallback returns `plan: null` DELIBERATELY (enforced by the
135
+ * caller, not here): the prose plan token (e.g. `02`) is a bare in-phase
136
+ * plan number, a different namespace from the sentinel's `plan_id` (e.g.
137
+ * `03-02-hardening`, which is phase-prefixed AND slugged). Reporting the
138
+ * prose plan token as the parsed `plan` is exactly the false-mismatch bug
139
+ * this module exists to fix — an absent value is honestly "cannot compare",
140
+ * while a wrong value silently forges a mismatch on every dispatch. Do not
141
+ * "fix" this by threading the plan token through — that was the bug.
142
+ */
143
+ function findProse(texts) {
144
+ for (const text of texts) {
145
+ if (typeof text !== 'string' || text.length === 0) continue;
146
+ const match = PROSE_RE.exec(text);
147
+ if (match) {
148
+ return { phase: match[2] };
149
+ }
150
+ }
151
+ return null;
152
+ }
153
+
154
+ /**
155
+ * Parse dispatch identity out of one or more texts (a short description, a
156
+ * full prompt body, etc). Never throws for any input type; non-string
157
+ * entries are skipped.
158
+ *
159
+ * Pass 1: scan all texts in order for a well-formed marker; the first one
160
+ * wins outright (marker beats prose even if prose appears earlier in the
161
+ * same text — see the design doc's row 8).
162
+ * Pass 2 (only if no marker was found anywhere): scan all texts in order for
163
+ * the prose frame; the first match wins. `plan` is always `null` from this
164
+ * path (see `findProse`'s doc comment).
165
+ *
166
+ * Returns `{ phase, plan, source }` where `source` is `'marker' | 'prose' |
167
+ * null`.
168
+ */
169
+ function parseDispatchIdentity(...texts) {
170
+ const marker = findMarker(texts);
171
+ if (marker) {
172
+ return { phase: marker.phase, plan: marker.plan, source: 'marker' };
173
+ }
174
+
175
+ const prose = findProse(texts);
176
+ if (prose) {
177
+ return { phase: prose.phase, plan: null, source: 'prose' };
178
+ }
179
+
180
+ return emptyResult();
181
+ }
182
+
183
+ module.exports = {
184
+ DISPATCH_PHASE_TOKEN_SOURCE,
185
+ renderDispatchIdentityMarker,
186
+ parseDispatchIdentity,
187
+ };
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+ // hooks/lib/filename-classification.js — hand-written, NOT generated. One
3
+ // tiny, deliberately-named filename slicer.
4
+ //
5
+ // WHY this exists: issue #4580 was caused by comparing "everything after the
6
+ // `.env.` prefix" — a multi-segment token like `local.example` — against a
7
+ // set whose members are FINAL EXTENSIONS (`example`). `.env.local.example`
8
+ // was classified by its full `local.example` tail, which is not in a set
9
+ // built from bare extensions, so the comparison silently failed. This module
10
+ // deliberately exposes ONLY the final-extension answer so the wrong token
11
+ // can't be picked by accident at a call site. The "everything after the
12
+ // first dot" form is intentionally NOT exported: no caller needs it, and an
13
+ // unused export is dead code.
14
+
15
+ /**
16
+ * The segment after the LAST dot in `name`. A string with no dot IS its own
17
+ * final extension. Inert on non-strings/empty input: never throws, returns
18
+ * ''.
19
+ *
20
+ * @param {*} name
21
+ * @returns {string}
22
+ */
23
+ function finalExtension(name) {
24
+ if (typeof name !== 'string' || name === '') return '';
25
+ const i = name.lastIndexOf('.');
26
+ return i === -1 ? name : name.slice(i + 1);
27
+ }
28
+
29
+ /**
30
+ * Strips ALL trailing dots and spaces from `name`, repeatedly, from the end
31
+ * of the basename.
32
+ *
33
+ * WHY: Win32 strips trailing dots and trailing spaces from each path
34
+ * component when resolving a filesystem path — `.env.`, `.env..`, `.env `,
35
+ * and `.env. ` all resolve to the same on-disk file as `.env` on Windows.
36
+ * These are therefore ALIASES for the protected name, not distinct names,
37
+ * and a guard that classifies the literal string without normalizing first
38
+ * can be bypassed by any of them. This is not cosmetic tidying — it closes
39
+ * that Windows path-alias bypass.
40
+ *
41
+ * This runs UNCONDITIONALLY on every host platform (macOS, Linux, Windows),
42
+ * not only when actually running on Windows: the guard must behave
43
+ * identically everywhere, and a name is judged by what Win32 would resolve
44
+ * it to, regardless of what OS the hook happens to run on.
45
+ *
46
+ * @param {*} name
47
+ * @returns {string}
48
+ */
49
+ function normalizeWindowsBasename(name) {
50
+ if (typeof name !== 'string' || name === '') return '';
51
+ let end = name.length;
52
+ while (end > 0 && (name[end - 1] === '.' || name[end - 1] === ' ')) end--;
53
+ return name.slice(0, end);
54
+ }
55
+
56
+ // Last `/`- or `\`-separated segment, ignoring trailing separators. A string
57
+ // with no separator IS its own last segment.
58
+ function lastSegment(tok) {
59
+ const s = tok.replace(/[\\/]+$/, '');
60
+ const i = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\'));
61
+ return i === -1 ? s : s.slice(i + 1);
62
+ }
63
+
64
+ module.exports = { finalExtension, normalizeWindowsBasename, lastSegment };
@@ -36,4 +36,56 @@ const REASON_CODE = Object.freeze({
36
36
  NOT_ISOLATED_WORKTREE: 'not_isolated_worktree',
37
37
  });
38
38
 
39
- module.exports = { REASON_CODE };
39
+ // #4594 row 15 / F2: values interpolated into a deny reason (sentinel/dispatch
40
+ // phase and plan) come from a sentinel file on disk and, transitively, from
41
+ // model-authored prompt text, neither of which is trusted — bound length and
42
+ // strip control characters/newlines so a crafted value cannot forge extra
43
+ // lines or otherwise inject content into the guard's stdout/stderr message
44
+ // (same discipline as escaping an untrusted token before embedding it in a
45
+ // message, e.g. phase-plan-index's `depends_on` warning).
46
+ //
47
+ // Originally duplicated byte-for-byte in hooks/gsd-agent-isolation-guard.js
48
+ // and hooks/gsd-cursor-subagent-start.js (#4594 F2 review finding) — both
49
+ // hooks already require this dependency-free module, so there is no
50
+ // cold-load justification for the duplication the way there is for
51
+ // hooks/lib/dispatch-identity.js's grammar mirror. Moved here as the single
52
+ // owner; both hooks now import it.
53
+ const REASON_INTERPOLATION_MAX_LEN = 64;
54
+
55
+ // F6: also strip Unicode line/paragraph separators (U+2028/U+2029) and the
56
+ // bidi-override/isolate control characters (U+202A-U+202E, U+2066-U+2069) —
57
+ // none of these are in `[\x00-\x1f\x7f]`, so a crafted sentinel or dispatch
58
+ // value carrying them could still visually reflow the deny message onto a
59
+ // new line or reverse/hide part of it despite the ASCII control-char strip.
60
+ const REASON_UNSAFE_CHARS_RE = new RegExp(
61
+ // eslint-disable-next-line no-control-regex -- deliberately stripping control chars/newlines
62
+ '[\\x00-\\x1f\\x7f\\u2028\\u2029\\u202a-\\u202e\\u2066-\\u2069]',
63
+ 'g',
64
+ );
65
+
66
+ function sanitizeForReason(value) {
67
+ if (typeof value !== 'string' || value.length === 0) return '(none)';
68
+ const stripped = value.replace(REASON_UNSAFE_CHARS_RE, '');
69
+ return stripped.length > REASON_INTERPOLATION_MAX_LEN
70
+ ? `${stripped.slice(0, REASON_INTERPOLATION_MAX_LEN)}…`
71
+ : stripped;
72
+ }
73
+
74
+ function describeSentinelDiscard(sentinelDiscarded) {
75
+ const sentinelPhase = sanitizeForReason(sentinelDiscarded.sentinel.phase);
76
+ const sentinelPlan = sanitizeForReason(sentinelDiscarded.sentinel.plan);
77
+ const dispatchPhase = sanitizeForReason(sentinelDiscarded.dispatch.phase);
78
+ const dispatchPlan = sanitizeForReason(sentinelDiscarded.dispatch.plan);
79
+ return (
80
+ ` A fresh dispatch-isolation sentinel was present but did not apply to this dispatch ` +
81
+ `(sentinel phase="${sentinelPhase}" plan="${sentinelPlan}"; dispatch phase="${dispatchPhase}" ` +
82
+ `plan="${dispatchPlan}"), so it was not consulted.`
83
+ );
84
+ }
85
+
86
+ module.exports = {
87
+ REASON_CODE,
88
+ REASON_INTERPOLATION_MAX_LEN,
89
+ sanitizeForReason,
90
+ describeSentinelDiscard,
91
+ };