@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
@@ -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
  }
package/hooks/hooks.json CHANGED
@@ -28,6 +28,12 @@
28
28
  { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-write-guard.js\"", "timeout": 5 }
29
29
  ]
30
30
  },
31
+ {
32
+ "matcher": "Read|Grep|Bash",
33
+ "hooks": [
34
+ { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-secret-read-guard.js\"", "timeout": 5 }
35
+ ]
36
+ },
31
37
  {
32
38
  "matcher": "Agent|Task",
33
39
  "hooks": [
@@ -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 };
@@ -180,4 +180,213 @@ function isGitSubcommand(cmd, sub) {
180
180
  return tokens[subIdx] === sub;
181
181
  }
182
182
 
183
- module.exports = { isGitSubcommand, tokenize, extractBranchArgument, skipToSubcommand };
183
+ /**
184
+ * Resolve a `-m` message argument to its SUBJECT — the first line of the commit
185
+ * message — resolving the command-substituted heredoc form to the heredoc
186
+ * BODY's first line.
187
+ *
188
+ * PURE STRING FUNCTION. It deliberately does NOT tokenize or walk the command
189
+ * line: selecting *which* argument is the message stays with the caller, exactly
190
+ * as before, so this cannot change which commands are validated. An earlier
191
+ * revision of this fix did walk tokens and regressed four separate cases —
192
+ * `git commit -- -m WIP` (a pathspec), `git commit --amend && echo -m WIP` (a
193
+ * later command's flag), `-m "" --allow-empty-message` (the scanner drops empty
194
+ * tokens, so the following flag became the subject), and unquoted
195
+ * `git commit -m WIP`. All were allowed upstream and would have started being
196
+ * blocked. Reported in review of #3802.
197
+ *
198
+ * The defect this DOES fix: `gsd-validate-commit.sh` captured the message with
199
+ * `-m[[:space:]]+"([^"]+)"`, and bash `[^"]` matches newlines, so the widely
200
+ * used agent-authored commit idiom
201
+ *
202
+ * git commit -m "$(cat <<'EOF'
203
+ * feat(auth): add login flow
204
+ * EOF
205
+ * )"
206
+ *
207
+ * captured the whole span up to the final quote at `)"`. Taking its first line
208
+ * yielded the literal `$(cat <<'EOF'`, which can never satisfy Conventional
209
+ * Commits, so every heredoc-form commit was blocked regardless of its message.
210
+ *
211
+ * Recognition is anchored at BOTH ends and requires a command substitution, so
212
+ * an ordinary message merely CONTAINING — or ending in — `<<WORD` is not
213
+ * mistaken for an opener. Without the `^\$\(` anchor,
214
+ * `-m "WIP notes <<EOF\nfix: smuggled subject"` resolved to the second line and
215
+ * ALLOWED a non-conforming commit: an enforcement bypass, not just a
216
+ * misclassification (review of #3802).
217
+ *
218
+ * NOT resolved, by design: an UNQUOTED delimiter (`<<EOF`). bash expands `$var`
219
+ * and `$(...)` in that body, so the literal text here is not what git receives.
220
+ * An earlier revision resolved it anyway and called the gap "the same
221
+ * pre-existing limit as expansions in a plain `-m` argument" — that framing was
222
+ * wrong on both halves: on base the whole heredoc form was blocked, so this fix
223
+ * CREATED the path, and it dodged the length gate as well as the format gate.
224
+ * See the expansion guard in the body (review of #3816, round 4).
225
+ *
226
+ * KNOWN LIMIT: the DOUBLE-QUOTED delimiter spelling (`<<"EOF"`) is resolvable
227
+ * here but unreachable through the caller — `gsd-validate-commit.sh`'s
228
+ * double-quoted `-m` capture stops at the first `"`, which in that spelling is
229
+ * the delimiter's own quote, so the resolver only ever sees a truncated opener
230
+ * and the commit stays blocked. Its single-quoted `-m` capture DOES deliver the
231
+ * spelling intact, which is why an earlier revision's claim of unreachability
232
+ * was false; the caller now gates the resolver on the double-quoted arm alone
233
+ * (review of #3816, round 4), so the claim holds again — for that reason, not
234
+ * by luck. That is the pre-fix behaviour for the whole form (fail closed, a
235
+ * false positive on one rare spelling), and widening the bash capture to span
236
+ * inner quotes would change what is captured for EVERY message containing one —
237
+ * a regression class this fix deliberately does not touch (Codex review of
238
+ * #3816). The same capture truncation blocks a `"` in the SUBJECT LINE itself,
239
+ * where it lands inside the line being measured. It does NOT block a `"` on a
240
+ * later body line: the subject is already complete before the truncation point,
241
+ * so that message resolves and is allowed (measured; an earlier revision of this
242
+ * comment and of the changeset claimed a `"` ANYWHERE blocked, which is false —
243
+ * Codex review of #3816, round 4). The truncation guard below is what keeps the
244
+ * unmeasurable half fail-closed. Two more legal-but-unrecognized spellings
245
+ * stay blocked the same fail-closed way: an env-prefixed cat
246
+ * (`$(A=1 cat <<'EOF'`) and an option-terminated cat (`$(cat -- <<'EOF'`) —
247
+ * recognizing either would mean modelling bash prefix words here, cost with
248
+ * no reported user (review of #3816, round 3).
249
+ *
250
+ * @param {string} messageArg - the raw `-m` argument, already selected by the caller
251
+ * @returns {string} the subject to validate
252
+ */
253
+ function resolveCommitSubject(messageArg) {
254
+ // CRLF-tolerant split. With a bare split('\n') every body line kept its \r,
255
+ // so `body.indexOf(delimiter)` never matched on CRLF input: the truncation
256
+ // guard was inert, an empty CRLF message resolved to 'EOF\r' instead of '',
257
+ // and a real 72-char subject measured 73 (review of #3816). Splitting on
258
+ // /\r?\n/ is the repo-wide remedy for this recurring defect class.
259
+ const lines = String(messageArg == null ? '' : messageArg).split(/\r?\n/);
260
+ // The path prefix is a PATH-CHARACTER class, not \S*: `\S*` accepted
261
+ // `id;/bin/cat`, so `$(id;/bin/cat <<'EOF' ...` was resolved to its heredoc
262
+ // body while bash actually runs `id` first and git's real subject is `id`'s
263
+ // OUTPUT — an enforcement bypass (Codex review of #3816). A prefix carrying
264
+ // any shell metacharacter now fails recognition, which falls back to the
265
+ // opener line and the format gate: fail closed, exactly the pre-fix
266
+ // behaviour for the whole form.
267
+ //
268
+ // Whitespace inside the recognition is ASCII space/tab — [ \t], never \s —
269
+ // because JavaScript \s includes Unicode whitespace bash does NOT split on:
270
+ // `$(<NBSP>/bin/cat <<'EOF'` was recognized here while bash reads
271
+ // `<NBSP>/bin/cat` as the executable NAME, so recognition claimed a
272
+ // substitution that does not run cat (Codex review of #3816, round 2). The
273
+ // same ASCII rule as the blank-line skip below, for the same reason.
274
+ // `cat[ \t]*<<`, not `+`: bash accepts `cat<<'EOF'` with no space (review of
275
+ // #3816, round 3), and recognizing it costs nothing — the token before `<<`
276
+ // is still literally `cat`.
277
+ //
278
+ // The path prefix must be ABSOLUTE (Codex review of #3816, round 4). The old
279
+ // `[\w./-]*\/` also accepted `./cat` and `../evil/cat`, so a relative
280
+ // executable that merely ENDS in `cat` was trusted to echo its stdin: with a
281
+ // planted `../evil/cat` printing `WIP injected`, the resolver validated the
282
+ // heredoc body while git's real subject was `WIP injected` (measured
283
+ // base=2 -> head=0 against a real commit). Requiring `/` up front costs
284
+ // nothing real — `/bin/cat` and a bare `cat` both still resolve.
285
+ //
286
+ // AN ABSOLUTE PATH IS NOT AN IDENTITY EITHER (independent review of #3816,
287
+ // round 8). Round 4 stopped at "must be absolute", so any absolute path
288
+ // ENDING in `/cat` was still trusted to echo its stdin — the very thing the
289
+ // round-4 reasoning rejected one spelling earlier. With an executable at
290
+ // `/some/scratch/dir/cat` printing `WIP injected`, the resolver validated
291
+ // the conforming heredoc body while git's real subject was `WIP injected`
292
+ // (measured on bash 3.2.57 and 5.3.15 against a real commit: hook exit 0,
293
+ // `git cat-file -p` subject `WIP injected`, while the same command through
294
+ // `./cat` was already refused). The prefix is now the canonical system
295
+ // locations, which is the only claim a string can support. `/usr/local/bin`
296
+ // is deliberately excluded: it is user-writable on ordinary machines, which
297
+ // is the plantable case this guard exists for.
298
+ //
299
+ // RESIDUAL, not fixable from a string: a bare `cat` shadowed earlier on PATH
300
+ // has the same effect and is indistinguishable here. It is also not a
301
+ // meaningful boundary — anyone who can plant an executable on PATH can run
302
+ // `git commit` directly — so this hook stays an authoring guard, not a
303
+ // security control.
304
+ //
305
+ // The delimiter alternatives are split so the BARE spelling is its own group:
306
+ // `\\(...)` (backslash-quoted) and `(...)` (bare) were one `\\?(...)` branch,
307
+ // which conflated the only two spellings that differ in bash. See the
308
+ // expansion guard below.
309
+ const opener = /^\$\([ \t]*(?:\/(?:usr\/)?bin\/)?cat[ \t]*<<(-?)[ \t]*(?:'([^']+)'|"([^"]+)"|\\([^\s'"();|&<>\\]+)|([^\s'"();|&<>\\]+))[ \t]*$/
310
+ .exec(lines[0]);
311
+ if (!opener) return lines[0];
312
+
313
+ // EXPANSION GUARD (review of #3816, round 4 — BLOCKER). Only `<<'D'`, `<<"D"`
314
+ // and `<<\D` suppress expansion. A BARE `<<D` is expanded by bash, so the body
315
+ // captured here is NOT the text git receives, and measuring it is an
316
+ // enforcement bypass in both gates at once:
317
+ //
318
+ // -m "$(cat <<EOF\nfeat: $UNSET_VAR\nEOF\n)" git gets `feat:` -> format gate dodged
319
+ // -m "$(cat <<EOF\nfeat: ${LONG}\nEOF\n)" git gets any length -> length gate dodged
320
+ //
321
+ // Both measured base=2 -> head=0 against the real hook. This is NOT the
322
+ // pre-existing plain-`-m` expansion limit an earlier revision claimed it was:
323
+ // on base the whole heredoc form was blocked, so no expansion inside a body
324
+ // ever reached an allow — this fix created the path, and closes it here.
325
+ // Same rule every other guard in this function follows: when the real subject
326
+ // cannot be known, fall back to the opener line, which fails the format gate.
327
+ if (opener[5]) return lines[0];
328
+
329
+ // `<<-` strips leading TABS from every body line, including the terminator.
330
+ const stripTabs = opener[1] === '-';
331
+ const delimiter = opener[2] || opener[3] || opener[4];
332
+ const body = lines.slice(1).map((l) => (stripTabs ? l.replace(/^\t+/, '') : l));
333
+
334
+ // TRUNCATION GUARD. The capture that produced this argument stops at the first
335
+ // `"`, so a message containing one arrives here missing its tail — and its
336
+ // terminator. Resolving anyway would hand the length gate a PREFIX of the real
337
+ // subject and let an over-long message through, an enforcement hole that did
338
+ // not exist before this fix (review of #3802). A body with no terminator is
339
+ // therefore not resolved at all: returning the opener line fails the format
340
+ // gate, which is exactly what this whole form did before the fix. The fix
341
+ // applies where the capture is complete and changes nothing where it is not.
342
+ const end = body.indexOf(delimiter);
343
+
344
+ // POST-TERMINATOR GUARD (review of #3816, round 3 — BLOCKER). Everything
345
+ // after the terminator is still part of the real message once bash
346
+ // substitutes: `-m "$(cat <<'EOF'\nfeat: ok\nEOF\n) <200 a's>"` expands to a
347
+ // single 200+ char subject, but discarding the tail measured 8 and DODGED
348
+ // COMMIT_SUBJECT_TOO_LONG — the same prefix-measurement class the truncation
349
+ // guard below exists for, missed on the other side of the terminator. The
350
+ // canonical idiom's tail is exactly one closing-paren line; anything else
351
+ // means the substitution is composed with more text, so fall back to the
352
+ // opener line — blocked, the pre-fix behaviour for the whole form.
353
+ if (end !== -1) {
354
+ const tail = body.slice(end + 1);
355
+ if (tail.length !== 1 || !/^[ \t]*\)[ \t]*$/.test(tail[0])) return lines[0];
356
+ }
357
+
358
+ // git's default `cleanup=whitespace` strips leading blank lines, so the subject
359
+ // is the first NON-EMPTY body line, not blindly the first one. Taking lines[1]
360
+ // returned '' for a body that starts blank and falsely blocked a conforming
361
+ // commit — the very defect class #3802 reports (review of #3802).
362
+ const scan = end === -1 ? body : body.slice(0, end);
363
+ // "Blank" is git's ASCII definition — space and tab — never JavaScript's
364
+ // trim(), whose Unicode whitespace class skips lines git KEEPS: a body whose
365
+ // first line is a NBSP resolved to the SECOND line while git's real subject
366
+ // is the NBSP line — an enforcement bypass (Codex review of #3816, verified
367
+ // against `git stripspace`, which preserves the c2a0 bytes). A Unicode-blank
368
+ // first line is now returned as the subject and fails the format gate: the
369
+ // same fail-closed direction git itself takes.
370
+ const idx = scan.findIndex((l) => !/^[ \t]*$/.test(l));
371
+ if (idx === -1) return end === -1 ? lines[0] : '';
372
+
373
+ // Truncation is only fatal to the line it lands IN. A captured line is complete
374
+ // exactly when another line follows it, because the capture kept its newline.
375
+ // So an unterminated body whose subject line is followed by more text is still
376
+ // measurable; only a subject line that runs to the end of a truncated capture
377
+ // is not, and that one falls back to the opener — which fails the format gate,
378
+ // exactly as this whole form did before the fix. Without this, the length gate
379
+ // measured a PREFIX of the real subject and let an over-long message through
380
+ // (review of #3802).
381
+ if (end === -1 && idx >= body.length - 1) return lines[0];
382
+
383
+ // git's `cleanup=whitespace` strips whitespace at BOTH ends of the line, not
384
+ // just leading blanks. Measuring the raw line rejected `feat: <66 x's>` plus
385
+ // three trailing spaces as 75 chars when git's actual subject is a conforming
386
+ // 72 — a still-blocked conforming commit, the very defect #3802 reports
387
+ // (review of #3816). Trailing only, here: leading blank-LINE handling is the
388
+ // findIndex above, and `<<-` leading-tab stripping already happened.
389
+ return scan[idx].replace(/[ \t]+$/, '');
390
+ }
391
+
392
+ module.exports = { isGitSubcommand, tokenize, extractBranchArgument, skipToSubcommand, resolveCommitSubject };
@@ -26,11 +26,29 @@
26
26
  */
