@opengsd/gsd-core 1.9.1 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (426) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +27 -3
  5. package/agents/gsd-debug-session-manager.md +11 -0
  6. package/agents/gsd-debugger.md +12 -246
  7. package/agents/gsd-doc-synthesizer.md +2 -4
  8. package/agents/gsd-executor.md +12 -10
  9. package/agents/gsd-integration-checker.md +3 -0
  10. package/agents/gsd-mempalace-curator.md +5 -2
  11. package/agents/gsd-phase-researcher.md +20 -1
  12. package/agents/gsd-plan-checker.md +46 -0
  13. package/agents/gsd-planner.md +49 -54
  14. package/agents/gsd-roadmapper.md +21 -3
  15. package/agents/gsd-user-profiler.md +3 -0
  16. package/agents/gsd-verifier.md +26 -73
  17. package/bin/install.js +1272 -1238
  18. package/bin/lib/ui-safety-gate.cjs +2 -0
  19. package/commands/gsd/code-review.md +1 -1
  20. package/commands/gsd/execute-phase.md +1 -1
  21. package/commands/gsd/map-codebase.md +1 -1
  22. package/commands/gsd/mempalace-capture.md +2 -2
  23. package/commands/gsd/mempalace-recall.md +1 -1
  24. package/commands/gsd/new-milestone.md +2 -2
  25. package/commands/gsd/plan-phase.md +1 -1
  26. package/commands/gsd/quick.md +1 -1
  27. package/commands/gsd/review-backlog.md +2 -1
  28. package/commands/gsd/verify-work.md +1 -1
  29. package/gsd-core/bin/gsd-tools.cjs +1009 -115
  30. package/gsd-core/bin/lib/active-workstream-store.cjs +153 -12
  31. package/gsd-core/bin/lib/agent-install-check.cjs +268 -38
  32. package/gsd-core/bin/lib/api-coverage.cjs +123 -5
  33. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  34. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  35. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  36. package/gsd-core/bin/lib/audit.cjs +926 -202
  37. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  38. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  39. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  40. package/gsd-core/bin/lib/capability-registry.cjs +608 -148
  41. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  42. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  43. package/gsd-core/bin/lib/capability-validator.cjs +507 -24
  44. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  45. package/gsd-core/bin/lib/check-command-router.cjs +114 -38
  46. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  47. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  48. package/gsd-core/bin/lib/command-aliases.cjs +94 -0
  49. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  50. package/gsd-core/bin/lib/commands.cjs +665 -99
  51. package/gsd-core/bin/lib/commonjs-marker.cjs +142 -0
  52. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  53. package/gsd-core/bin/lib/config-loader.cjs +76 -0
  54. package/gsd-core/bin/lib/config.cjs +22 -2
  55. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  56. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  57. package/gsd-core/bin/lib/core-utils.cjs +217 -40
  58. package/gsd-core/bin/lib/decisions.cjs +23 -0
  59. package/gsd-core/bin/lib/docs.cjs +3 -2
  60. package/gsd-core/bin/lib/external-job.cjs +19 -4
  61. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  62. package/gsd-core/bin/lib/frontmatter.cjs +239 -32
  63. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  64. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  65. package/gsd-core/bin/lib/git-base-branch.cjs +160 -15
  66. package/gsd-core/bin/lib/graphify.cjs +142 -27
  67. package/gsd-core/bin/lib/gsd2-import.cjs +37 -5
  68. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  69. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  70. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  71. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  72. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  73. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  74. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  75. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  76. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  77. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  78. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  79. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  80. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  81. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  82. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  83. package/gsd-core/bin/lib/init.cjs +1325 -169
  84. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  85. package/gsd-core/bin/lib/install-engine.cjs +805 -264
  86. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  87. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  88. package/gsd-core/bin/lib/install-profiles.cjs +160 -57
  89. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  90. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  91. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  92. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  93. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  94. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  95. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  96. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  97. package/gsd-core/bin/lib/installer-migrations.cjs +206 -13
  98. package/gsd-core/bin/lib/io.cjs +38 -3
  99. package/gsd-core/bin/lib/markdown-sectionizer.cjs +8 -1
  100. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  101. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  102. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  103. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  104. package/gsd-core/bin/lib/milestone.cjs +821 -109
  105. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  106. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  107. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  108. package/gsd-core/bin/lib/pattern.cjs +122 -0
  109. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  110. package/gsd-core/bin/lib/phase-id.cjs +507 -36
  111. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  112. package/gsd-core/bin/lib/phase-locator.cjs +258 -58
  113. package/gsd-core/bin/lib/phase.cjs +891 -156
  114. package/gsd-core/bin/lib/plan-dependency-graph.cjs +303 -0
  115. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  116. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  117. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  118. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  119. package/gsd-core/bin/lib/planning-workspace.cjs +60 -6
  120. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  121. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  122. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  123. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  124. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +85 -0
  125. package/gsd-core/bin/lib/review-lane-descriptor.cjs +108 -0
  126. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  127. package/gsd-core/bin/lib/review-lane-runner.cjs +447 -68
  128. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  129. package/gsd-core/bin/lib/roadmap-command-router.cjs +76 -9
  130. package/gsd-core/bin/lib/roadmap-parser.cjs +1035 -194
  131. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  132. package/gsd-core/bin/lib/roadmap.cjs +405 -84
  133. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +795 -100
  134. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  135. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +440 -57
  136. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  137. package/gsd-core/bin/lib/runtime-homes.cjs +220 -41
  138. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +220 -44
  139. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  140. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  141. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  142. package/gsd-core/bin/lib/security.cjs +104 -5
  143. package/gsd-core/bin/lib/shell-command-projection.cjs +388 -30
  144. package/gsd-core/bin/lib/smart-entry.cjs +154 -22
  145. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  146. package/gsd-core/bin/lib/state-document.cjs +152 -8
  147. package/gsd-core/bin/lib/state-transition.cjs +424 -105
  148. package/gsd-core/bin/lib/state.cjs +1927 -401
  149. package/gsd-core/bin/lib/surface.cjs +35 -10
  150. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  151. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  152. package/gsd-core/bin/lib/uat-predicate.cjs +20 -4
  153. package/gsd-core/bin/lib/uat.cjs +706 -64
  154. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  155. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  156. package/gsd-core/bin/lib/unusable-input.cjs +33 -0
  157. package/gsd-core/bin/lib/update-context.cjs +8 -2
  158. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  159. package/gsd-core/bin/lib/validate.cjs +20 -6
  160. package/gsd-core/bin/lib/vendor/README.md +37 -0
  161. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  162. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  163. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  164. package/gsd-core/bin/lib/verification.cjs +287 -20
  165. package/gsd-core/bin/lib/verify.cjs +368 -880
  166. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  167. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +203 -19
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +576 -31
  169. package/gsd-core/bin/lib/workstream.cjs +8 -2
  170. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  171. package/gsd-core/bin/lib/worktree-safety.cjs +450 -125
  172. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  173. package/gsd-core/bin/shared/config-schema.manifest.json +9 -1
  174. package/gsd-core/references/agent-contracts.md +43 -26
  175. package/gsd-core/references/artifact-types.md +10 -3
  176. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  177. package/gsd-core/references/checkpoints.md +2 -2
  178. package/gsd-core/references/context-budget.md +1 -1
  179. package/gsd-core/references/debugger-techniques.md +255 -0
  180. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  181. package/gsd-core/references/doc-conflict-engine.md +1 -1
  182. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  184. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  185. package/gsd-core/references/execute-phase-response-language.md +1 -1
  186. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  187. package/gsd-core/references/gate-prompts.md +1 -1
  188. package/gsd-core/references/git-planning-commit.md +2 -1
  189. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  190. package/gsd-core/references/model-profiles.md +12 -4
  191. package/gsd-core/references/mvp-concepts.md +9 -9
  192. package/gsd-core/references/planner-guidance.md +3 -9
  193. package/gsd-core/references/planner-preconditions.md +1 -1
  194. package/gsd-core/references/planner-reviews.md +1 -1
  195. package/gsd-core/references/planning-config.md +8 -6
  196. package/gsd-core/references/research-documentation-lookup.md +5 -3
  197. package/gsd-core/references/revision-loop.md +1 -1
  198. package/gsd-core/references/specless-probe-fallback.md +8 -7
  199. package/gsd-core/references/universal-anti-patterns.md +3 -3
  200. package/gsd-core/references/verifier-phase-gates.md +192 -0
  201. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  202. package/gsd-core/references/verify-mvp-mode.md +1 -1
  203. package/gsd-core/references/workstream-flag.md +22 -6
  204. package/gsd-core/references/worktree-branch-check.md +2 -2
  205. package/gsd-core/templates/discussion-log.md +1 -1
  206. package/gsd-core/templates/phase-prompt.md +2 -4
  207. package/gsd-core/templates/state.md +4 -4
  208. package/gsd-core/templates/summary-complex.md +2 -0
  209. package/gsd-core/templates/summary-minimal.md +2 -0
  210. package/gsd-core/templates/summary-standard.md +2 -0
  211. package/gsd-core/templates/summary.md +2 -0
  212. package/gsd-core/templates/verification-report.md +9 -1
  213. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  214. package/gsd-core/workflows/audit-milestone.md +3 -0
  215. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  216. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  217. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  218. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  219. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  220. package/gsd-core/workflows/autonomous.md +33 -70
  221. package/gsd-core/workflows/cleanup.md +62 -3
  222. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  223. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +93 -0
  224. package/gsd-core/workflows/code-review-fix.md +37 -10
  225. package/gsd-core/workflows/code-review.md +74 -166
  226. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  227. package/gsd-core/workflows/complete-milestone.md +160 -95
  228. package/gsd-core/workflows/debug.md +16 -17
  229. package/gsd-core/workflows/diagnose-issues.md +56 -8
  230. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  231. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  232. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  233. package/gsd-core/workflows/discuss-phase-assumptions.md +7 -17
  234. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  235. package/gsd-core/workflows/docs-update.md +8 -51
  236. package/gsd-core/workflows/edit-phase.md +26 -1
  237. package/gsd-core/workflows/eval-review.md +3 -5
  238. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +64 -7
  239. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  240. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  241. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  242. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +21 -0
  243. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  244. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  245. package/gsd-core/workflows/execute-phase.md +103 -187
  246. package/gsd-core/workflows/execute-plan.md +36 -4
  247. package/gsd-core/workflows/explore.md +131 -4
  248. package/gsd-core/workflows/fast.md +10 -2
  249. package/gsd-core/workflows/health.md +73 -4
  250. package/gsd-core/workflows/help/modes/full.md +6 -1
  251. package/gsd-core/workflows/import.md +4 -4
  252. package/gsd-core/workflows/ingest-docs.md +7 -6
  253. package/gsd-core/workflows/mvp-phase.md +6 -3
  254. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  255. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  256. package/gsd-core/workflows/new-milestone.md +35 -47
  257. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  258. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  259. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  260. package/gsd-core/workflows/new-project.md +27 -240
  261. package/gsd-core/workflows/next.md +12 -0
  262. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  263. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  264. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  265. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  266. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  267. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  268. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  269. package/gsd-core/workflows/plan-phase.md +89 -209
  270. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  271. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  272. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  273. package/gsd-core/workflows/progress.md +45 -159
  274. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  275. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  276. package/gsd-core/workflows/quick/steps/quick-verification.md +67 -0
  277. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  278. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  279. package/gsd-core/workflows/quick.md +55 -405
  280. package/gsd-core/workflows/resume-project.md +3 -0
  281. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  282. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  283. package/gsd-core/workflows/review.md +41 -13
  284. package/gsd-core/workflows/section-manifest.json +219 -0
  285. package/gsd-core/workflows/secure-phase.md +1 -1
  286. package/gsd-core/workflows/session-report.md +2 -1
  287. package/gsd-core/workflows/settings.md +66 -2
  288. package/gsd-core/workflows/ship.md +104 -44
  289. package/gsd-core/workflows/sketch.md +1 -1
  290. package/gsd-core/workflows/spec-phase.md +41 -20
  291. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  292. package/gsd-core/workflows/spike.md +50 -16
  293. package/gsd-core/workflows/sync-skills.md +106 -13
  294. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  295. package/gsd-core/workflows/transition.md +53 -31
  296. package/gsd-core/workflows/ui-phase.md +13 -12
  297. package/gsd-core/workflows/ui-review.md +2 -2
  298. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  299. package/gsd-core/workflows/update.md +19 -8
  300. package/gsd-core/workflows/validate-phase.md +1 -1
  301. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  302. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  303. package/gsd-core/workflows/verify-work.md +17 -65
  304. package/hooks/dist/gsd-agent-isolation-guard.js +517 -0
  305. package/hooks/dist/gsd-check-update-worker.js +64 -12
  306. package/hooks/dist/gsd-check-update.js +19 -1
  307. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  308. package/hooks/dist/gsd-cursor-subagent-start.js +607 -26
  309. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  310. package/hooks/dist/gsd-prompt-guard.js +21 -20
  311. package/hooks/dist/gsd-read-injection-scanner.js +45 -24
  312. package/hooks/dist/gsd-statusline.js +90 -6
  313. package/hooks/dist/gsd-update-banner.js +22 -1
  314. package/hooks/dist/gsd-workflow-guard.js +134 -36
  315. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  316. package/hooks/dist/gsd-write-guard.js +359 -0
  317. package/hooks/dist/lib/git-cmd.js +92 -59
  318. package/hooks/dist/lib/injection-patterns.js +45 -0
  319. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  320. package/hooks/dist/lib/isolation-sentinel.js +277 -0
  321. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  322. package/hooks/gsd-agent-isolation-guard.js +517 -0
  323. package/hooks/gsd-check-update-worker.js +64 -12
  324. package/hooks/gsd-check-update.js +19 -1
  325. package/hooks/gsd-cursor-pre-tool.js +0 -3
  326. package/hooks/gsd-cursor-subagent-start.js +607 -26
  327. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  328. package/hooks/gsd-prompt-guard.js +21 -20
  329. package/hooks/gsd-read-injection-scanner.js +45 -24
  330. package/hooks/gsd-statusline.js +90 -6
  331. package/hooks/gsd-update-banner.js +22 -1
  332. package/hooks/gsd-workflow-guard.js +134 -36
  333. package/hooks/gsd-worktree-path-guard.js +2 -1
  334. package/hooks/gsd-write-guard.js +359 -0
  335. package/hooks/hooks.json +12 -0
  336. package/hooks/lib/git-cmd.js +92 -59
  337. package/hooks/lib/injection-patterns.js +45 -0
  338. package/hooks/lib/isolation-deny-reason.js +39 -0
  339. package/hooks/lib/isolation-sentinel.js +277 -0
  340. package/hooks/managed-hooks-registry.cjs +2 -0
  341. package/package.json +31 -10
  342. package/pi/gsd.cjs +71 -12
  343. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  344. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  345. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  346. package/scripts/build-hooks.js +9 -0
  347. package/scripts/changeset/lint.cjs +68 -6
  348. package/scripts/changeset/serialize.cjs +5 -1
  349. package/scripts/check-alias-drift.cjs +7 -43
  350. package/scripts/check-contract-drift.cjs +297 -0
  351. package/scripts/ci-test-scope.cjs +19 -2
  352. package/scripts/command-contract-helpers.cjs +903 -1
  353. package/scripts/gen-adr-index.cjs +728 -38
  354. package/scripts/gen-capability-matrix.cjs +1 -1
  355. package/scripts/gen-capability-registry.cjs +3 -15
  356. package/scripts/gen-context-index.cjs +439 -0
  357. package/scripts/gen-health-docs.cjs +390 -0
  358. package/scripts/gen-inventory-manifest.cjs +150 -4
  359. package/scripts/gen-loop-host-contract.cjs +4 -24
  360. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  361. package/scripts/gen-registry.cjs +3 -14
  362. package/scripts/gen-section-manifest.cjs +638 -0
  363. package/scripts/generate-package-identity.cjs +4 -2
  364. package/scripts/lib/alias-drift-families.cjs +46 -0
  365. package/scripts/lib/drift-scan.cjs +278 -0
  366. package/scripts/lint-allow-test-rule-refs.allowlist.json +15 -54
  367. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  368. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  369. package/scripts/lint-canary-version-leak.cjs +73 -0
  370. package/scripts/lint-command-contract.cjs +96 -13
  371. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  372. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  373. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  374. package/scripts/lint-default-flip-documentation.cjs +193 -0
  375. package/scripts/lint-docs-command-form.cjs +195 -0
  376. package/scripts/lint-docs-required.cjs +9 -1
  377. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  378. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  379. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  380. package/scripts/lint-example-parser-parity.cjs +395 -0
  381. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  382. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  383. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  384. package/scripts/lint-milestone-window-drift.cjs +468 -0
  385. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  386. package/scripts/lint-plan-count-drift.cjs +318 -0
  387. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  388. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  389. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  390. package/scripts/lint-regression-test-names.cjs +15 -13
  391. package/scripts/lint-removed-but-needed.cjs +320 -0
  392. package/scripts/lint-state-field-drift.cjs +805 -0
  393. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  394. package/scripts/lint-test-file-count.allowlist.json +40 -3
  395. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  396. package/scripts/lint-vendored-deps.cjs +124 -0
  397. package/scripts/mutation-matrix.cjs +13 -0
  398. package/scripts/pr-changed-files.cjs +63 -0
  399. package/scripts/pr-template-policy.cjs +14 -4
  400. package/scripts/prompt-injection-scan.sh +52 -6
  401. package/scripts/require-issue-link-policy.cjs +192 -0
  402. package/scripts/state-write-path-drift-baseline.json +19 -0
  403. package/scripts/sync-runtime-launcher.cjs +2 -4
  404. package/skills/gsd-autonomous/SKILL.md +0 -1
  405. package/skills/gsd-code-review/SKILL.md +1 -1
  406. package/skills/gsd-execute-phase/SKILL.md +1 -2
  407. package/skills/gsd-map-codebase/SKILL.md +1 -1
  408. package/skills/gsd-mempalace-capture/SKILL.md +2 -2
  409. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  410. package/skills/gsd-new-milestone/SKILL.md +2 -2
  411. package/skills/gsd-next/SKILL.md +0 -1
  412. package/skills/gsd-plan-phase/SKILL.md +1 -2
  413. package/skills/gsd-progress/SKILL.md +0 -1
  414. package/skills/gsd-quick/SKILL.md +1 -1
  415. package/skills/gsd-review-backlog/SKILL.md +2 -1
  416. package/skills/gsd-stats/SKILL.md +0 -1
  417. package/skills/gsd-verify-work/SKILL.md +1 -1
  418. package/vscode/package.json +1 -1
  419. package/gsd-core/workflows/discovery-phase.md +0 -298
  420. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  421. package/gsd-core/workflows/verify-phase.md +0 -577
  422. package/scripts/affected-tests-lib.cjs +0 -554
  423. package/scripts/gen-emitted-baseline.cjs +0 -145
  424. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  425. package/scripts/run-affected-tests.cjs +0 -7
  426. package/scripts/run-tests.cjs +0 -1050
