heyspex 0.1.0 → 0.2.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 (322) hide show
  1. package/README.md +21 -0
  2. package/bin/hs.mjs +931 -98
  3. package/bin/spexcode.mjs +262 -0
  4. package/dev-factory/engine/worktree.mjs +740 -0
  5. package/dev-factory/lib/code-repo-root.mjs +67 -0
  6. package/dev-factory/lib/context-budget.mjs +84 -0
  7. package/dev-factory/lib/epic-detect.mjs +190 -0
  8. package/dev-factory/lib/fleet-queue-file.mjs +846 -0
  9. package/dev-factory/lib/fleet-queue.mjs +426 -0
  10. package/dev-factory/lib/fleet-state.mjs +14 -18
  11. package/dev-factory/lib/model-catalog.mjs +338 -0
  12. package/dev-factory/lib/seat-context.mjs +199 -0
  13. package/dev-factory/lib/spec-io.mjs +93 -0
  14. package/dev-factory/lib/sufficiency-score.mjs +8 -2
  15. package/dev-factory/lib/worktree-allocator.mjs +744 -0
  16. package/dev-factory/model-catalog.json +222 -0
  17. package/dist/cli/hs-check.mjs +1085 -103
  18. package/dist/cli/hs-claim.mjs +936 -63
  19. package/dist/cli/hs-connect.mjs +9786 -1910
  20. package/dist/cli/hs-defer.mjs +5267 -3053
  21. package/dist/cli/hs-dev.mjs +564 -0
  22. package/dist/cli/hs-env.mjs +385 -299
  23. package/dist/cli/hs-index-rebuild.mjs +7129 -0
  24. package/dist/cli/hs-index-reconcile.mjs +7852 -0
  25. package/dist/cli/hs-init.mjs +777 -138
  26. package/dist/cli/hs-install.mjs +6583 -485
  27. package/dist/cli/hs-join.mjs +10137 -2368
  28. package/dist/cli/hs-vault-pull.mjs +4659 -5882
  29. package/dist/cli/hs-vault.mjs +3793 -1537
  30. package/dist/cli/hs-workspace.mjs +2550 -524
  31. package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
  32. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
  33. package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
  34. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
  35. package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
  36. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
  37. package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
  38. package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
  39. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
  40. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
  41. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
  42. package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
  43. package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
  44. package/dist/cli-templates/harness-commands/claude/hs-ground.md +28 -0
  45. package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
  46. package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
  47. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
  48. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
  49. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
  50. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
  51. package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
  52. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
  53. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
  54. package/dist/cli-templates/harness-commands/claude/hs-view.md +26 -0
  55. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
  56. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
  57. package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
  58. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
  59. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
  60. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
  61. package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
  62. package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
  63. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +31 -0
  64. package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
  65. package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
  66. package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
  67. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
  68. package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
  69. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
  70. package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
  71. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
  72. package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
  73. package/dist/cli-templates/harness-commands/cursor/hs-view.md +30 -0
  74. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
  75. package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
  76. package/docs/mcp-client-config.md +259 -6
  77. package/package.json +231 -124
  78. package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
  79. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
  80. package/plugins/heyspex/.mcp.json +12 -0
  81. package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
  82. package/plugins/heyspex/bin/run-hook.mjs +62 -0
  83. package/plugins/heyspex/hooks/cursor-hooks.json +227 -0
  84. package/plugins/heyspex/hooks/hooks.json +361 -0
  85. package/plugins/heyspex/mcp.json +13 -0
  86. package/scripts/check-mcp-freshness.mjs +17 -9
  87. package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
  88. package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
  89. package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
  90. package/scripts/claude-hooks/_intent-classifier-router.mjs +410 -0
  91. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +351 -0
  92. package/scripts/claude-hooks/_run.mjs +120 -0
  93. package/scripts/claude-hooks/_transcript-text.mjs +60 -0
  94. package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
  95. package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
  96. package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
  97. package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
  98. package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
  99. package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
  100. package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
  101. package/scripts/claude-hooks/claim-liveness-shared.mjs +575 -0
  102. package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
  103. package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
  104. package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
  105. package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
  106. package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
  107. package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
  108. package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
  109. package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
  110. package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
  111. package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
  112. package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
  113. package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
  114. package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
  115. package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
  116. package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
  117. package/scripts/claude-hooks/inject-communication-style.mjs +66 -0
  118. package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
  119. package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
  120. package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
  121. package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
  122. package/scripts/claude-hooks/intent-classifier-trigger.mjs +247 -0
  123. package/scripts/claude-hooks/log-session-activity.mjs +341 -0
  124. package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
  125. package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
  126. package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
  127. package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
  128. package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
  129. package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
  130. package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
  131. package/scripts/claude-hooks/research-method-guard.mjs +398 -0
  132. package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
  133. package/scripts/claude-hooks/session-end.mjs +449 -0
  134. package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
  135. package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
  136. package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
  137. package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
  138. package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
  139. package/scripts/claude-hooks/wip-checkpoint.mjs +298 -0
  140. package/scripts/cursor-hooks/manifest.mjs +235 -0
  141. package/scripts/dev-server-status.mjs +34 -13
  142. package/scripts/dev-server-stop.mjs +313 -0
  143. package/scripts/heyspex-mode.mjs +268 -0
  144. package/scripts/hs-auto-resolve.mjs +240 -0
  145. package/scripts/hs-auto-source.mjs +398 -0
  146. package/scripts/hs-auto.mjs +134 -0
  147. package/scripts/hs-capture-cli.mjs +326 -0
  148. package/scripts/hs-classifier.mjs +136 -0
  149. package/scripts/hs-companion.mjs +88 -0
  150. package/scripts/hs-gateway.mjs +115 -0
  151. package/scripts/hs-mode.mjs +125 -0
  152. package/scripts/hs-next.mjs +441 -0
  153. package/scripts/hs-settings.mjs +338 -0
  154. package/scripts/hs-view.mjs +333 -0
  155. package/scripts/hs-worktree.mjs +201 -0
  156. package/scripts/install-claude-code-hooks.mjs +270 -0
  157. package/scripts/install-cursor-hooks.mjs +471 -0
  158. package/scripts/lib/agent-env-markers.mjs +38 -0
  159. package/scripts/lib/console-url-resolver.mjs +197 -60
  160. package/scripts/lib/customer-kit.mjs +886 -0
  161. package/scripts/lib/dev-mode.mjs +244 -28
  162. package/scripts/lib/heyspex-home.mjs +72 -0
  163. package/scripts/lib/hs-command.mjs +277 -0
  164. package/scripts/lib/hs-companion-settings.mjs +472 -0
  165. package/scripts/lib/hs-source-commit.mjs +59 -5
  166. package/scripts/lib/hs-source-identity.mjs +526 -0
  167. package/scripts/lib/hs-source-log.mjs +11 -21
  168. package/scripts/lib/hs-source-push.mjs +107 -58
  169. package/scripts/lib/hs-worktree-args.mjs +115 -0
  170. package/scripts/lib/shared-path-helpers.mjs +134 -0
  171. package/scripts/lib/standalone-doctor.mjs +540 -3
  172. package/scripts/lib/subagent-git-lock.mjs +109 -0
  173. package/scripts/lib/template-pin.mjs +34 -0
  174. package/scripts/provenance-lib.mjs +91 -0
  175. package/scripts/stamp-provenance-on-edit.mjs +222 -0
  176. package/scripts/wip-ledger-health.mjs +653 -0
  177. package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
  178. package/spexharness/src/adapters/audit.mjs +197 -0
  179. package/spexharness/src/adapters/claude-code.mjs +441 -0
  180. package/spexharness/src/chat/tui/ansi.mjs +448 -0
  181. package/spexharness/src/chat/tui/palette.mjs +22 -0
  182. package/spexharness/src/chat/tui/select.mjs +208 -0
  183. package/spexharness/src/config-root.mjs +54 -0
  184. package/spexharness/src/context-budget.mjs +141 -0
  185. package/spexharness/src/dev-server-control.mjs +389 -0
  186. package/spexharness/src/doctrine/behaviour.mjs +371 -0
  187. package/spexharness/src/doctrine/corpus.mjs +733 -0
  188. package/spexharness/src/doctrine/inject.mjs +338 -0
  189. package/spexharness/src/doctrine/merge.mjs +184 -0
  190. package/spexharness/src/doctrine/sources.mjs +252 -0
  191. package/spexharness/src/doctrine/synthesize.mjs +401 -0
  192. package/spexharness/src/doctrine/vault-source.mjs +574 -0
  193. package/spexharness/src/doctrine/writer.mjs +176 -0
  194. package/spexharness/src/events.mjs +84 -0
  195. package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
  196. package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
  197. package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
  198. package/spexharness/src/gates/deep-research-cost-gate.mjs +274 -0
  199. package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
  200. package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
  201. package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
  202. package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
  203. package/spexharness/src/gates/git-identity-guard.mjs +61 -0
  204. package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
  205. package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
  206. package/spexharness/src/gates/git-stash-gate.mjs +159 -0
  207. package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
  208. package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
  209. package/spexharness/src/gates/inject-vault-context.mjs +239 -0
  210. package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
  211. package/spexharness/src/gates/plan-review-gate.mjs +233 -0
  212. package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
  213. package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
  214. package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
  215. package/spexharness/src/gates/settings-write-gate.mjs +104 -0
  216. package/spexharness/src/git-reflog-audit.mjs +294 -0
  217. package/spexharness/src/intent-classifier.mjs +194 -0
  218. package/spexharness/src/intent-directive.mjs +541 -0
  219. package/spexharness/src/profile/learn.mjs +551 -0
  220. package/spexharness/src/profile/proposals.mjs +357 -0
  221. package/spexharness/src/profile/vault-inclusion.mjs +386 -0
  222. package/spexharness/src/redact.mjs +151 -0
  223. package/spexharness/src/rule-table.mjs +819 -0
  224. package/spexharness/src/seat-context.mjs +122 -0
  225. package/spexharness/src/session-bootstrap.mjs +512 -0
  226. package/spexharness/src/session-record.mjs +815 -0
  227. package/spexharness/src/token-inventory.mjs +207 -0
  228. package/vendor/heyspex-mcp/account-switch.mjs +95 -0
  229. package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
  230. package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
  231. package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
  232. package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
  233. package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
  234. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +46 -5
  235. package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
  236. package/vendor/heyspex-mcp/capture-console-client.mjs +80 -1
  237. package/vendor/heyspex-mcp/capture-focus.mjs +1035 -29
  238. package/vendor/heyspex-mcp/capture-outbox.mjs +5 -1
  239. package/vendor/heyspex-mcp/capture-tools.mjs +1219 -0
  240. package/vendor/heyspex-mcp/claim-tools.mjs +1124 -0
  241. package/vendor/heyspex-mcp/cli-verbs.mjs +2076 -0
  242. package/vendor/heyspex-mcp/cli.mjs +201 -15524
  243. package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
  244. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +412 -0
  245. package/vendor/heyspex-mcp/config-seed.yaml +15 -0
  246. package/vendor/heyspex-mcp/config-settings.mjs +290 -7
  247. package/vendor/heyspex-mcp/console-fetch.mjs +657 -16
  248. package/vendor/heyspex-mcp/console-transport.mjs +166 -0
  249. package/vendor/heyspex-mcp/credentials.mjs +584 -0
  250. package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
  251. package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
  252. package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
  253. package/vendor/heyspex-mcp/design-tools.mjs +4 -1
  254. package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
  255. package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
  256. package/vendor/heyspex-mcp/dev-tools.mjs +124 -0
  257. package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
  258. package/vendor/heyspex-mcp/events-tools.mjs +207 -0
  259. package/vendor/heyspex-mcp/filing-tools.mjs +688 -0
  260. package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
  261. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +69 -14
  262. package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
  263. package/vendor/heyspex-mcp/folder-trust-store.mjs +59 -0
  264. package/vendor/heyspex-mcp/http-transport.mjs +300 -0
  265. package/vendor/heyspex-mcp/list-tools.mjs +257 -0
  266. package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
  267. package/vendor/heyspex-mcp/mcp-config.mjs +94 -0
  268. package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
  269. package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
  270. package/vendor/heyspex-mcp/occupied-keys.mjs +141 -0
  271. package/vendor/heyspex-mcp/on-deck-brief.mjs +3360 -0
  272. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
  273. package/vendor/heyspex-mcp/package-root.mjs +32 -0
  274. package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
  275. package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
  276. package/vendor/heyspex-mcp/plan-push.mjs +906 -21
  277. package/vendor/heyspex-mcp/plan-review.mjs +124 -30
  278. package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
  279. package/vendor/heyspex-mcp/procedure-handlers.mjs +325 -0
  280. package/vendor/heyspex-mcp/procedure-tools.mjs +540 -351
  281. package/vendor/heyspex-mcp/promote-capture.mjs +386 -69
  282. package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
  283. package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
  284. package/vendor/heyspex-mcp/promote-research.mjs +660 -53
  285. package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
  286. package/vendor/heyspex-mcp/prompts.mjs +1460 -0
  287. package/vendor/heyspex-mcp/read-path.mjs +29 -9
  288. package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
  289. package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
  290. package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
  291. package/vendor/heyspex-mcp/register-spec.mjs +266 -54
  292. package/vendor/heyspex-mcp/request-context.mjs +59 -0
  293. package/vendor/heyspex-mcp/research-focus.mjs +207 -4
  294. package/vendor/heyspex-mcp/research-handlers.mjs +623 -0
  295. package/vendor/heyspex-mcp/research-tools.mjs +4 -4
  296. package/vendor/heyspex-mcp/response-budget.mjs +348 -0
  297. package/vendor/heyspex-mcp/review-focus.mjs +25 -1
  298. package/vendor/heyspex-mcp/search-specs.mjs +280 -0
  299. package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
  300. package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
  301. package/vendor/heyspex-mcp/session-owner.mjs +108 -0
  302. package/vendor/heyspex-mcp/session-tools.mjs +152 -0
  303. package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
  304. package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
  305. package/vendor/heyspex-mcp/spec-edit-tools.mjs +446 -0
  306. package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
  307. package/vendor/heyspex-mcp/split-research.mjs +301 -56
  308. package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
  309. package/vendor/heyspex-mcp/submit-for-review.mjs +132 -6
  310. package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
  311. package/vendor/heyspex-mcp/summary-write-tools.mjs +224 -0
  312. package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
  313. package/vendor/heyspex-mcp/test-helpers.mjs +108 -1
  314. package/vendor/heyspex-mcp/tool-catalog.mjs +2813 -0
  315. package/vendor/heyspex-mcp/tool-dispatch.mjs +639 -0
  316. package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
  317. package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
  318. package/vendor/heyspex-mcp/vault-io.mjs +724 -0
  319. package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
  320. package/vendor/heyspex-mcp/vault-resolve.mjs +366 -68
  321. package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
  322. package/vendor/heyspex-mcp/whoami.mjs +91 -0
