@opengsd/gsd-core 1.12.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (455) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +12 -0
  4. package/agents/gsd-advisor-researcher.compact.md +85 -0
  5. package/agents/gsd-ai-researcher.compact.md +96 -0
  6. package/agents/gsd-assumptions-analyzer.compact.md +81 -0
  7. package/agents/gsd-code-fixer.compact.md +458 -0
  8. package/agents/gsd-code-fixer.md +5 -5
  9. package/agents/gsd-code-reviewer.compact.md +269 -0
  10. package/agents/gsd-code-reviewer.md +15 -3
  11. package/agents/gsd-codebase-mapper.compact.md +760 -0
  12. package/agents/gsd-debug-session-manager.compact.md +345 -0
  13. package/agents/gsd-doc-classifier.compact.md +192 -0
  14. package/agents/gsd-doc-synthesizer.compact.md +200 -0
  15. package/agents/gsd-doc-verifier.compact.md +143 -0
  16. package/agents/gsd-doc-writer.compact.md +440 -0
  17. package/agents/gsd-dom-verifier.compact.md +138 -0
  18. package/agents/gsd-domain-researcher.compact.md +141 -0
  19. package/agents/gsd-eval-auditor.compact.md +160 -0
  20. package/agents/gsd-eval-planner.compact.md +137 -0
  21. package/agents/gsd-executor.md +63 -35
  22. package/agents/gsd-framework-selector.compact.md +82 -0
  23. package/agents/gsd-integration-checker.compact.md +245 -0
  24. package/agents/gsd-intel-updater.compact.md +226 -0
  25. package/agents/gsd-mempalace-curator.compact.md +45 -0
  26. package/agents/gsd-nyquist-auditor.compact.md +179 -0
  27. package/agents/gsd-pattern-mapper.compact.md +275 -0
  28. package/agents/gsd-plan-checker.md +76 -57
  29. package/agents/gsd-planner.md +14 -0
  30. package/agents/gsd-project-researcher.compact.md +587 -0
  31. package/agents/gsd-research-synthesizer.compact.md +212 -0
  32. package/agents/gsd-roadmapper.compact.md +454 -0
  33. package/agents/gsd-roadmapper.md +13 -0
  34. package/agents/gsd-security-auditor.compact.md +162 -0
  35. package/agents/gsd-ui-auditor.compact.md +404 -0
  36. package/agents/gsd-ui-checker.compact.md +277 -0
  37. package/agents/gsd-ui-checker.md +19 -3
  38. package/agents/gsd-ui-researcher.compact.md +282 -0
  39. package/agents/gsd-ui-researcher.md +29 -0
  40. package/agents/gsd-user-profiler.compact.md +108 -0
  41. package/agents/gsd-verifier.md +23 -1
  42. package/bin/install.js +444 -134
  43. package/commands/gsd/cleanup.md +1 -0
  44. package/commands/gsd/code-review.md +2 -1
  45. package/commands/gsd/complete-milestone.md +1 -0
  46. package/commands/gsd/config.md +1 -0
  47. package/commands/gsd/debug.md +1 -0
  48. package/commands/gsd/execute-phase.md +1 -1
  49. package/commands/gsd/graphify.md +1 -0
  50. package/commands/gsd/health.md +1 -0
  51. package/commands/gsd/mempalace-capture.md +1 -0
  52. package/commands/gsd/mempalace-recall.md +1 -0
  53. package/commands/gsd/new-milestone.md +1 -0
  54. package/commands/gsd/new-project.md +1 -0
  55. package/commands/gsd/next.md +1 -0
  56. package/commands/gsd/ns-workflow.md +2 -1
  57. package/commands/gsd/pause-work.md +1 -0
  58. package/commands/gsd/phase.md +2 -1
  59. package/commands/gsd/pr-branch.md +1 -0
  60. package/commands/gsd/quick-batch.md +105 -0
  61. package/commands/gsd/resume-work.md +1 -0
  62. package/commands/gsd/review-backlog.md +1 -0
  63. package/commands/gsd/settings.md +2 -1
  64. package/commands/gsd/stats.md +1 -0
  65. package/commands/gsd/surface.md +18 -8
  66. package/commands/gsd/thread.md +1 -0
  67. package/commands/gsd/workspace.md +1 -0
  68. package/commands/gsd/workstreams.md +1 -0
  69. package/gsd-core/bin/check-latest-version.cjs +8 -3
  70. package/gsd-core/bin/gsd-tools.cjs +532 -174
  71. package/gsd-core/bin/lib/adr-parser.cjs +1 -1
  72. package/gsd-core/bin/lib/artifacts.cjs +2 -1
  73. package/gsd-core/bin/lib/audit.cjs +39 -22
  74. package/gsd-core/bin/lib/broken-windows.cjs +168 -49
  75. package/gsd-core/bin/lib/capability-activation.cjs +27 -0
  76. package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
  77. package/gsd-core/bin/lib/capability-loader.cjs +135 -1
  78. package/gsd-core/bin/lib/capability-registry.cjs +528 -116
  79. package/gsd-core/bin/lib/capability-source.cjs +19 -2
  80. package/gsd-core/bin/lib/capability-state.cjs +7 -1
  81. package/gsd-core/bin/lib/capability-validator.cjs +134 -5
  82. package/gsd-core/bin/lib/capability-writer.cjs +14 -4
  83. package/gsd-core/bin/lib/check-command-router.cjs +198 -38
  84. package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
  85. package/gsd-core/bin/lib/clusters.cjs +1 -0
  86. package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
  87. package/gsd-core/bin/lib/command-aliases.cjs +16 -0
  88. package/gsd-core/bin/lib/commands.cjs +981 -79
  89. package/gsd-core/bin/lib/config-loader.cjs +4 -0
  90. package/gsd-core/bin/lib/config.cjs +153 -38
  91. package/gsd-core/bin/lib/core-utils.cjs +34 -7
  92. package/gsd-core/bin/lib/coverage.cjs +1 -1
  93. package/gsd-core/bin/lib/decisions.cjs +343 -28
  94. package/gsd-core/bin/lib/edge-probe.cjs +14 -1
  95. package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
  96. package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
  97. package/gsd-core/bin/lib/frontmatter.cjs +137 -23
  98. package/gsd-core/bin/lib/gap-checker.cjs +22 -13
  99. package/gsd-core/bin/lib/git-base-branch.cjs +10 -2
  100. package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
  101. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
  102. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +54 -11
  103. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +87 -23
  104. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
  105. package/gsd-core/bin/lib/host-integration.cjs +57 -5
  106. package/gsd-core/bin/lib/init-command-router.cjs +14 -0
  107. package/gsd-core/bin/lib/init.cjs +539 -60
  108. package/gsd-core/bin/lib/install-engine.cjs +199 -14
  109. package/gsd-core/bin/lib/install-model-override-resolver.cjs +45 -0
  110. package/gsd-core/bin/lib/install-profiles.cjs +36 -14
  111. package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
  112. package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
  113. package/gsd-core/bin/lib/io.cjs +35 -0
  114. package/gsd-core/bin/lib/loop-resolver.cjs +64 -39
  115. package/gsd-core/bin/lib/markdown-table.cjs +123 -0
  116. package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
  117. package/gsd-core/bin/lib/milestone.cjs +41 -10
  118. package/gsd-core/bin/lib/model-resolver.cjs +101 -10
  119. package/gsd-core/bin/lib/phase-command-router.cjs +20 -7
  120. package/gsd-core/bin/lib/phase-id.cjs +412 -31
  121. package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
  122. package/gsd-core/bin/lib/phase.cjs +941 -98
  123. package/gsd-core/bin/lib/plan-document.cjs +10 -0
  124. package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
  125. package/gsd-core/bin/lib/planning-snapshot.cjs +206 -30
  126. package/gsd-core/bin/lib/planning-workspace.cjs +153 -29
  127. package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
  128. package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
  129. package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
  130. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
  131. package/gsd-core/bin/lib/quick-batch.cjs +840 -0
  132. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
  133. package/gsd-core/bin/lib/research-store.cjs +11 -12
  134. package/gsd-core/bin/lib/review-lane-descriptor.cjs +53 -5
  135. package/gsd-core/bin/lib/review-lane-invocation.cjs +96 -1
  136. package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
  137. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
  138. package/gsd-core/bin/lib/roadmap-parser.cjs +555 -41
  139. package/gsd-core/bin/lib/roadmap.cjs +292 -69
  140. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +260 -43
  141. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +28 -20
  142. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +294 -108
  143. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -47
  144. package/gsd-core/bin/lib/security.cjs +126 -7
  145. package/gsd-core/bin/lib/shell-command-projection.cjs +4 -0
  146. package/gsd-core/bin/lib/smart-entry.cjs +7 -9
  147. package/gsd-core/bin/lib/state-document.cjs +159 -32
  148. package/gsd-core/bin/lib/state-md-schema.cjs +44 -27
  149. package/gsd-core/bin/lib/state-transition.cjs +465 -62
  150. package/gsd-core/bin/lib/state.cjs +906 -151
  151. package/gsd-core/bin/lib/surface.cjs +83 -10
  152. package/gsd-core/bin/lib/task-command-router.cjs +12 -6
  153. package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
  154. package/gsd-core/bin/lib/uat.cjs +1420 -516
  155. package/gsd-core/bin/lib/update-context.cjs +36 -26
  156. package/gsd-core/bin/lib/validate.cjs +230 -12
  157. package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
  158. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  159. package/gsd-core/bin/lib/verification.cjs +316 -23
  160. package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
  161. package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
  162. package/gsd-core/bin/lib/verify.cjs +531 -36
  163. package/gsd-core/bin/lib/workstream-inventory.cjs +21 -2
  164. package/gsd-core/bin/lib/worktree-safety.cjs +21 -7
  165. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  166. package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
  167. package/gsd-core/bin/verify-reapply-patches.cjs +507 -81
  168. package/gsd-core/references/agent-contracts.md +3 -3
  169. package/gsd-core/references/compact-content-gate.md +66 -0
  170. package/gsd-core/references/edge-probe.md +17 -13
  171. package/gsd-core/references/execute-mvp-tdd.md +18 -16
  172. package/gsd-core/references/execute-phase-response-language.md +6 -0
  173. package/gsd-core/references/executor-examples.md +42 -0
  174. package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
  175. package/gsd-core/references/loop-hook-dispatch.md +18 -0
  176. package/gsd-core/references/model-profiles.md +12 -3
  177. package/gsd-core/references/mvp-concepts.md +2 -2
  178. package/gsd-core/references/plan-checker-examples.md +41 -0
  179. package/gsd-core/references/planner-antipatterns.md +25 -0
  180. package/gsd-core/references/planner-chunked.md +5 -1
  181. package/gsd-core/references/planner-coupling.md +42 -0
  182. package/gsd-core/references/planner-quick-batch.md +71 -0
  183. package/gsd-core/references/planner-reviews.md +47 -0
  184. package/gsd-core/references/planner-revision.md +75 -2
  185. package/gsd-core/references/planning-config.md +5 -1
  186. package/gsd-core/references/response-language-directive.md +9 -0
  187. package/gsd-core/references/revision-loop.md +118 -11
  188. package/gsd-core/references/tdd.md +17 -9
  189. package/gsd-core/references/thinking-models-planning.md +18 -2
  190. package/gsd-core/references/verification-patterns.md +17 -4
  191. package/gsd-core/references/verifier-evidence-gate.md +160 -0
  192. package/gsd-core/references/worktree-path-safety.md +112 -2
  193. package/gsd-core/templates/README.md +7 -1
  194. package/gsd-core/templates/phase-prompt.md +4 -0
  195. package/gsd-core/templates/state.md +6 -3
  196. package/gsd-core/templates/summary.compact.md +212 -0
  197. package/gsd-core/templates/user-setup.compact.md +199 -0
  198. package/gsd-core/templates/user-setup.md +0 -9
  199. package/gsd-core/templates/verification-report.md +5 -0
  200. package/gsd-core/workflows/add-backlog.md +2 -0
  201. package/gsd-core/workflows/add-phase.md +2 -0
  202. package/gsd-core/workflows/add-tests.md +1 -1
  203. package/gsd-core/workflows/add-todo.md +4 -3
  204. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  205. package/gsd-core/workflows/analyze-dependencies.md +2 -0
  206. package/gsd-core/workflows/audit-fix.md +2 -0
  207. package/gsd-core/workflows/audit-milestone.md +2 -0
  208. package/gsd-core/workflows/audit-uat.md +2 -0
  209. package/gsd-core/workflows/autonomous.md +15 -10
  210. package/gsd-core/workflows/check-todos.md +5 -3
  211. package/gsd-core/workflows/cleanup.md +4 -2
  212. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +22 -13
  213. package/gsd-core/workflows/code-review-fix.md +5 -3
  214. package/gsd-core/workflows/code-review.md +211 -43
  215. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
  216. package/gsd-core/workflows/complete-milestone.md +40 -254
  217. package/gsd-core/workflows/debug.md +1 -1
  218. package/gsd-core/workflows/diagnose-issues.md +5 -1
  219. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -0
  220. package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
  221. package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
  222. package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
  223. package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
  224. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -0
  225. package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
  226. package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
  227. package/gsd-core/workflows/discuss-phase/modes/text.md +2 -0
  228. package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
  229. package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
  230. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  231. package/gsd-core/workflows/discuss-phase-power.md +2 -0
  232. package/gsd-core/workflows/discuss-phase.md +1 -1
  233. package/gsd-core/workflows/do.md +43 -13
  234. package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
  235. package/gsd-core/workflows/docs-update.md +15 -156
  236. package/gsd-core/workflows/edit-phase.md +2 -0
  237. package/gsd-core/workflows/eval-review.md +1 -1
  238. package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
  239. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +20 -3
  240. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
  241. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +24 -3
  242. package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
  243. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +8 -2
  244. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -0
  245. package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
  246. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
  247. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
  248. package/gsd-core/workflows/execute-phase.md +78 -159
  249. package/gsd-core/workflows/execute-plan.md +28 -15
  250. package/gsd-core/workflows/explore.md +2 -0
  251. package/gsd-core/workflows/extract-learnings.md +2 -0
  252. package/gsd-core/workflows/fast.md +6 -0
  253. package/gsd-core/workflows/forensics.md +2 -0
  254. package/gsd-core/workflows/graduation.md +1 -1
  255. package/gsd-core/workflows/health.md +1 -1
  256. package/gsd-core/workflows/help/modes/brief.md +2 -0
  257. package/gsd-core/workflows/help/modes/default.md +2 -0
  258. package/gsd-core/workflows/help/modes/full.compact.md +398 -0
  259. package/gsd-core/workflows/help/modes/full.md +12 -0
  260. package/gsd-core/workflows/help/modes/topic.md +2 -0
  261. package/gsd-core/workflows/help.md +3 -1
  262. package/gsd-core/workflows/import.md +3 -3
  263. package/gsd-core/workflows/inbox.md +1 -1
  264. package/gsd-core/workflows/ingest-docs.md +1 -1
  265. package/gsd-core/workflows/insert-phase.md +2 -0
  266. package/gsd-core/workflows/list-phase-assumptions.md +2 -0
  267. package/gsd-core/workflows/list-seeds.md +2 -0
  268. package/gsd-core/workflows/list-workspaces.md +2 -0
  269. package/gsd-core/workflows/manager.md +3 -3
  270. package/gsd-core/workflows/map-codebase.md +52 -3
  271. package/gsd-core/workflows/milestone-summary.md +2 -0
  272. package/gsd-core/workflows/mvp-phase.md +1 -1
  273. package/gsd-core/workflows/new-milestone.md +55 -13
  274. package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
  275. package/gsd-core/workflows/new-project.md +37 -205
  276. package/gsd-core/workflows/new-workspace.md +1 -1
  277. package/gsd-core/workflows/next.md +2 -0
  278. package/gsd-core/workflows/node-repair.md +2 -0
  279. package/gsd-core/workflows/note.md +2 -0
  280. package/gsd-core/workflows/onboard.md +1 -1
  281. package/gsd-core/workflows/pause-work.md +19 -4
  282. package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
  283. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
  284. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -0
  285. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +9 -0
  286. package/gsd-core/workflows/plan-phase.md +144 -185
  287. package/gsd-core/workflows/plan-review-convergence.md +102 -10
  288. package/gsd-core/workflows/plant-seed.md +1 -1
  289. package/gsd-core/workflows/pr-branch.md +30 -10
  290. package/gsd-core/workflows/profile-user.md +1 -1
  291. package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
  292. package/gsd-core/workflows/progress.md +25 -3
  293. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +37 -2
  294. package/gsd-core/workflows/quick/steps/research-phase.md +3 -3
  295. package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
  296. package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
  297. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
  298. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
  299. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
  300. package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
  301. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
  302. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
  303. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
  304. package/gsd-core/workflows/quick-batch.md +203 -0
  305. package/gsd-core/workflows/quick.md +21 -4
  306. package/gsd-core/workflows/reapply-patches.md +79 -3
  307. package/gsd-core/workflows/remove-phase.md +2 -0
  308. package/gsd-core/workflows/remove-workspace.md +1 -1
  309. package/gsd-core/workflows/resume-project.md +6 -2
  310. package/gsd-core/workflows/review.md +215 -10
  311. package/gsd-core/workflows/scan.md +2 -0
  312. package/gsd-core/workflows/section-manifest.json +12 -0
  313. package/gsd-core/workflows/secure-phase.md +1 -1
  314. package/gsd-core/workflows/session-report.md +2 -0
  315. package/gsd-core/workflows/settings-advanced.md +2 -0
  316. package/gsd-core/workflows/settings-integrations.md +9 -8
  317. package/gsd-core/workflows/settings.md +19 -6
  318. package/gsd-core/workflows/ship.md +10 -10
  319. package/gsd-core/workflows/sketch-wrap-up.md +2 -0
  320. package/gsd-core/workflows/sketch.md +1 -1
  321. package/gsd-core/workflows/smart-entry.md +1 -1
  322. package/gsd-core/workflows/spec-phase.md +24 -19
  323. package/gsd-core/workflows/spike-wrap-up.md +2 -0
  324. package/gsd-core/workflows/spike.md +1 -1
  325. package/gsd-core/workflows/stats.md +2 -0
  326. package/gsd-core/workflows/sync-skills.md +12 -4
  327. package/gsd-core/workflows/thread.md +2 -0
  328. package/gsd-core/workflows/transition.md +2 -0
  329. package/gsd-core/workflows/ui-phase.md +26 -5
  330. package/gsd-core/workflows/ui-review.md +1 -1
  331. package/gsd-core/workflows/ultraplan-phase.md +2 -0
  332. package/gsd-core/workflows/undo.md +1 -1
  333. package/gsd-core/workflows/update.md +48 -43
  334. package/gsd-core/workflows/validate-phase.md +1 -1
  335. package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
  336. package/gsd-core/workflows/verify-work.md +68 -182
  337. package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
  338. package/hooks/dist/gsd-check-update-worker.js +19 -2
  339. package/hooks/dist/gsd-context-monitor.js +371 -27
  340. package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
  341. package/hooks/dist/gsd-node-runner.sh +1 -0
  342. package/hooks/dist/gsd-prompt-guard.js +30 -5
  343. package/hooks/dist/gsd-read-guard.js +2 -0
  344. package/hooks/dist/gsd-read-injection-scanner.js +5 -5
  345. package/hooks/dist/gsd-secret-read-guard.js +1105 -0
  346. package/hooks/dist/gsd-statusline.js +18 -10
  347. package/hooks/dist/gsd-validate-commit.sh +474 -7
  348. package/hooks/dist/gsd-workflow-guard.js +2 -1
  349. package/hooks/dist/gsd-worktree-path-guard.js +25 -14
  350. package/hooks/dist/gsd-write-guard.js +46 -1
  351. package/hooks/dist/lib/dispatch-identity.js +187 -0
  352. package/hooks/dist/lib/filename-classification.js +64 -0
  353. package/hooks/dist/lib/git-cmd.js +210 -1
  354. package/hooks/dist/lib/injection-patterns.js +36 -6
  355. package/hooks/dist/lib/isolation-deny-reason.js +53 -1
  356. package/hooks/dist/lib/isolation-sentinel.js +58 -19
  357. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  358. package/hooks/gsd-agent-isolation-guard.js +42 -16
  359. package/hooks/gsd-check-update-worker.js +19 -2
  360. package/hooks/gsd-context-monitor.js +371 -27
  361. package/hooks/gsd-cursor-subagent-start.js +34 -14
  362. package/hooks/gsd-node-runner.sh +1 -0
  363. package/hooks/gsd-prompt-guard.js +30 -5
  364. package/hooks/gsd-read-guard.js +2 -0
  365. package/hooks/gsd-read-injection-scanner.js +5 -5
  366. package/hooks/gsd-secret-read-guard.js +1105 -0
  367. package/hooks/gsd-statusline.js +18 -10
  368. package/hooks/gsd-validate-commit.sh +474 -7
  369. package/hooks/gsd-workflow-guard.js +2 -1
  370. package/hooks/gsd-worktree-path-guard.js +25 -14
  371. package/hooks/gsd-write-guard.js +46 -1
  372. package/hooks/hooks.json +6 -0
  373. package/hooks/lib/dispatch-identity.js +187 -0
  374. package/hooks/lib/filename-classification.js +64 -0
  375. package/hooks/lib/git-cmd.js +210 -1
  376. package/hooks/lib/injection-patterns.js +36 -6
  377. package/hooks/lib/isolation-deny-reason.js +53 -1
  378. package/hooks/lib/isolation-sentinel.js +58 -19
  379. package/hooks/managed-hooks-registry.cjs +1 -0
  380. package/package.json +13 -9
  381. package/scripts/benchmark-compact-content-variants.cjs +298 -0
  382. package/scripts/benchmark-compact-content.cjs +368 -0
  383. package/scripts/build-hooks.js +11 -4
  384. package/scripts/check-contract-drift.cjs +4 -1
  385. package/scripts/check-env.cjs +36 -8
  386. package/scripts/check-glossary-refs.cjs +25 -21
  387. package/scripts/ci-next-health.cjs +271 -0
  388. package/scripts/ci-prepare-test-scope.cjs +7 -7
  389. package/scripts/ci-test-scope.cjs +133 -20
  390. package/scripts/ci-timeout-report.cjs +1 -1
  391. package/scripts/diff-touches-shipped-paths.cjs +1 -1
  392. package/scripts/docs-guard-registry.cjs +17 -2
  393. package/scripts/gen-adr-index.cjs +8 -2
  394. package/scripts/gen-inventory-manifest.cjs +12 -0
  395. package/scripts/gen-loop-host-contract.cjs +67 -15
  396. package/scripts/gen-platform-conformance-tier.cjs +557 -0
  397. package/scripts/lib/drift-scan.cjs +1 -1
  398. package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
  399. package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
  400. package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
  401. package/scripts/lib/shellcheck-fetch.cjs +247 -0
  402. package/scripts/lib/suite-detection.cjs +32 -0
  403. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
  404. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  405. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  406. package/scripts/lint-allowed-tools-parity.cjs +221 -0
  407. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +24 -2
  408. package/scripts/lint-phase-enumeration-drift.cjs +24 -6
  409. package/scripts/lint-phase-id-drift.cjs +465 -15
  410. package/scripts/lint-portable-grep.cjs +176 -0
  411. package/scripts/lint-response-language-coverage.cjs +530 -0
  412. package/scripts/lint-source-test-name-collision.cjs +1 -1
  413. package/scripts/lint-test-file-count.allowlist.json +4 -1
  414. package/scripts/lint-vendored-deps.cjs +128 -17
  415. package/scripts/lint-workflow-shellcheck-baseline.json +1112 -0
  416. package/scripts/lint-workflow-shellcheck.cjs +614 -0
  417. package/scripts/npm-audit-baseline.cjs +376 -0
  418. package/scripts/prompt-injection-scan.sh +22 -0
  419. package/scripts/require-issue-link-policy.cjs +16 -1
  420. package/scripts/workflow-size.cjs +139 -0
  421. package/skills/gsd-cleanup/SKILL.md +1 -0
  422. package/skills/gsd-code-review/SKILL.md +2 -1
  423. package/skills/gsd-complete-milestone/SKILL.md +1 -0
  424. package/skills/gsd-config/SKILL.md +1 -0
  425. package/skills/gsd-debug/SKILL.md +1 -0
  426. package/skills/gsd-execute-phase/SKILL.md +1 -1
  427. package/skills/gsd-graphify/SKILL.md +1 -0
  428. package/skills/gsd-health/SKILL.md +1 -0
  429. package/skills/gsd-mempalace-capture/SKILL.md +1 -0
  430. package/skills/gsd-mempalace-recall/SKILL.md +1 -0
  431. package/skills/gsd-new-milestone/SKILL.md +1 -0
  432. package/skills/gsd-new-project/SKILL.md +1 -0
  433. package/skills/gsd-next/SKILL.md +1 -0
  434. package/skills/gsd-ns-workflow/SKILL.md +1 -0
  435. package/skills/gsd-pause-work/SKILL.md +1 -0
  436. package/skills/gsd-phase/SKILL.md +2 -1
  437. package/skills/gsd-pr-branch/SKILL.md +1 -0
  438. package/skills/gsd-quick-batch/SKILL.md +105 -0
  439. package/skills/gsd-resume-work/SKILL.md +1 -0
  440. package/skills/gsd-review-backlog/SKILL.md +1 -0
  441. package/skills/gsd-settings/SKILL.md +2 -1
  442. package/skills/gsd-stats/SKILL.md +1 -0
  443. package/skills/gsd-surface/SKILL.md +18 -8
  444. package/skills/gsd-thread/SKILL.md +1 -0
  445. package/skills/gsd-workspace/SKILL.md +1 -0
  446. package/skills/gsd-workstreams/SKILL.md +1 -0
  447. package/vscode/package.json +1 -1
  448. package/gsd-core/templates/claude-md.md +0 -145
  449. package/gsd-core/templates/codebase/concerns.md +0 -310
  450. package/gsd-core/templates/codebase/conventions.md +0 -307
  451. package/gsd-core/templates/codebase/integrations.md +0 -280
  452. package/gsd-core/templates/codebase/structure.md +0 -285
  453. package/gsd-core/templates/codebase/testing.md +0 -480
  454. package/gsd-core/templates/debug-subagent-prompt.md +0 -91
  455. package/gsd-core/templates/discovery.md +0 -146
