@opengsd/gsd-core 1.10.0 → 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 (328) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-debug-session-manager.md +11 -0
  4. package/agents/gsd-doc-synthesizer.md +2 -4
  5. package/agents/gsd-executor.md +5 -5
  6. package/agents/gsd-mempalace-curator.md +5 -2
  7. package/agents/gsd-phase-researcher.md +20 -1
  8. package/agents/gsd-plan-checker.md +37 -0
  9. package/agents/gsd-planner.md +44 -46
  10. package/agents/gsd-user-profiler.md +3 -0
  11. package/agents/gsd-verifier.md +12 -3
  12. package/bin/install.js +841 -971
  13. package/bin/lib/ui-safety-gate.cjs +2 -0
  14. package/commands/gsd/code-review.md +1 -1
  15. package/commands/gsd/execute-phase.md +1 -1
  16. package/commands/gsd/map-codebase.md +1 -1
  17. package/commands/gsd/mempalace-capture.md +1 -1
  18. package/commands/gsd/mempalace-recall.md +1 -1
  19. package/commands/gsd/new-milestone.md +1 -1
  20. package/commands/gsd/quick.md +1 -1
  21. package/commands/gsd/review-backlog.md +2 -1
  22. package/commands/gsd/verify-work.md +1 -1
  23. package/gsd-core/bin/gsd-tools.cjs +469 -88
  24. package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
  25. package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
  26. package/gsd-core/bin/lib/api-coverage.cjs +3 -5
  27. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  28. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  29. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  30. package/gsd-core/bin/lib/audit.cjs +876 -240
  31. package/gsd-core/bin/lib/broken-windows.cjs +1 -1
  32. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  33. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  34. package/gsd-core/bin/lib/capability-registry.cjs +575 -101
  35. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  36. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  37. package/gsd-core/bin/lib/capability-validator.cjs +495 -22
  38. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  39. package/gsd-core/bin/lib/check-command-router.cjs +71 -37
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  41. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  42. package/gsd-core/bin/lib/command-aliases.cjs +22 -0
  43. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  44. package/gsd-core/bin/lib/commands.cjs +651 -86
  45. package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  47. package/gsd-core/bin/lib/config-loader.cjs +75 -0
  48. package/gsd-core/bin/lib/config.cjs +10 -1
  49. package/gsd-core/bin/lib/core-utils.cjs +127 -29
  50. package/gsd-core/bin/lib/decisions.cjs +23 -0
  51. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  52. package/gsd-core/bin/lib/frontmatter.cjs +155 -20
  53. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  54. package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
  55. package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
  56. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  57. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  58. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  59. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  60. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  61. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  62. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  63. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  64. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  65. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  66. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  67. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  68. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  69. package/gsd-core/bin/lib/init.cjs +321 -129
  70. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  71. package/gsd-core/bin/lib/install-engine.cjs +745 -258
  72. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  73. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  74. package/gsd-core/bin/lib/install-profiles.cjs +134 -57
  75. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  76. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  77. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  78. package/gsd-core/bin/lib/installer-migrations.cjs +138 -31
  79. package/gsd-core/bin/lib/io.cjs +10 -0
  80. package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
  81. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  82. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  83. package/gsd-core/bin/lib/milestone.cjs +754 -70
  84. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  85. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  86. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  87. package/gsd-core/bin/lib/pattern.cjs +122 -0
  88. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  89. package/gsd-core/bin/lib/phase-id.cjs +444 -36
  90. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  91. package/gsd-core/bin/lib/phase-locator.cjs +125 -18
  92. package/gsd-core/bin/lib/phase.cjs +646 -143
  93. package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
  94. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  95. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  96. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  97. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  98. package/gsd-core/bin/lib/planning-workspace.cjs +56 -6
  99. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  100. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  101. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  102. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
  103. package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
  104. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  105. package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
  106. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  107. package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
  108. package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
  109. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  110. package/gsd-core/bin/lib/roadmap.cjs +385 -94
  111. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
  112. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  113. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
  114. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  115. package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
  116. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
  117. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  118. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  119. package/gsd-core/bin/lib/security.cjs +104 -5
  120. package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
  121. package/gsd-core/bin/lib/smart-entry.cjs +142 -22
  122. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  123. package/gsd-core/bin/lib/state-document.cjs +152 -8
  124. package/gsd-core/bin/lib/state-transition.cjs +371 -117
  125. package/gsd-core/bin/lib/state.cjs +1794 -357
  126. package/gsd-core/bin/lib/surface.cjs +23 -9
  127. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  128. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  129. package/gsd-core/bin/lib/uat-predicate.cjs +9 -3
  130. package/gsd-core/bin/lib/uat.cjs +399 -56
  131. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  132. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  133. package/gsd-core/bin/lib/unusable-input.cjs +24 -0
  134. package/gsd-core/bin/lib/update-context.cjs +8 -2
  135. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  136. package/gsd-core/bin/lib/validate.cjs +20 -6
  137. package/gsd-core/bin/lib/vendor/README.md +37 -0
  138. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  139. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  140. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  141. package/gsd-core/bin/lib/verification.cjs +258 -8
  142. package/gsd-core/bin/lib/verify.cjs +368 -888
  143. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
  144. package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
  145. package/gsd-core/bin/lib/workstream.cjs +2 -2
  146. package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
  147. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  148. package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
  149. package/gsd-core/references/agent-contracts.md +43 -26
  150. package/gsd-core/references/checkpoints.md +2 -2
  151. package/gsd-core/references/context-budget.md +1 -1
  152. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  153. package/gsd-core/references/doc-conflict-engine.md +1 -1
  154. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  155. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  156. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  157. package/gsd-core/references/execute-phase-response-language.md +1 -1
  158. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  159. package/gsd-core/references/gate-prompts.md +1 -1
  160. package/gsd-core/references/git-planning-commit.md +2 -1
  161. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  162. package/gsd-core/references/model-profiles.md +12 -4
  163. package/gsd-core/references/mvp-concepts.md +9 -9
  164. package/gsd-core/references/planner-guidance.md +3 -9
  165. package/gsd-core/references/planner-preconditions.md +1 -1
  166. package/gsd-core/references/planner-reviews.md +1 -1
  167. package/gsd-core/references/planning-config.md +8 -6
  168. package/gsd-core/references/revision-loop.md +1 -1
  169. package/gsd-core/references/specless-probe-fallback.md +1 -1
  170. package/gsd-core/references/universal-anti-patterns.md +3 -3
  171. package/gsd-core/references/verifier-phase-gates.md +192 -0
  172. package/gsd-core/references/verify-mvp-mode.md +1 -1
  173. package/gsd-core/references/workstream-flag.md +22 -6
  174. package/gsd-core/templates/discussion-log.md +1 -1
  175. package/gsd-core/templates/phase-prompt.md +2 -4
  176. package/gsd-core/templates/state.md +4 -4
  177. package/gsd-core/templates/verification-report.md +9 -1
  178. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  179. package/gsd-core/workflows/autonomous.md +1 -1
  180. package/gsd-core/workflows/cleanup.md +62 -3
  181. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
  182. package/gsd-core/workflows/code-review-fix.md +37 -10
  183. package/gsd-core/workflows/code-review.md +38 -12
  184. package/gsd-core/workflows/complete-milestone.md +141 -18
  185. package/gsd-core/workflows/debug.md +7 -5
  186. package/gsd-core/workflows/diagnose-issues.md +35 -9
  187. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  188. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  189. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -1
  190. package/gsd-core/workflows/edit-phase.md +26 -1
  191. package/gsd-core/workflows/eval-review.md +3 -5
  192. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +31 -6
  193. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  194. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +2 -0
  195. package/gsd-core/workflows/execute-phase.md +38 -50
  196. package/gsd-core/workflows/execute-plan.md +36 -4
  197. package/gsd-core/workflows/explore.md +131 -4
  198. package/gsd-core/workflows/fast.md +10 -2
  199. package/gsd-core/workflows/health.md +73 -4
  200. package/gsd-core/workflows/import.md +4 -4
  201. package/gsd-core/workflows/ingest-docs.md +5 -5
  202. package/gsd-core/workflows/mvp-phase.md +6 -3
  203. package/gsd-core/workflows/new-milestone.md +14 -9
  204. package/gsd-core/workflows/new-project.md +14 -14
  205. package/gsd-core/workflows/next.md +12 -0
  206. package/gsd-core/workflows/plan-phase.md +41 -17
  207. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  208. package/gsd-core/workflows/progress.md +34 -6
  209. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
  210. package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
  211. package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
  212. package/gsd-core/workflows/quick.md +35 -15
  213. package/gsd-core/workflows/review.md +26 -5
  214. package/gsd-core/workflows/secure-phase.md +1 -1
  215. package/gsd-core/workflows/session-report.md +2 -1
  216. package/gsd-core/workflows/settings.md +66 -2
  217. package/gsd-core/workflows/ship.md +104 -44
  218. package/gsd-core/workflows/spec-phase.md +30 -12
  219. package/gsd-core/workflows/sync-skills.md +63 -8
  220. package/gsd-core/workflows/transition.md +46 -11
  221. package/gsd-core/workflows/ui-phase.md +5 -5
  222. package/gsd-core/workflows/ui-review.md +2 -2
  223. package/gsd-core/workflows/update.md +1 -1
  224. package/gsd-core/workflows/validate-phase.md +1 -1
  225. package/gsd-core/workflows/verify-work.md +9 -7
  226. package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
  227. package/hooks/dist/gsd-check-update-worker.js +56 -13
  228. package/hooks/dist/gsd-check-update.js +19 -1
  229. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  230. package/hooks/dist/gsd-cursor-subagent-start.js +77 -2
  231. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  232. package/hooks/dist/gsd-prompt-guard.js +21 -20
  233. package/hooks/dist/gsd-read-injection-scanner.js +38 -24
  234. package/hooks/dist/gsd-statusline.js +18 -0
  235. package/hooks/dist/gsd-update-banner.js +22 -1
  236. package/hooks/dist/gsd-workflow-guard.js +134 -36
  237. package/hooks/dist/lib/git-cmd.js +92 -59
  238. package/hooks/dist/lib/injection-patterns.js +45 -0
  239. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  240. package/hooks/dist/lib/isolation-sentinel.js +9 -0
  241. package/hooks/gsd-agent-isolation-guard.js +103 -14
  242. package/hooks/gsd-check-update-worker.js +56 -13
  243. package/hooks/gsd-check-update.js +19 -1
  244. package/hooks/gsd-cursor-pre-tool.js +0 -3
  245. package/hooks/gsd-cursor-subagent-start.js +77 -2
  246. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  247. package/hooks/gsd-prompt-guard.js +21 -20
  248. package/hooks/gsd-read-injection-scanner.js +38 -24
  249. package/hooks/gsd-statusline.js +18 -0
  250. package/hooks/gsd-update-banner.js +22 -1
  251. package/hooks/gsd-workflow-guard.js +134 -36
  252. package/hooks/lib/git-cmd.js +92 -59
  253. package/hooks/lib/injection-patterns.js +45 -0
  254. package/hooks/lib/isolation-deny-reason.js +39 -0
  255. package/hooks/lib/isolation-sentinel.js +9 -0
  256. package/package.json +21 -9
  257. package/pi/gsd.cjs +19 -5
  258. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  259. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  260. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  261. package/scripts/changeset/lint.cjs +60 -5
  262. package/scripts/check-alias-drift.cjs +7 -43
  263. package/scripts/check-contract-drift.cjs +297 -0
  264. package/scripts/ci-test-scope.cjs +19 -2
  265. package/scripts/command-contract-helpers.cjs +903 -1
  266. package/scripts/gen-adr-index.cjs +728 -38
  267. package/scripts/gen-capability-registry.cjs +3 -15
  268. package/scripts/gen-context-index.cjs +2 -11
  269. package/scripts/gen-health-docs.cjs +390 -0
  270. package/scripts/gen-inventory-manifest.cjs +50 -4
  271. package/scripts/gen-loop-host-contract.cjs +4 -24
  272. package/scripts/gen-registry.cjs +3 -14
  273. package/scripts/lib/alias-drift-families.cjs +46 -0
  274. package/scripts/lib/drift-scan.cjs +278 -0
  275. package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
  276. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  277. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  278. package/scripts/lint-canary-version-leak.cjs +73 -0
  279. package/scripts/lint-command-contract.cjs +96 -13
  280. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  281. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  282. package/scripts/lint-default-flip-documentation.cjs +193 -0
  283. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  284. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  285. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  286. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  287. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  288. package/scripts/lint-milestone-window-drift.cjs +468 -0
  289. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  290. package/scripts/lint-plan-count-drift.cjs +318 -0
  291. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  292. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  293. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  294. package/scripts/lint-regression-test-names.cjs +15 -13
  295. package/scripts/lint-removed-but-needed.cjs +320 -0
  296. package/scripts/lint-state-field-drift.cjs +805 -0
  297. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  298. package/scripts/lint-test-file-count.allowlist.json +21 -10
  299. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  300. package/scripts/lint-vendored-deps.cjs +124 -0
  301. package/scripts/pr-changed-files.cjs +63 -0
  302. package/scripts/pr-template-policy.cjs +14 -4
  303. package/scripts/prompt-injection-scan.sh +25 -0
  304. package/scripts/require-issue-link-policy.cjs +192 -0
  305. package/scripts/state-write-path-drift-baseline.json +19 -0
  306. package/scripts/sync-runtime-launcher.cjs +2 -4
  307. package/skills/gsd-autonomous/SKILL.md +0 -1
  308. package/skills/gsd-code-review/SKILL.md +1 -1
  309. package/skills/gsd-execute-phase/SKILL.md +1 -2
  310. package/skills/gsd-map-codebase/SKILL.md +1 -1
  311. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  312. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  313. package/skills/gsd-new-milestone/SKILL.md +1 -1
  314. package/skills/gsd-next/SKILL.md +0 -1
  315. package/skills/gsd-plan-phase/SKILL.md +0 -1
  316. package/skills/gsd-progress/SKILL.md +0 -1
  317. package/skills/gsd-quick/SKILL.md +1 -1
  318. package/skills/gsd-review-backlog/SKILL.md +2 -1
  319. package/skills/gsd-stats/SKILL.md +0 -1
  320. package/skills/gsd-verify-work/SKILL.md +1 -1
  321. package/vscode/package.json +1 -1
  322. package/gsd-core/workflows/discovery-phase.md +0 -298
  323. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  324. package/gsd-core/workflows/verify-phase.md +0 -574
  325. package/scripts/affected-tests-lib.cjs +0 -554
  326. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  327. package/scripts/run-affected-tests.cjs +0 -7
  328. package/scripts/run-tests.cjs +0 -1051