@@ -0,0 +1,294 @@
1
+ // spexharness/src/git-reflog-audit.mjs
2
+ //
3
+ // The SHARED session-close git audit — moved here verbatim from
4
+ // scripts/claude-hooks/git-reflog-audit.mjs (task HS-610I-P3TJ, epic
5
+ // HS-GG01-P3TJ) as wave 6 of [[HS-AZ61-NYPR]] turned that hook into a thin
6
+ // shim over ./gates/git-reflog-audit.mjs. There is exactly ONE copy of the
7
+ // reflog parsing in the tree: this one. The gate imports it; the Claude Code
8
+ // shim reaches it through the gate; scripts/claude-hooks/git-reflog-audit.test.mjs
9
+ // (the original regression suite, unchanged apart from its import path) drives
10
+ // it directly.
11
+ //
12
+ // WHY the rule exists (kept from the original header): every other gate in this
13
+ // family (git-stash-gate, protected-action-intent) works by INTERCEPTING a
14
+ // destructive command as it is issued — bypassed, disabled, or routed around a
15
+ // gap in coverage, the action leaves no independent trace. This is a WITNESS,
16
+ // not a gate: it snapshots git state at session start and diffs it at session
17
+ // close, so a stash or a reset/checkout/rebase that slipped past every other
18
+ // control still shows up in .heyspex/guardrail-history.jsonl — a record
19
+ // independent of the acting agent's honesty.
20
+ //
21
+ // SCOPE: warn only. Never blocks, never denies.
22
+ //
23
+ // Snapshot (session start): writes `{ stash_count, reflog_head_entry,
24
+ // written_at }` to a SESSION-SCOPED file — the filename carries the session id
25
+ // so concurrent sessions on the same working tree never clobber each other's
26
+ // baseline — at `.heyspex/git-reflog-audit/<sessionId>.json`.
27
+ //
28
+ // Diff (stop / session end):
29
+ // - stash: any INCREASE in `git stash list`'s entry count since the snapshot
30
+ // names the new top entries (a push always lands at stash@{0}).
31
+ // - reflog: any HEAD reflog entry newer than the snapshot's recorded head
32
+ // entry whose action is reset/checkout/rebase is named. Plain commits newer
33
+ // than the snapshot are NOT flagged — that's normal work. Matching strips
34
+ // the `HEAD@{N}:` index label before comparing (that label shifts every time
35
+ // a new entry lands, even when the underlying hash+message didn't change) —
36
+ // see [[never-chain-cd-with-destructive-git-a]] for why a naive hash-only
37
+ // compare is not enough: `git stash push` logs its own internal
38
+ // `reset: moving to HEAD` entry against the SAME hash as the just-committed
39
+ // HEAD, so hash equality alone can't tell old from new.
40
+ // A clean session (no stash growth, no reset/checkout/rebase reflog entries)
41
+ // appends nothing.
42
+ //
43
+ // TEST ISOLATION: every git call here takes an explicit repoRootPath and shells
44
+ // out as `git -C <repoRootPath> ...` — never a bare `git` relying on cwd, and
45
+ // never a `cd X && git ...` chain.
46
+ //
47
+ // FAIL-SAFE: every exported function degrades to an inert value (null / [] /
48
+ // false) on any git or filesystem failure rather than throwing — this must
49
+ // never break a session start or a session close on its own bug.
50
+
51
+ import fs from 'node:fs';
52
+ import path from 'node:path';
53
+ import { spawnSync } from 'node:child_process';
54
+ import { fileURLToPath } from 'node:url';
55
+
56
+ import { appendGuardrailEvent } from '../../scripts/claude-hooks/_guardrail-events.mjs';
57
+
58
+ /** The `rule` id every row this module appends is stamped with. */
59
+ export const RULE_ID = 'git-reflog-audit';
60
+
61
+ /** spexharness/src/ → repo root is two levels up. */
62
+ const DEFAULT_REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
63
+
64
+ /** Env-first repo-root resolution, matching every pre-cutover hook's own helper. */
65
+ export function repoRoot(env = process.env) {
66
+ const fromEnv = env.CLAUDE_PROJECT_DIR?.trim();
67
+ if (fromEnv) return fromEnv.replace(/[\\/]+$/, '');
68
+ return DEFAULT_REPO_ROOT;
69
+ }
70
+
71
+ function git(repoRootPath, args) {
72
+ try {
73
+ const res = spawnSync('git', ['-C', repoRootPath, ...args], { encoding: 'utf8', timeout: 5000 });
74
+ if (res.status !== 0) return null;
75
+ return typeof res.stdout === 'string' ? res.stdout : '';
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * `git stash list`, newest first (`stash@{0}` is the top). [] on any failure
83
+ * (not a repo, git unavailable, etc.) — never throws.
84
+ */
85
+ export function readStashList(repoRootPath) {
86
+ const out = git(repoRootPath, ['stash', 'list']);
87
+ if (out === null) return [];
88
+ return out.split(/\r?\n/).filter(Boolean);
89
+ }
90
+
91
+ /**
92
+ * `git reflog -n <limit>`, newest first. [] on any failure (including a
93
+ * brand-new repo with no commits yet, where HEAD has no reflog at all).
94
+ */
95
+ export function readReflogLines(repoRootPath, limit = 50) {
96
+ const out = git(repoRootPath, ['reflog', '-n', String(limit)]);
97
+ if (out === null) return [];
98
+ return out.split(/\r?\n/).filter(Boolean);
99
+ }
100
+
101
+ /**
102
+ * Strip the `HEAD@{N}:` index label from a reflog line before comparing two
103
+ * lines captured at different times — the label shifts by one every time a new
104
+ * entry is pushed, even when the underlying hash+message is unchanged, so
105
+ * comparing raw lines would never match a genuinely-unchanged entry.
106
+ */
107
+ export function normalizeReflogLine(line) {
108
+ return typeof line === 'string' ? line.replace(/HEAD@\{\d+\}:/, '').trim() : null;
109
+ }
110
+
111
+ /**
112
+ * The reflog "action" word for a line — the text right after `HEAD@{N}: ` and
113
+ * before the next space/colon/paren (`reset: moving to ...` → `reset`;
114
+ * `rebase (finish): ...` → `rebase`). null when the line doesn't have the
115
+ * expected shape — never throws.
116
+ */
117
+ export function reflogAction(line) {
118
+ if (typeof line !== 'string') return null;
119
+ const m = /HEAD@\{\d+\}:\s*([a-zA-Z-]+)/.exec(line);
120
+ return m ? m[1].toLowerCase() : null;
121
+ }
122
+
123
+ const FLAGGED_REFLOG_ACTIONS = new Set(['reset', 'checkout', 'rebase']);
124
+
125
+ /**
126
+ * Snapshot the git state this audit cares about. Never throws — a repo with no
127
+ * commits yet (empty reflog) or no stash entries yields zeros/nulls.
128
+ */
129
+ export function captureGitState(repoRootPath) {
130
+ const stashLines = readStashList(repoRootPath);
131
+ const reflogLines = readReflogLines(repoRootPath, 1);
132
+ return {
133
+ stash_count: stashLines.length,
134
+ reflog_head_entry: reflogLines[0] ?? null,
135
+ };
136
+ }
137
+
138
+ function sanitizeSessionId(sessionId) {
139
+ const raw =
140
+ typeof sessionId === 'string' && sessionId.trim() ? sessionId.trim() : 'unknown-session';
141
+ return raw.replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 120);
142
+ }
143
+
144
+ /** Default session-scoped snapshot path — `.heyspex/git-reflog-audit/<sessionId>.json`. */
145
+ export function snapshotPathFor(repoRootPath, sessionId, snapshotDir = 'git-reflog-audit') {
146
+ return path.join(repoRootPath, '.heyspex', snapshotDir, `${sanitizeSessionId(sessionId)}.json`);
147
+ }
148
+
149
+ /**
150
+ * Session-start half: write the snapshot file. Returns the written snapshot
151
+ * object, or null on any failure (fail-safe — a write failure here must never
152
+ * break session start).
153
+ */
154
+ export function writeSnapshot({ repoRootPath = repoRoot(), sessionId, snapshotPath } = {}) {
155
+ try {
156
+ const state = captureGitState(repoRootPath);
157
+ const snapshot = { ...state, written_at: new Date().toISOString() };
158
+ const filePath = snapshotPath || snapshotPathFor(repoRootPath, sessionId);
159
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
160
+ fs.writeFileSync(filePath, JSON.stringify(snapshot));
161
+ return snapshot;
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+
167
+ /** Read a previously-written snapshot. null on any failure (missing/corrupt file). */
168
+ export function readSnapshot(snapshotPath) {
169
+ try {
170
+ const raw = fs.readFileSync(snapshotPath, 'utf8');
171
+ const parsed = JSON.parse(raw);
172
+ return parsed && typeof parsed === 'object' ? parsed : null;
173
+ } catch {
174
+ return null;
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Diff current git state against a snapshot. Returns
180
+ * { newStashEntries: string[], flaggedReflogEntries: string[] }
181
+ * — both empty on a clean session.
182
+ */
183
+ export function diffAgainstSnapshot(repoRootPath, snapshot) {
184
+ const result = { newStashEntries: [], flaggedReflogEntries: [] };
185
+ if (!snapshot || typeof snapshot !== 'object') return result;
186
+
187
+ // --- stash growth -------------------------------------------------------
188
+ const currentStash = readStashList(repoRootPath);
189
+ const priorCount = Number.isFinite(snapshot.stash_count) ? snapshot.stash_count : 0;
190
+ const grown = currentStash.length - priorCount;
191
+ if (grown > 0) {
192
+ result.newStashEntries = currentStash.slice(0, grown);
193
+ }
194
+
195
+ // --- reflog: reset/checkout/rebase entries newer than the snapshot ------
196
+ const priorEntry =
197
+ typeof snapshot.reflog_head_entry === 'string' ? snapshot.reflog_head_entry : null;
198
+ const currentReflog = readReflogLines(repoRootPath, 100);
199
+ if (currentReflog.length && currentReflog[0] !== priorEntry) {
200
+ const priorNormalized = normalizeReflogLine(priorEntry);
201
+ const idx =
202
+ priorNormalized === null
203
+ ? -1
204
+ : currentReflog.findIndex((line) => normalizeReflogLine(line) === priorNormalized);
205
+ // idx === -1 means no baseline (fresh repo) or the old entry fell outside
206
+ // the fetch window — bounded fallback: everything fetched is "new".
207
+ const newLines = idx === -1 ? currentReflog : currentReflog.slice(0, idx);
208
+ result.flaggedReflogEntries = newLines.filter((line) =>
209
+ FLAGGED_REFLOG_ACTIONS.has(reflogAction(line)),
210
+ );
211
+ }
212
+
213
+ return result;
214
+ }
215
+
216
+ /**
217
+ * The human-readable half of a flagged diff — `new stash: … ; reflog: …`, the
218
+ * exact `matchContext` string the guardrail-history row carries. Pure.
219
+ *
220
+ * @param {{ newStashEntries?: string[], flaggedReflogEntries?: string[] }} diff
221
+ * @returns {string} '' when nothing was flagged.
222
+ */
223
+ export function describeDiff(diff) {
224
+ const parts = [];
225
+ if (diff?.newStashEntries?.length) parts.push(`new stash: ${diff.newStashEntries.join(' | ')}`);
226
+ if (diff?.flaggedReflogEntries?.length) {
227
+ parts.push(`reflog: ${diff.flaggedReflogEntries.join(' | ')}`);
228
+ }
229
+ return parts.join(' ; ');
230
+ }
231
+
232
+ /** True when a diff has anything worth warning about. Pure. */
233
+ export function isFlagged(diff) {
234
+ return Boolean(diff?.newStashEntries?.length || diff?.flaggedReflogEntries?.length);
235
+ }
236
+
237
+ /**
238
+ * Stop / session-end half: read the snapshot, diff, and — only if something was
239
+ * flagged — append ONE warn row to guardrail-history.jsonl naming it. Returns
240
+ * the diff result (so a caller/test can assert on it), or null when there was
241
+ * no snapshot to compare against (nothing to do, no false-flag risk).
242
+ *
243
+ * @param {{ repoRootPath?: string, sessionId?: string, snapshotPath?: string,
244
+ * append?: (row: object, repoRoot: string) => unknown }} [opts]
245
+ * `append` — override the guardrail-history writer (tests, and any front that
246
+ * wants the decision without the file write).
247
+ */
248
+ export function runSessionEnd({
249
+ repoRootPath = repoRoot(),
250
+ sessionId,
251
+ snapshotPath,
252
+ append = appendGuardrailEvent,
253
+ } = {}) {
254
+ const filePath = snapshotPath || snapshotPathFor(repoRootPath, sessionId);
255
+ const snapshot = readSnapshot(filePath);
256
+ if (!snapshot) return null;
257
+
258
+ const diff = diffAgainstSnapshot(repoRootPath, snapshot);
259
+ if (!isFlagged(diff)) return diff;
260
+
261
+ try {
262
+ append(
263
+ {
264
+ rule: RULE_ID,
265
+ surface: 'session-close',
266
+ severity: 'warn',
267
+ decision: 'warn',
268
+ matchContext: describeDiff(diff),
269
+ },
270
+ repoRootPath,
271
+ );
272
+ } catch {
273
+ // observability must never break the session close
274
+ }
275
+
276
+ return diff;
277
+ }
278
+
279
+ export default {
280
+ RULE_ID,
281
+ captureGitState,
282
+ describeDiff,
283
+ diffAgainstSnapshot,
284
+ isFlagged,
285
+ normalizeReflogLine,
286
+ readReflogLines,
287
+ readSnapshot,
288
+ readStashList,
289
+ reflogAction,
290
+ repoRoot,
291
+ runSessionEnd,
292
+ snapshotPathFor,
293
+ writeSnapshot,
294
+ };
@@ -0,0 +1,194 @@
1
+ // spexharness/src/intent-classifier.mjs
2
+ //
3
+ // Intent classification as an engine module on the prompt_submit event (HS-5ISS, epic
4
+ // HS-1YDJ), replacing scripts/claude-hooks/_intent-classifier-router.mjs's per-prompt prose
5
+ // injection with a mechanical lexical check against the engine's own compiled rule table
6
+ // (./rule-table.mjs's loadRuleTable — the same registry _guardrail-loader.mjs reads, so this
7
+ // module and the Claude Code hook agree about what a rule's pattern set and inject text are;
8
+ // neither re-derives the other's regex).
9
+ //
10
+ // THE COST FIX (why this task exists): the original hook's Layer 2 ("self_check" semantic
11
+ // mode) injects a multi-behavior "gate" block — one section per classifier behavior, several
12
+ // KB of behavior descriptions — on every prompt that MISSES every lexical pattern and is at
13
+ // least 12 characters long. That fires on almost every substantive prompt (an ordinary work
14
+ // order matches none of the five behaviors' regex), which is the ~6.5 KB/prompt the epic
15
+ // measured. This module drops that speculative layer entirely: classification here is
16
+ // LEXICAL ONLY. A rule's `inject` text is resolved and returned only when one of its own
17
+ // compiled patterns actually matches the prompt (AC2, "the directive is fetched only when a
18
+ // gate fires") — a prompt that matches nothing produces `{ decision: 'allow' }` with no
19
+ // injected text at all, which is the <500-byte target on a plain prompt (AC3).
20
+ //
21
+ // SCOPE NOTE: `classifyPrompt` is the LEXICAL core and stays pure — it returns the matched
22
+ // rule's raw inject, nothing more. The per-behavior enrichment the plugin applies on top —
23
+ // capture-on-idea's {{CONFIRM_MODE}} substitution, the capture-kind and thread-split-mode
24
+ // hints, research-before-decision's appended source hint, the env/config kill switches, the
25
+ // injected-content guard and the CONFIRMED MATCH marker — used to live only in
26
+ // scripts/claude-hooks/_intent-classifier-router.mjs, so a SpexCode seat saw a different
27
+ // (and, with the literal `{{CONFIRM_MODE}}` token, visibly unfinished) directive from the
28
+ // one Claude Code injects for the same prompt. HS-N01T moved that assembly into
29
+ // ./intent-directive.mjs, which BOTH the router and this gate's `run()` call — so the inject
30
+ // this gate returns is now byte-identical to the plugin's. The semantic (Layer 2 self-check /
31
+ // opt-in model-classify) layer stays in the router, unported.
32
+ //
33
+ // Pure: no I/O of its own beyond loadRuleTable's fs.readFileSync (itself fail-safe — see
34
+ // rule-table.mjs's header). No process.stdin/stdout/exit.
35
+
36
+ import { loadRuleTable } from './rule-table.mjs';
37
+ import { buildIntentDirective } from './intent-directive.mjs';
38
+
39
+ /** This gate's id, for a registry or an audit row. */
40
+ export const GATE_ID = 'intent-classifier';
41
+ /** Runs on the prompt-submit event, mirroring the retired hook's UserPromptSubmit wiring. */
42
+ export const EVENT = 'prompt_submit';
43
+
44
+ /**
45
+ * The rule table carries every `userpromptsubmit` rule, including
46
+ * `communication-style-prime` (HS-XIYJ's rule — an unconditional `[\s\S]` pattern that would
47
+ * "match" every prompt) and severity `prime` rules with no patterns at all. Intent
48
+ * classification is specifically the five mutually-exclusive INTENT rules the original
49
+ * router's `CLASSIFIER_BEHAVIORS` catalog named — restricting to these ids is what keeps
50
+ * this module from reinventing the always-on communication-style injection XIYJ retires
51
+ * separately.
52
+ * @type {ReadonlyArray<string>}
53
+ */
54
+ export const CLASSIFIER_RULE_IDS = Object.freeze([
55
+ 'capture-on-idea',
56
+ 'research-before-decision',
57
+ 'settings-intent',
58
+ 'defer-intent',
59
+ 'spec-reference-lookup',
60
+ ]);
61
+
62
+ /** Compile a rule's string patterns into case-insensitive RegExps, skipping any that fail
63
+ * to compile (a corrupt pattern degrades to "never matches", never a throw). */
64
+ function compilePatterns(rule) {
65
+ const patterns = Array.isArray(rule?.patterns) ? rule.patterns : [];
66
+ const compiled = [];
67
+ for (const source of patterns) {
68
+ try {
69
+ compiled.push(new RegExp(source, 'i'));
70
+ } catch {
71
+ // corrupt pattern in the table — skip it, fail-safe.
72
+ }
73
+ }
74
+ return compiled;
75
+ }
76
+
77
+ /** The classifier rules eligible to fire, in {@link CLASSIFIER_RULE_IDS} order (the
78
+ * deterministic lexical tiebreak, mirroring the original router's BEHAVIOR_ORDER). */
79
+ function classifierRules(table) {
80
+ const byId = new Map((table?.rules ?? []).map((r) => [r?.rule, r]));
81
+ const rules = [];
82
+ for (const id of CLASSIFIER_RULE_IDS) {
83
+ const rule = byId.get(id);
84
+ if (rule && typeof rule.inject === 'string' && rule.inject.trim() && rule.surfaces?.includes('userpromptsubmit')) {
85
+ rules.push(rule);
86
+ }
87
+ }
88
+ return rules;
89
+ }
90
+
91
+ /**
92
+ * @typedef {object} IntentClassification
93
+ * @property {string} rule the matched rule's id (e.g. 'capture-on-idea').
94
+ * @property {string} sourceProtocol the protocol that authored the rule.
95
+ * @property {string} inject the rule's raw inject text (unmodified — see the
96
+ * SCOPE NOTE above; per-behavior enrichment/substitution is not applied here).
97
+ */
98
+
99
+ /**
100
+ * Classify a prompt against the engine's compiled rule table. First matching rule (in
101
+ * {@link CLASSIFIER_RULE_IDS} order) wins; a full miss returns null — no directive is
102
+ * resolved, matching AC2/AC3.
103
+ *
104
+ * @param {string} prompt
105
+ * @param {{ vaultRoot?: string, ruleTable?: { rules: object[] } }} [opts]
106
+ * `ruleTable` is injectable so a test never depends on the live
107
+ * `.heyspex/guardrails.json`/built-in table; omitted, it loads the real one via
108
+ * {@link loadRuleTable}.
109
+ * @returns {IntentClassification|null}
110
+ */
111
+ export function classifyPrompt(prompt, { vaultRoot, ruleTable } = {}) {
112
+ if (typeof prompt !== 'string' || !prompt.trim()) return null;
113
+ const table = ruleTable ?? loadRuleTable({ vaultRoot });
114
+ for (const rule of classifierRules(table)) {
115
+ const patterns = compilePatterns(rule);
116
+ if (patterns.some((re) => re.test(prompt))) {
117
+ return { rule: rule.rule, sourceProtocol: rule.sourceProtocol ?? null, inject: rule.inject };
118
+ }
119
+ }
120
+ return null;
121
+ }
122
+
123
+ /**
124
+ * The engine gate: runs on `prompt_submit`, classifies the prompt, and only when a rule
125
+ * actually fires does it return an `inject` decision carrying that rule's directive. A miss
126
+ * — including the common "plain prompt, matches nothing" case — is a plain `allow` with no
127
+ * injected text, satisfying AC3's <500-byte-on-a-plain-prompt target by construction (an
128
+ * `allow` decision carries no `inject` field at all).
129
+ *
130
+ * HS-N01T: a hit is then run through ./intent-directive.mjs's `buildIntentDirective` — the
131
+ * SAME eligibility check (env kill switch, config toggle, injected-content guard) and the
132
+ * SAME directive assembly (confirm-mode substitution, kind/routing hints, CONFIRMED MATCH
133
+ * marker) the Claude Code hook applies — so `inject.additionalContext` is what the plugin
134
+ * would have injected, never the raw inject with a literal `{{CONFIRM_MODE}}`. An
135
+ * ineligible behavior is an `allow`. A rule the behavior catalog does not know (or a row
136
+ * the plugin router would never fire) keeps the pre-N01T raw passthrough.
137
+ *
138
+ * `ctx` (all optional): `vaultRoot`/`repoRoot` — the app root holding `.heyspex/`;
139
+ * `ruleTable` — an injected table (tests); `env` — env overrides instead of `process.env`;
140
+ * `getSetting(repoRoot, key)` — an injected settings reader so a test never reads the real
141
+ * config (see ./intent-directive.mjs's header for the accepted shapes).
142
+ *
143
+ * @param {{ point: string, prompt?: string }} evt
144
+ * @param {{ vaultRoot?: string, repoRoot?: string, ruleTable?: object, env?: object, getSetting?: Function }} [ctx]
145
+ * @returns {{ decision: 'allow' }|{ decision: 'inject', reason: string, inject: { additionalContext: string, rule: string, behavior: string|null } }}
146
+ */
147
+ export function run(evt, ctx = {}) {
148
+ const repoRoot = ctx.repoRoot ?? ctx.vaultRoot;
149
+ // One table load for both the lexical match and the directive resolution.
150
+ const ruleTable = ctx.ruleTable ?? loadRuleTable({ vaultRoot: repoRoot });
151
+ const hit = classifyPrompt(evt?.prompt, { ruleTable });
152
+ if (!hit) return { decision: 'allow' };
153
+ let built;
154
+ try {
155
+ built = buildIntentDirective(hit.rule, evt.prompt, {
156
+ repoRoot,
157
+ ruleTable,
158
+ env: ctx.env,
159
+ getSetting: ctx.getSetting,
160
+ });
161
+ } catch {
162
+ // The directive assembly is best-effort enrichment; a crash in it must never turn a
163
+ // confirmed lexical fire into silence. Degrade to the raw inject.
164
+ built = { behavior: null, additionalContext: null };
165
+ }
166
+ if (built === null) return { decision: 'allow' }; // killed by env/config/injected-content guard
167
+ return {
168
+ decision: 'inject',
169
+ reason: `intent-classifier: ${hit.rule} fired`,
170
+ inject: {
171
+ additionalContext: built.additionalContext ?? hit.inject,
172
+ rule: hit.rule,
173
+ behavior: built.behavior,
174
+ },
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Caller-defined severity label. `prime` is the severity every rule in
180
+ * {@link CLASSIFIER_RULE_IDS} carries in the compiled registry — this gate never
181
+ * denies, it only primes a turn with the matched rule's directive.
182
+ *
183
+ * Added in wave 6 of [[HS-AZ61-NYPR]]: the module was written as a gate but was
184
+ * registered by nothing, so the classification it owns ran on no front at all.
185
+ * spexharness/src/gates/index.mjs now registers it (a registry row needs a
186
+ * severity for `hs harness gates` to print, hence this export), and
187
+ * spexharness/src/plugin-hook-coverage.test.mjs pins that registration so the
188
+ * Claude Code `intent-classifier-trigger` hook's coverage cannot silently lapse
189
+ * again.
190
+ */
191
+ export const SEVERITY = 'prime';
192
+
193
+ const gate = { id: GATE_ID, event: EVENT, severity: SEVERITY, run };
194
+ export default gate;