@@ -0,0 +1,359 @@
1
+ #!/usr/bin/env node
2
+ // gsd-hook-version: {{GSD_VERSION}}
3
+ // GSD Write Guard — PreToolUse hook
4
+ // Blocks a whole-file Write that catastrophically shrinks a curated .planning/
5
+ // artifact (ROADMAP.md, milestone roadmaps, STATE.md).
6
+ //
7
+ // Problem (#973, fix 3 of 3): a planner read a ~16-line window of ROADMAP.md
8
+ // and Write-overwrote the whole 292-line file with it — three milestones of
9
+ // committed history destroyed. Fixes 1 and 2 (PR #989) are instructions to a
10
+ // model: they lower the probability of a clobber but cannot prevent one, and
11
+ // they protect only the agents that were audited. This hook is enforced by
12
+ // code rather than by instruction: it compares the pending Write payload
13
+ // against the file on disk and hard-blocks a catastrophic shrink BEFORE it
14
+ // happens. An advisory will not do — #973 records an agent reading the
15
+ // advisory, classifying it as non-binding, and reasoning past it while
16
+ // holding a false model of what Write does.
17
+ //
18
+ // The guarantee is bounded, and the bound is worth stating where the code
19
+ // lives: this stops accidental and single-shot collapse, not a determined
20
+ // agent. The sentinel hatch below is a plain file, so an agent that would
21
+ // reason past an advisory can arm one with a single Bash call it is already
22
+ // permitted to make. What ships is the conversion of "ignore a sentence" into
23
+ // "take one deliberate, path-bound, single-use, auditable action" — a real
24
+ // improvement against the confused-agent threat #973 records, not a defense
25
+ // against an evader.
26
+ //
27
+ // Deliberately narrow trigger:
28
+ // - Write only (Edit/MultiEdit are scoped by construction);
29
+ // - the target already exists on disk;
30
+ // - the target is a curated .planning/ artifact — the project ROADMAP.md,
31
+ // milestone roadmaps (.planning/milestones/*-ROADMAP.md), and STATE.md.
32
+ // NOT arbitrary markdown: free-prose docs get legitimately rewritten
33
+ // wholesale, and a guard that fires on those trains override-fatigue
34
+ // until nobody reads it.
35
+ //
36
+ // Threshold: block when the pending payload carries fewer than SHRINK_RATIO
37
+ // (40%) of the on-disk line count. The docs-update fix-loop's 90% bar is far
38
+ // too permissive for a curated artifact — the #973 incident was a ~94.5%
39
+ // collapse and clears a 90% bar only barely. The same ~40%/floor-40 tuning
40
+ // has run clean (no false positives) as a commit-time twin downstream.
41
+ //
42
+ // Floor: files under FLOOR_LINES are exempt, so a 10 → 2 line stub never
43
+ // trips the ratio check.
44
+ //
45
+ // Escape hatches — both named in the block message; a guard whose bypass is
46
+ // undocumented gets bypassed with the blunt instrument instead, with every
47
+ // other guard disabled at the same time:
48
+ // - GSD_ALLOW_PLANNING_SHRINK=1 (env) — for a human running interactively,
49
+ // where the variable can actually reach the hook's environment.
50
+ // - .planning/.gsd-allow-shrink (single-use sentinel file) — for workflow
51
+ // steps. A PreToolUse hook inherits the RUNTIME's environment, so a
52
+ // per-step env prefix can never reach it (#2255 round 5 M1); the sentinel
53
+ // is a transport that code consults, not prose an agent obeys. The step
54
+ // writes the target's path into the sentinel; at the block point the
55
+ // guard checks it is fresh (15 min) and names the pending target, then
56
+ // CONSUMES it and allows that one write. Path-bound + single-use +
57
+ // freshness is what keeps it from becoming a standing unlock left on disk.
58
+ //
59
+ // Known design limits (out of #2255's scope by review, disclosed here AND in
60
+ // the changeset + USER-GUIDE — round 9 required the user-facing docs to match):
61
+ // - Stateless per-write: sequential shrinks (292→120→50) each clear the 40%
62
+ // floor against CURRENT disk state, so cumulative erosion is invisible.
63
+ // - Unconditionally case-insensitive matching (required on the
64
+ // case-insensitive filesystems macOS/Windows default to): on
65
+ // case-sensitive Linux a genuinely distinct '.planning/roadmap.md' is
66
+ // also treated as curated. Narrow, accepted cost.
67
+ // (A third limit — a symlinked path into a curated file escaping the lexical
68
+ // match — was closed in round 9: the target is realpath-resolved before the
69
+ // curated match.)
70
+ //
71
+ // Triggers on: Write tool calls
72
+ // Action: BLOCK (decision: 'block', exit 2) on catastrophic shrink of a curated file
73
+ // No-op: other tools, new files, non-curated paths, sub-floor files, override set,
74
+ // hook errors (silent fail)
75
+
76
+ const fs = require('fs');
77
+ const path = require('path');
78
+
79
+ // Block when the pending payload has fewer than this fraction of the on-disk
80
+ // line count (0.4 → a Write shrinking a file below 40% of its current size).
81
+ const SHRINK_RATIO = 0.4;
82
+
83
+ // Files with fewer lines than this are exempt — small stubs get legitimately
84
+ // rewritten far below any ratio.
85
+ const FLOOR_LINES = 40;
86
+
87
+ // Curated .planning/ artifacts, matched against the resolved target path with
88
+ // separators normalized to '/'. Deliberately a closed set (see header).
89
+ // Case-insensitive: on the case-insensitive filesystems macOS and Windows
90
+ // default to, a differently-cased path is the SAME real file — a Write to
91
+ // '.planning/roadmap.md' clobbers ROADMAP.md while a case-sensitive match
92
+ // waves it through.
93
+ const CURATED_PATTERNS = [
94
+ /(?:^|\/)\.planning\/ROADMAP\.md$/i,
95
+ /(?:^|\/)\.planning\/STATE\.md$/i,
96
+ /(?:^|\/)\.planning\/milestones\/[^/]+-ROADMAP\.md$/i,
97
+ ];
98
+
99
+ // Count logical lines, ignoring a single trailing newline so that
100
+ // "a\nb\n" and "a\nb" both count as 2.
101
+ function countLines(text) {
102
+ if (!text) return 0;
103
+ const lines = text.split('\n');
104
+ if (lines[lines.length - 1] === '') lines.pop();
105
+ return lines.length;
106
+ }
107
+
108
+ function isOverrideSet() {
109
+ const v = process.env.GSD_ALLOW_PLANNING_SHRINK;
110
+ return typeof v === 'string' && v !== '' && v !== '0' && v.toLowerCase() !== 'false';
111
+ }
112
+
113
+ // Single-use sentinel (see header). Consulted ONLY at the shrink-block point —
114
+ // a write that would pass anyway never burns the token, so first-shrink-wins
115
+ // for the write the workflow armed it for.
116
+ const SENTINEL_NAME = '.gsd-allow-shrink';
117
+ const SENTINEL_REL = '.planning/' + SENTINEL_NAME;
118
+ const SENTINEL_TTL_MS = 15 * 60 * 1000;
119
+
120
+ function consumeSentinelFor(filePath, normalized) {
121
+ try {
122
+ // The curated match guarantees the target lives under a .planning/ dir;
123
+ // normalized is filePath with separators flipped, so offsets line up.
124
+ const m = normalized.match(/^(.*\/\.planning)\//i);
125
+ if (!m) return false;
126
+ const planningDir = filePath.slice(0, m[1].length);
127
+ const sentinelPath = path.join(planningDir, SENTINEL_NAME);
128
+ let st;
129
+ try {
130
+ st = fs.statSync(sentinelPath);
131
+ } catch {
132
+ return false; // not armed
133
+ }
134
+ if (Date.now() - st.mtimeMs > SENTINEL_TTL_MS) {
135
+ // A stale token is a leftover, not an authorization — housekeep it.
136
+ try { fs.unlinkSync(sentinelPath); } catch { /* best-effort */ }
137
+ return false;
138
+ }
139
+ const token = fs.readFileSync(sentinelPath, 'utf8').split('\n')[0].trim();
140
+ if (!token) return false;
141
+ // Path-bound: the token names exactly one file, resolved against the
142
+ // .planning/ dir's parent (repo root) — same case-insensitive stance as
143
+ // the curated match itself.
144
+ const namedNorm = path.resolve(path.join(planningDir, '..'), token).replace(/\\/g, '/').toLowerCase();
145
+ if (namedNorm !== normalized.toLowerCase()) {
146
+ return false; // armed for a different file — leave it for that write
147
+ }
148
+ // Consume BEFORE allowing: even if the Write then fails, the safe
149
+ // direction is a spent token, never a lingering one.
150
+ fs.unlinkSync(sentinelPath);
151
+ return true;
152
+ } catch {
153
+ // Any sentinel-machinery error means "not exempt" — the guard's normal
154
+ // (blocking) flow proceeds; the hatch may never fail a guard open.
155
+ return false;
156
+ }
157
+ }
158
+
159
+ // m2 (round 5): the block emission must itself be exception-safe. An EPIPE
160
+ // from writeSync inside the outer try would land in the fail-OPEN catch —
161
+ // the one outcome the fail-closed branches exist to prevent. The decision
162
+ // stands regardless of whether the payload could be delivered.
163
+ function emitBlock(output) {
164
+ try {
165
+ // writeSync: pipe writes via process.stdout/stderr are async on Windows
166
+ // and process.exit() does not flush them — a truncated block payload is
167
+ // a guard that silently half-fired.
168
+ fs.writeSync(1, JSON.stringify(output));
169
+ // Kimi feeds stderr (not stdout) back to the model on exit 2.
170
+ fs.writeSync(2, output.reason);
171
+ } catch {
172
+ // Emission failed; the block still stands.
173
+ }
174
+ process.exit(2);
175
+ }
176
+
177
+ // #2304: Kimi's native hook bus delivers Kimi's tool vocabulary in the payload
178
+ // (Write → WriteFile, Edit/MultiEdit → StrReplaceFile) while the [[hooks]]
179
+ // matcher is registered pre-translated (runtime-hooks-surface.cts
180
+ // buildKimiHooksTomlBlock) — so without normalizing the payload too, the
181
+ // matcher fires but the tool_name check below exits 0 and the guard is dormant
182
+ // on Kimi. The tool_input field names differ as well (kimi-cli
183
+ // src/kimi_cli/tools/file/write.py): WriteFile takes `path`/`content`, and
184
+ // kimi-cli's hooks/events.py forwards tool_input verbatim, so both layers need
185
+ // mapping. Only WriteFile is mapped: this guard exits 0 for any tool but
186
+ // Write, so an Edit-class mapping here would be dead code. Accepts bare and
187
+ // module-qualified ('kimi_cli.tools.file:WriteFile') names; unknown names fall
188
+ // through untouched. Inlined per guard (not hooks/lib/): hook scripts are
189
+ // staged as standalone files, and a sibling require is a staging dependency
190
+ // that can fail silently.
191
+ // A Map, not an object literal: bare bracket lookup resolves prototype keys
192
+ // ('constructor', '__proto__', 'toString') to truthy functions/objects, so the
193
+ // !mapped fall-through never fires for them; Map.get returns undefined (same
194
+ // shape as canonicalizeRuntimeName in src/runtime-name-policy.cts).
195
+ const KIMI_TOOL_NAMES = new Map([['WriteFile', 'Write']]);
196
+ function normalizeKimiPayload(data) {
197
+ // #2595: total over everything JSON can express — JSON.parse('null') is
198
+ // null, and reading .tool_name off a primitive would throw into the outer
199
+ // fail-open catch. A null payload has nothing to guard; pass it through
200
+ // deliberately rather than by crash.
201
+ if (data === null || typeof data !== 'object') return data;
202
+ const raw = data.tool_name;
203
+ if (typeof raw !== 'string') return data;
204
+ const mapped = KIMI_TOOL_NAMES.get(raw.slice(raw.lastIndexOf(':') + 1));
205
+ if (!mapped) return data;
206
+ data.tool_name = mapped;
207
+ const input = data.tool_input;
208
+ if (input && typeof input === 'object') {
209
+ // #2595 (review): Kimi's `path` is AUTHORITATIVE — it must win outright,
210
+ // not merely fill in when `file_path` happens to be absent. kimi-cli's
211
+ // WriteFile schema carries no `file_path` at all (src/kimi_cli/tools/
212
+ // file/write.py), so a `file_path` in a Kimi payload is ALWAYS
213
+ // model-supplied; under the old `=== undefined` condition a payload
214
+ // pairing a curated `path` with a spurious `file_path: ""` left this
215
+ // guard reading '' and exiting 0 while kimi-cli wrote to `path` — a
216
+ // one-key bypass needing no crash. Overwriting can only narrow what the
217
+ // guard inspects to the path that will actually be written.
218
+ if (typeof input.path === 'string') {
219
+ input.file_path = input.path;
220
+ }
221
+ }
222
+ return data;
223
+ }
224
+
225
+ let input = '';
226
+ const stdinTimeout = setTimeout(() => process.exit(0), 3000);
227
+ process.stdin.setEncoding('utf8');
228
+ process.stdin.on('data', chunk => input += chunk);
229
+ process.stdin.on('end', () => {
230
+ clearTimeout(stdinTimeout);
231
+ try {
232
+ const data = normalizeKimiPayload(JSON.parse(input));
233
+
234
+ // A null/primitive payload has nothing to guard — exit deliberately
235
+ // rather than throwing into the fail-open catch below (#2595 class).
236
+ if (data === null || typeof data !== 'object') {
237
+ process.exit(0);
238
+ }
239
+
240
+ // Only whole-file Write is catastrophic-by-construction; Edit/MultiEdit
241
+ // replace bounded spans and are out of scope by design (#2255).
242
+ if (data.tool_name !== 'Write') {
243
+ process.exit(0);
244
+ }
245
+
246
+ if (isOverrideSet()) {
247
+ process.exit(0); // documented escape hatch — legitimate reset in progress
248
+ }
249
+
250
+ // Typed read (#2547 class): `[]`/`{}` are truthy, pass a `!value`
251
+ // early-out, then throw inside path.resolve() — crash-to-allow via the
252
+ // outer catch. A non-string path field degrades to '' and exits here.
253
+ const rawInput = data.tool_input;
254
+ const rawFilePath = typeof rawInput?.file_path === 'string' ? rawInput.file_path : '';
255
+ const content = rawInput?.content;
256
+ if (!rawFilePath || typeof content !== 'string') {
257
+ process.exit(0);
258
+ }
259
+
260
+ // Resolve relative paths against the session cwd (the same base the
261
+ // runtime uses), then normalize separators for the curated match.
262
+ const cwd = data.cwd || process.cwd();
263
+ let filePath = path.resolve(cwd, rawFilePath);
264
+ // Resolve symlinks before the curated match (round 9, Minor 1): a Write
265
+ // to a non-curated path that symlinks into a curated file was not
266
+ // matched, while writeFileSync follows the link and clobbers the real
267
+ // target. ENOENT (new file) keeps the lexical resolution; any other
268
+ // realpath error also keeps it, and the read below then fails closed.
269
+ try {
270
+ filePath = fs.realpathSync(filePath);
271
+ } catch { /* keep the lexical path */ }
272
+ const normalized = filePath.replace(/\\/g, '/');
273
+
274
+ if (!CURATED_PATTERNS.some(re => re.test(normalized))) {
275
+ process.exit(0); // not a curated planning artifact
276
+ }
277
+
278
+ // Only guard overwrites — creating a curated file fresh is fine.
279
+ // ENOENT alone fails open (no baseline to protect); any OTHER read error
280
+ // (EACCES, EISDIR, ELOOP, EMFILE, a Windows lock) fails CLOSED — a guard
281
+ // that waves a curated Write through on a transient read error is not
282
+ // enforced by code at all, it is a race away from #973.
283
+ let onDisk;
284
+ try {
285
+ onDisk = fs.readFileSync(filePath, 'utf8');
286
+ } catch (err) {
287
+ if (err && err.code === 'ENOENT') {
288
+ process.exit(0); // does not exist — new-file Write, nothing to clobber
289
+ }
290
+ emitBlock({
291
+ decision: 'block',
292
+ readError: err && err.code ? String(err.code) : 'UNKNOWN',
293
+ overrideEnvVar: 'GSD_ALLOW_PLANNING_SHRINK',
294
+ overrideSentinel: SENTINEL_REL,
295
+ reason:
296
+ `Write guard: could not read '${filePath}' to compare against the pending ` +
297
+ `Write (${err && err.code ? err.code : 'unknown read error'}). ` +
298
+ `'${path.basename(filePath)}' is a curated planning artifact, so this guard ` +
299
+ `fails closed rather than risk a blind overwrite. Retry once the file is ` +
300
+ `readable, or — if this overwrite is intentional — re-run with the ` +
301
+ `environment variable GSD_ALLOW_PLANNING_SHRINK=1 to bypass this guard once.`,
302
+ });
303
+ }
304
+
305
+ const oldLines = countLines(onDisk);
306
+ const newLines = countLines(content);
307
+
308
+ if (oldLines < FLOOR_LINES) {
309
+ process.exit(0); // sub-floor stub — ratio checks are meaningless here
310
+ }
311
+
312
+ if (newLines >= oldLines * SHRINK_RATIO) {
313
+ process.exit(0); // shrink (if any) is within tolerance
314
+ }
315
+
316
+ // The mechanical hatch for workflow steps (see header): consulted only
317
+ // here, at the block point, so a within-tolerance write never burns it.
318
+ if (consumeSentinelFor(filePath, normalized)) {
319
+ process.exit(0); // armed for exactly this file, fresh, now consumed
320
+ }
321
+
322
+ const pct = Math.round((newLines / oldLines) * 100);
323
+ // Typed fields (oldLines/newLines/overrideEnvVar/overrideSentinel) ride
324
+ // alongside the free-form reason so consumers — including this repo's
325
+ // tests — never have to regex the prose (CONTRIBUTING.md: no raw text
326
+ // matching).
327
+ emitBlock({
328
+ decision: 'block',
329
+ oldLines,
330
+ newLines,
331
+ overrideEnvVar: 'GSD_ALLOW_PLANNING_SHRINK',
332
+ overrideSentinel: SENTINEL_REL,
333
+ // Round 9 Major 2: the denial deliberately does NOT explain how to arm
334
+ // the sentinel — #973 was an agent reasoning past an advisory, and a
335
+ // block message that prints the bypass recipe hands that same agent a
336
+ // mechanical self-authorization at the moment it is blocked. The
337
+ // sentinel transport stays documented where humans and the workflow
338
+ // engine read (USER-GUIDE, complete-milestone.md); the typed
339
+ // overrideSentinel field above stays for the binding tests. The env
340
+ // var stays named per #2255's acceptance criterion ("the override must
341
+ // be real and its name must appear in the block message") — it cannot
342
+ // reach a hook from a per-step prefix, so naming it does not hand the
343
+ // blocked agent a same-tool bypass.
344
+ reason:
345
+ `Write guard: this Write would shrink '${filePath}' from ${oldLines} lines to ` +
346
+ `${newLines} (${pct}% of current). '${path.basename(filePath)}' is a curated planning ` +
347
+ `artifact; a whole-file Write this much smaller usually means the payload was built ` +
348
+ `from a partial read of the file and would destroy the sections outside that window ` +
349
+ `(#973: a planner collapsed ROADMAP.md 292 → 16 lines this way). To fix: use Edit for ` +
350
+ `a scoped change, or Read the full file and include every section in the Write. ` +
351
+ `Intentional milestone resets go through the workflow's documented escape hatch; ` +
352
+ `interactively, re-run with the environment variable GSD_ALLOW_PLANNING_SHRINK=1 ` +
353
+ `to bypass this guard once.`,
354
+ });
355
+ } catch {
356
+ // Silent fail — never block valid tool calls due to hook errors
357
+ process.exit(0);
358
+ }
359
+ });
package/hooks/hooks.json CHANGED
@@ -21,6 +21,18 @@
21
21
  "hooks": [
22
22
  { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-worktree-path-guard.js\"", "timeout": 5 }
23
23
  ]
24
+ },
25
+ {
26
+ "matcher": "Write",
27
+ "hooks": [
28
+ { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-write-guard.js\"", "timeout": 5 }
29
+ ]
30
+ },
31
+ {
32
+ "matcher": "Agent|Task",
33
+ "hooks": [
34
+ { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-agent-isolation-guard.js\"", "timeout": 5 }
35
+ ]
24
36
  }