@@ -72,23 +72,10 @@ const MARKDOWN_LINK_PATTERNS = [
72
72
  },
73
73
  ];
74
74
 
75
- // Standard injection patterns — mirrors gsd-prompt-guard.js, inlined for hook independence.
76
- const INJECTION_PATTERNS = [
77
- /ignore\s+(all\s+)?previous\s+instructions/i,
78
- /ignore\s+(all\s+)?above\s+instructions/i,
79
- /disregard\s+(all\s+)?previous/i,
80
- /forget\s+(all\s+)?(your\s+)?instructions/i,
81
- /override\s+(system|previous)\s+(prompt|instructions)/i,
82
- /you\s+are\s+now\s+(?:a|an|the)\s+/i,
83
- /act\s+as\s+(?:a|an|the)\s+(?!plan|phase|wave)/i,
84
- /pretend\s+(?:you(?:'re| are)\s+|to\s+be\s+)/i,
85
- /from\s+now\s+on,?\s+you\s+(?:are|will|should|must)/i,
86
- /(?:print|output|reveal|show|display|repeat)\s+(?:your\s+)?(?:system\s+)?(?:prompt|instructions)/i,
87
- /<\/?(?:system|assistant|human)>/i,
88
- /\[SYSTEM\]/i,
89
- /\[INST\]/i,
90
- /<<\s*SYS\s*>>/i,
91
- ];
75
+ // Standard injection patterns — shared with gsd-prompt-guard.js via
76
+ // hooks/lib/injection-patterns.js so the two surfaces cannot drift (#3504).
77
+ // Staging of the lib helper is allowlisted in GSD_HOOK_LIB_FILES (bin/install.js).
78
+ const { INJECTION_PATTERNS } = require('./lib/injection-patterns.js');
92
79
 
93
80
  const ALL_PATTERNS = [...INJECTION_PATTERNS, ...SUMMARISATION_PATTERNS];
94
81
 
@@ -98,6 +85,16 @@ const ALL_PATTERNS = [...INJECTION_PATTERNS, ...SUMMARISATION_PATTERNS];
98
85
  // construction. Normalized to forward slashes to match `p` below.
99
86
  const OWN_BUNDLE_PREFIX = __dirname.replace(/\\/g, '/').replace(/\/+$/, '') + '/';
100
87
 
88
+ // Synthetic rule ids for the finding classes that have no entry in
89
+ // MARKDOWN_LINK_PATTERNS. Frozen and referenced from BOTH the push sites and
90
+ // renderFinding so the two can never drift — a bare literal repeated at each
91
+ // site is how a rename silently falls through to the generic render branch.
92
+ const RULE_IDS = Object.freeze({
93
+ INJECTION_PATTERN: 'INJECTION-PATTERN',
94
+ INVISIBLE_UNICODE: 'INVISIBLE-UNICODE',
95
+ UNICODE_TAG_BLOCK: 'UNICODE-TAG-BLOCK',
96
+ });
97
+
101
98
  function isExcludedPath(filePath) {
102
99
  const p = filePath.replace(/\\/g, '/');
103
100
  return (
@@ -267,12 +264,19 @@ process.stdin.on('end', () => {
267
264
  process.exit(0);
268
265
  }
269
266
 
267
+ // Typed findings IR — single source of truth for both the machine-readable
268
+ // `findings` array and the rendered advisory prose. Never build these as two
269
+ // parallel arrays: that invites the generative-fix-divergence defect class
270
+ // where the rendered text and the structured data silently drift apart.
270
271
  const findings = [];
271
272
 
272
273
  for (const pattern of ALL_PATTERNS) {
273
274
  if (pattern.test(content)) {
274
275
  // Trim pattern source for readable output
275
- findings.push(pattern.source.replace(/\\s\+/g, '-').replace(/[()\\]/g, '').substring(0, 50));
276
+ findings.push({
277
+ ruleId: RULE_IDS.INJECTION_PATTERN,
278
+ match: pattern.source.replace(/\\s\+/g, '-').replace(/[()\\]/g, '').substring(0, 50),
279
+ });
276
280
  }
277
281
  }
278
282
 
@@ -284,19 +288,19 @@ process.stdin.on('end', () => {
284
288
  const m = line.match(entry.pattern);
285
289
  if (!m) continue;
286
290
  if (entry.safePredicate && entry.safePredicate(line)) continue;
287
- findings.push(`${entry.ruleId}:${m[0].substring(0, 40)}`);
291
+ findings.push({ ruleId: entry.ruleId, match: m[0].substring(0, 40) });
288
292
  }
289
293
  }
290
294
 
291
295
  // Invisible Unicode (zero-width, RTL override, soft hyphen, BOM)
292
296
  if (/[\u200B-\u200F\u2028-\u202F\uFEFF\u00AD\u2060-\u2069]/.test(content)) {
293
- findings.push('invisible-unicode');
297
+ findings.push({ ruleId: RULE_IDS.INVISIBLE_UNICODE, match: null });
294
298
  }
295
299
 
296
300
  // Unicode tag block U+E0000–E007F (invisible instruction injection vector)
297
301
  try {
298
302
  if (/[\u{E0000}-\u{E007F}]/u.test(content)) {
299
- findings.push('unicode-tag-block');
303
+ findings.push({ ruleId: RULE_IDS.UNICODE_TAG_BLOCK, match: null });
300
304
  }
301
305
  } catch {
302
306
  // Engine does not support Unicode property escapes — skip this check
@@ -306,6 +310,16 @@ process.stdin.on('end', () => {
306
310
  process.exit(0);
307
311
  }
308
312
 
313
+ // Renders one finding back into the exact prose fragment the advisory has
314
+ // always embedded. Kept as the ONLY place that maps IR -> text, so the
315
+ // `additionalContext` string and the `findings` array can never diverge.
316
+ function renderFinding(f) {
317
+ if (f.ruleId === RULE_IDS.INVISIBLE_UNICODE) return 'invisible-unicode';
318
+ if (f.ruleId === RULE_IDS.UNICODE_TAG_BLOCK) return 'unicode-tag-block';
319
+ if (f.ruleId === RULE_IDS.INJECTION_PATTERN) return f.match;
320
+ return `${f.ruleId}:${f.match}`;
321
+ }
322
+
309
323
  const severity = findings.length >= 3 ? 'HIGH' : 'LOW';
310
324
  const label = toolName === 'Read' ? path.basename(source) : source;
311
325
  const detail = severity === 'HIGH'
@@ -313,7 +327,7 @@ process.stdin.on('end', () => {
313
327
  : 'Single pattern match may be a false positive (e.g., documentation). Proceed with awareness.';
314
328
  const advisory =
315
329
  `\u26a0\ufe0f INJECTION SCAN [${severity}] (${toolName}): "${label}" triggered ` +
316
- `${findings.length} pattern(s): ${findings.join(', ')}. ` +
330
+ `${findings.length} pattern(s): ${findings.map(renderFinding).join(', ')}. ` +
317
331
  `This content is now in your conversation context. ${detail} Source: ${source}`;
318
332
 
319
333
  // Opt-in blocking: only when configured AND high-confidence
@@ -330,8 +344,8 @@ process.stdin.on('end', () => {
330
344
  const output = blocking
331
345
  ? { decision: 'block',
332
346
  reason: `Prompt-injection blocked (${toolName}). ${advisory}`,
333
- hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: advisory } }
334
- : { hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: advisory } };
347
+ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: advisory, findings } }
348
+ : { hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: advisory, findings } };
335
349
 
336
350
  process.stdout.write(JSON.stringify(output));
337
351
  } catch {
@@ -9,6 +9,24 @@ const os = require('os');
9
9
  // Namespace (not destructured) so tests can inject spawn failures by
10
10
  // monkeypatching childProcess.execFileSync.
11
11
  const childProcess = require('child_process');
12
+ // #3582: gsd-core/bin/lib/*.cjs (semver-compare.cjs, state-document.cjs,
13
+ // active-workstream-store.cjs, planning-workspace.cjs — required below) and
14
+ // package-identity.cjs are tsc build artifacts (ADR-457), gitignored and
15
+ // absent on a raw plugin-marketplace / git-clone install that never ran
16
+ // `npm run build:lib`. The statusline renders on EVERY prompt, so a build
17
+ // failure here must DEGRADE (print nothing, exit 0) rather than crash
18
+ // Claude Code's per-render statusline hook. Scoped to the spawned-as-a-script
19
+ // path (`require.main === module`) — a test `require()` of this module for
20
+ // its pure helpers assumes a built tree, same as every other hook test.
21
+ if (require.main === module) {
22
+ try {
23
+ const { ensureRuntimeBuild } = require('../gsd-core/bin/ensure-runtime-build.cjs');
24
+ ensureRuntimeBuild();
25
+ } catch (e) {
26
+ process.stdout.write('');
27
+ process.exit(0);
28
+ }
29
+ }
12
30
  const { isSemverNewer } = require('../gsd-core/bin/lib/semver-compare.cjs');
13
31
  const { PACKAGE_NAME, updateCacheFileName } = require('../gsd-core/bin/lib/package-identity.cjs');
14
32
  const { normalizeStateStatus } = require('../gsd-core/bin/lib/state-document.cjs');
@@ -15,7 +15,28 @@
15
15
  const fs = require('fs');
16
16
  const path = require('path');
17
17
  const os = require('os');
18
- const { PACKAGE_NAME, updateCacheFileName } = require('../gsd-core/bin/lib/package-identity.cjs');
18
+
19
+ // #3582: gsd-core/bin/lib/package-identity.cjs is a tsc build artifact
20
+ // (ADR-457), gitignored and absent on a raw plugin-marketplace / git-clone
21
+ // install that never ran `npm run build:lib`. This is an opt-in SessionStart
22
+ // hook — a build failure here must DEGRADE, not crash session start. With
23
+ // PACKAGE_NAME left null, buildBannerOutput's own lineage guard
24
+ // (`!cache.package_name || cache.package_name !== PACKAGE_NAME`) always
25
+ // treats the cache as untrusted, so main() falls through to its existing
26
+ // silent "print nothing" path below — no separate degrade branch needed.
27
+ // This try/require/ensureRuntimeBuild/require/catch shape is deliberately
28
+ // duplicated (not extracted to hooks/lib/) — see
29
+ // gsd-check-update-worker.js's identical #3582 comment for why.
30
+ let PACKAGE_NAME = null;
31
+ let updateCacheFileName = 'gsd-update-check.json';
32
+ try {
33
+ const { ensureRuntimeBuild } = require('../gsd-core/bin/ensure-runtime-build.cjs');
34
+ ensureRuntimeBuild();
35
+ ({ PACKAGE_NAME, updateCacheFileName } = require('../gsd-core/bin/lib/package-identity.cjs'));
36
+ } catch (e) {
37
+ // Runtime library missing/broken and could not self-build — degrade to the
38
+ // fallbacks above rather than crash the SessionStart hook.
39
+ }
19
40
 
20
41
  // Suppress repeat parse-error banners for 24 hours so a genuinely broken
21
42
  // cache file doesn't nag the user every session.
@@ -4,9 +4,15 @@
4
4
  // Detects when Claude attempts file edits outside a GSD workflow context
5
5
  // (no active /gsd- skill or Task subagent) and injects an advisory warning.
6
6
  //
7
- // This is a SOFT guard — it advises, not blocks. The edit still proceeds.
8
- // The warning nudges Claude to use /gsd:quick or /gsd:fast instead of
9
- // making direct edits that bypass state tracking.
7
+ // This is a SOFT guard for edits — it advises, not blocks. The edit still
8
+ // proceeds. The warning nudges Claude to use /gsd:quick or /gsd:fast instead
9
+ // of making direct edits that bypass state tracking.
10
+ //
11
+ // ONE hard block lives here: `git add -f` on an agent/worktree-agent branch
12
+ // (WORKTREE_AGENT_FORCE_ADD_FORBIDDEN) — and that block leg fails CLOSED on
13
+ // internal error (#3504): when the guard is enabled and the blocking context
14
+ // holds, a thrown error exits 2 (block), not 0. The advisory legs keep the
15
+ // fail-open posture — a broken advisory must never wedge every tool call.
10
16
  //
11
17
  // Enable via config: hooks.workflow_guard: true (default: false)
12
18
  // Only triggers on Write/Edit tool calls to non-.planning/ files.
@@ -14,40 +20,46 @@
14
20
  const fs = require('fs');
15
21
  const path = require('path');
16
22
  const { spawnSync } = require('child_process');
17
- const { tokenize } = require('./lib/git-cmd.js');
23
+ const { tokenize, skipToSubcommand } = require('./lib/git-cmd.js');
18
24
 
19
25
  function forceGitAddCwds(command, defaultCwd) {
20
26
  const tokens = tokenize(command || '');
21
27
  const separators = new Set(['&&', '||', ';', '|']);
22
28
  const cwdList = [];
23
- for (let i = 0; i < tokens.length; i++) {
24
- if (path.basename(tokens[i]) !== 'git') continue;
29
+ // #3504: per-segment walk driven by git-cmd.js's canonical skipToSubcommand.
30
+ // The previous inline walk knew six global flags and silently missed the
31
+ // rest — `git -c core.hooksPath=/tmp/x add -f x`, `git --no-optional-locks
32
+ // add -f x`, `--literal-pathspecs`, `--namespace=…` and friends all fell
33
+ // through to a silent exit 0, a no-crash bypass the fail-closed catch is
34
+ // structurally blind to (it only fires on throws). Sharing the classifier
35
+ // makes the block's flag knowledge exactly the classifier's.
36
+ const segments = [];
37
+ let start = 0;
38
+ for (let i = 0; i <= tokens.length; i++) {
39
+ if (i === tokens.length || separators.has(tokens[i])) {
40
+ if (i > start) segments.push(tokens.slice(start, i));
41
+ start = i + 1;
42
+ }
43
+ }
44
+ for (const seg of segments) {
45
+ const subIdx = skipToSubcommand(seg);
46
+ if (subIdx === -1 || subIdx >= seg.length || seg[subIdx] !== 'add') continue;
25
47
 
26
- let j = i + 1;
48
+ // Resolve a `-C <dir>` (separate-arg form) preceding the subcommand so
49
+ // the branch is probed at the repository the add targets.
27
50
  let gitCwd = defaultCwd;
28
- while (j < tokens.length) {
29
- const token = tokens[j];
30
- const flagName = token.includes('=') ? token.slice(0, token.indexOf('=')) : token;
31
- if (token === '-C' && tokens[j + 1]) {
32
- gitCwd = path.resolve(gitCwd, tokens[j + 1]);
33
- j += 2;
34
- continue;
35
- }
36
- if (['-C', '--git-dir', '--work-tree'].includes(flagName) && !token.includes('=')) {
37
- j += 2;
38
- continue;
39
- }
40
- if (['--git-dir', '--work-tree', '--no-pager', '-p', '-P'].includes(flagName)) {
41
- j++;
51
+ for (let k = 0; k < subIdx; ) {
52
+ if (seg[k] === '-C' && k + 1 < subIdx) {
53
+ gitCwd = path.resolve(gitCwd, seg[k + 1]);
54
+ k += 2;
42
55
  continue;
43
56
  }
44
- break;
57
+ k++;
45
58
  }
46
59
 
47
- if (tokens[j] !== 'add') continue;
48
- for (let k = j + 1; k < tokens.length && !separators.has(tokens[k]); k++) {
49
- if (tokens[k] === '--') break;
50
- if (tokens[k] === '--force' || tokens[k] === '-f' || /^-[A-Za-z]*f[A-Za-z]*$/.test(tokens[k])) {
60
+ for (let k = subIdx + 1; k < seg.length; k++) {
61
+ if (seg[k] === '--') break;
62
+ if (seg[k] === '--force' || seg[k] === '-f' || /^-[A-Za-z]*f[A-Za-z]*$/.test(seg[k])) {
51
63
  cwdList.push(gitCwd);
52
64
  break;
53
65
  }
@@ -62,11 +74,86 @@ function currentBranch(cwd) {
62
74
  encoding: 'utf8',
63
75
  stdio: ['ignore', 'pipe', 'ignore'],
64
76
  windowsHide: true,
77
+ // #3504: bounded — this ran unbounded before, an indefinite hang under a
78
+ // wedged git would hang every PreToolUse call. Host wiring allows a 5s
79
+ // budget for the whole hook, so the probe gets 2s of it; a timeout
80
+ // returns '' (branch unknown), which both the block decision and the
81
+ // fail-closed re-check treat as "cannot establish agent branch".
82
+ timeout: 2000,
83
+ killSignal: 'SIGTERM',
65
84
  });
66
85
  if (result.status !== 0) return '';
67
86
  return result.stdout.trim();
68
87
  }
69
88
 
89
+ // Agent-branch predicate, shared by the happy-path detector and the fail-closed
90
+ // re-check so the block's scope cannot drift between the two paths (#3504).
91
+ function isAgentBranch(branch) {
92
+ return /^(worktree-)?agent-/.test(branch);
93
+ }
94
+
95
+ // Typed cwd read, shared by both paths for the same reason: a non-string cwd
96
+ // degrades to process.cwd() instead of throwing inside path.join().
97
+ function payloadCwd(data) {
98
+ return typeof data?.cwd === 'string' && data.cwd ? data.cwd : process.cwd();
99
+ }
100
+
101
+ // The force-add block payload, emitted by the happy-path detector and the
102
+ // fail-closed catch — one source so the two exits cannot drift. `origin`
103
+ // distinguishes them structurally ('force-add-detected' vs 'fail-closed') so a
104
+ // consumer — or the model reading Kimi's stderr feedback — is never told a
105
+ // force-add was detected when the call was in fact blocked unanalyzed.
106
+ function emitForceAddBlock(origin) {
107
+ const failClosed = origin === 'fail-closed';
108
+ const reason = failClosed
109
+ ? 'workflow guard internal error on an agent branch - failing closed. The command was NOT analyzed and was NOT confirmed to be a force-add. Retry the call or inspect the guard.'
110
+ : 'agent/worktree-agent branches must not run git add -f or git add --force. Respect the SDK skipped_gitignored/skipped_commit_docs_false contract and leave gitignored files untracked.';
111
+ const output = {
112
+ decision: 'block',
113
+ code: 'WORKTREE_AGENT_FORCE_ADD_FORBIDDEN',
114
+ origin: failClosed ? 'fail-closed' : 'force-add-detected',
115
+ reason,
116
+ };
117
+ process.stdout.write(JSON.stringify(output));
118
+ // Kimi CLI's exit-2 protocol feeds stderr back to the model (#2304)
119
+ process.stderr.write(output.reason);
120
+ }
121
+
122
+ // #3504 fail-closed context re-derivation. Runs INSIDE the outer catch, after
123
+ // an internal error, and answers exactly one question: does the blocking
124
+ // context of the force-add guard hold for this payload? Each stage is guarded —
125
+ // a re-derivation that itself throws must degrade to "cannot establish" (false),
126
+ // never take down the catch. Deliberately does NOT re-detect the force-add: the
127
+ // error may live in the detector itself, and on an agent branch with the guard
128
+ // enabled, a Bash call under an internal error is conservative-correct to block.
129
+ // Anything it cannot establish (unparseable payload, non-Bash tool, guard
130
+ // disabled, branch not determinably agent-*) fails open, preserving the
131
+ // advisory legs' fail-open posture.
132
+ function failClosedBlockContext(rawInput) {
133
+ let data;
134
+ try {
135
+ data = normalizeKimiPayload(JSON.parse(rawInput));
136
+ } catch {
137
+ return false;
138
+ }
139
+ if (data === null || typeof data !== 'object' || data.tool_name !== 'Bash') return false;
140
+ const cwd = payloadCwd(data);
141
+ let enabled;
142
+ try {
143
+ enabled = workflowGuardEnabled(cwd);
144
+ } catch {
145
+ return false;
146
+ }
147
+ if (!enabled) return false;
148
+ let branch;
149
+ try {
150
+ branch = currentBranch(cwd);
151
+ } catch {
152
+ return false;
153
+ }
154
+ return isAgentBranch(branch);
155
+ }
156
+
70
157
  function workflowGuardEnabled(cwd) {
71
158
  const configPath = path.join(cwd, '.planning', 'config.json');
72
159
  if (!fs.existsSync(configPath)) return false;
@@ -180,6 +267,15 @@ process.stdin.on('end', () => {
180
267
  clearTimeout(stdinTimeout);
181
268
  try {
182
269
  const data = normalizeKimiPayload(JSON.parse(input));
270
+ // #3504 test-only fault seam: throws right after parse so the fail-closed
271
+ // posture of the outer catch is exercisable — no JSON-expressible input
272
+ // throws in this handler today (#2547/#2595 hardened every read). Gated on
273
+ // GSD_TEST_MODE as well so a leaked GSD_TEST_WORKFLOW_GUARD_FAULT in a real
274
+ // shell cannot wedge a production session. The fault's failure direction
275
+ // is CLOSED: the catch below may block, never bypass a block.
276
+ if (process.env.GSD_TEST_MODE === '1' && process.env.GSD_TEST_WORKFLOW_GUARD_FAULT === '1') {
277
+ throw new Error('GSD_TEST_WORKFLOW_GUARD_FAULT: injected fault');
278
+ }
183
279
  const toolName = data.tool_name;
184
280
  const cwd = data.cwd || process.cwd();
185
281
  const isWorkflowGuardEnabled = workflowGuardEnabled(cwd);
@@ -192,14 +288,7 @@ process.stdin.on('end', () => {
192
288
  for (const gitCwd of forceGitAddCwds(command, cwd)) {
193
289
  const branch = currentBranch(gitCwd);
194
290
  if (/^(worktree-)?agent-/.test(branch)) {
195
- const output = {
196
- decision: 'block',
197
- code: 'WORKTREE_AGENT_FORCE_ADD_FORBIDDEN',
198
- reason: 'agent/worktree-agent branches must not run git add -f or git add --force. Respect the SDK skipped_gitignored/skipped_commit_docs_false contract and leave gitignored files untracked.',
199
- };
200
- process.stdout.write(JSON.stringify(output));
201
- // Kimi CLI's exit-2 protocol feeds stderr back to the model (#2304)
202
- process.stderr.write(output.reason);
291
+ emitForceAddBlock();
203
292
  process.exit(2);
204
293
  }
205
294
  }
@@ -264,8 +353,17 @@ process.stdin.on('end', () => {
264
353
  };
265
354
 
266
355
  process.stdout.write(JSON.stringify(output));
267
- } catch (e) {
268
- // Silent fail — never block tool execution
356
+ } catch {
357
+ // #3504: split posture on internal error. The ONE hard block in this hook
358
+ // (force-add on agent branches) fails CLOSED — if the blocking context can
359
+ // be re-derived from the payload (Bash tool + guard enabled + determinably
360
+ // an agent branch), exit 2 rather than silently allowing. Everything else
361
+ // keeps the historical fail-open posture: a broken advisory guard must
362
+ // never wedge the session's tool calls.
363
+ if (failClosedBlockContext(input)) {
364
+ emitForceAddBlock('fail-closed');
365
+ process.exit(2);
366
+ }
269
367
  process.exit(0);
270
368
  }
271
369
  });
@@ -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 };