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,639 @@
1
+ // JSON-RPC method handlers for initialize, tools/list, tools/call (the
2
+ // dispatchHeySpexTool switch), session/end and hooks/*, plus the seat banner and
3
+ // peer-presence injection applied to every tool result. Extracted from cli.mjs under
4
+ // [[HS-BLNF-6W61-DIAG-cli-mjs-has-grown-into-a-13-665-line]] (no behavior change).
5
+
6
+ import { executeHook, finalizeToolResultWithHooks, listHookConfig, loadHooksConfig, matchHooks, runHooks } from './hooks-engine.mjs';
7
+ import { detectAgentIdentity, getConfig } from './mcp-config.mjs';
8
+ import { existsSync, readFileSync } from 'node:fs';
9
+ import { evaluatePreflightGate } from './preflight-gate.mjs';
10
+ import { TOOLS, handleLoadTools } from './tool-catalog.mjs';
11
+ import { handleNextOnDeck, handleNextResolution, handleNextReview, handleResolvePrerequisite, handleSuggestPillar } from './on-deck-brief.mjs';
12
+ import { handleFileSpec, handleGroundPlan, handlePlanQc, handlePlanReview, handlePushPlan, handleRegisterSpec, handleValidateLinkage, handleValidateUserSkills } from './filing-tools.mjs';
13
+ import { handleActiveResearch, handleCompleteResearch, handlePromoteResearch, handlePromoteResearchToDiagnose, handleSaveResearch, handleSplitResearch } from './research-handlers.mjs';
14
+ import { handleRecallMemoriesViaTier } from './memory-tools.mjs';
15
+ import { handleCanvasInbox, handleEditFile, handleListKnownTags, handleProposeMetadata, handleRegenTask, handleRegenTasksForEpic, handleShipEpic } from './spec-edit-tools.mjs';
16
+ import { handleAuthorSummary } from './summary-write-tools.mjs';
17
+ import { handleActiveCapture, handlePinCapture, handlePromoteCapture, handlePromoteCaptureToDiagnose, handleUpdateCapture } from './capture-tools.mjs';
18
+ import { handleClaimAcquire, handleClaimFile, handleClaimList, handleClaimRelease, handleClaimSpec, handleDeferSpec, handleRecordAlignmentViaTier, handleRecordWip, handleReleaseFileClaim, handleReleaseSpec, handleReportProgress, handleSubmitForReview } from './claim-tools.mjs';
19
+ import { handlePromoteDiagnose, handleVerifyReproduction } from './diagnose-handlers.mjs';
20
+ import { handleHandoffLoad, handleHandoffRecord, handleImportAgent } from './session-tools.mjs';
21
+ import { handleEventsShow, handleEvidenceShow, handleExportEvents, handleMilestoneShow } from './events-tools.mjs';
22
+ import { handleClearFocusAreas, handleGetFocusAreas, handlePinGet, handlePinSet, handleResolveFocusIntent, handleSetFocusAreas } from './focus-tools.mjs';
23
+ import { handleSearchSpecs } from './search-specs.mjs';
24
+ import { handleDesignGetDoc, handleDesignGetPattern, handleDesignGetToken, handleDesignListPatterns, handleDesignProposePattern } from './design-handlers.mjs';
25
+ import { handleGetProcedureViaTier, handleListProceduresViaTier, handleRunPreflightViaTier } from './procedure-handlers.mjs';
26
+ import { handleListRecipes, handleStartRecipe } from './recipe-handlers.mjs';
27
+ import { handleFleetQueueClaim, handleFleetQueueComplete, handleFleetQueueList, handleFleetQueueSubmit } from './fleet-handlers.mjs';
28
+ import { handleDevMode, handleDevServerStatus } from './dev-tools.mjs';
29
+ import { wrapDesignResult } from './tool-helpers.mjs';
30
+ import { executeSettingsGet, executeSettingsList, executeSettingsSet } from './settings-tools.mjs';
31
+ import { executeFirecrawlResearch, executeResolveResearchPlan } from './research-tools.mjs';
32
+ import { formatSeatBanner, readSeatsRegistry, resolveSeat } from './seat-identity.mjs';
33
+ import { seatBlockForSessionSurface } from './seat-block-ledger.mjs';
34
+ import { resolveCooModeEnabled } from '../../dev-factory/lib/fleet-readiness.mjs';
35
+ import { applyToolBudget } from './response-budget.mjs';
36
+ import { resolveDevMode } from '../../scripts/lib/dev-mode.mjs';
37
+ import { dirname, join } from 'node:path';
38
+ import { MODULE_DIR } from './package-root.mjs';
39
+
40
+ export const SERVER_NAME = 'heyspex';
41
+
42
+ /**
43
+ * Read the server version from the nearest `package.json`, walking up from
44
+ * THIS file's own directory (`vendor/heyspex-mcp/package.json` if one ever
45
+ * exists, else the repo root's) — [[HS-7OKX-0JNK-TASK]]. Previously a
46
+ * hard-coded `'0.9.0'` that had drifted from the packed `heyspex@0.1.0`, so
47
+ * the `initialize` handshake's `serverInfo.version` disagreed with what was
48
+ * actually installed. Never throws: an unreadable/missing/malformed
49
+ * package.json anywhere up the tree falls through to `'0.0.0-unknown'`.
50
+ */
51
+ export function readServerVersion() {
52
+ let dir = MODULE_DIR;
53
+ for (let i = 0; i < 16; i += 1) {
54
+ const candidate = join(dir, 'package.json');
55
+ if (existsSync(candidate)) {
56
+ try {
57
+ const pkg = JSON.parse(readFileSync(candidate, 'utf8'));
58
+ if (pkg && typeof pkg.version === 'string' && pkg.version.trim()) {
59
+ return pkg.version;
60
+ }
61
+ } catch {
62
+ // malformed package.json at this level — keep walking up rather than throw
63
+ }
64
+ }
65
+ const parent = dirname(dir);
66
+ if (parent === dir) break; // reached filesystem root
67
+ dir = parent;
68
+ }
69
+ return '0.0.0-unknown';
70
+ }
71
+
72
+ export const SERVER_VERSION = readServerVersion();
73
+
74
+ // MCP Channels capability is advertised in the initialize handshake.
75
+ // Per https://code.claude.com/docs/en/channels-reference#server-options the
76
+ // channel capability is nested under `experimental` and pushed events use
77
+ // the `notifications/claude/channel` method. The instructions string is
78
+ // injected into Claude's system prompt so it knows how to react to
79
+ // <channel source="heyspex" …> events emitted by capture-channel.mjs.
80
+ // Owning epic: [[HS-EPIC-QXJ3-modal-agent-handshake-mcp-channels-push-]].
81
+ export const CHANNEL_INSTRUCTIONS =
82
+ 'Events from the heyspex capture flow arrive as <channel source="heyspex" capture_id="HS-CAP-XXXX" event_type="user_quote">…</channel>. ' +
83
+ 'When you receive one, call heyspex_active_capture WITH spec_id set to that exact capture_id ' +
84
+ '(e.g. heyspex_active_capture({ spec_id: "HS-CAP-XXXX" })) to fetch the latest capture state, and pass that same spec_id ' +
85
+ 'to every heyspex_update_capture call for this capture. Do NOT call these tools without spec_id when you have the channel capture_id — ' +
86
+ 'the no-spec_id path attaches to whichever capture was most recently active, which may be a different session (another console tab, ' +
87
+ 'the companion app, or another agent) and would write to the wrong capture id. Then respond per the /hs-capture capture protocol ' +
88
+ '(Capture → Ground=Expand [grounding auto-writes agent_expansion, no confirm] → Promote to research). Do not infer user_quote ' +
89
+ 'contents from the channel hint alone — always fetch fresh state via the tool.';
90
+
91
+ // ── Dev-factory seat identity delivery (epic HS-CXO0) ────────────────────────
92
+ // Resolve the seat THIS caller holds — gated by the shared fleet-readiness
93
+ // predicate (identical to the SessionStart hook → no drift). Returns null (inject
94
+ // nothing) on any error, cooMode off / auto-not-ready, or an unknown caller. A
95
+ // Builder worker resolves to the Builder seat, a COO session to COO — each is told
96
+ // its OWN seat, never another's.
97
+ export function resolveInjectableSeat() {
98
+ try {
99
+ const { vaultRoot } = getConfig();
100
+ const registry = readSeatsRegistry(vaultRoot);
101
+ if (!registry) return null;
102
+ if (!resolveCooModeEnabled(registry.settings?.cooMode).enabled) return null;
103
+ const identity = detectAgentIdentity();
104
+ return resolveSeat(identity, { registry, env: process.env });
105
+ } catch {
106
+ return null; // seat logic must never break the server
107
+ }
108
+ }
109
+
110
+ export async function handleInitialize(params) {
111
+ let instructions = CHANNEL_INSTRUCTIONS;
112
+ try {
113
+ const banner = formatSeatBanner(resolveInjectableSeat());
114
+ if (banner) {
115
+ // Gate on the shared per-session ledger (HS-8U8X) rather than emitting
116
+ // unconditionally — the first tool result of the session (see cli.mjs
117
+ // maybePrependSeatBanner) shares this same ledger, so whichever surface
118
+ // asks first wins and the other gets null, never a second copy.
119
+ const { sessionId } = detectAgentIdentity();
120
+ const toSend = await seatBlockForSessionSurface(sessionId, 'mcp_instructions', banner);
121
+ if (toSend) instructions += `\n\n${toSend}`;
122
+ }
123
+ } catch {
124
+ // never break the initialize handshake on seat logic
125
+ }
126
+ return {
127
+ protocolVersion: '2024-11-05',
128
+ capabilities: {
129
+ // listChanged: heyspex_load_tools expands the visible surface at runtime
130
+ // ([[HS-LCRS-TASK]] progressive disclosure) and notifies the client.
131
+ tools: { listChanged: true },
132
+ prompts: {},
133
+ // [[HS-EPIC-QXJ3]] — channels capability nested under `experimental`
134
+ // per https://code.claude.com/docs/en/channels-reference#server-options.
135
+ // Presence registers the notification listener; emission happens in
136
+ // capture-channel.mjs via sendNotification('notifications/claude/channel', …).
137
+ experimental: {
138
+ 'claude/channel': {},
139
+ },
140
+ },
141
+ instructions,
142
+ serverInfo: {
143
+ name: SERVER_NAME,
144
+ version: SERVER_VERSION,
145
+ },
146
+ };
147
+ }
148
+
149
+ export function handleToolsList() {
150
+ // `budget` is a server-side contract (enforced below), not part of the MCP
151
+ // Tool wire shape — strip it so the client sees only name/description/schema.
152
+ return { tools: TOOLS.map(({ budget: _budget, ...tool }) => tool) };
153
+ }
154
+
155
+ /** The catalog's declared byte budget for a tool, or `null` when it declares none. */
156
+ export function toolBudgetFor(name) {
157
+ const tool = TOOLS_BY_NAME.get(name);
158
+ return tool?.budget && Number(tool.budget.max_bytes) > 0 ? tool.budget : null;
159
+ }
160
+
161
+ const TOOLS_BY_NAME = new Map(TOOLS.map((t) => [t.name, t]));
162
+
163
+ /**
164
+ * Every tool call goes through here. A tool that declares `budget` in the
165
+ * catalog ([[HS-4H8T-8KK5-TASK-every-mcp-read-tool-declares-a-byte]]) has
166
+ * its reply bounded to `budget.max_bytes` on the way out — trailing list items
167
+ * dropped behind a `next_cursor`, `truncated: true` — and a budget cursor on
168
+ * the way in resumes from the first dropped item (see response-budget.mjs).
169
+ * A tool without a budget is dispatched untouched.
170
+ */
171
+ export function dispatchHeySpexTool(name, args, vaultRoot, config) {
172
+ return applyToolBudget(
173
+ name,
174
+ args,
175
+ (effectiveArgs) => dispatchHeySpexToolUnbudgeted(name, effectiveArgs, vaultRoot, config),
176
+ toolBudgetFor(name),
177
+ );
178
+ }
179
+
180
+ function dispatchHeySpexToolUnbudgeted(name, args, vaultRoot, config) {
181
+ switch (name) {
182
+ case 'heyspex_load_tools':
183
+ // Normally intercepted parent-side at the tools/call dispatch; this case is
184
+ // defense-in-depth for direct handleToolsCall callers (subprocess/CLI paths).
185
+ return handleLoadTools(args);
186
+ case 'heyspex_next_on_deck':
187
+ return handleNextOnDeck(vaultRoot, config, args);
188
+ case 'heyspex_next_review':
189
+ return handleNextReview(vaultRoot, config, args);
190
+ case 'heyspex_next_resolution':
191
+ return handleNextResolution(args);
192
+ case 'heyspex_validate_linkage':
193
+ return handleValidateLinkage(vaultRoot, args);
194
+ case 'heyspex_suggest_pillar':
195
+ return handleSuggestPillar(args);
196
+ case 'heyspex_validate_user_skills':
197
+ return handleValidateUserSkills(vaultRoot, args);
198
+ case 'heyspex_register_spec':
199
+ return handleRegisterSpec(vaultRoot, args);
200
+ case 'heyspex_save_research':
201
+ return handleSaveResearch(vaultRoot, args);
202
+ case 'heyspex_recall_memories':
203
+ return handleRecallMemoriesViaTier(vaultRoot, args);
204
+ case 'heyspex_push_plan':
205
+ return handlePushPlan(vaultRoot, args);
206
+ case 'heyspex_backfill_epic':
207
+ return handleShipEpic(vaultRoot, args);
208
+ case 'heyspex_ground_plan':
209
+ return handleGroundPlan(vaultRoot, args);
210
+ case 'heyspex_plan_review':
211
+ return handlePlanReview(vaultRoot, args);
212
+ case 'heyspex_plan_qc':
213
+ return handlePlanQc(vaultRoot, args);
214
+ case 'heyspex_active_capture':
215
+ return handleActiveCapture(vaultRoot, args);
216
+ case 'heyspex_update_capture':
217
+ return handleUpdateCapture(vaultRoot, args);
218
+ case 'heyspex_pin_capture':
219
+ return handlePinCapture(vaultRoot, args);
220
+ case 'heyspex_active_research':
221
+ return handleActiveResearch(vaultRoot, args);
222
+ case 'heyspex_complete_research':
223
+ return handleCompleteResearch(vaultRoot, args);
224
+ case 'heyspex_claim_spec':
225
+ return handleClaimSpec(args);
226
+ case 'heyspex_report_progress':
227
+ return handleReportProgress(args);
228
+ case 'heyspex_submit_for_review':
229
+ return handleSubmitForReview(vaultRoot, args);
230
+ case 'heyspex_verify_reproduction':
231
+ return handleVerifyReproduction(vaultRoot, args);
232
+ case 'heyspex_promote_diagnose':
233
+ return handlePromoteDiagnose(vaultRoot, args);
234
+ case 'heyspex_release_spec':
235
+ return handleReleaseSpec(args);
236
+ case 'heyspex_record_wip':
237
+ return handleRecordWip(args);
238
+ case 'heyspex_author_summary':
239
+ return handleAuthorSummary(vaultRoot, args);
240
+ case 'heyspex_defer_spec':
241
+ return handleDeferSpec(args);
242
+ case 'heyspex_handoff_record':
243
+ return handleHandoffRecord(args);
244
+ case 'heyspex_handoff_load':
245
+ return handleHandoffLoad(args);
246
+ case 'heyspex_milestone_show':
247
+ return handleMilestoneShow(args);
248
+ case 'heyspex_pin_get':
249
+ return handlePinGet(args);
250
+ case 'heyspex_pin_set':
251
+ return handlePinSet(args);
252
+ case 'heyspex_resolve_prerequisite':
253
+ return handleResolvePrerequisite(args);
254
+ case 'heyspex_claim_acquire':
255
+ return handleClaimAcquire(args);
256
+ case 'heyspex_claim_release':
257
+ return handleClaimRelease(args);
258
+ case 'heyspex_claim_list':
259
+ return handleClaimList(args);
260
+ case 'heyspex_claim_file':
261
+ return handleClaimFile(args);
262
+ case 'heyspex_release_file_claim':
263
+ return handleReleaseFileClaim(args);
264
+ case 'heyspex_edit_file':
265
+ return handleEditFile(args);
266
+ case 'heyspex_list_known_tags':
267
+ return handleListKnownTags(args);
268
+ case 'heyspex_set_focus_areas':
269
+ return handleSetFocusAreas(args);
270
+ case 'heyspex_get_focus_areas':
271
+ return handleGetFocusAreas(args);
272
+ case 'heyspex_clear_focus_areas':
273
+ return handleClearFocusAreas(args);
274
+ case 'heyspex_resolve_focus_intent':
275
+ return handleResolveFocusIntent(args);
276
+ case 'heyspex_search_specs':
277
+ return handleSearchSpecs(args);
278
+ case 'heyspex_events_show':
279
+ return handleEventsShow(args);
280
+ case 'heyspex_evidence_show':
281
+ return handleEvidenceShow(args);
282
+ case 'heyspex_export_events':
283
+ return handleExportEvents(args);
284
+ case 'heyspex_propose_metadata':
285
+ return handleProposeMetadata(args);
286
+ case 'heyspex_promote_research':
287
+ return handlePromoteResearch(vaultRoot, args);
288
+ case 'heyspex_promote_capture':
289
+ return handlePromoteCapture(vaultRoot, args);
290
+ case 'heyspex_promote_capture_to_diagnose':
291
+ return handlePromoteCaptureToDiagnose(vaultRoot, args);
292
+ case 'heyspex_promote_research_to_diagnose':
293
+ return handlePromoteResearchToDiagnose(vaultRoot, args);
294
+ case 'heyspex_split_research':
295
+ return handleSplitResearch(vaultRoot, args);
296
+ case 'heyspex_record_alignment':
297
+ return handleRecordAlignmentViaTier(vaultRoot, config, args);
298
+ case 'heyspex_design_list_patterns':
299
+ return handleDesignListPatterns(vaultRoot, args);
300
+ case 'heyspex_design_get_pattern':
301
+ return handleDesignGetPattern(vaultRoot, args);
302
+ case 'heyspex_design_get_token':
303
+ return handleDesignGetToken(vaultRoot, args);
304
+ case 'heyspex_design_get_doc':
305
+ return handleDesignGetDoc(vaultRoot, args);
306
+ case 'heyspex_design_propose_pattern':
307
+ return handleDesignProposePattern(vaultRoot, args);
308
+ case 'heyspex_list_procedures':
309
+ return handleListProceduresViaTier(vaultRoot, args);
310
+ case 'heyspex_get_procedure':
311
+ return handleGetProcedureViaTier(vaultRoot, args);
312
+ case 'heyspex_file_spec':
313
+ return handleFileSpec(args, vaultRoot);
314
+ case 'heyspex_import_agent':
315
+ return handleImportAgent(args);
316
+ case 'heyspex_preflight':
317
+ return handleRunPreflightViaTier(vaultRoot, args);
318
+ case 'heyspex_list_recipes':
319
+ return handleListRecipes(vaultRoot, args);
320
+ case 'heyspex_start_recipe':
321
+ return handleStartRecipe(vaultRoot, args);
322
+ case 'heyspex_canvas_inbox':
323
+ return handleCanvasInbox(vaultRoot, args);
324
+ case 'heyspex_fleet_queue_submit':
325
+ return handleFleetQueueSubmit(args);
326
+ case 'heyspex_fleet_queue_claim':
327
+ return handleFleetQueueClaim(args);
328
+ case 'heyspex_fleet_queue_complete':
329
+ return handleFleetQueueComplete(args);
330
+ case 'heyspex_fleet_queue_list':
331
+ return handleFleetQueueList(args);
332
+
333
+ case 'heyspex_dev_server_status':
334
+ return handleDevServerStatus(vaultRoot);
335
+ case 'heyspex_dev_mode':
336
+ return handleDevMode(vaultRoot, args);
337
+ case 'heyspex_settings_list':
338
+ return wrapDesignResult(executeSettingsList(vaultRoot, args));
339
+ case 'heyspex_settings_get':
340
+ return wrapDesignResult(executeSettingsGet(vaultRoot, args));
341
+ case 'heyspex_settings_set':
342
+ return wrapDesignResult(executeSettingsSet(vaultRoot, args));
343
+ case 'heyspex_resolve_research_plan':
344
+ // async since HS-ZJSP (the Firecrawl endpoint is resolved through the lazy
345
+ // dev-factory bridge) — same Promise-returning shape as the case below.
346
+ return executeResolveResearchPlan(vaultRoot).then(wrapDesignResult);
347
+ case 'heyspex_firecrawl_research':
348
+ // dispatchHeySpexTool is NOT async (mirrors the heyspex_dev_server_status precedent
349
+ // above) — return the chained Promise directly; finalizeToolResultWithHooks/
350
+ // Promise.resolve() at the call site unwrap it.
351
+ return executeFirecrawlResearch(args).then(wrapDesignResult);
352
+ case 'heyspex_regen_task':
353
+ return handleRegenTask(vaultRoot, args);
354
+ case 'heyspex_regen_tasks_for_epic':
355
+ return handleRegenTasksForEpic(vaultRoot, args);
356
+ default:
357
+ return {
358
+ isError: true,
359
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
360
+ };
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Mode-aware vault gate, shared by `tools/call` (here) and `prompts/get`
366
+ * (cli.mjs). Returns `null` to proceed, or a `{ error, message }` payload.
367
+ *
368
+ * The old gate was `if (!existsSync(vaultRoot)) refuse` — mode-blind. In
369
+ * prod / cloud reliance (`resolveDevMode().readTier === 'neon'`) the server
370
+ * runs locally under Claude Code but every read is Neon-via-console and every
371
+ * write goes to the console, so a machine may legitimately have NO vault
372
+ * clone on disk; refusing there took every tool offline for exactly the
373
+ * customers the cloud path exists for. So: an absent root only refuses on the
374
+ * disk tier. On the neon tier the call proceeds with `vaultRoot` possibly
375
+ * absent — the `*ViaTier` handlers never touch it for reads, and the few
376
+ * machine-local writes (preflight markers) fall back to the HeySpex home
377
+ * (preflight-gate.mjs `preflightMarkerDir`).
378
+ *
379
+ * `resolveDevMode` is synchronous (env -> ~/.heyspex/dev-mode.json -> default
380
+ * prod) so `handleToolsCall` keeps its sync contract; a failure to resolve the
381
+ * mode degrades to the disk-tier refusal, the safe direction.
382
+ *
383
+ * Coverage: vault-gate-mode-aware.test.mjs drives `handleToolsCall` itself with
384
+ * a nonexistent root on both tiers. Note that cloud-reliant-session.test.mjs
385
+ * BYPASSES this gate on purpose — it calls the `*ViaTier` helpers directly —
386
+ * so it proves the handlers, not the gate; keep both.
387
+ */
388
+ export function vaultRootGateVerdict(vaultRoot, { env = process.env } = {}) {
389
+ if (typeof vaultRoot === 'string' && vaultRoot && existsSync(vaultRoot)) return null;
390
+ let readTier = 'disk';
391
+ try {
392
+ readTier = resolveDevMode({ env }).readTier;
393
+ } catch {
394
+ readTier = 'disk';
395
+ }
396
+ if (readTier === 'neon') return null;
397
+ return { error: 'VAULT_NOT_FOUND', message: `Vault root not found: ${vaultRoot}` };
398
+ }
399
+
400
+ export function handleToolsCall(params) {
401
+ let { name, arguments: args = {} } = params;
402
+ const config = getConfig();
403
+ const vaultRoot = config.vaultRoot;
404
+
405
+ const gate = vaultRootGateVerdict(vaultRoot);
406
+ if (gate) {
407
+ return {
408
+ isError: true,
409
+ content: [{ type: 'text', text: JSON.stringify(gate) }],
410
+ };
411
+ }
412
+
413
+ const hooksCfg = loadHooksConfig(vaultRoot);
414
+
415
+ // Meta hook plumbing — avoid Pre/PostToolUse recursion on hook tools themselves.
416
+ if (name === 'heyspex_session_end') return handleSessionEnd(vaultRoot, args, hooksCfg);
417
+ if (name === 'heyspex_hooks_list') return applyToolBudget(name, args, () => handleHooksList(vaultRoot), toolBudgetFor(name));
418
+ if (name === 'heyspex_hooks_invoke') return handleHooksInvoke(vaultRoot, args, hooksCfg);
419
+
420
+ // ── PreToolUse hooks ──
421
+ const preResult = runHooks('PreToolUse', name, args, null, hooksCfg);
422
+ if (preResult.blocked) {
423
+ return {
424
+ isError: true,
425
+ content: [{
426
+ type: 'text',
427
+ text: JSON.stringify({
428
+ error: 'HOOK_BLOCKED',
429
+ message: preResult.reason ?? 'Blocked by PreToolUse hook',
430
+ }),
431
+ }],
432
+ };
433
+ }
434
+ if (preResult.modifiedInput) {
435
+ args = preResult.modifiedInput;
436
+ }
437
+
438
+ // Preflight proof-of-use ([[preflight-gate]]). Deliberately here and not in a
439
+ // PreToolUse hook: hooks fail open and `.heyspex/mode.json` can disarm them,
440
+ // whereas this runs on every call in every session. `refuse` returns instead
441
+ // of dispatching; `warn` lets the call through and rides along on the result.
442
+ const preflightVerdict = evaluatePreflightGate(
443
+ vaultRoot,
444
+ name,
445
+ args,
446
+ detectAgentIdentity(args.agent, args.session_id).sessionId,
447
+ );
448
+ if (preflightVerdict && preflightVerdict.level === 'refuse') {
449
+ return {
450
+ isError: true,
451
+ content: [{
452
+ type: 'text',
453
+ text: JSON.stringify({
454
+ error: preflightVerdict.error,
455
+ message: preflightVerdict.message,
456
+ required_intents: preflightVerdict.intents,
457
+ tool: preflightVerdict.tool,
458
+ }, null, 2),
459
+ }],
460
+ };
461
+ }
462
+
463
+ const result = dispatchHeySpexTool(name, args, vaultRoot, config);
464
+ const finalized = finalizeToolResultWithHooks(name, args, result, hooksCfg, preResult);
465
+ return preflightVerdict && preflightVerdict.level === 'warn'
466
+ ? attachPreflightWarning(finalized, preflightVerdict)
467
+ : finalized;
468
+ }
469
+
470
+ /**
471
+ * Ride a warn-level preflight verdict along on an otherwise successful result.
472
+ * Best-effort by construction — a result whose text is not JSON is left exactly
473
+ * as it was rather than mangled to carry a warning.
474
+ */
475
+ export function attachPreflightWarning(result, verdict) {
476
+ try {
477
+ const block = result?.content?.[0];
478
+ if (!block || block.type !== 'text' || typeof block.text !== 'string') return result;
479
+ const payload = JSON.parse(block.text);
480
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return result;
481
+ payload.preflight_warning = {
482
+ code: verdict.error,
483
+ message: verdict.message,
484
+ required_intents: verdict.intents,
485
+ };
486
+ return {
487
+ ...result,
488
+ content: [{ ...block, text: JSON.stringify(payload, null, 2) }, ...result.content.slice(1)],
489
+ };
490
+ } catch {
491
+ return result;
492
+ }
493
+ }
494
+
495
+ /**
496
+ * Handle heyspex_session_end — fire SessionEnd hooks and return results.
497
+ *
498
+ * The agent calls this explicitly at session close to trigger cleanup hooks
499
+ * (release claim, record WIP, notify console). In Claude Code these fire
500
+ * automatically via SessionEnd/Stop hooks; in Command Code the agent must
501
+ * call this tool.
502
+ */
503
+ export function handleSessionEnd(vaultRoot, args, hooksCfg) {
504
+ const cfg = hooksCfg ?? loadHooksConfig(vaultRoot);
505
+ const groups = matchHooks('SessionEnd', 'heyspex_session_end', cfg);
506
+ if (groups.length === 0) {
507
+ return {
508
+ content: [{
509
+ type: 'text',
510
+ text: JSON.stringify({
511
+ session_ended: true,
512
+ hooks_run: 0,
513
+ note: 'No SessionEnd hooks configured. Call heyspex_release_spec if a spec is claimed.',
514
+ }, null, 2),
515
+ }],
516
+ };
517
+ }
518
+
519
+ const results = [];
520
+ const payload = {
521
+ hook_event_name: 'SessionEnd',
522
+ tool_name: 'heyspex_session_end',
523
+ tool_input: { reason: args?.reason ?? 'completed' },
524
+ cwd: vaultRoot,
525
+ };
526
+
527
+ for (const group of groups) {
528
+ const handlers = group.hooks ?? [];
529
+ for (const hookDef of handlers) {
530
+ try {
531
+ const r = executeHook(hookDef, payload, { vaultRoot });
532
+ results.push({
533
+ command: hookDef.command,
534
+ args: hookDef.args,
535
+ success: !r.blocked,
536
+ ...(r.reason ? { reason: r.reason } : {}),
537
+ });
538
+ } catch (err) {
539
+ results.push({
540
+ command: hookDef.command,
541
+ success: false,
542
+ error: err?.message ?? String(err),
543
+ });
544
+ }
545
+ }
546
+ }
547
+
548
+ return {
549
+ content: [{
550
+ type: 'text',
551
+ text: JSON.stringify({
552
+ session_ended: true,
553
+ hooks_run: results.length,
554
+ results,
555
+ }, null, 2),
556
+ }],
557
+ };
558
+ }
559
+
560
+ /**
561
+ * Handle heyspex_hooks_invoke — run lifecycle hooks on demand (Command Code parity).
562
+ */
563
+ export function handleHooksInvoke(vaultRoot, args, hooksCfg) {
564
+ const event = args?.event;
565
+ const validEvents = ['UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'SessionEnd'];
566
+ if (!validEvents.includes(event)) {
567
+ return {
568
+ isError: true,
569
+ content: [{
570
+ type: 'text',
571
+ text: JSON.stringify({
572
+ error: 'INVALID_EVENT',
573
+ message: `event must be one of: ${validEvents.join(', ')}`,
574
+ }),
575
+ }],
576
+ };
577
+ }
578
+
579
+ if (event === 'SessionEnd') {
580
+ return handleSessionEnd(vaultRoot, { reason: args?.reason ?? 'hooks_invoke' }, hooksCfg);
581
+ }
582
+
583
+ if (event === 'UserPromptSubmit') {
584
+ const prompt = args?.prompt;
585
+ if (typeof prompt !== 'string' || !prompt.trim()) {
586
+ return {
587
+ isError: true,
588
+ content: [{
589
+ type: 'text',
590
+ text: JSON.stringify({
591
+ error: 'MISSING_PROMPT',
592
+ message: 'prompt is required for UserPromptSubmit',
593
+ }),
594
+ }],
595
+ };
596
+ }
597
+ const hookResult = runHooks('UserPromptSubmit', '*', { prompt }, null, hooksCfg);
598
+ return formatHookInvokeResult(hookResult);
599
+ }
600
+
601
+ const toolName = args?.tool_name ?? '*';
602
+ const toolInput = args?.tool_input ?? {};
603
+
604
+ if (event === 'PreToolUse') {
605
+ const hookResult = runHooks('PreToolUse', toolName, toolInput, null, hooksCfg);
606
+ return formatHookInvokeResult(hookResult);
607
+ }
608
+
609
+ const toolResponse = args?.tool_response ?? {};
610
+ const hookResult = runHooks('PostToolUse', toolName, toolInput, toolResponse, hooksCfg);
611
+ return formatHookInvokeResult(hookResult);
612
+ }
613
+
614
+ export function formatHookInvokeResult(hookResult) {
615
+ return {
616
+ content: [{
617
+ type: 'text',
618
+ text: JSON.stringify({
619
+ blocked: hookResult.blocked ?? false,
620
+ reason: hookResult.reason ?? null,
621
+ additional_contexts: hookResult.additionalContexts ?? [],
622
+ modified_input: hookResult.modifiedInput ?? null,
623
+ }, null, 2),
624
+ }],
625
+ };
626
+ }
627
+
628
+ /**
629
+ * Handle heyspex_hooks_list — return the active hook configuration.
630
+ */
631
+ export function handleHooksList(vaultRoot) {
632
+ const cfg = listHookConfig(vaultRoot);
633
+ return {
634
+ content: [{
635
+ type: 'text',
636
+ text: JSON.stringify(cfg, null, 2),
637
+ }],
638
+ };
639
+ }