@@ -389,8 +389,12 @@ function formatGsdState(s) {
389
389
  // Scene 2: idle + a recommended next command is visible to the user.
390
390
  // Surfaces "what to run next" without the user opening STATE.md.
391
391
  parts.push(`next ${s.nextAction} ${phasesStr}`);
392
- } else if (Number(s.percent) === 100 || (s.completedPhases && s.totalPhases && s.completedPhases === s.totalPhases)) {
393
- // Scene 3: milestone complete (every phase done).
392
+ } else if (Number(s.percent) === 100 || (Number(s.totalPhases) > 0 && Number(s.completedPhases) === Number(s.totalPhases))) {
393
+ // Scene 3: milestone complete (every phase done). #3945: the counters are
394
+ // regex-captured STRINGS, so the old `cp && tp && cp === tp` guard fired on
395
+ // the empty set ('0' is truthy, '0' === '0') — "0% · milestone complete".
396
+ // Numeric coercion + a non-empty denominator makes "nothing to measure"
397
+ // stop meaning "everything is done".
394
398
  parts.push('milestone complete');
395
399
  } else {
396
400
  // Backward-compatible default — preserved EXACTLY for STATE.md files that
@@ -445,13 +449,17 @@ function contextTokenSuffix(currentUsage) {
445
449
  // --- Compact state format (opt-in) ---------------------------------------------
446
450
 
447
451
  /**
448
- * Collapse GSD's free-text status (often a multi-sentence narrative) to a
449
- * single keyword, built on the canonical normalizer (#2162 approval
450
- * condition): normalizeStateStatus() in state-document.cjs owns the status
451
- * vocabulary (discussing / planning / executing / verifying / completed /
452
- * paused) so the two can't drift. "paused" — the canonical stuck state — is
453
- * uppercased to PAUSED, the one state worth shouting about. Statuses the
454
- * normalizer passes through unrecognized fall back to their first word,
452
+ * Collapse GSD's status value to a single keyword, built on the canonical
453
+ * normalizer (#2162 approval condition): normalizeStateStatus() in
454
+ * state-document.cjs owns the status vocabulary (discussing / planning /
455
+ * executing / verifying / completed / paused) so the two can't drift.
456
+ * #4186: the normalizer recognizes the DECLARED vocabulary by anchored
457
+ * whole-field match — vocabulary values (the state writer persists tokens)
458
+ * collapse to their keyword; free-text narratives are no longer
459
+ * keyword-guessed from substrings (a `.planning/` mention in non-English
460
+ * prose used to render `planning`), and pass through unrecognized to the
461
+ * first-word fallback below. "paused" — the canonical stuck state — is
462
+ * uppercased to PAUSED, the one state worth shouting about. The fallback is
455
463
  * capped at 16 chars so a rogue STATE.md can't blow up the line.
456
464
  * Returns null for empty input.
457
465
  */
@@ -500,7 +508,7 @@ function formatGsdStateCompact(s) {
500
508
  // still completes) wins over milestone-complete (Scene 3), even if a
501
509
  // non-atomic STATE.md edit leaves percent=100 alongside a lifecycle phase.
502
510
  const done = !s.activePhase && (Number(s.percent) === 100 ||
503
- (s.completedPhases && s.totalPhases && s.completedPhases === s.totalPhases));
511
+ (Number(s.totalPhases) > 0 && Number(s.completedPhases) === Number(s.totalPhases)));
504
512
 
505
513
  if (done) {
506
514
  parts.push('complete');
@@ -24,13 +24,38 @@ cleanup_temp_files() {
24
24
  }
25
25
  trap cleanup_temp_files EXIT
26
26
 
27
+ # The 10 built-in Conventional Commits types — the SINGLE declaration (#3811
28
+ # review finding: this was previously hand-typed a second time inside the
29
+ # node -e script below, a generative-fix-divergence risk per CLAUDE.md's
30
+ # known-defect list). Threaded into node via an env var; reused directly by
31
+ # bash below when building COMMIT_TYPES.
32
+ BUILTIN_COMMIT_TYPES=(feat fix docs style refactor perf test build ci chore)
33
+
27
34
  # Check opt-in config — exit silently if not enabled
28
35
  if [ -f .planning/config.json ]; then
29
36
  ENABLED_ERR=$(mktemp)
30
- ENABLED=$(node -e "
37
+ # Single node invocation reads BOTH hooks.community (line 1: '1'/'0') and
38
+ # hooks.commit_types (remaining lines: one sanitized extra type per line) —
39
+ # see #3811. Sanitizing here, not in bash, keeps the safe-token check in one
40
+ # place and guarantees only [a-z][a-z0-9-]* strings ever reach the regex
41
+ # built below, so a configured value can never alter the compiled pattern's
42
+ # structure.
43
+ BUILTIN_COMMIT_TYPES_CSV=$(IFS=,; echo "${BUILTIN_COMMIT_TYPES[*]}")
44
+ CONFIG_OUT=$(GSD_BUILTIN_COMMIT_TYPES="$BUILTIN_COMMIT_TYPES_CSV" node -e "
31
45
  try{
32
46
  const c=require('./.planning/config.json');
33
47
  process.stdout.write(c.hooks?.community===true?'1':'0');
48
+ process.stdout.write('\n');
49
+ const raw=c.hooks?.commit_types;
50
+ const list=Array.isArray(raw)?raw:[];
51
+ const seen=new Set((process.env.GSD_BUILTIN_COMMIT_TYPES||'').split(',').filter(Boolean));
52
+ for (const t of list){
53
+ if (typeof t!=='string') continue;
54
+ if (!/^[a-z][a-z0-9-]*\$/.test(t)) continue;
55
+ if (seen.has(t)) continue;
56
+ seen.add(t);
57
+ process.stdout.write(t+'\n');
58
+ }
34
59
  }catch(e){
35
60
  process.stderr.write('CONFIG_READ_FAILED: '+(e&&e.message?e.message:String(e)));
36
61
  process.exit(3);
@@ -44,7 +69,20 @@ if [ -f .planning/config.json ]; then
44
69
  echo "gsd-validate-commit.sh: could not read .planning/config.json (opt-in check) — validator disabled for this call. $(cat "$ENABLED_ERR")" >&2
45
70
  exit 0
46
71
  fi
72
+ # Pure parameter expansion, not `printf ... | head -1`: same SIGPIPE race
73
+ # class as the SUBJECT extraction below (`echo "$MSG" | head -1`) — CONFIG_OUT
74
+ # is multi-line whenever extra commit types are configured, and `head -1`
75
+ # closing early can SIGPIPE `printf` under `set -euo pipefail`.
76
+ ENABLED="${CONFIG_OUT%%$'\n'*}"
47
77
  if [ "$ENABLED" != "1" ]; then exit 0; fi
78
+ # Remaining lines (if any) are the sanitized, deduped configured commit
79
+ # types beyond the 10 built-ins (#3811). Read into a bash-3.2-safe array —
80
+ # `mapfile`/`readarray` are bash 4+ only and this hook is tested against
81
+ # bash 3.2.57 (macOS default).
82
+ EXTRA_COMMIT_TYPES=()
83
+ while IFS= read -r _extra_type; do
84
+ [ -n "$_extra_type" ] && EXTRA_COMMIT_TYPES+=("$_extra_type")
85
+ done < <(printf '%s\n' "$CONFIG_OUT" | tail -n +2)
48
86
  else
49
87
  exit 0
50
88
  fi
@@ -104,21 +142,450 @@ if [ "$CLASSIFY_STATUS" != "0" ] && [ "$CLASSIFY_STATUS" != "1" ]; then
104
142
  exit 0
105
143
  fi
106
144
  if [ "$CLASSIFY_STATUS" = "0" ]; then
107
- # Extract message from -m flag
145
+ # Extract message from -m flag.
146
+ #
147
+ # MSG_QUOTE records WHICH arm matched. bash treats the two arms differently
148
+ # and the subject step below depends on that difference — see the resolver
149
+ # gate (review of #3816, round 4).
108
150
  MSG=""
151
+ MSG_QUOTE=""
152
+ MSG_MATCH=""
109
153
  if [[ "$CMD" =~ -m[[:space:]]+\"([^\"]+)\" ]]; then
110
154
  MSG="${BASH_REMATCH[1]}"
155
+ MSG_QUOTE=dq
156
+ MSG_MATCH="${BASH_REMATCH[0]}"
111
157
  elif [[ "$CMD" =~ -m[[:space:]]+\'([^\']+)\' ]]; then
112
158
  MSG="${BASH_REMATCH[1]}"
159
+ MSG_QUOTE=sq
160
+ MSG_MATCH="${BASH_REMATCH[0]}"
113
161
  fi
114
162
 
115
163
  if [ -n "$MSG" ]; then
116
- SUBJECT=$(echo "$MSG" | head -1)
164
+ # Subject = first line of the message, EXCEPT for the command-substituted
165
+ # heredoc form, where the first line is the opener rather than the message:
166
+ #
167
+ # git commit -m "$(cat <<'EOF'
168
+ # feat(auth): add login flow
169
+ # EOF
170
+ # )"
171
+ #
172
+ # The capture above spans it whole, because bash `[^"]` matches newlines, so
173
+ # `head -1` yielded the literal `$(cat <<'EOF'` and EVERY heredoc-form commit
174
+ # was blocked regardless of its message (#3802).
175
+ #
176
+ # Selection of WHICH argument is the message is unchanged above — only the
177
+ # subject-from-message step is delegated. Falls back to the previous `head -1`
178
+ # if node or the library is unavailable, so a broken extractor degrades to the
179
+ # old behavior instead of becoming a new silent-allow path.
180
+ #
181
+ # SINGLE-QUOTE GATE (review of #3816, round 4 — BLOCKER). The resolver may
182
+ # only run on the DOUBLE-quoted arm. Inside `-m '...'` bash performs NO
183
+ # command substitution, so `$(cat <<'EOF'` is literal text and git's real
184
+ # subject is that opener line — resolving the body there validates a
185
+ # message git never receives. Measured against the real hook, all four
186
+ # spellings (`<<'E'`, `<<"E"`, `<<\E`, `<<E`) went base=2 -> head=0: a
187
+ # net-new bypass reachable by the ordinary authoring slip of typing `'`
188
+ # for `"`. The sq arm therefore keeps the pre-fix `head -1`, which is exact
189
+ # base parity.
190
+ #
191
+ # ADJACENCY GUARD (review of #3816): text glued to the CLOSING quote —
192
+ # `-m "$(cat <<'EOF' ... )"suffix` — is concatenated by bash into the SAME
193
+ # argument, so the capture above holds only a PREFIX of the real message.
194
+ # Resolving a heredoc from a prefix hands the length gate a fraction of the
195
+ # real subject: a net-new bypass relative to base, which measured the
196
+ # opener line and blocked. When the quote is not followed by whitespace or
197
+ # the end of the command, skip the resolver and keep the pre-fix subject
198
+ # (first captured line): the heredoc form then fails the format gate
199
+ # exactly as it did on base, and the plain single-line form keeps base
200
+ # behavior unchanged. The guard is tested against the arm that MATCHED,
201
+ # not against both: testing both let a double-quoted heredoc whose BODY
202
+ # mentions a glued single-quoted token (`-m "... -m 'foo'bar ..."`) trip
203
+ # the sq arm and lose the fix for a message that never had a prefix
204
+ # problem (review of #3816, round 4, Minor 1).
205
+ # RESOLVER PRECONDITIONS. The resolver may run only where the captured text
206
+ # is provably the subject git receives. Each guard names an input where it
207
+ # is not; every refusal falls back to `head -1`, the pre-fix subject, which
208
+ # fails the format gate exactly as this whole form did before the fix.
209
+ RESOLVE=0
210
+ if [ "$MSG_QUOTE" = dq ]; then
211
+ RESOLVE=1
212
+ # Text before the message we matched. The heredoc BODY always sits after
213
+ # the match, so this window cannot be contaminated by message content —
214
+ # which is what lets the two guards below scan for tokens that would also
215
+ # be legal inside a commit message.
216
+ MSG_PREFIX="${CMD%%"$MSG_MATCH"*}"
217
+ # Text after it. Together, PREFIX and SUFFIX are the whole command MINUS
218
+ # the message — the window a guard must use when the token it scans for
219
+ # is also legal English inside a commit message, but may legally appear
220
+ # on EITHER side of the message on the command line.
221
+ # Indexed, not searched (#4492). `${CMD#*"$MSG_MATCH"}` is quadratic in
222
+ # the message: bash walks every prefix length and compares the whole
223
+ # matched literal at each one, and MSG_MATCH is BASH_REMATCH[0] — the
224
+ # entire `-m "..."` — so the cost grows with the thing being scanned.
225
+ # Measured on the path EVERY commit takes (conforming and non-conforming
226
+ # cost the same): 10.0 s at a 64 KB message, 22.0 s at 96 KB, 30.2 s at
227
+ # 112 KB. Sizes stop there deliberately — a single argument above Linux's
228
+ # MAX_ARG_STRLEN (131072 on a 4 KB-page kernel) never reaches this code
229
+ # at all, because execve fails and the hook fails open, so a larger
230
+ # "measurement" would be timing the wrong thing.
231
+ #
232
+ # MSG_PREFIX above has already located the match, so the suffix is
233
+ # arithmetic rather than a search: skip the prefix and the match. This
234
+ # removes the quadratic SEARCH; the expansion still counts characters and
235
+ # materialises a substring, so it is linear in the command, not O(1).
236
+ # Same first-occurrence assumption both expansions here always made —
237
+ # MSG_MATCH is a literal substring of CMD by construction.
238
+ MSG_SUFFIX="${CMD:$(( ${#MSG_PREFIX} + ${#MSG_MATCH} ))}"
239
+ # LINE CONTINUATIONS ARE NOT SEPARATORS (review of #3816, rounds 8 and 9).
240
+ # `git commit \` newline ` -m "$(cat <<'EOF' …` is an ordinary way to
241
+ # spread an invocation over lines, and every guard below reads a newline in
242
+ # a window as a command separator, so the whole form was refused. That was
243
+ # disclosed as a fail-closed limit in round 8 because "is this newline a
244
+ # continuation" looked like the segmentation question this file has
245
+ # reverted twice. It is not: bash's rule is local and character-level. A
246
+ # newline preceded by an ODD run of backslashes is a continuation and bash
247
+ # removes both; an EVEN run (`\\` then newline) is a literal backslash
248
+ # followed by a real newline, which IS a separator. So the windows are
249
+ # joined the way bash joins them, in three bash-3.2-safe steps: every `\\`
250
+ # pair is parked on \x01, a byte no real command line carries, any
251
+ # backslash-newline that remains is a lone (odd) one and is removed, then
252
+ # the pairs are restored. Applied to BOTH windows, BEFORE the dequote
253
+ # copies are derived, so every scan sees the joined text.
254
+ #
255
+ # KNOWN OVER-BLOCK, fail-closed: a literal \x01 that IS present in the
256
+ # command is restored as `\\`, so an option-shaped token carrying one
257
+ # (`-\x01m`) reads as `-\\m`, dequotes to `-m`, and refuses where it did
258
+ # not before (independent review, round 9). Refusing is the recoverable
259
+ # direction; a control byte in an option name is not a spelling anyone
260
+ # types, and it is not a hole in the accept direction.
261
+ #
262
+ # Direction check: a continuation glued to the closing quote
263
+ # (`"$(…)"\` newline `suffix`) joins to `"$(…)"suffix`, which the glue
264
+ # guard refuses exactly as bash would have glued it; `\\` + newline keeps
265
+ # its newline and is still refused by the separator guard. Measured on
266
+ # bash 3.2.57 and 5.3.15 in tests/hooks-opt-in.test.cjs.
267
+ CONT_PARK=$'\x01'
268
+ MSG_PREFIX="${MSG_PREFIX//\\\\/$CONT_PARK}"
269
+ MSG_PREFIX="${MSG_PREFIX//\\$'\n'/}"
270
+ MSG_PREFIX="${MSG_PREFIX//$CONT_PARK/\\\\}"
271
+ MSG_SUFFIX="${MSG_SUFFIX//\\\\/$CONT_PARK}"
272
+ MSG_SUFFIX="${MSG_SUFFIX//\\$'\n'/}"
273
+ MSG_SUFFIX="${MSG_SUFFIX//$CONT_PARK/\\\\}"
274
+
275
+ # QUOTE-SPLICED SPELLINGS (independent review of #3816, round 6). Bash
276
+ # removes quotes before git ever sees an argument, so the same option has
277
+ # unboundedly many spellings on the command line: `--clean""up=verbatim`
278
+ # IS `--cleanup=verbatim` to git, and `-""m` IS `-m`. Both matched no
279
+ # literal and were measured ACCEPTING a 75-byte subject the length gate
280
+ # had recorded as 72. The guards below therefore scan a copy of their
281
+ # window with quote characters removed, which is what bash does to it.
282
+ # Only the two OPTION-NAME scans use it; the adjacency test deliberately
283
+ # does not, because it asks about a literal character position, and the
284
+ # message span itself is excluded from both windows either way.
285
+ MSG_PREFIX_DEQ="${MSG_PREFIX//[\"\']/}"
286
+ MSG_SUFFIX_DEQ="${MSG_SUFFIX//[\"\']/}"
287
+ # BACKSLASH-SPLICED SPELLINGS (independent review of #3816, round 7).
288
+ # Quote removal alone was not "the command as bash hands it to git": bash
289
+ # also removes syntactic backslashes, so `-\m WIP` IS `-m WIP` and
290
+ # `--clean\up=verbatim` IS `--cleanup=verbatim` to git, and both matched
291
+ # no literal. Measured: `-\m WIP -m <conforming heredoc>` accepted the
292
+ # heredoc while git recorded `WIP`, and a trailing `--clean\up=verbatim`
293
+ # accepted a 75-byte subject the length gate measured as 72. Stripped in a
294
+ # second pass so the class is unambiguous.
295
+ MSG_PREFIX_DEQ="${MSG_PREFIX_DEQ//\\/}"
296
+ MSG_SUFFIX_DEQ="${MSG_SUFFIX_DEQ//\\/}"
297
+ # DOLLAR-QUOTED SPELLINGS (independent review of #3816, round 8). The two
298
+ # passes above still were not "the command as bash hands it to git": bash
299
+ # has TWO more quoting forms whose introducer is a `$`, and removing the
300
+ # quote characters alone leaves that `$` stranded in the middle of the
301
+ # option name. `-$"m"` became `-$m` here while bash passes a real `-m` to
302
+ # git, and `--mes$'sage'=WIP` became `--mes$sage=WIP`; neither matched any
303
+ # literal, so the first-message guard below never fired. Measured on bash
304
+ # 3.2.57 and 5.3.15 against a real repository: the hook allowed
305
+ # `-$"m" WIP -m <conforming heredoc>` (exit 0) while `git cat-file -p`
306
+ # recorded the subject `WIP` — the same command spelled `-m WIP` is
307
+ # refused (exit 2). Stripping `$` closes both dollar-quote forms.
308
+ #
309
+ # RESIDUAL, and not fixable from a string: an option name assembled by an
310
+ # EXPANSION — `-${x}m`, `-$(printf m)` — is not knowable without running
311
+ # the command, the same limit this file already documents for expanded
312
+ # heredoc bodies. Stripping `$` makes those spellings collapse toward the
313
+ # literal too, which over-matches, and over-matching only refuses more.
314
+ MSG_PREFIX_DEQ="${MSG_PREFIX_DEQ//\$/}"
315
+ MSG_SUFFIX_DEQ="${MSG_SUFFIX_DEQ//\$/}"
316
+
317
+ # ADJACENCY GUARD (review of #3816): text glued to the CLOSING quote —
318
+ # `-m "$(cat <<'EOF' ... )"suffix` — is concatenated by bash into the SAME
319
+ # argument, so the capture holds only a PREFIX of the real message, and
320
+ # the length gate would measure a fraction of the real subject.
321
+ # SCOPE (review of #3816, round 6 — MAJOR). Glue is a property of the ONE
322
+ # character following the MATCHED span, so that character is the whole
323
+ # window. Scanning $CMD for the shape anywhere refused any conforming
324
+ # commit whose command merely CONTAINED a glued `-m` elsewhere —
325
+ # `git commit -m "<heredoc>" && echo -m "test"z` stayed blocked with
326
+ # CONVENTIONAL_COMMITS_VIOLATION. Base blocks it too, because base blocks
327
+ # EVERY heredoc form (that is #3802): this was the fix not reaching the
328
+ # shape, measured base=2 -> pre=2 -> post=0, not a regression.
329
+ # The separators and redirections are excluded because bash does NOT
330
+ # concatenate across them: in `-m "msg"&& echo hi` the argument ends at
331
+ # the quote, so there is no truncated capture to defend against.
332
+ # The class is held in a VARIABLE, not written inline. Inline, every
333
+ # member needs a backslash to get past the `[[ ]]` parser (`;`, `&` and
334
+ # `|` are metacharacters there) — and on bash 3.2, the system /bin/bash on
335
+ # macOS, those backslashes are passed THROUGH to the regex engine instead
336
+ # of being consumed by the shell, silently adding a literal `\` to the
337
+ # class. Unquoted expansion of a variable on the right of `=~` is the one
338
+ # spelling that is a plain regex on 3.2 and 5.x alike (review of #3816,
339
+ # round 8). Writing `[^[:space:];&|()<>]` inline is NOT the fix: it is a
340
+ # bash syntax error on both versions.
341
+ GLUE_CLASS='^[^[:space:];&|()<>]'
342
+ if [[ "$MSG_SUFFIX" =~ $GLUE_CLASS ]]; then RESOLVE=0; fi
343
+
344
+ # FIRST-MESSAGE GUARD (Codex review of #3816, round 4 — BLOCKER). The
345
+ # capture is a SEARCH over the whole command and the double-quoted arm is
346
+ # tried first, so it can select a `-m` that is not git's subject at all:
347
+ #
348
+ # git commit -m 'WIP first' -m "$(cat <<'EOF' -> git concatenates; the
349
+ # git commit -m WIP -m "$(cat <<'EOF' subject is `WIP first`
350
+ # git commit -m WIP -- -m "$(cat <<'EOF' -> after --, not a message
351
+ # git commit -m WIP && echo -m "$(cat <<'EOF' -> belongs to `echo`
352
+ #
353
+ # All four measured base=2 -> head=0, with git recording the FIRST message
354
+ # as the subject (verified against real commits, not the man page). The
355
+ # mis-selection is pre-existing; resolving it is what turned it into an
356
+ # enforcement bypass. Resolve only when nothing before the match could
357
+ # have been an earlier message, an end-of-options marker, or another
358
+ # command.
359
+ # BUNDLED SHORT OPTIONS (independent review of #3816, round 6). git splits
360
+ # `-am 'WIP first'` into `-a -m`, so the real subject is `WIP first` and
361
+ # the heredoc is git's SECOND message — measured accepting the heredoc's
362
+ # subject while git recorded `WIP first`. A standalone `-m` is therefore
363
+ # not the only spelling that claims the message; any short-option cluster
364
+ # ending in `m` does.
365
+ # ATTACHED VALUES AND --message ABBREVIATIONS (independent review of
366
+ # #3816, round 7). The scan required a space or `=` after the option name,
367
+ # so two spellings git accepts matched nothing: an ATTACHED short-option
368
+ # value (`-mWIP`, which git reads as `-m WIP`) and a long-option
369
+ # abbreviation (`--mes=WIP`), the same abbreviation behaviour this file
370
+ # already models for `--cleanup`. Both were measured accepting a later
371
+ # conforming heredoc while git recorded `WIP` as the subject — confirmed
372
+ # against the raw commit object, not `git log --pretty=%s`. The short arm
373
+ # therefore drops its trailing requirement entirely: a `-` followed by
374
+ # letters ending in `m` claims the message however it is spelled. Wider
375
+ # than git's own abbreviation set on purpose — over-matching only refuses
376
+ # more, which is the recoverable direction.
377
+ # Variable-held for the same bash-3.2 reason as GLUE_CLASS above.
378
+ # AN OPTION NAME BUILT BY A COMMAND SUBSTITUTION IS UNRESOLVABLE
379
+ # (independent review of #3816, round 8). Stripping `$` above collapses the
380
+ # two dollar-QUOTE forms onto their literals, but `--clean$(printf up)=`
381
+ # is a different thing: bash RUNS a program to finish the option name, so
382
+ # the argv git receives is not derivable from this string at all. Measured
383
+ # accepting a 75-byte subject the length gate had recorded as 72.
384
+ #
385
+ # SCOPED TO THE NAME, NOT THE VALUE. The class is a `-`-leading token whose
386
+ # characters up to the substitution contain no `=` — an option NAME being
387
+ # assembled. `--author="$(git config user.name)"` and `--author "$(…)"`
388
+ # both put the substitution in the VALUE, which this file never models and
389
+ # which stays allowed; only `-…$(` before any `=` refuses. Scanned on the
390
+ # RAW windows on purpose: the dequoted copies have had their `$` removed,
391
+ # so the shape is no longer visible there.
392
+ #
393
+ # This is a SHAPE, not a segmentation: it never tries to decide where
394
+ # git's own command ends. Two attempts at that were reverted for opening
395
+ # accept-direction holes, and the reasoning above still stands.
396
+ # WIDENED, and the strategy changed with it (independent review, round 9).
397
+ # The `$(`-only spelling above was the fourth patch in a row that tried to
398
+ # EMULATE what bash does to an argument before git sees it -- round 6
399
+ # removed quotes, round 7 backslashes, round 8 the `$` of a dollar-quote,
400
+ # and each time review found another transform that had been missed. Round
401
+ # 9 found four more, all measured accepting `WIP` as the real subject on
402
+ # bash 3.2.57 and 5.3.15 while the plain spelling of the same command is
403
+ # refused:
404
+ #
405
+ # -$'\155' WIP ANSI-C octal escape decodes to `m`
406
+ # -$'\x6d' WIP ANSI-C hex escape decodes to `m`
407
+ # -`printf m` WIP command substitution, backtick spelling
408
+ # x= … -${x}m WIP parameter expansion
409
+ # -? WIP pathname expansion, with a file named `-m`
410
+ #
411
+ # The last two settle the strategy: an option name finished by a PARAMETER
412
+ # expansion depends on a variable's runtime value, and one finished by a
413
+ # PATHNAME expansion depends on the contents of the working directory.
414
+ # Neither is derivable from the command string at any level of effort, so
415
+ # emulation cannot be completed -- not "has not been completed yet".
416
+ #
417
+ # So the rule is no longer "normalise it and match the literal". It is: an
418
+ # option NAME containing a shell expansion or quoting construct is
419
+ # UNRESOLVABLE, and unresolvable refuses. One rule covers every spelling
420
+ # above, and every spelling nobody has thought of yet, in the fail-closed
421
+ # direction. The dequoting passes above are kept: they still normalise the
422
+ # deterministic removals so the guards RECOGNISE `--clean""up=` and `-\m`
423
+ # rather than merely refusing them, which keeps the existing rows honest.
424
+ #
425
+ # SCOPED TO THE NAME, NOT THE VALUE, exactly as before: the class is a
426
+ # `-`-leading token whose characters up to the substitution contain no `=`.
427
+ # `--author="$(git config user.name)"` and `--author "$(…)"` put the
428
+ # construct in the VALUE and still resolve, pinned in both directions.
429
+ # Scanned on the RAW windows, because the dequoted copies have had `$` and
430
+ # the quote characters removed and the shape is no longer visible there.
431
+ #
432
+ # The class is bracket-only and holds no backslash, per round 8: a POSIX
433
+ # bracket expression has no escape mechanism, and a backslash written
434
+ # inside one becomes a literal member on bash 3.2.
435
+ SUBST_NAME_CLASS='(^|[[:space:]])-[^[:space:]=]*[$`?*[]'
436
+ if [[ "$MSG_PREFIX" =~ $SUBST_NAME_CLASS ]] \
437
+ || [[ "$MSG_SUFFIX" =~ $SUBST_NAME_CLASS ]]; then RESOLVE=0; fi
438
+ SEP_CLASS='[;&|]'
439
+ if [[ "$MSG_PREFIX_DEQ" =~ (^|[[:space:]])(-[a-zA-Z]*m|--m[a-z]*([=[:space:]]|$)) ]] \
440
+ || [[ "$MSG_PREFIX" =~ (^|[[:space:]])--([[:space:]]|$) ]] \
441
+ || [[ "$MSG_PREFIX" =~ $SEP_CLASS ]] \
442
+ || [[ "$MSG_PREFIX" == *$'\n'* ]]; then RESOLVE=0; fi
443
+ # NEWLINE IS A COMMAND SEPARATOR TOO (independent review of #3816, round
444
+ # 7) — the test above. The separator scan covered `;`, `&` and `|` but not
445
+ # a literal newline, so a LATER command's heredoc-shaped `-m` was taken
446
+ # for this commit's message:
447
+ #
448
+ # git commit --amend --no-edit
449
+ # echo -m "$(cat <<'EOF'
450
+ # fix: conforming text unrelated to the commit
451
+ # EOF
452
+ # )"
453
+ #
454
+ # The classifier recognises the leading commit, the capture reaches across
455
+ # the newline into `echo`'s argument, and a conforming string with no
456
+ # relationship to the commit was validated and allowed. Tested as a glob
457
+ # rather than folded into the bracket class, because a literal newline
458
+ # inside a bash regex bracket expression is not portably expressible.
459
+
460
+ # CLEANUP-MODE GUARD (Codex review of #3816, round 4 — BLOCKER). The
461
+ # resolver skips leading blank lines and strips trailing whitespace
462
+ # because git's DEFAULT cleanup=whitespace does. Under
463
+ # `--cleanup=verbatim` git does neither, so a 72-char subject plus three
464
+ # trailing spaces is committed as a 75-byte subject while the hook
465
+ # measured 72 — COMMIT_SUBJECT_TOO_LONG dodged (measured base=2 -> head=0;
466
+ # confirmed by reading the raw commit object, since `git log --pretty=%s`
467
+ # strips trailing whitespace in its own output and hides it).
468
+ # Any named mode other than `whitespace` refuses. A mode set persistently
469
+ # in git config is invisible here and stays a documented residual limit.
470
+ # SCOPE (review of #3816, round 5 — BLOCKER). This scan must exclude the
471
+ # message. `--cleanup=` and `commit.cleanup=` are ordinary English inside
472
+ # a commit message — this repository's own hooks and docs discuss them
473
+ # constantly — and the heredoc BODY sits verbatim inside $CMD, so
474
+ # scanning $CMD refused to resolve any conforming message that merely
475
+ # MENTIONED the token, blocking it with CONVENTIONAL_COMMITS_VIOLATION.
476
+ # Scanning $MSG_PREFIX alone (the fix as first prescribed) would reopen
477
+ # the bypass this guard exists for: git accepts the flag on either side
478
+ # of -m, and `git commit -m "<heredoc>" --cleanup=verbatim` is caught
479
+ # today only because the scan is command-wide. PREFIX + SUFFIX keeps both
480
+ # positions covered while excluding the one span that is message text.
481
+ # The two are joined with a space so a token cannot be forged across the
482
+ # seam out of a prefix tail and a suffix head.
483
+ # KNOWN LIMIT, deliberately fail-closed (#3816, round 6). This window is
484
+ # the whole command minus the message, so a `--cleanup=` that belongs to a
485
+ # DIFFERENT command — `git commit -m "<heredoc>" && echo --cleanup=verbatim`
486
+ # — also refuses, and a conforming commit git would accept stays blocked.
487
+ # Narrowing it to git's own segment was tried and reverted: deciding where
488
+ # git's command ends needs a shell parse, and a substring scan is not one.
489
+ # Trimming at the first `;&|` cut the window short whenever a separator sat
490
+ # inside an ordinary argument — `--author "a&b"`, and equally `--author
491
+ # a\&b` — which hid a REAL trailing `--cleanup=verbatim` and ACCEPTED a
492
+ # 75-byte subject the length gate had measured as 72. Two successive
493
+ # narrowings each reopened that hole on a shape the previous one missed, so
494
+ # the scan stays wide: refusing a commit git would take is recoverable,
495
+ # accepting an over-long subject is not.
496
+ # ABBREVIATIONS (independent review of #3816, round 6). git accepts any
497
+ # unambiguous prefix of a long option, so `--cle=verbatim` sets the mode
498
+ # while matching no literal `--cleanup` — measured accepting a 75-byte
499
+ # subject recorded as 72. The class is deliberately wider than git's own
500
+ # abbreviation set: over-matching only refuses more, which is the safe
501
+ # direction, and no other `--cl` option exists for git commit.
502
+ # LAST DIRECTIVE WINS, AND ONE MATCH CANNOT SEE IT (independent review of
503
+ # #3816, round 7). A bash regex yields ONE BASH_REMATCH, so only the
504
+ # FIRST cleanup directive was inspected — and git applies the LAST one.
505
+ # `--cleanup=whitespace -m <heredoc> --cleanup=verbatim` therefore read as
506
+ # mode=whitespace, resolution stayed enabled, and a 72-character subject
507
+ # plus trailing spaces was accepted while git recorded 75 bytes with the
508
+ # whitespace preserved (confirmed against the raw commit object). Deciding
509
+ # WHICH directive is last needs an argv order this substring scan does not
510
+ # have, so multiplicity itself refuses: more than one directive is
511
+ # unresolvable, not "probably fine". Single-directive behaviour is
512
+ # unchanged.
513
+ CLEANUP_WINDOW="$MSG_PREFIX_DEQ $MSG_SUFFIX_DEQ"
514
+ # `|| true` is load-bearing: this script runs under `set -euo pipefail`,
515
+ # and grep exits 1 when it matches NOTHING — which is the common case, a
516
+ # command with no cleanup directive at all. Without it the pipeline's
517
+ # non-zero status killed the hook outright (exit 1, no verdict) for every
518
+ # ordinary commit. Caught by running the real hook rather than the scan.
519
+ CLEANUP_HITS=$( { printf '%s' "$CLEANUP_WINDOW" | grep -oE '(--cl[a-z]*|commit\.cleanup)[=[:space:]]+[^[:space:]]+' || true; } | wc -l | tr -d ' ')
520
+ if [ "${CLEANUP_HITS:-0}" -gt 1 ]; then
521
+ RESOLVE=0
522
+ elif [[ "$CLEANUP_WINDOW" =~ (--cl[a-z]*|commit\.cleanup)[=[:space:]]+([^[:space:]]+) ]]; then
523
+ if [ "${BASH_REMATCH[2]}" != "whitespace" ]; then RESOLVE=0; fi
524
+ fi
525
+
526
+ # GIT-GENERATED SUBJECTS (independent review of #3816, round 7). With
527
+ # `--squash=<commit>` or `--fixup=<commit>` git composes the subject
528
+ # itself — measured recording `squash! base: something` while a conforming
529
+ # heredoc supplied via -m sailed through. The supplied message is not the
530
+ # subject in these modes at all, so there is nothing here worth measuring
531
+ # and resolution is refused outright. Abbreviations included for the same
532
+ # reason as --cleanup's. Deliberately NOT extended to the other
533
+ # message-SOURCE options (-C/--reuse-message, -c/--reedit-message,
534
+ # -F/--file, -t/--template): `-c` is also a git GLOBAL option that legally
535
+ # precedes the subcommand, so a scan for it would refuse ordinary
536
+ # `git -c k=v commit` invocations. Those remain a disclosed gap rather
537
+ # than a guessed guard.
538
+ if [[ "$MSG_PREFIX_DEQ $MSG_SUFFIX_DEQ" =~ (^|[[:space:]])--(squash|fixup|sq[a-z]*|fix[a-z]*)[=[:space:]] ]]; then RESOLVE=0; fi
539
+ fi
540
+
541
+ if [ "$RESOLVE" = 1 ]; then
542
+ SUBJECT=$(GIT_CMD_LIB="$HOOK_DIR/lib/git-cmd.js" MSG="$MSG" node -e "
543
+ const {resolveCommitSubject}=require(process.env.GIT_CMD_LIB);
544
+ process.stdout.write(resolveCommitSubject(process.env.MSG));
545
+ " 2>/dev/null) || SUBJECT="${MSG%%$'\n'*}"
546
+ else
547
+ # Pure parameter expansion, not `echo "$MSG" | head -1`: that pipeline
548
+ # raced a SIGPIPE under `set -euo pipefail` whenever $MSG had a body
549
+ # (the common case) — `head -1` can close its read end as soon as it
550
+ # has the first line, and if `echo`'s write lands after that close,
551
+ # `echo` dies with signal 13 (exit 141), which is NOT suppressed by
552
+ # `set -e` and aborted the whole hook intermittently (observed in
553
+ # tests/hooks-opt-in.test.cjs's --fixup=HEAD "round 7" case). Zero
554
+ # subprocesses here means zero pipe/race surface. Equivalent to
555
+ # `head -1` for single-line, multi-line, and trailing-newline input.
556
+ SUBJECT="${MSG%%$'\n'*}"
557
+ fi
558
+ # Single source of truth for the accepted commit-type list (#3811): the
559
+ # 10 built-ins plus whatever passed the safe-token filter above. Both the
560
+ # regex alternation and the human-readable error text below are derived
561
+ # from this ONE array — no hand-synced second copy.
562
+ #
563
+ # The `"${EXTRA_COMMIT_TYPES[@]+"${EXTRA_COMMIT_TYPES[@]}"}"` form (not
564
+ # plain `"${EXTRA_COMMIT_TYPES[@]}"`) is required: on bash 3.2.57 (this
565
+ # repo's macOS test target), expanding `[@]` on an array that is declared
566
+ # but has zero elements throws "unbound variable" under `set -u` (which
567
+ # this script has via `set -euo pipefail`). Verified directly against
568
+ # /bin/bash 3.2.57 on macOS. The `${arr[@]+word}` form is the
569
+ # nounset-safe idiom for "expand if set, empty otherwise" on empty arrays.
570
+ COMMIT_TYPES=("${BUILTIN_COMMIT_TYPES[@]}" "${EXTRA_COMMIT_TYPES[@]+"${EXTRA_COMMIT_TYPES[@]}"}")
571
+ COMMIT_TYPE_ALT=$(IFS='|'; echo "${COMMIT_TYPES[*]}")
572
+ COMMIT_TYPE_LIST=$(printf '%s, ' "${COMMIT_TYPES[@]}")
573
+ COMMIT_TYPE_LIST="${COMMIT_TYPE_LIST%, }"
574
+ # Typed `valid_types` array (#3811 review finding): CONTRIBUTING.md bans
575
+ # substring/prose matching on `reason` in tests — a test needing to
576
+ # verify the accepted-type set must have a typed field, not grep prose.
577
+ # Safe to build with a bare printf (no JSON-escaping needed): every
578
+ # element of COMMIT_TYPES has already passed the `^[a-z][a-z0-9-]*$`
579
+ # safe-token filter (or is a literal built-in), so none can contain `"`
580
+ # or `\`.
581
+ COMMIT_TYPES_JSON=$(printf '"%s",' "${COMMIT_TYPES[@]}")
582
+ COMMIT_TYPES_JSON="[${COMMIT_TYPES_JSON%,}]"
117
583
  # Validate Conventional Commits format
118
- if ! [[ "$SUBJECT" =~ ^(feat|fix|docs|style|refactor|perf|test|build|ci|chore)(\(.+\))?:[[:space:]].+ ]]; then
119
- # Emit a typed `code` field alongside `reason` (#2974). Tests assert
120
- # on the stable code string; the reason is the human-readable copy.
121
- echo '{"decision": "block", "code": "CONVENTIONAL_COMMITS_VIOLATION", "reason": "Commit message must follow Conventional Commits: <type>(<scope>): <subject>. Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore. Subject must be <=72 chars, lowercase, imperative mood, no trailing period."}'
584
+ if ! [[ "$SUBJECT" =~ ^($COMMIT_TYPE_ALT)(\(.+\))?:[[:space:]].+ ]]; then
585
+ # Emit typed `code` and `valid_types` fields alongside `reason` (#2974,
586
+ # #3811). Tests assert on the stable code string and the typed array;
587
+ # the reason is the human-readable copy, never grepped by tests.
588
+ echo "{\"decision\": \"block\", \"code\": \"CONVENTIONAL_COMMITS_VIOLATION\", \"valid_types\": $COMMIT_TYPES_JSON, \"reason\": \"Commit message must follow Conventional Commits: <type>(<scope>): <subject>. Valid types: $COMMIT_TYPE_LIST. Subject must be <=72 chars, lowercase, imperative mood, no trailing period.\"}"
122
589
  exit 2
123
590
  fi
124
591
  if [ ${#SUBJECT} -gt 72 ]; then
@@ -366,7 +366,8 @@ process.stdin.on('end', () => {
366
366
  'This edit will not be tracked in STATE.md or produce a SUMMARY.md. ' +
367
367
  'Consider using /gsd:fast for trivial fixes or /gsd:quick for larger changes ' +
368
368
  'to maintain project state tracking. ' +
369
- 'If this is intentional (e.g., user explicitly asked for a direct edit), proceed normally.'
369
+ 'If this is intentional (e.g., user explicitly asked for a direct edit), proceed normally.',
370
+ code: 'WORKFLOW_ADVISORY'
370
371
  }
371
372
  };
372
373
 
@@ -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