27
27
 
28
28
  const INJECTION_PATTERNS = Object.freeze([
29
- /ignore\s+(all\s+)?previous\s+instructions/i,
30
- /ignore\s+(all\s+)?above\s+instructions/i,
31
- /disregard\s+(all\s+)?previous/i,
32
- /forget\s+(all\s+)?(your\s+)?instructions/i,
33
- /override\s+(system|previous)\s+(prompt|instructions)/i,
29
+ // #4016: ONE filler-tolerant imperative-override pattern. It REPLACES the
30
+ // five narrow verb patterns (ignore x2 / disregard / forget / override) that
31
+ // used to sit here: they tolerated no filler between verb and noun, so a
32
+ // planted "forget all of your ..." phrasing (measured in the wild) matched
33
+ // nothing. A single pattern rather than narrow-plus-combined because both
34
+ // consumers count one finding PER PATTERN toward the severity threshold
35
+ // (3 = HIGH, blockable under security.injection_blocking): overlapping
36
+ // patterns made one sentence count twice and pushed a two-phrasing LOW
37
+ // payload to HIGH (PR #4061 review).
38
+ // verbs ignore|disregard|forget|discard|override. `override` carries
39
+ // the old `override system|previous prompt|instructions` family;
40
+ // `discard` is the synonym seen in the same planted phrasings.
41
+ // fillers at least ONE of all|of|the|your|my|<qualifier> must sit between
42
+ // verb and noun (a lookahead, no repetition). Bare "override
43
+ // rules" / "ignore instructions" are ordinary repo prose
44
+ // (measured: 6 hits across docs and source) and stay unmatched.
45
+ // tails `disregard (all) previous` with no noun, and bare `forget` +
46
+ // `instructions`, are kept verbatim because the old narrow
47
+ // patterns accepted them; the superset is pinned by
48
+ // tests/injection-patterns-parity.security.test.cjs.
49
+ // known FP determined linter-doc prose such as "ignore (the) rules" trips
50
+ // a single-pattern LOW advisory; one pattern can never block.
51
+ /(?:ignore|disregard|forget|discard|override)\s+(?=(?:all|of|the|your|my|system|previous|prior|above|earlier)\s)(?:all\s+)?(?:of\s+)?(?:the\s+|your\s+|my\s+)?(?:(?:system|previous|prior|above|earlier)\s+)?(?:instructions|directives|prompts?|rules)|disregard\s+(?:all\s+)?previous|forget\s+instructions/i,
34
52
  /you\s+are\s+now\s+(?:a|an|the)\s+/i,
35
53
  /act\s+as\s+(?:a|an|the)\s+(?!plan|phase|wave)/i,
36
54
  /pretend\s+(?:you(?:'re| are)\s+|to\s+be\s+)/i,
@@ -42,4 +60,16 @@ const INJECTION_PATTERNS = Object.freeze([
42
60
  /<<\s*SYS\s*>>/i,
43
61
  ]);
44
62
 
45
- module.exports = { INJECTION_PATTERNS };
63
+ // Short single-line label for a matched pattern, used for both the rendered
64
+ // advisory prose and the typed `findings[].match` field in BOTH consumer
65
+ // hooks. The raw regex source is not user-facing: the #4016 superset pattern
66
+ // is ~280 characters, and gsd-prompt-guard.js echoed it verbatim into its
67
+ // advisory (PR #4061 review nit). Byte-identical to the transform
68
+ // gsd-read-injection-scanner.js carried inline since #3523, hoisted here so
69
+ // one finding renders the same way in both hooks. Both hooks already require
70
+ // this module, so this adds no new staging dependency.
71
+ function describePattern(pattern) {
72
+ return pattern.source.replace(/\\s\+/g, '-').replace(/[()\\]/g, '').substring(0, 50);
73
+ }
74
+
75
+ module.exports = { INJECTION_PATTERNS, describePattern };
@@ -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
+ };