heyspex 0.1.0 → 0.2.1

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 (352) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/README.md +21 -0
  3. package/bin/hs.mjs +1089 -104
  4. package/bin/spexcode.mjs +262 -0
  5. package/dev-factory/engine/worktree.mjs +740 -0
  6. package/dev-factory/lib/code-repo-root.mjs +67 -0
  7. package/dev-factory/lib/context-budget.mjs +84 -0
  8. package/dev-factory/lib/epic-detect.mjs +190 -0
  9. package/dev-factory/lib/fleet-queue-file.mjs +846 -0
  10. package/dev-factory/lib/fleet-queue.mjs +426 -0
  11. package/dev-factory/lib/fleet-state.mjs +14 -18
  12. package/dev-factory/lib/model-catalog.mjs +338 -0
  13. package/dev-factory/lib/seat-context.mjs +199 -0
  14. package/dev-factory/lib/spec-io.mjs +93 -0
  15. package/dev-factory/lib/sufficiency-score.mjs +8 -2
  16. package/dev-factory/lib/worktree-allocator.mjs +744 -0
  17. package/dev-factory/model-catalog.json +222 -0
  18. package/dist/cli/hs-check.mjs +1127 -103
  19. package/dist/cli/hs-claim.mjs +981 -63
  20. package/dist/cli/hs-connect.mjs +10059 -1729
  21. package/dist/cli/hs-defer.mjs +5507 -3072
  22. package/dist/cli/hs-dev.mjs +576 -0
  23. package/dist/cli/hs-env.mjs +416 -302
  24. package/dist/cli/hs-index-rebuild.mjs +7334 -0
  25. package/dist/cli/hs-index-reconcile.mjs +8138 -0
  26. package/dist/cli/hs-init.mjs +2840 -149
  27. package/dist/cli/hs-install.mjs +7214 -594
  28. package/dist/cli/hs-join.mjs +10640 -2421
  29. package/dist/cli/hs-vault-pull.mjs +8157 -6370
  30. package/dist/cli/hs-vault-push.mjs +6433 -0
  31. package/dist/cli/hs-vault.mjs +4029 -1555
  32. package/dist/cli/hs-workspace.mjs +2702 -538
  33. package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
  34. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
  35. package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
  36. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
  37. package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
  38. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
  39. package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
  40. package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
  41. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
  42. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
  43. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
  44. package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
  45. package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
  46. package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -0
  47. package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
  48. package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
  49. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
  50. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
  51. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
  52. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
  53. package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
  54. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
  55. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
  56. package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -0
  57. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
  58. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
  59. package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
  60. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
  61. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
  62. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
  63. package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
  64. package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
  65. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +33 -0
  66. package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
  67. package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
  68. package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
  69. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
  70. package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
  71. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
  72. package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
  73. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
  74. package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
  75. package/dist/cli-templates/harness-commands/cursor/hs-view.md +32 -0
  76. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
  77. package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
  78. package/docs/copilot-plugin-install.md +162 -0
  79. package/docs/driving-heyspex-from-copilot.md +165 -0
  80. package/docs/mcp-client-config.md +259 -6
  81. package/package.json +249 -124
  82. package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
  83. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
  84. package/plugins/heyspex/.mcp.json +12 -0
  85. package/plugins/heyspex/README.md +309 -0
  86. package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
  87. package/plugins/heyspex/bin/run-hook.mjs +80 -0
  88. package/plugins/heyspex/hooks/cursor-hooks.json +308 -0
  89. package/plugins/heyspex/hooks/hooks.json +418 -0
  90. package/plugins/heyspex/mcp.json +13 -0
  91. package/scripts/check-mcp-freshness.mjs +17 -9
  92. package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
  93. package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
  94. package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
  95. package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -0
  96. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +356 -0
  97. package/scripts/claude-hooks/_run.mjs +413 -0
  98. package/scripts/claude-hooks/_transcript-text.mjs +60 -0
  99. package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
  100. package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
  101. package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
  102. package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
  103. package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
  104. package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
  105. package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
  106. package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
  107. package/scripts/claude-hooks/claim-liveness-shared.mjs +577 -0
  108. package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
  109. package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
  110. package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
  111. package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
  112. package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
  113. package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
  114. package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
  115. package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
  116. package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
  117. package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
  118. package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
  119. package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
  120. package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
  121. package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
  122. package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
  123. package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
  124. package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
  125. package/scripts/claude-hooks/inject-communication-style.mjs +86 -0
  126. package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
  127. package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
  128. package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
  129. package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
  130. package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -0
  131. package/scripts/claude-hooks/log-session-activity.mjs +341 -0
  132. package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
  133. package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
  134. package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
  135. package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
  136. package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
  137. package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
  138. package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
  139. package/scripts/claude-hooks/research-method-guard.mjs +398 -0
  140. package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
  141. package/scripts/claude-hooks/session-end.mjs +451 -0
  142. package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
  143. package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
  144. package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
  145. package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
  146. package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
  147. package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
  148. package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
  149. package/scripts/claude-hooks/wip-checkpoint.mjs +302 -0
  150. package/scripts/cursor-hooks/manifest.mjs +281 -0
  151. package/scripts/dev-server-status.mjs +34 -13
  152. package/scripts/dev-server-stop.mjs +313 -0
  153. package/scripts/heyspex-mode.mjs +288 -0
  154. package/scripts/hs-auto-resolve.mjs +240 -0
  155. package/scripts/hs-auto-source.mjs +398 -0
  156. package/scripts/hs-auto.mjs +134 -0
  157. package/scripts/hs-capture-cli.mjs +326 -0
  158. package/scripts/hs-classifier.mjs +136 -0
  159. package/scripts/hs-companion.mjs +88 -0
  160. package/scripts/hs-gateway.mjs +115 -0
  161. package/scripts/hs-mode.mjs +125 -0
  162. package/scripts/hs-next.mjs +441 -0
  163. package/scripts/hs-settings.mjs +338 -0
  164. package/scripts/hs-view.mjs +496 -0
  165. package/scripts/hs-worktree.mjs +201 -0
  166. package/scripts/install-claude-code-hooks.mjs +272 -0
  167. package/scripts/install-cursor-hooks.mjs +471 -0
  168. package/scripts/lib/agent-env-markers.mjs +38 -0
  169. package/scripts/lib/console-url-resolver.mjs +199 -61
  170. package/scripts/lib/customer-kit.mjs +1203 -0
  171. package/scripts/lib/dev-mode.mjs +249 -32
  172. package/scripts/lib/directive-obligations.mjs +349 -0
  173. package/scripts/lib/heyspex-home.mjs +72 -0
  174. package/scripts/lib/hook-dispositions.mjs +210 -0
  175. package/scripts/lib/hook-vault-header.mjs +30 -0
  176. package/scripts/lib/hs-command.mjs +277 -0
  177. package/scripts/lib/hs-companion-settings.mjs +472 -0
  178. package/scripts/lib/hs-source-branch.mjs +196 -0
  179. package/scripts/lib/hs-source-commit.mjs +59 -5
  180. package/scripts/lib/hs-source-identity.mjs +526 -0
  181. package/scripts/lib/hs-source-log.mjs +13 -22
  182. package/scripts/lib/hs-source-preflight.mjs +18 -4
  183. package/scripts/lib/hs-source-push.mjs +386 -307
  184. package/scripts/lib/hs-worktree-args.mjs +115 -0
  185. package/scripts/lib/shared-path-helpers.mjs +134 -0
  186. package/scripts/lib/standalone-doctor.mjs +540 -3
  187. package/scripts/lib/subagent-git-lock.mjs +109 -0
  188. package/scripts/lib/template-pin.mjs +34 -0
  189. package/scripts/lib/window-placement.mjs +109 -0
  190. package/scripts/provenance-lib.mjs +91 -0
  191. package/scripts/stamp-provenance-on-edit.mjs +222 -0
  192. package/scripts/wip-ledger-health.mjs +653 -0
  193. package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
  194. package/spexharness/src/adapters/audit.mjs +197 -0
  195. package/spexharness/src/adapters/claude-code.mjs +449 -0
  196. package/spexharness/src/adapters/copilot.mjs +236 -0
  197. package/spexharness/src/adapters/detect-harness.mjs +34 -0
  198. package/spexharness/src/chat/tui/ansi.mjs +448 -0
  199. package/spexharness/src/chat/tui/palette.mjs +22 -0
  200. package/spexharness/src/chat/tui/select.mjs +208 -0
  201. package/spexharness/src/config-root.mjs +54 -0
  202. package/spexharness/src/context-budget.mjs +141 -0
  203. package/spexharness/src/dev-server-control.mjs +389 -0
  204. package/spexharness/src/doctrine/behaviour.mjs +371 -0
  205. package/spexharness/src/doctrine/corpus.mjs +733 -0
  206. package/spexharness/src/doctrine/inject.mjs +338 -0
  207. package/spexharness/src/doctrine/merge.mjs +184 -0
  208. package/spexharness/src/doctrine/sources.mjs +252 -0
  209. package/spexharness/src/doctrine/synthesize.mjs +401 -0
  210. package/spexharness/src/doctrine/vault-source.mjs +574 -0
  211. package/spexharness/src/doctrine/writer.mjs +176 -0
  212. package/spexharness/src/events.mjs +84 -0
  213. package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
  214. package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
  215. package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
  216. package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
  217. package/spexharness/src/gates/deep-research-cost-gate.mjs +275 -0
  218. package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
  219. package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
  220. package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
  221. package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
  222. package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
  223. package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
  224. package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
  225. package/spexharness/src/gates/git-identity-guard.mjs +61 -0
  226. package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
  227. package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
  228. package/spexharness/src/gates/git-stash-gate.mjs +159 -0
  229. package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
  230. package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
  231. package/spexharness/src/gates/inject-vault-context.mjs +239 -0
  232. package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
  233. package/spexharness/src/gates/plan-review-gate.mjs +233 -0
  234. package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
  235. package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
  236. package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
  237. package/spexharness/src/gates/settings-write-gate.mjs +104 -0
  238. package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
  239. package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
  240. package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
  241. package/spexharness/src/git-reflog-audit.mjs +294 -0
  242. package/spexharness/src/intent-classifier.mjs +196 -0
  243. package/spexharness/src/intent-directive.mjs +626 -0
  244. package/spexharness/src/profile/learn.mjs +551 -0
  245. package/spexharness/src/profile/proposals.mjs +357 -0
  246. package/spexharness/src/profile/vault-inclusion.mjs +386 -0
  247. package/spexharness/src/redact.mjs +151 -0
  248. package/spexharness/src/rule-table.mjs +849 -0
  249. package/spexharness/src/seat-context.mjs +122 -0
  250. package/spexharness/src/session-bootstrap.mjs +512 -0
  251. package/spexharness/src/session-record.mjs +815 -0
  252. package/spexharness/src/token-inventory.mjs +207 -0
  253. package/vendor/heyspex-mcp/account-switch.mjs +96 -0
  254. package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
  255. package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
  256. package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
  257. package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
  258. package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
  259. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +77 -8
  260. package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
  261. package/vendor/heyspex-mcp/capture-console-client.mjs +117 -3
  262. package/vendor/heyspex-mcp/capture-focus.mjs +1061 -31
  263. package/vendor/heyspex-mcp/capture-outbox.mjs +15 -5
  264. package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
  265. package/vendor/heyspex-mcp/capture-tools.mjs +1264 -0
  266. package/vendor/heyspex-mcp/claim-tools.mjs +1147 -0
  267. package/vendor/heyspex-mcp/cli-verbs.mjs +2074 -0
  268. package/vendor/heyspex-mcp/cli.mjs +207 -15524
  269. package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
  270. package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
  271. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +602 -0
  272. package/vendor/heyspex-mcp/config-seed.yaml +15 -0
  273. package/vendor/heyspex-mcp/config-settings.mjs +290 -7
  274. package/vendor/heyspex-mcp/console-fetch.mjs +822 -17
  275. package/vendor/heyspex-mcp/console-transport.mjs +172 -0
  276. package/vendor/heyspex-mcp/credentials.mjs +585 -0
  277. package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
  278. package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
  279. package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
  280. package/vendor/heyspex-mcp/design-tools.mjs +4 -1
  281. package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
  282. package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
  283. package/vendor/heyspex-mcp/dev-tools.mjs +131 -0
  284. package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
  285. package/vendor/heyspex-mcp/events-tools.mjs +207 -0
  286. package/vendor/heyspex-mcp/filing-tools.mjs +723 -0
  287. package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
  288. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +71 -15
  289. package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
  290. package/vendor/heyspex-mcp/folder-trust-store.mjs +60 -0
  291. package/vendor/heyspex-mcp/http-transport.mjs +302 -0
  292. package/vendor/heyspex-mcp/list-tools.mjs +257 -0
  293. package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
  294. package/vendor/heyspex-mcp/mcp-config.mjs +107 -0
  295. package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
  296. package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
  297. package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
  298. package/vendor/heyspex-mcp/occupied-keys.mjs +142 -0
  299. package/vendor/heyspex-mcp/on-deck-brief.mjs +3377 -0
  300. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
  301. package/vendor/heyspex-mcp/package-root.mjs +32 -0
  302. package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
  303. package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
  304. package/vendor/heyspex-mcp/plan-push.mjs +908 -22
  305. package/vendor/heyspex-mcp/plan-review.mjs +124 -30
  306. package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
  307. package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -0
  308. package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -970
  309. package/vendor/heyspex-mcp/promote-capture.mjs +388 -69
  310. package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
  311. package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
  312. package/vendor/heyspex-mcp/promote-research.mjs +666 -53
  313. package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
  314. package/vendor/heyspex-mcp/prompts.mjs +1460 -0
  315. package/vendor/heyspex-mcp/read-path.mjs +191 -15
  316. package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
  317. package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
  318. package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
  319. package/vendor/heyspex-mcp/register-spec.mjs +266 -54
  320. package/vendor/heyspex-mcp/request-context.mjs +83 -0
  321. package/vendor/heyspex-mcp/research-focus.mjs +207 -4
  322. package/vendor/heyspex-mcp/research-handlers.mjs +625 -0
  323. package/vendor/heyspex-mcp/research-tools.mjs +4 -4
  324. package/vendor/heyspex-mcp/response-budget.mjs +348 -0
  325. package/vendor/heyspex-mcp/review-focus.mjs +25 -1
  326. package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
  327. package/vendor/heyspex-mcp/search-specs.mjs +280 -0
  328. package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
  329. package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
  330. package/vendor/heyspex-mcp/session-owner.mjs +109 -0
  331. package/vendor/heyspex-mcp/session-tools.mjs +162 -0
  332. package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
  333. package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
  334. package/vendor/heyspex-mcp/spec-edit-tools.mjs +453 -0
  335. package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
  336. package/vendor/heyspex-mcp/split-research.mjs +304 -56
  337. package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
  338. package/vendor/heyspex-mcp/submit-for-review.mjs +150 -7
  339. package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
  340. package/vendor/heyspex-mcp/summary-write-tools.mjs +228 -0
  341. package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
  342. package/vendor/heyspex-mcp/test-helpers.mjs +123 -1
  343. package/vendor/heyspex-mcp/tool-catalog.mjs +2819 -0
  344. package/vendor/heyspex-mcp/tool-dispatch.mjs +657 -0
  345. package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
  346. package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
  347. package/vendor/heyspex-mcp/vault-io.mjs +720 -0
  348. package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
  349. package/vendor/heyspex-mcp/vault-resolve.mjs +554 -68
  350. package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
  351. package/vendor/heyspex-mcp/whoami.mjs +92 -0
  352. package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