25
37
  ],
26
38
  "PostToolUse": [
@@ -19,9 +19,19 @@
19
19
  * hook's own __dirname:
20
20
  *
21
21
  * const { isGitSubcommand } = require(path.join(__dirname, 'lib', 'git-cmd.js'));
22
+ *
23
+ * `tokenize()` delegates to the shared `src/token-scanner.cts` seam (ADR-3212
24
+ * §4, epic #3212 Phase 3, #3414) — the built `gsd-core/bin/lib/token-scanner.cjs`
25
+ * artifact, not a sibling hooks/-tree file, because hook scripts are staged as
26
+ * standalone files at install time and a sibling require is a staging
27
+ * dependency that can fail silently (see gsd-workflow-guard.js's own
28
+ * KIMI_TOOL_NAMES comment for the precedent this follows). Re-exported here
29
+ * unchanged — every existing caller's behavior is identical (parity-asserted
30
+ * in tests/token-scanner.test.cjs row 5).
22
31
  */
23
32
 
24
33
  const path = require('path');
34
+ const { tokenizeShellLike } = require(path.join(__dirname, '..', '..', 'gsd-core', 'bin', 'lib', 'token-scanner.cjs'));
25
35
 
26
36
  /**
27
37
  * Git global options that take a following argument.
@@ -29,6 +39,7 @@ const path = require('path');
29
39
  */
30
40
  const ARGUMENT_TAKING_FLAGS = new Set([
31
41
  '-C', // working directory
42
+ '-c', // config override (separate-arg form: `git -c k=v …`; #3504)
32
43
  '--git-dir', // path to git repository
33
44
  '--work-tree', // path to working tree
34
45
  '--namespace', // git namespace
@@ -57,94 +68,116 @@ const BOOLEAN_FLAGS = new Set([
57
68
  * Handles single-quoted strings, double-quoted strings, and unquoted tokens.
58
69
  * Does NOT perform variable expansion or brace expansion.
59
70
  *
71
+ * Delegates to the shared `src/token-scanner.cts` seam — see the module
72
+ * header comment for why the built artifact, not a sibling require, is used.
73
+ *
60
74
  * @param {string} cmd
61
75
  * @returns {string[]}
62
76
  */
63
77
  function tokenize(cmd) {
64
- const tokens = [];
65
- let i = 0;
66
- const len = cmd.length;
67
-
68
- while (i < len) {
69
- // Skip whitespace
70
- while (i < len && /\s/.test(cmd[i])) i++;
71
- if (i >= len) break;
72
-
73
- let token = '';
74
- while (i < len && !/\s/.test(cmd[i])) {
75
- if (cmd[i] === "'") {
76
- // Single-quoted string: take everything until closing '
77
- i++;
78
- while (i < len && cmd[i] !== "'") token += cmd[i++];
79
- if (i < len) i++; // consume closing '
80
- } else if (cmd[i] === '"') {
81
- // Double-quoted string: take everything until closing " (no escape handling)
82
- i++;
83
- while (i < len && cmd[i] !== '"') token += cmd[i++];
84
- if (i < len) i++; // consume closing "
85
- } else {
86
- token += cmd[i++];
87
- }
88
- }
89
- if (token) tokens.push(token);
90
- }
91
-
92
- return tokens;
78
+ return tokenizeShellLike(cmd);
93
79
  }
94
80
 
95
81
  /**
96
- * Return true if `cmd` invokes the git subcommand `sub`.
82
+ * Walk past leading env-prefix assignments and global git options, same as
83
+ * `isGitSubcommand`'s phases 1-3. Returns the index of the subcommand token,
84
+ * or -1 if the command does not resolve to a git invocation at all.
97
85
  *
98
- * @param {string} cmd - Full shell command string (may include env vars, full paths)
99
- * @param {string} sub - Subcommand to test for, e.g. 'commit'
100
- * @returns {boolean}
86
+ * @param {string[]} tokens
87
+ * @returns {number}
101
88
  */
102
- function isGitSubcommand(cmd, sub) {
103
- if (!cmd || !sub) return false;
104
-
105
- const tokens = tokenize(cmd);
89
+ function skipToSubcommand(tokens) {
106
90
  let i = 0;
107
-
108
- // Phase 1: skip leading VAR=VALUE environment assignments
109
91
  while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i])) {
110
92
  i++;
111
93
  }
112
-
113
- // Phase 2: the next token must be the git executable
114
- if (i >= tokens.length) return false;
94
+ if (i >= tokens.length) return -1;
115
95
  const gitToken = tokens[i++];
116
- if (path.basename(gitToken) !== 'git') return false;
96
+ if (path.basename(gitToken) !== 'git') return -1;
117
97
 
118
- // Phase 3: consume git global options
119
98
  while (i < tokens.length) {
120
99
  const t = tokens[i];
121
-
122
- // --flag=value form for argument-taking flags
123
100
  const eqIdx = t.indexOf('=');
124
101
  const flagName = eqIdx !== -1 ? t.slice(0, eqIdx) : t;
125
102
  if (ARGUMENT_TAKING_FLAGS.has(flagName)) {
126
- if (eqIdx !== -1) {
127
- // consumed as one token: --git-dir=.git
128
- i++;
129
- } else {
130
- // consumed as two tokens: -C /path
131
- i += 2;
132
- }
103
+ i += eqIdx !== -1 ? 1 : 2;
104
+ continue;
105
+ }
106
+ // #3504: glued `-ckey=value` form — git accepts the config override with
107
+ // its argument attached (`git -cfoo.bar=1 …`). The eq-slice above yields
108
+ // flagName `-cfoo`, which no set contains, so without this arm the walk
109
+ // stops and the whole invocation is misclassified as not-git.
110
+ if (/^-c\S*=/.test(t)) {
111
+ i++;
133
112
  continue;
134
113
  }
135
-
136
114
  if (BOOLEAN_FLAGS.has(t)) {
137
115
  i++;
138
116
  continue;
139
117
  }
140
-
141
- // Not a global option — this is the subcommand
142
118
  break;
143
119
  }
120
+ return i;
121
+ }
122
+
123
+ /**
124
+ * Extract the branch-name argument from a git command line that creates or
125
+ * references one — `git checkout -b <name>` or `git branch <name>`. Returns
126
+ * null for any other command, including plain `git checkout <ref>` (switches
127
+ * branches, does not create one) and commands where a checkout/branch-shaped
128
+ * substring appears only inside a quoted argument (e.g. a commit message).
129
+ *
130
+ * New capability (ADR-3212 §4, epic #3212 Phase 3, #3414) exercising the
131
+ * shared scanner on the domain the ADR names ("a branch name... [is] not
132
+ * regular") — not a migration of existing duplicated logic; no prior
133
+ * implementation of this existed in the repo (design doc §1.2).
134
+ *
135
+ * @param {string} cmd
136
+ * @returns {string | null}
137
+ */
138
+ function extractBranchArgument(cmd) {
139
+ if (!cmd) return null;
140
+ const tokens = tokenizeShellLike(cmd);
141
+ const subIdx = skipToSubcommand(tokens);
142
+ if (subIdx === -1 || subIdx >= tokens.length) return null;
143
+ const sub = tokens[subIdx];
144
+
145
+ if (sub === 'checkout') {
146
+ for (let j = subIdx + 1; j < tokens.length; j++) {
147
+ if (tokens[j] === '-b' && j + 1 < tokens.length) return tokens[j + 1];
148
+ }
149
+ return null;
150
+ }
151
+
152
+ if (sub === 'branch') {
153
+ for (let j = subIdx + 1; j < tokens.length; j++) {
154
+ if (!tokens[j].startsWith('-')) return tokens[j];
155
+ }
156
+ return null;
157
+ }
158
+
159
+ return null;
160
+ }
161
+
162
+ /**
163
+ * Return true if `cmd` invokes the git subcommand `sub`.
164
+ *
165
+ * @param {string} cmd - Full shell command string (may include env vars, full paths)
166
+ * @param {string} sub - Subcommand to test for, e.g. 'commit'
167
+ * @returns {boolean}
168
+ */
169
+ function isGitSubcommand(cmd, sub) {
170
+ if (!cmd || !sub) return false;
171
+
172
+ // Phases 1-3 (env-prefix skip, git-executable check, global-option consume)
173
+ // extracted verbatim into skipToSubcommand — byte-identical logic, shared
174
+ // with extractBranchArgument rather than a second copy (#3212 Phase 3).
175
+ const tokens = tokenizeShellLike(cmd);
176
+ const subIdx = skipToSubcommand(tokens);
144
177
 
145
178
  // Phase 4: check the subcommand
146
- if (i >= tokens.length) return false;
147
- return tokens[i] === sub;
179
+ if (subIdx === -1 || subIdx >= tokens.length) return false;
180
+ return tokens[subIdx] === sub;
148
181
  }
149
182
 
150
- module.exports = { isGitSubcommand, tokenize };
183
+ module.exports = { isGitSubcommand, tokenize, extractBranchArgument, skipToSubcommand };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * injection-patterns.js — the shared prompt-injection pattern list (#3504, epic #1900).
5
+ *
6
+ * Single source of truth for the standard injection signatures used by BOTH
7
+ * gsd-prompt-guard.js (PreToolUse scan of writes into .planning/) and
8
+ * gsd-read-injection-scanner.js (PostToolUse scan of Read/WebFetch/WebSearch
9
+ * content). Previously each hook carried a byte-identical copy ("inlined for
10
+ * hook independence") that could silently drift — a pattern tightened in one
11
+ * would stop protecting the other surface.
12
+ *
13
+ * Why a shared lib require is safe here (the old inlining rationale, retired):
14
+ * the installer stages hooks/lib/ from the GSD_HOOK_LIB_FILES allowlist for the
15
+ * shared-bundle surfaces (Claude-family settings.json runtimes and Kimi), the
16
+ * Cursor stager auto-discovers require('./lib/...') in staged scripts and fails
17
+ * the install loudly when a helper is missing (#2587), and the plugin path
18
+ * ships this directory wholesale.
19
+ *
20
+ * Deliberately NOT unified with src/security.cts's scanForInjection set: that
21
+ * set runs inside the compiled lib tree; hooks must stay loadable standalone
22
+ * without it. The two lists are different surfaces by design, not drift.
23
+ *
24
+ * Keep this file free of literal 'gsd:' text — the stager rewrites that marker
25
+ * in staged hook content.
26
+ */
27
+
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,
34
+ /you\s+are\s+now\s+(?:a|an|the)\s+/i,
35
+ /act\s+as\s+(?:a|an|the)\s+(?!plan|phase|wave)/i,
36
+ /pretend\s+(?:you(?:'re| are)\s+|to\s+be\s+)/i,
37
+ /from\s+now\s+on,?\s+you\s+(?:are|will|should|must)/i,
38
+ /(?:print|output|reveal|show|display|repeat)\s+(?:your\s+)?(?:system\s+)?(?:prompt|instructions)/i,
39
+ /<\/?(?:system|assistant|human)>/i,
40
+ /\[SYSTEM\]/i,
41
+ /\[INST\]/i,
42
+ /<<\s*SYS\s*>>/i,
43
+ ]);
44
+
45
+ module.exports = { INJECTION_PATTERNS };
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+ // hooks/lib/isolation-deny-reason.js — shared, frozen reason-code enum for
3
+ // the #3045 dispatch-isolation guards' block/deny decisions
4
+ // (hooks/gsd-agent-isolation-guard.js, hooks/gsd-cursor-subagent-start.js).
5
+ //
6
+ // CONTRIBUTING.md ("Prohibited: Raw Text Matching on Test Outputs") bans
7
+ // asserting on a hook's free-form, human-readable reason/user_message prose
8
+ // — that text is for the operator/model reading the denial and may change
9
+ // wording without notice. Every block/deny decision therefore ALSO carries
10
+ // one of these STABLE codes (surfaced on the hook's stdout JSON as
11
+ // `reason_code`), so tests assert `out.reason_code === REASON_CODE.X`
12
+ // instead of regexing the message (mirrors the REASON enum convention in
13
+ // gsd-core/bin/verify-reapply-patches.cjs).
14
+ //
15
+ // Adding a new code requires updating this enum AND any test that locks the
16
+ // documented set.
17
+ const REASON_CODE = Object.freeze({
18
+ // The compiled runtime library (gsd-core/bin/lib/*.cjs) is missing and
19
+ // could not be self-built (ensure-runtime-build.cjs's RuntimeBuildError).
20
+ RUNTIME_BUILD_FAILED: 'runtime_build_failed',
21
+ // The project's dispatch-isolation configuration ('.planning/config.json')
22
+ // could not be read or resolved for a reason OTHER than a runtime-build
23
+ // failure (unreadable/malformed config, unexpected resolver error).
24
+ CONFIG_UNREADABLE: 'config_unreadable',
25
+ // Isolation resolves to "harness-worktree" but the Agent()/Task() dispatch
26
+ // is missing the harness's isolation flag/kwarg.
27
+ HARNESS_FLAG_MISSING: 'harness_flag_missing',
28
+ // Isolation resolves to "harness-worktree" but the dispatch payload carries
29
+ // no usable subagent_type, so the guard cannot confirm it is a GSD executor.
30
+ NO_SUBAGENT_TYPE: 'no_subagent_type',
31
+ // Isolation resolves to "harness-worktree" but whether the workspace root
32
+ // is an isolated worktree could not be determined (e.g. git unresponsive).
33
+ CANNOT_DETERMINE_ISOLATION: 'cannot_determine_isolation',
34
+ // Isolation resolves to "harness-worktree" and the workspace root is
35
+ // confirmed NOT an isolated worktree.
36
+ NOT_ISOLATED_WORKTREE: 'not_isolated_worktree',
37
+ });
38
+
39
+ module.exports = { REASON_CODE };