@@ -0,0 +1,213 @@
1
+ // spexharness/src/gates/naughty-list-gate.mjs
2
+ //
3
+ // Engine port of scripts/claude-hooks/naughty-list-gate.mjs — the Stop hook
4
+ // that enforces the "naughty list" (banned metaphorical jargon,
5
+ // [[HS-PROTO-terminology-governance]]) on the agent's own final message. See
6
+ // ./PORTING.md for the recipe this follows and ./env-vault-guard.mjs for the
7
+ // reference port.
8
+ //
9
+ // This module does NOT reimplement the vocabulary rule itself (AC5's own
10
+ // words: "read their word lists ... from loadRuleTable, not a second copy").
11
+ // The actual decision — does this prose contain a banned term? — comes
12
+ // straight from the shared, already-pure loader:
13
+ // - scripts/claude-hooks/_guardrail-loader.mjs — loadGuardrails,
14
+ // rulesForSurface, isRuleEnabled, vocabularyViolations.
15
+ // vocabularyViolations owns the per-user `.heyspex/terminology.local.json`
16
+ // override merge (resolveVocabulary) AND the prose exemptions (code
17
+ // spans, fenced blocks, [[wikilinks]], URLs, blockquotes via proseOnly)
18
+ // internally — this module never reimplements either.
19
+ // - scripts/claude-hooks/_transcript-text.mjs — lastAssistantText, the same
20
+ // shared transcript-text reader id-display-format-gate.mjs's port also
21
+ // uses (split out of this hook's original sole ownership specifically so
22
+ // a second Stop-hook scanner wouldn't fork a copy).
23
+ // Both are dependency-free / no import hazard, matching enforce-session-
24
+ // sequence.mjs's port.
25
+ //
26
+ // JUDGMENT CALL — the original script does NOT export a pure decision
27
+ // function; its logic is tangled inside main()'s stdin read. Per PORTING.md
28
+ // step 1's "otherwise" branch this would normally move into the original
29
+ // script itself, but this task's Do-nots forbid touching anything under
30
+ // scripts/claude-hooks/. What is re-derived here is NOT the vocabulary rule
31
+ // (that stays imported, per AC5) — it is pure string-formatting glue
32
+ // (`reasonLines`, `blockReason`, the warn-stderr message template) copied
33
+ // verbatim from the original's own (unexported) helpers, plus the
34
+ // interactive-rung resolver (`interactiveRung`, reading
35
+ // HEYSPEX_NAUGHTY_LIST_INTERACTIVE) copied verbatim too.
36
+ //
37
+ // Event mapping: Stop → stop. `stdin.transcript_path` → `evt.transcriptPath`.
38
+ //
39
+ // Decision mapping — TWO rungs (AC4's "including the naughty-list gate's
40
+ // warn-versus-block rung"): CLAUDE_AGENT=true selects the "isAgent" branch,
41
+ // which blocks on any block-severity hit; the interactive default (agent
42
+ // unset, HEYSPEX_NAUGHTY_LIST_INTERACTIVE not raised to "block") maps the
43
+ // SAME hits to a `rewrite` envelope (`{ warn: true, reason, hits }`) instead
44
+ // — the stop is still allowed, matching the original's stderr-only warn path
45
+ // exactly (never silently upgraded to deny, never silently downgraded to a
46
+ // plain allow that would drop the reason text). This gives this gate natural
47
+ // coverage of all three engine decisions (AC3): allow on clean prose or no
48
+ // vocabulary rule compiled, deny on a block-severity hit in the real-block
49
+ // rung, rewrite on the warn rung (or on a warn-severity-only hit in any
50
+ // rung).
51
+ //
52
+ // Not ported HERE: the original's `appendGuardrailEvent(...)` audit-log
53
+ // write on every block-severity hit (fired even in the warn rung, to build
54
+ // the evidence log the rung is raised on) — matching every other port in
55
+ // this set. This `run()` still carries the block-severity hits forward as a
56
+ // top-level `blockHits` field on its `deny` AND `rewrite` results (never on
57
+ // a plain `allow`, and never including warn-severity-only hits — the
58
+ // original's own loop only ever logs block-severity hits, whichever rung
59
+ // the run happens to be on) so a FRONT can rebuild that exact per-hit audit
60
+ // trail without re-deriving hit words/contexts from `reason` text — see
61
+ // spexharness/src/adapters/audit.mjs, called from
62
+ // spexharness/src/adapters/claude-code.mjs's runGateAsHook.
63
+ //
64
+ // Env hatches (unchanged):
65
+ // HEYSPEX_NAUGHTY_LIST_OFF=1 Hard kill switch — silences both rungs.
66
+ // HEYSPEX_NAUGHTY_LIST_INTERACTIVE=block Raise the interactive rung to a real deny.
67
+ // CLAUDE_AGENT=true Unattended session → real deny (unchanged).
68
+ // Also honors the rule's own registry configKey
69
+ // (agent_behaviors.terminology_enforcement.enabled) via isRuleEnabled.
70
+ //
71
+ // `ctx` contract (all optional; every field defaults to a real fs/env read):
72
+ // ctx.cwd - repo root to resolve the guardrail registry + terminology
73
+ // override from. Defaults to CLAUDE_PROJECT_DIR, else this
74
+ // repo's root. When given it is a self-contained fixture root and
75
+ // the rule's config toggle is read from it too.
76
+ // ctx.configRoot - the directory holding `.heyspex/config.yaml` (the rule's
77
+ // configKey toggle); wins over ctx.cwd for that read. With neither,
78
+ // the VAULT root (../config-root.mjs `defaultConfigRoot()`) — never
79
+ // CLAUDE_PROJECT_DIR, which under Claude Code is the app checkout,
80
+ // not where the settings file lives
81
+ // ([[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]]).
82
+ // The terminology override stays on ctx.cwd (a code-repo artifact).
83
+ // ctx.env - env var overrides (tests use this instead of mutating
84
+ // process.env).
85
+
86
+ import path from 'node:path';
87
+ import { fileURLToPath } from 'node:url';
88
+
89
+ import { lastAssistantText } from '../../../scripts/claude-hooks/_transcript-text.mjs';
90
+ import {
91
+ loadGuardrails,
92
+ rulesForSurface,
93
+ isRuleEnabled,
94
+ vocabularyViolations,
95
+ } from '../../../scripts/claude-hooks/_guardrail-loader.mjs';
96
+ import { defaultConfigRoot } from '../config-root.mjs';
97
+
98
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
99
+ const REPO_ROOT = path.resolve(HERE, '..', '..', '..');
100
+
101
+ // Copied verbatim from the original's own (unexported) reasonLines.
102
+ function reasonLines(hits) {
103
+ const seen = new Map();
104
+ for (const h of hits) if (!seen.has(h.word)) seen.set(h.word, h.fix);
105
+ return [...seen.entries()].map(([word, fix]) => ` • "${word}" → ${fix}`);
106
+ }
107
+
108
+ // Copied verbatim from the original's own (unexported) blockReason.
109
+ function blockReason(hits) {
110
+ return (
111
+ `Naughty-list violation: your last message used banned metaphorical jargon ` +
112
+ `(the HeySpex default list from specs/naughty-list.md, also the owner's preference). ` +
113
+ `Rewrite those words in plain literal language and resend:\n${reasonLines(hits).join('\n')}\n` +
114
+ `(To refer to a banned word as a word, wrap it in backticks. Customize per checkout via ` +
115
+ `.heyspex/terminology.local.json {add,allow,remove}. Break-glass: HEYSPEX_NAUGHTY_LIST_OFF=1.)`
116
+ );
117
+ }
118
+
119
+ // Copied verbatim from the original's own (unexported) warn message template.
120
+ function warnReason(hits) {
121
+ return `[naughty-list] warning — banned terms in your last message (not blocked):\n${reasonLines(hits).join('\n')}\n`;
122
+ }
123
+
124
+ // Copied verbatim from the original's own (unexported) interactiveRung.
125
+ function interactiveRung(env) {
126
+ const raw = String(env.HEYSPEX_NAUGHTY_LIST_INTERACTIVE ?? '').trim().toLowerCase();
127
+ return raw === 'block' ? 'block' : 'warn';
128
+ }
129
+
130
+ /**
131
+ * The prose this stop event is about. A front that ALREADY holds the reply (`hs chat`, a
132
+ * headless seat, the bench — none of which write a Claude Code transcript file) passes it on
133
+ * the event as `text`, or on the ctx as `reply`; a Claude Code hook shell passes only
134
+ * `transcriptPath` and the text is read from it. Checked in that order, so the same gate runs
135
+ * on every front instead of silently allowing everything on the ones with no transcript —
136
+ * which is exactly what it did before (QC finding, 2026-09-10). The `ctx.reply` name matches
137
+ * enforce-session-sequence.mjs's stop-time corrector, which already took the reply that way.
138
+ *
139
+ * @param {{ text?: string, transcriptPath?: string }} evt
140
+ * @param {{ reply?: string }} ctx
141
+ * @param {(p: string) => string|null} readTranscript
142
+ * @returns {string}
143
+ */
144
+ function stopEventText(evt, ctx, readTranscript) {
145
+ if (typeof ctx?.reply === 'string' && ctx.reply) return ctx.reply;
146
+ if (typeof evt?.text === 'string' && evt.text) return evt.text;
147
+ const transcriptPath = evt?.transcriptPath;
148
+ if (!transcriptPath) return '';
149
+ return readTranscript(transcriptPath) ?? '';
150
+ }
151
+
152
+ /** The naughty-list gate's id — unique across the built-in registry. */
153
+ export const id = 'naughty-list-gate';
154
+ /** Runs on the Stop event, same as its Stop Claude Code hook registration. */
155
+ export const event = 'stop';
156
+ /** Caller-defined severity label; this gate can deny, so it is "block". */
157
+ export const severity = 'block';
158
+
159
+ /**
160
+ * @param {{ point: string, text?: string, transcriptPath?: string }} evt
161
+ * @param {{ cwd?: string, configRoot?: string, reply?: string, env?: Record<string, string | undefined> }} [ctx]
162
+ * @returns {{ decision: 'allow'|'deny'|'rewrite', reason?: string, rewrite?: object }}
163
+ */
164
+ export function run(evt, ctx = {}) {
165
+ const env = ctx.env ?? process.env;
166
+ if (String(env.HEYSPEX_NAUGHTY_LIST_OFF ?? '') === '1') {
167
+ return { decision: 'allow' };
168
+ }
169
+
170
+ const isAgent = env.CLAUDE_AGENT === 'true';
171
+ const warnOnly = !isAgent && interactiveRung(env) !== 'block';
172
+
173
+ const text = stopEventText(evt, ctx, lastAssistantText);
174
+ if (!text) return { decision: 'allow' };
175
+
176
+ const root = ctx.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? REPO_ROOT;
177
+ const configRoot = ctx.configRoot ?? (ctx.cwd ? ctx.cwd : defaultConfigRoot());
178
+ const guardrails = loadGuardrails(root);
179
+ const rules = rulesForSurface(guardrails, 'stop').filter((r) => r && r.kind === 'vocabulary');
180
+ if (!rules.length) return { decision: 'allow' };
181
+
182
+ const blockHits = [];
183
+ const warnHits = [];
184
+ for (const rule of rules) {
185
+ if (!isRuleEnabled(rule, configRoot)) continue;
186
+ const hits = vocabularyViolations(rule, text, root);
187
+ if (!hits.length) continue;
188
+ (rule.severity === 'block' ? blockHits : warnHits).push(...hits);
189
+ }
190
+
191
+ if (blockHits.length && !warnOnly) {
192
+ return { decision: 'deny', reason: blockReason(blockHits), blockHits };
193
+ }
194
+
195
+ const softHits = warnOnly ? [...blockHits, ...warnHits] : warnHits;
196
+ if (softHits.length) {
197
+ const reason = warnReason(softHits);
198
+ // `blockHits` (block-severity hits ONLY, never the warn-severity ones
199
+ // folded into `softHits`/`rewrite.hits` for display) rides separately so
200
+ // a front's audit log matches the original's own loop, which logs a row
201
+ // per block-severity hit on every rung — never for a warn-severity hit.
202
+ return {
203
+ decision: 'rewrite',
204
+ reason,
205
+ rewrite: { warn: true, reason, hits: softHits },
206
+ ...(blockHits.length ? { blockHits } : {}),
207
+ };
208
+ }
209
+
210
+ return { decision: 'allow' };
211
+ }
212
+
213
+ export default { id, event, severity, run };
@@ -0,0 +1,233 @@
1
+ // spexharness/src/gates/plan-review-gate.mjs
2
+ //
3
+ // Engine port of scripts/claude-hooks/plan-review-gate.mjs — the PreToolUse
4
+ // gate over ExitPlanMode that enforces the V1 → quality+canonical check → V2
5
+ // contract (a heyspex_plan_review marker) and, once that passes, the QC
6
+ // gut-check marker (heyspex_plan_qc) — both keyed by sha256(plan content),
7
+ // [[HS-EPIC-PLRV]]. See ./PORTING.md for the recipe this follows and
8
+ // ./env-vault-guard.mjs for the reference port.
9
+ //
10
+ // JUDGMENT CALL — read before touching this file: the original script does
11
+ // NOT export a pure decision function; its logic (resolvePlan, denyReason,
12
+ // denyReasonQc, shouldBlockForReal, fireDeny) is tangled with main()'s stdin
13
+ // read. This task's Do-nots forbid touching anything under
14
+ // scripts/claude-hooks/, so per PORTING.md step 1's "otherwise" branch this
15
+ // module re-derives the plumbing here. What is re-derived is NOT
16
+ // security-relevant regex/rule logic — it is (a) the marker-path
17
+ // construction (a plain sha256(plan content) → `.heyspex/plan-reviews/` /
18
+ // `.heyspex/plan-qc/` join, no judgment call baked in) and (b) the deny-
19
+ // reason string TEMPLATES, copied verbatim from the original's own
20
+ // (unexported) `denyReason`/`denyReasonQc` functions so the text a caller
21
+ // sees is byte-identical to what the original would have written — the same
22
+ // "copy verbatim where the original's own strings are copyable" approach
23
+ // deep-research-cost-gate.mjs's port documents for its own unexported COST
24
+ // table and message templates. If the original script is ever changed to
25
+ // export these, this module's copies should be deleted in favor of the real
26
+ // thing.
27
+ //
28
+ // Event mapping: PreToolUse (ExitPlanMode) → before_tool. The original's own
29
+ // defense-in-depth tool-name check (`tool_name !== 'ExitPlanMode'` → allow)
30
+ // is preserved here.
31
+ //
32
+ // Decision mapping — TWO independent rungs, same two-rung shape as
33
+ // enforce-session-sequence.mjs's port: CLAUDE_AGENT=true or
34
+ // HEYSPEX_PLAN_REVIEW_GATE_INTERACTIVE=block ("forReal") maps a missing
35
+ // marker to a real `deny`; the interactive default maps the SAME missing-
36
+ // marker finding to a `rewrite` envelope (`{ warn: true, reason, rule }`) —
37
+ // the stop is still allowed, matching the original's stderr-only warn path
38
+ // exactly (never silently upgraded to deny, never silently downgraded to a
39
+ // plain allow that would drop the reason text). This gives this gate natural
40
+ // coverage of all three engine decisions (AC3): allow when both markers are
41
+ // present (or the tool isn't ExitPlanMode), deny on the unattended/raised
42
+ // rung, rewrite on the interactive-default rung.
43
+ //
44
+ // Not ported: the original's `appendGuardrailEvent(...)` audit-log write on
45
+ // a fire — matching every other port in this set (a gate module here is a
46
+ // pure decision function with no other observability side effects; a front
47
+ // that wants the same audit trail logs `denials`/`rewrites` itself).
48
+ //
49
+ // Env hatches (unchanged):
50
+ // HEYSPEX_PLAN_REVIEW_GATE_OFF=1 Hard kill switch — silences both rungs and both markers.
51
+ // HEYSPEX_PLAN_REVIEW_GATE_INTERACTIVE=block Raise the interactive rung to a real deny (V1 half only).
52
+ // HEYSPEX_PLAN_QC_GATE_OFF=1 Mute the QC-marker half only (the V1 half still gates).
53
+ // CLAUDE_AGENT=true Unattended session → real deny (unchanged).
54
+ //
55
+ // `ctx` contract (all optional; every field defaults to a real fs/env read):
56
+ // ctx.cwd - repo root to resolve the marker directories
57
+ // from. Defaults to CLAUDE_PROJECT_DIR, else
58
+ // this repo's root.
59
+ // ctx.env - env var overrides (tests use this instead of
60
+ // mutating process.env).
61
+ // ctx.resolvePlanFallback - override for the original's `~/.claude/plans/`
62
+ // most-recently-mtimed-file fallback (used only
63
+ // when the tool input carries no inline
64
+ // `plan`). Defaults to a real fs scan of the
65
+ // real home directory; every fixture here
66
+ // supplies an inline plan, so this fallback is
67
+ // never exercised by the golden/regression
68
+ // tests, only kept for behavioral parity.
69
+
70
+ import fs from 'node:fs';
71
+ import path from 'node:path';
72
+ import os from 'node:os';
73
+ import { createHash } from 'node:crypto';
74
+ import { fileURLToPath } from 'node:url';
75
+
76
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
77
+ const REPO_ROOT = path.resolve(HERE, '..', '..', '..');
78
+ const PROCEDURE_NAME = 'plan-quality-canonical-check';
79
+
80
+ // Copied verbatim from the original's own (unexported) resolvePlan — see the
81
+ // judgment-call note above.
82
+ function defaultResolvePlanFallback() {
83
+ try {
84
+ const plansDir = path.join(os.homedir(), '.claude', 'plans');
85
+ const entries = fs
86
+ .readdirSync(plansDir)
87
+ .map((name) => path.join(plansDir, name))
88
+ .filter((p) => {
89
+ try {
90
+ return fs.statSync(p).isFile();
91
+ } catch {
92
+ return false;
93
+ }
94
+ })
95
+ .map((p) => ({ p, mtime: fs.statSync(p).mtimeMs }))
96
+ .sort((a, b) => b.mtime - a.mtime);
97
+ if (entries.length > 0) {
98
+ const content = fs.readFileSync(entries[0].p, 'utf8');
99
+ return { content, source: entries[0].p };
100
+ }
101
+ } catch {
102
+ // No plans dir / unreadable — fall through.
103
+ }
104
+ return null;
105
+ }
106
+
107
+ function resolvePlan(toolInput, ctx) {
108
+ const inline = toolInput?.plan;
109
+ if (typeof inline === 'string' && inline.trim()) {
110
+ return { content: inline, source: 'tool_input.plan' };
111
+ }
112
+ const fallback = ctx.resolvePlanFallback ?? defaultResolvePlanFallback;
113
+ try {
114
+ return fallback();
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
119
+
120
+ // Copied verbatim from the original's own (unexported) denyReason.
121
+ function denyReason(planSource) {
122
+ const where = planSource ? ` (plan at ${planSource})` : '';
123
+ return (
124
+ `Plan V1 detected${where}. Before leaving plan mode, run ` +
125
+ `heyspex_plan_review({ plan_content: <the plan you are about to submit> }) ` +
126
+ `(or plan_path if the plan is on disk), fold the findings into a V2 revision ` +
127
+ `of the plan, then call ExitPlanMode again with the revised plan. This gate ` +
128
+ `is non-skippable — it enforces the V1 → quality+canonical check → V2 ` +
129
+ `contract; see ${PROCEDURE_NAME}.md. ` +
130
+ `(Break-glass: set HEYSPEX_PLAN_REVIEW_GATE_OFF=1.)`
131
+ );
132
+ }
133
+
134
+ // Copied verbatim from the original's own (unexported) denyReasonQc.
135
+ function denyReasonQc(planSource) {
136
+ const where = planSource ? ` (plan at ${planSource})` : '';
137
+ return (
138
+ `Plan-review passed, but the QC gut-check has not run for this plan ` +
139
+ `version${where}. Before leaving plan mode, run the \`/hs-qc\` gut-check, fold ` +
140
+ `its findings into the plan, then call ` +
141
+ `heyspex_plan_qc({ plan_content: <the plan you are about to submit>, ` +
142
+ `findings_summary: <what the gut-check actually found> }) to record ` +
143
+ `Proof-of-Use, then call ExitPlanMode again. This gate is non-skippable; ` +
144
+ `see ${PROCEDURE_NAME}.md. (Break-glass for the QC half only: set ` +
145
+ `HEYSPEX_PLAN_QC_GATE_OFF=1.)`
146
+ );
147
+ }
148
+
149
+ function shouldBlockForReal(env) {
150
+ return env.CLAUDE_AGENT === 'true' || env.HEYSPEX_PLAN_REVIEW_GATE_INTERACTIVE === 'block';
151
+ }
152
+
153
+ // Byte-for-byte the original hook's own interactive-warn stderr banner
154
+ // (fireDeny()'s `else` branch) — kept here rather than left to the adapter's
155
+ // generic warn formatter because the second line names THIS gate's own env
156
+ // var, same convention as claim-before-code-gate.mjs's warnReason.
157
+ function warnStderrText(reason) {
158
+ return (
159
+ `[plan-review-gate] warning — ${reason}\n` +
160
+ '(Interactive session: this only warns. Set HEYSPEX_PLAN_REVIEW_GATE_INTERACTIVE=block to enforce it here too.)\n'
161
+ );
162
+ }
163
+
164
+ /** The plan-review gate's id — unique across the built-in registry. */
165
+ export const id = 'plan-review-gate';
166
+ /** Runs pre-tool, same as its PreToolUse Claude Code hook registration. */
167
+ export const event = 'before_tool';
168
+ /** Caller-defined severity label; this gate can deny, so it is "block". */
169
+ export const severity = 'block';
170
+
171
+ /**
172
+ * @param {{ point: string, toolName?: string, toolInput?: Record<string, unknown> }} evt
173
+ * @param {{ cwd?: string, env?: Record<string, string | undefined>,
174
+ * resolvePlanFallback?: () => { content: string, source: string } | null }} [ctx]
175
+ * @returns {{ decision: 'allow'|'deny'|'rewrite', reason?: string, rewrite?: object }}
176
+ */
177
+ export function run(evt, ctx = {}) {
178
+ const env = ctx.env ?? process.env;
179
+ if (String(env.HEYSPEX_PLAN_REVIEW_GATE_OFF ?? '') === '1') {
180
+ return { decision: 'allow' };
181
+ }
182
+
183
+ // Defense-in-depth: the real settings.json matcher already scopes this to
184
+ // ExitPlanMode, but never gate any other tool even if mis-wired.
185
+ if (evt?.toolName !== 'ExitPlanMode') {
186
+ return { decision: 'allow' };
187
+ }
188
+
189
+ const plan = resolvePlan(evt?.toolInput ?? {}, ctx);
190
+ if (!plan) {
191
+ // Can't find the plan content to hash → fail open (allow), same as the original.
192
+ return { decision: 'allow' };
193
+ }
194
+
195
+ const root = ctx.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? REPO_ROOT;
196
+ const hash = createHash('sha256').update(plan.content).digest('hex');
197
+ const reviewMarker = path.join(root, '.heyspex', 'plan-reviews', `${hash}.json`);
198
+ const forReal = shouldBlockForReal(env);
199
+
200
+ if (!fs.existsSync(reviewMarker)) {
201
+ const reason = denyReason(plan.source);
202
+ return forReal
203
+ ? { decision: 'deny', reason }
204
+ : {
205
+ decision: 'rewrite',
206
+ reason,
207
+ rewrite: { warn: true, reason, rule: 'plan-review-gate:v1', stderrText: warnStderrText(reason) },
208
+ };
209
+ }
210
+
211
+ // Plan-review passed. Now require the QC gut-check marker for the SAME
212
+ // plan hash — unless the QC half is muted (the master kill switch above
213
+ // mutes both).
214
+ if (String(env.HEYSPEX_PLAN_QC_GATE_OFF ?? '') === '1') {
215
+ return { decision: 'allow' };
216
+ }
217
+
218
+ const qcMarker = path.join(root, '.heyspex', 'plan-qc', `${hash}.json`);
219
+ if (fs.existsSync(qcMarker)) {
220
+ return { decision: 'allow' };
221
+ }
222
+
223
+ const reasonQc = denyReasonQc(plan.source);
224
+ return forReal
225
+ ? { decision: 'deny', reason: reasonQc }
226
+ : {
227
+ decision: 'rewrite',
228
+ reason: reasonQc,
229
+ rewrite: { warn: true, reason: reasonQc, rule: 'plan-review-gate:qc', stderrText: warnStderrText(reasonQc) },
230
+ };
231
+ }
232
+
233
+ export default { id, event, severity, run };