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,298 @@
1
+ #!/usr/bin/env node
2
+ // Claude Code hook → WIP before/after safety net for implementation sessions.
3
+ //
4
+ // Hardens the two ends of the WIP handoff contract that were, until now, pure
5
+ // agent discipline (session-close-protocol says record WIP *after*; the claim
6
+ // contract says read WIP *before*) and — critically — covers the gap NEITHER
7
+ // side covered: context-window rollover *mid*-implementation. The session-close
8
+ // protocol is too heavy for a rollover (it flips status, writes HS-REV, treats
9
+ // work as done) but work is still continuing. This hook injects the right
10
+ // reminder at the right moment instead of relying on the agent remembering.
11
+ //
12
+ // Two events, one script (mirrors emit-claim-status.mjs's multi-event shape):
13
+ //
14
+ // SessionStart (source: startup | resume)
15
+ // → the *before* side. Remind the agent to run heyspex_handoff_load + read
16
+ // the WIP ledger before claiming/continuing, and preview the most recent
17
+ // ledger entry so the resume context is non-empty.
18
+ //
19
+ // SessionStart (source: compact)
20
+ // → the reliable *mid-flight* net. Claude Code re-runs SessionStart right
21
+ // AFTER an auto/manual compaction, so additionalContext here lands in the
22
+ // fresh post-compaction context — the one place a reminder is guaranteed
23
+ // to be seen. Framed as "rollover, not fresh start — work continues."
24
+ //
25
+ // PreCompact
26
+ // → the *before-compaction* warning. Best-effort: fires just before context
27
+ // is compacted. Nudges a heyspex_record_wip + heyspex_handoff_record flush
28
+ // to the self-purging Neon lane while the full context is still present.
29
+ //
30
+ // IMPORTANT — what this hook does NOT do: a shell hook cannot call MCP tools, so
31
+ // it cannot perform the flush/load itself. It injects the reminder (same model
32
+ // as preflight-reminder.mjs). The agent still makes the tool calls.
33
+ //
34
+ // No CLAUDE_AGENT gate ([[HS-DT9L-NLR7-DIAG-nine-hooks-are-switched-off-in]]).
35
+ // This used to exit early in interactive sessions, so the mid-session rollover net
36
+ // never fired where a human is actually doing implementation work. Safe to run
37
+ // everywhere: this hook only ever injects additionalContext, it can never deny
38
+ // anything, so there is no approval-fatigue risk in removing the gate.
39
+ //
40
+ // Env gate:
41
+ // HEYSPEX_WIP_CHECKPOINT_OFF=1 hard kill switch — silent even when opted in.
42
+ //
43
+ // MUST exit 0 on every failure path — a hook that breaks Claude Code is worse
44
+ // than a hook that silently does nothing.
45
+ //
46
+ // Output shape: { hookSpecificOutput: { hookEventName, additionalContext } }.
47
+
48
+ import fs from 'node:fs';
49
+ import path from 'node:path';
50
+ import { fileURLToPath } from 'node:url';
51
+ import { getActiveConfig } from '../../vendor/heyspex-mcp/vault-resolve.mjs';
52
+ import { resolveConsoleUrl as resolveConsoleUrlShared } from '../lib/console-url-resolver.mjs';
53
+ import { readCredentialsMcpToken, resolveDevMode } from '../lib/dev-mode.mjs';
54
+
55
+ const NOTES_EXCERPT_MAX = 320;
56
+ const REMAINING_ITEM_MAX = 200;
57
+ const REMAINING_ITEMS_MAX = 3;
58
+ const LEDGER_REL = ['.heyspex', 'wip-state.json'];
59
+ const WIP_FETCH_TIMEOUT_MS = 1500;
60
+
61
+ function repoRoot() {
62
+ // scripts/claude-hooks/ → up two levels.
63
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
64
+ }
65
+
66
+ // Since the repo split (2026-08-31) the WIP ledger lives in the VAULT, not the
67
+ // code checkout. Reading repoRoot() here previewed a frozen pre-split snapshot to
68
+ // every new session — the HS-OR8H repo-split "quiet window OPEN" notice kept
69
+ // re-appearing for days after the window closed, because the live ledger had moved
70
+ // and this copy never advanced. Resolve the vault first; fall back to the code repo
71
+ // so a pre-split or standalone checkout still previews its own ledger.
72
+ function vaultRoot() {
73
+ try {
74
+ return getActiveConfig()?.repoRootAbs ?? null;
75
+ } catch {
76
+ return null; // registry unreadable — the code-repo base still applies
77
+ }
78
+ }
79
+
80
+ // Ledger roots in precedence order: vault first, code repo second.
81
+ function ledgerRoots() {
82
+ const roots = [];
83
+ const vault = vaultRoot();
84
+ if (vault) roots.push(vault);
85
+ const repo = repoRoot();
86
+ if (!roots.includes(repo)) roots.push(repo);
87
+ return roots;
88
+ }
89
+
90
+ async function readStdinJson() {
91
+ if (process.stdin.isTTY) return null;
92
+ return new Promise((resolve) => {
93
+ let buf = '';
94
+ let settled = false;
95
+ const finish = (value) => {
96
+ if (settled) return;
97
+ settled = true;
98
+ resolve(value);
99
+ };
100
+ const timer = setTimeout(() => finish(buf || ''), 200);
101
+ process.stdin.setEncoding('utf8');
102
+ process.stdin.on('data', (chunk) => {
103
+ buf += chunk;
104
+ });
105
+ process.stdin.on('end', () => {
106
+ clearTimeout(timer);
107
+ finish(buf);
108
+ });
109
+ process.stdin.on('error', () => {
110
+ clearTimeout(timer);
111
+ finish(buf);
112
+ });
113
+ });
114
+ }
115
+
116
+ function emit(eventName, additionalContext) {
117
+ if (!additionalContext) return;
118
+ process.stdout.write(
119
+ JSON.stringify({
120
+ hookSpecificOutput: { hookEventName: eventName, additionalContext },
121
+ }),
122
+ );
123
+ }
124
+
125
+ function truncate(value, limit) {
126
+ if (typeof value !== 'string') return '';
127
+ const trimmed = value.trim();
128
+ if (!trimmed) return '';
129
+ return trimmed.length > limit ? `${trimmed.slice(0, limit)}…` : trimmed;
130
+ }
131
+
132
+ // Read the file ledger and return the single most-recently-updated entry, or
133
+ // null when the ledger is missing/empty/malformed. The file ledger runs
134
+ // alongside the Neon handoff_context lane until the file→Neon cutover
135
+ // (HS-EPIC-PFMN); reading the file keeps SessionStart robust when console/Neon
136
+ // is offline.
137
+ function mostRecentLedgerEntry() {
138
+ let parsed = null;
139
+ for (const root of ledgerRoots()) {
140
+ try {
141
+ const candidate = JSON.parse(fs.readFileSync(path.join(root, ...LEDGER_REL), 'utf8'));
142
+ if (candidate && typeof candidate === 'object') {
143
+ parsed = candidate;
144
+ break;
145
+ }
146
+ } catch {
147
+ // missing or unreadable at this root — try the next one
148
+ }
149
+ }
150
+ if (!parsed || typeof parsed !== 'object') return null;
151
+ let best = null;
152
+ for (const [epicId, entry] of Object.entries(parsed)) {
153
+ if (!entry || typeof entry !== 'object') continue;
154
+ const updatedAt = typeof entry.updatedAt === 'string' ? entry.updatedAt : '';
155
+ if (!best || updatedAt > best.updatedAt) {
156
+ best = { epicId, updatedAt, entry };
157
+ }
158
+ }
159
+ return best;
160
+ }
161
+
162
+ function ledgerPreview(best, source = 'disk') {
163
+ if (!best) return '';
164
+ const { epicId, entry } = best;
165
+ const lines = [`Most recent WIP ledger entry — ${epicId} (updated ${best.updatedAt || 'unknown'}):`];
166
+ const notes = truncate(entry.notes, NOTES_EXCERPT_MAX);
167
+ if (notes) lines.push(` notes: ${notes}`);
168
+ if (Array.isArray(entry.remaining) && entry.remaining.length) {
169
+ lines.push(' remaining:');
170
+ for (const item of entry.remaining.slice(0, REMAINING_ITEMS_MAX)) {
171
+ const text = truncate(item, REMAINING_ITEM_MAX);
172
+ if (text) lines.push(` • ${text}`);
173
+ }
174
+ }
175
+ lines.push(
176
+ source === 'cloud'
177
+ ? ' (This is a cloud WIP preview from /api/wip — trust it over reconstructing state from scratch.)'
178
+ : ' (This is a disk preview — the vault file on disk is the source of truth; ' +
179
+ 'do not reconstruct state by re-reading every child spec + git diff.)',
180
+ );
181
+ return lines.join('\n');
182
+ }
183
+
184
+ /**
185
+ * Cloud-reliance SessionStart preview: `GET {console}/api/wip` (bearer-gated)
186
+ * returns `{ wip_state: { "<SPEC-ID>": { sessionId, agent, updatedAt, notes,
187
+ * remaining, filesTouched, ... } } }` — the same shape `heyspex_record_wip`
188
+ * writes. Only consulted when `resolveDevMode()` reports cloud reliance; falls
189
+ * back to disk on ANY failure (unreachable console, bad token, malformed
190
+ * body) and never throws — this hook must stay fail-open and silent.
191
+ */
192
+ async function fetchCloudLedgerEntry(root) {
193
+ let mode;
194
+ try {
195
+ mode = resolveDevMode({});
196
+ } catch {
197
+ return null;
198
+ }
199
+ if (mode.reliance !== 'cloud') return null;
200
+
201
+ let consoleUrl;
202
+ try {
203
+ consoleUrl = resolveConsoleUrlShared({
204
+ repoRoot: root,
205
+ devMode: { reliance: mode.reliance, consoleUrl: mode.consoleUrl },
206
+ });
207
+ } catch {
208
+ return null;
209
+ }
210
+
211
+ const token = readCredentialsMcpToken();
212
+ const controller = new AbortController();
213
+ const timer = setTimeout(() => controller.abort(), WIP_FETCH_TIMEOUT_MS);
214
+ timer.unref?.();
215
+ try {
216
+ const res = await fetch(`${consoleUrl}/api/wip`, {
217
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
218
+ signal: controller.signal,
219
+ });
220
+ if (!res.ok) return null;
221
+ const body = await res.json();
222
+ const wipState = body?.wip_state;
223
+ if (!wipState || typeof wipState !== 'object') return null;
224
+ let best = null;
225
+ for (const [specId, entry] of Object.entries(wipState)) {
226
+ if (!entry || typeof entry !== 'object') continue;
227
+ const updatedAt = typeof entry.updatedAt === 'string' ? entry.updatedAt : '';
228
+ if (!best || updatedAt > best.updatedAt) {
229
+ best = { epicId: specId, updatedAt, entry };
230
+ }
231
+ }
232
+ return best;
233
+ } catch {
234
+ return null;
235
+ } finally {
236
+ clearTimeout(timer);
237
+ }
238
+ }
239
+
240
+ const BEFORE_REMINDER =
241
+ 'Session start. Before you claim or continue work, load the prior handoff: ' +
242
+ 'call `heyspex_handoff_load({})` for your Neon lane context, and read the ' +
243
+ '`/hs-next` WIP block (file ledger at `.heyspex/wip-state.json`). Trust the ' +
244
+ 'structured handoff over reconstructing state from scratch.';
245
+
246
+ const COMPACT_REMINDER =
247
+ 'Context was just compacted mid-session. This is a ROLLOVER, not a fresh ' +
248
+ 'start — work is continuing. Re-orient before your next action: run ' +
249
+ '`heyspex_handoff_load({})` and re-read the WIP ledger below. Do NOT treat ' +
250
+ 'this as session close — do not flip status or write an HS-REV; the work is ' +
251
+ 'not done.';
252
+
253
+ const PRECOMPACT_REMINDER =
254
+ '⚠️ Context is about to be compacted. CHECKPOINT NOW while the full context ' +
255
+ 'is still present, if you are mid-implementation: (1) `heyspex_record_wip` ' +
256
+ 'for the worked epic (filesTouched / untrackedArtifacts / specsFiled / ' +
257
+ 'verified / remaining / notes), and (2) `heyspex_handoff_record` to the ' +
258
+ 'self-purging Neon lane. This is a context-rollover CHECKPOINT, not session ' +
259
+ 'close — do NOT flip status or write an HS-REV; work is continuing.';
260
+
261
+ async function main() {
262
+ if (process.env.HEYSPEX_WIP_CHECKPOINT_OFF === '1') return;
263
+
264
+ const raw = await readStdinJson();
265
+ if (!raw) return;
266
+ let payload;
267
+ try {
268
+ payload = JSON.parse(raw);
269
+ } catch {
270
+ return;
271
+ }
272
+
273
+ const event = payload?.hook_event_name;
274
+ if (!event) return;
275
+
276
+ if (event === 'SessionStart') {
277
+ const source = payload?.source;
278
+ let best = await fetchCloudLedgerEntry(repoRoot());
279
+ let ledgerSource = 'cloud';
280
+ if (!best) {
281
+ best = mostRecentLedgerEntry();
282
+ ledgerSource = 'disk';
283
+ }
284
+ const preview = ledgerPreview(best, ledgerSource);
285
+ const head = source === 'compact' ? COMPACT_REMINDER : BEFORE_REMINDER;
286
+ emit(event, preview ? `${head}\n\n${preview}` : head);
287
+ return;
288
+ }
289
+
290
+ if (event === 'PreCompact') {
291
+ emit(event, PRECOMPACT_REMINDER);
292
+ return;
293
+ }
294
+ }
295
+
296
+ main().catch(() => {
297
+ // Never break Claude Code on a hook failure.
298
+ });
@@ -0,0 +1,235 @@
1
+ // manifest.mjs — the Cursor registration for every HeySpex lifecycle hook.
2
+ //
3
+ // SSOT for three consumers: install-cursor-hooks.mjs (writes `.cursor/hooks.json`),
4
+ // heyspex-doctor.mjs (the `cursor-hooks` check), and _run.smoke.mjs (asserts the
5
+ // manifest stays in parity with `.claude/settings.json`).
6
+ //
7
+ // HOW THE MAPPING WAS DERIVED
8
+ //
9
+ // Claude Code splits the tool lifecycle by PHASE (PreToolUse / PostToolUse) and
10
+ // selects with a regex over tool names. Cursor splits by CHANNEL — shell, MCP,
11
+ // file read, file edit, subagent each get their own event — and its `matcher`
12
+ // only understands tool KINDS (`Write`, `Shell`, `Read`, `Grep`, `Delete`,
13
+ // `Task`, `MCP:<name>`). So one Claude row can become a row on a different
14
+ // event here, and a Claude matcher like `mcp__heyspex__heyspex_claim_spec` moves
15
+ // into the shim's own `tool` filter.
16
+ //
17
+ // Two rules keep this honest:
18
+ // 1. NO DOUBLE-FIRE. A hook is registered on exactly one channel per phase.
19
+ // Shell-facing gates go on `beforeShellExecution` (the only event that
20
+ // reliably carries the command text) and are therefore NOT also on
21
+ // `preToolUse`, whose registrations are scoped to `Write|Delete`.
22
+ // 2. NO SILENT DOWNGRADE. A Claude hard gate maps to an event where Cursor
23
+ // honors a `permission` verdict, or it is listed in UNPORTABLE with the
24
+ // rule that covers it instead. A gate that can only "advise" in Cursor is
25
+ // not parity, and pretending otherwise is worse than a documented gap.
26
+ //
27
+ // `matcher` is Cursor-side (cheap, evaluated before the process spawns);
28
+ // `tool` becomes `--hs-tool=<regex>` and is evaluated by the shim after it
29
+ // parses the payload. Prefer `matcher`; use `tool` only for what it can't say.
30
+
31
+ export const SHIM = 'scripts/cursor-hooks/_run.mjs';
32
+ export const DEFAULT_TIMEOUT_SECONDS = 5;
33
+
34
+ /**
35
+ * @typedef {object} HookEntry
36
+ * @property {string} event Cursor hook event name (camelCase).
37
+ * @property {string} hook Hook name resolved under scripts/claude-hooks/,
38
+ * or a repo-relative path for a hook that lives
39
+ * elsewhere.
40
+ * @property {string[]} [args] Extra args forwarded to the hook verbatim.
41
+ * @property {string} [matcher] Cursor-side matcher.
42
+ * @property {string} [tool] Shim-side tool-name regex (`--hs-tool=`).
43
+ * @property {number} [timeout] Seconds; defaults to DEFAULT_TIMEOUT_SECONDS.
44
+ * @property {string} [claudeEvent] Override the Claude event name the hook is
45
+ * told it is running under, when the Cursor event
46
+ * maps ambiguously.
47
+ * @property {string} why One line: what this registration buys.
48
+ */
49
+
50
+ /** @type {HookEntry[]} */
51
+ export const HOOKS = [
52
+ // ── Session start ───────────────────────────────────────────────────────
53
+ // Cursor's sessionStart honors `additional_context`, so these injections
54
+ // land in context exactly as they do in Claude Code.
55
+ { event: 'sessionStart', hook: 'wip-checkpoint', why: 'Surface the previous session WIP handoff before work resumes.' },
56
+ { event: 'sessionStart', hook: 'inject-dev-server-awareness', why: 'Once-per-session reminder that dev-server status is a tool call, not a guess.' },
57
+ { event: 'sessionStart', hook: 'inject-coo-seat', why: 'Inject the dev-factory COO seat mandate when the COO window is open.' },
58
+ { event: 'sessionStart', hook: 'coo-guard-check', why: 'Warn at session start when a breakdown has not been delegated.' },
59
+ { event: 'sessionStart', hook: 'check-auto-memory-budget', why: 'Self-heal and budget-check the auto-memory index.' },
60
+ { event: 'sessionStart', hook: 'check-claude-md-budget', why: 'Advisory budget check on the always-loaded agent contract.' },
61
+ { event: 'sessionStart', hook: 'check-mcp-freshness-hook', why: 'Warn when the running MCP server is stale versus vendor.' },
62
+
63
+ // ── Compaction ──────────────────────────────────────────────────────────
64
+ // Claude re-runs SessionStart(source: compact) after compaction; Cursor fires
65
+ // preCompact BEFORE it, so the shim relays this one as SessionStart(compact)
66
+ // to keep wip-checkpoint's "rollover, not fresh start" framing.
67
+ { event: 'preCompact', hook: 'wip-checkpoint', claudeEvent: 'SessionStart', why: 'Carry the WIP handoff across a context compaction.' },
68
+
69
+ // ── Every user turn ─────────────────────────────────────────────────────
70
+ // beforeSubmitPrompt is Cursor's UserPromptSubmit. It honors `user_message`
71
+ // but not `additional_context`, so these three surface in the transcript
72
+ // rather than as invisible context — the routing still reaches the agent,
73
+ // and the user can see why it fired.
74
+ { event: 'beforeSubmitPrompt', hook: 'preflight-reminder', why: 'Name the required procedure before a filing/destructive/protocol turn.' },
75
+ { event: 'beforeSubmitPrompt', hook: 'intent-classifier-trigger', why: 'Route capture / research / settings / defer intents to the right skill.' },
76
+ { event: 'beforeSubmitPrompt', hook: 'inject-communication-style', why: 'Re-assert the plain-language communication rules each turn.' },
77
+
78
+ // ── Before every tool call (any tool) ───────────────────────────────────
79
+ { event: 'preToolUse', matcher: '.*', hook: 'emit-claim-status', args: ['--event=PreToolUse'], why: 'Claim liveness heartbeat to the console.' },
80
+ { event: 'preToolUse', matcher: '.*', hook: 'coo-denylist-gate', why: 'Hard deny the COO-window deny list.' },
81
+ { event: 'preToolUse', matcher: '.*', hook: 'log-session-activity', args: ['--event=PreToolUse'], why: 'Append agent.tool_called rows to the session log.' },
82
+
83
+ // ── Before a file write / delete ─────────────────────────────────────────
84
+ // Cursor's `Write` covers create AND patch (Claude's Write|Edit|MultiEdit);
85
+ // the shim re-derives which one from the input shape. `Delete` is included on
86
+ // the vault guard because removing a profile file is the loss it exists to
87
+ // prevent, and Claude had no separate delete tool to list.
88
+ { event: 'preToolUse', matcher: 'Write|Delete', hook: 'env-vault-guard', why: 'Block destructive writes to the encrypted env vault (ALWAYS ON).' },
89
+ { event: 'preToolUse', matcher: 'Write', hook: 'settings-write-gate', why: 'Require approval before .heyspex/config.yaml changes.' },
90
+ { event: 'preToolUse', matcher: 'Write', hook: 'enforce-vault-naming', why: 'Block vault filenames that violate the naming registry.' },
91
+ { event: 'preToolUse', matcher: 'Write', hook: 'guard-auto-memory-writes', why: 'Block new topic files in the auto-memory directory.' },
92
+ { event: 'preToolUse', matcher: 'Write', hook: 'preflight-reminder', why: 'Preflight reminder on the edit path itself.' },
93
+ { event: 'preToolUse', matcher: 'Write', hook: 'claim-before-code-gate', why: 'Deny the first code edit after plan exit with no claim.' },
94
+
95
+ // ── Before a shell command ──────────────────────────────────────────────
96
+ // beforeShellExecution, not preToolUse/Shell: it is the only event guaranteed
97
+ // to carry the command string, and Cursor honors allow/deny/ask on it.
98
+ { event: 'beforeShellExecution', hook: 'env-vault-guard', why: 'Block shell writes/exfiltration targeting the env vault (ALWAYS ON).' },
99
+ { event: 'beforeShellExecution', hook: 'settings-write-gate', why: 'Catch shell writes to .heyspex/config.yaml.' },
100
+ { event: 'beforeShellExecution', hook: 'enforce-vault-naming', why: 'Catch vault files created via the shell.' },
101
+ { event: 'beforeShellExecution', hook: 'guard-auto-memory-writes', why: 'Catch auto-memory topic files created via the shell.' },
102
+ { event: 'beforeShellExecution', hook: 'preflight-reminder', why: 'Preflight reminder on rm -rf / force-push shaped commands.' },
103
+ { event: 'beforeShellExecution', hook: 'dev-server-task-reminder', timeout: 10, why: 'Dev-server start/stop hard gate plus DB-safety reminder.' },
104
+ { event: 'beforeShellExecution', hook: 'git-stash-gate', timeout: 10, why: 'Always ask before a mutating git stash.' },
105
+
106
+ // ── Before specific MCP tools ───────────────────────────────────────────
107
+ // Cursor fires preToolUse for MCP tools too (matcher `MCP:<name>`), so these
108
+ // stay on preToolUse rather than beforeMCPExecution — registering both would
109
+ // fire each gate twice. Scoping is the shim's `tool` regex because a Cursor
110
+ // matcher cannot name one MCP tool per entry portably.
111
+ { event: 'preToolUse', matcher: '.*', tool: 'heyspex_claim_spec$', hook: 'coo-guard-check', why: 'Block a claim whose breakdown was never delegated.' },
112
+ { event: 'preToolUse', matcher: '.*', tool: 'heyspex_file_spec$', hook: 'capture-workflow-gate', timeout: 8, why: 'Deny filing an implementation spec straight out of an open capture.' },
113
+ { event: 'preToolUse', matcher: '.*', tool: 'heyspex_settings_set$', hook: 'settings-write-gate', why: 'Require approval for settings changes made through MCP.' },
114
+ { event: 'preToolUse', matcher: '.*', tool: '[Nn]eon__(prepare_database_migration|complete_database_migration|reset_from_parent|delete_branch)$', hook: 'dev-server-task-reminder', timeout: 10, why: 'DB-safety gate on Neon migration/reset/delete tools.' },
115
+ { event: 'preToolUse', matcher: '.*', tool: '^(Workflow|Skill)$', hook: 'deep-research-cost-gate', why: 'Cost confirmation before a paid deep-research harness launch.' },
116
+ { event: 'preToolUse', matcher: '.*', tool: '^(WebSearch|WebFetch|web_search|web_fetch)$|heyspex_save_research$', hook: 'research-protocol-gate', why: 'WebSearch/WebFetch/save_research refused until the research preflight ran (HS-K0ZP, HS-EN4F).' },
117
+
118
+ // ── Before a subagent spawn ─────────────────────────────────────────────
119
+ // subagentStart over preToolUse/Task: it carries subagent_type and the model,
120
+ // which is exactly what the routing gate decides on, and it can deny.
121
+ { event: 'subagentStart', hook: 'agent-spawn-delegation-gate', why: 'Block a build/breakdown/critic subagent on an off-routing model.' },
122
+
123
+ // ── After every tool call ───────────────────────────────────────────────
124
+ { event: 'postToolUse', matcher: '.*', hook: 'emit-claim-status', args: ['--event=PostToolUse'], why: 'Claim liveness heartbeat after the call resolves.' },
125
+ { event: 'postToolUse', matcher: '.*', hook: 'log-session-activity', args: ['--event=PostToolUse'], why: 'Append the tool result row to the session log.' },
126
+ { event: 'postToolUse', matcher: '.*', hook: 'close-terminal-on-ship', why: 'Close the Launch terminal once the spec ships.' },
127
+ { event: 'postToolUse', matcher: 'Write', hook: 'notify-status-flip', why: 'Sound on an in_review status flip.' },
128
+ { event: 'postToolUse', matcher: 'Write', hook: 'scripts/stamp-provenance-on-edit.mjs', why: 'Stamp assisted_by provenance on direct spec edits.' },
129
+
130
+ // afterFileEdit is a distinct signal, not a duplicate: it reports the edit
131
+ // that actually landed (with old/new strings) even when the write came from a
132
+ // path postToolUse did not describe as an edit.
133
+ { event: 'afterFileEdit', matcher: '.*', hook: 'close-terminal-on-ship', why: 'Ship detection from the concrete file edit.' },
134
+
135
+ // ── End of the agent turn ───────────────────────────────────────────────
136
+ // Cursor's `stop` honors `followup_message`, which is how a blocking Stop
137
+ // hook re-drives the agent — the same effect as Claude's exit-2 block.
138
+ { event: 'stop', hook: 'emit-claim-status', args: ['--event=Stop'], why: 'Mark the claim pending-on-response.' },
139
+ { event: 'stop', hook: 'naughty-list-gate', why: 'Re-drive the turn when the final message uses banned jargon.' },
140
+ { event: 'stop', hook: 'enforce-session-sequence', why: 'Re-drive the turn when work shipped without record_wip.' },
141
+
142
+ // ── Session end ─────────────────────────────────────────────────────────
143
+ { event: 'sessionEnd', hook: 'session-end', why: 'Release the claim and notify the console.' },
144
+ { event: 'sessionEnd', hook: 'close-terminal-on-ship', why: 'Final terminal cleanup.' },
145
+ ];
146
+
147
+ /**
148
+ * Claude registrations with no Cursor trigger. Kept in code, not in a comment,
149
+ * so the doctor can report the gap and the smoke test can assert the list has
150
+ * not quietly grown.
151
+ */
152
+ export const UNPORTABLE = [
153
+ {
154
+ hook: 'plan-review-gate',
155
+ claudeEvent: 'PreToolUse/ExitPlanMode',
156
+ reason: 'Cursor exposes no ExitPlanMode tool call to hooks.',
157
+ coveredBy: '.cursor/rules/plan-review-required.mdc (always-on rule)',
158
+ },
159
+ {
160
+ hook: 'plan-exit-ingest-gate',
161
+ claudeEvent: 'PreToolUse/ExitPlanMode',
162
+ reason: 'Same missing trigger; it is also what arms claim-before-code-gate.',
163
+ coveredBy: '.cursor/rules/plan-review-required.mdc + spec-creation-linkage.mdc',
164
+ },
165
+ ];
166
+
167
+ /**
168
+ * The command string Cursor runs for one entry.
169
+ *
170
+ * The tool filter is a REGEX, and its metacharacters are also shell
171
+ * metacharacters — `[Nn]eon__(a|b)$` unquoted is a syntax error in bash, not a
172
+ * pattern. It is therefore always double-quoted. A trailing `$` inside double
173
+ * quotes is literal in both bash and PowerShell, and the patterns contain no
174
+ * whitespace, so quoting is sufficient without any escaping.
175
+ */
176
+ export function commandFor(entry) {
177
+ const parts = ['node', SHIM, entry.hook];
178
+ if (entry.tool) parts.push(`--hs-tool="${entry.tool}"`);
179
+ if (entry.claudeEvent) parts.push(`--hs-claude-event=${entry.claudeEvent}`);
180
+ for (const arg of entry.args ?? []) parts.push(arg);
181
+ return parts.join(' ');
182
+ }
183
+
184
+ /**
185
+ * Same command, addressed from a PLUGIN install.
186
+ *
187
+ * A project hook runs with the repo as its working directory, so a relative
188
+ * path resolves. A plugin hook runs from the plugin's own cache directory,
189
+ * where `scripts/cursor-hooks/_run.mjs` does not exist — the shim and the hook
190
+ * bodies live in the host vault checkout. `$CURSOR_PROJECT_DIR` is how the
191
+ * plugin reaches back into it (the same reason the plugin's MCP entry goes
192
+ * through bin/launch-mcp.mjs instead of naming cli.mjs directly).
193
+ */
194
+ export function commandForPlugin(entry) {
195
+ return commandFor(entry).replace(`node ${SHIM}`, `node "$CURSOR_PROJECT_DIR/${SHIM}"`);
196
+ }
197
+
198
+ /**
199
+ * The committed launcher a CUSTOMER repo carries for Cursor, repo-relative.
200
+ * Written by `hs install` / `scripts/install-cursor-hooks.mjs` from
201
+ * `renderCustomerLauncher('cursor')` in scripts/lib/customer-kit.mjs; it finds
202
+ * the `heyspex` package (node_modules/heyspex) and hands off to this shim.
203
+ */
204
+ export const CUSTOMER_LAUNCHER = '.cursor/heyspex/run-hook.mjs';
205
+
206
+ /**
207
+ * Same command, addressed from a CUSTOMER repo: the committed launcher by
208
+ * `$CURSOR_PROJECT_DIR` — never this machine's path. A customer repo has no
209
+ * `scripts/cursor-hooks/` of its own, so the relative form failed there with
210
+ * MODULE_NOT_FOUND ([[HS-EN4F-F4V2-DIAG-a-repo-set-up-with-hs-install-gets-no]]);
211
+ * the 2026-10-02 fix wrote the platform checkout's ABSOLUTE path, which the
212
+ * customer committed and which existed on no other machine
213
+ * ([[HS-0AF8-WW9R-TASK-portable-hook-and-command-paths-in]]).
214
+ */
215
+ export function commandForCustomer(entry) {
216
+ return commandFor(entry).replace(`node ${SHIM}`, `node "$CURSOR_PROJECT_DIR/${CUSTOMER_LAUNCHER}"`);
217
+ }
218
+
219
+ /** Identity of an entry inside `.cursor/hooks.json` — what "already installed"
220
+ * means, and what a repair replaces. Two entries for the same hook on the same
221
+ * event are distinguished by their scoping, so the tool filter is part of it. */
222
+ export function identityFor(entry) {
223
+ return `${entry.hook}::${entry.tool ?? ''}::${(entry.args ?? []).join(' ')}`;
224
+ }
225
+
226
+ export function timeoutFor(entry) {
227
+ return entry.timeout ?? DEFAULT_TIMEOUT_SECONDS;
228
+ }
229
+
230
+ /** Every Cursor event this manifest registers, in a stable order. */
231
+ export function events() {
232
+ const seen = [];
233
+ for (const entry of HOOKS) if (!seen.includes(entry.event)) seen.push(entry.event);
234
+ return seen;
235
+ }
@@ -39,6 +39,13 @@ import http from 'node:http';
39
39
  import { spawnSync } from 'node:child_process';
40
40
  import { fileURLToPath, pathToFileURL } from 'node:url';
41
41
 
42
+ // The runnable form of a HeySpex command on THIS machine — `hs …` when the bin
43
+ // is on PATH, else an absolute-path `node "<install>/bin/hs.mjs" …`. Never an
44
+ // `npx` form: npx resolves from the READER's working directory, and outside a
45
+ // checkout `npx hs` installs the unrelated public package hs@2.0.1
46
+ // ([[HS-I6HT-EKYU-DIAG-every-message-tells-the-user-to-run-hs]]).
47
+ import { hsCommand } from './lib/hs-command.mjs';
48
+
42
49
  // `/api/version` is the spex-console liveness endpoint (cheap JSON, no page
43
50
  // render). Probing `/` false-negatives during Next dev JIT compiles — the full
44
51
  // app shell can exceed 2s while API routes the UI already uses keep returning
@@ -109,14 +116,30 @@ function readLockfile(vaultRoot) {
109
116
  return { present: false, pid: null, url: null, startedAt: null, host: null, lan: null };
110
117
  }
111
118
 
119
+ /**
120
+ * Count of rows in `.heyspex/index-write-failures.jsonl` — the durable, operator-visible
121
+ * record `spex-console/src/lib/vault.ts`'s `recordIndexWriteFailure` appends to whenever a
122
+ * Neon index upsert fails and the write falls through to disk-only (QC finding, harness
123
+ * security epic HS-KRK5). A `console.error` line alone scrolls out of view; surfacing the
124
+ * count here means `heyspex_dev_server_status` (and `hs dev-status`) actually show it.
125
+ * Missing/unreadable file => 0, never a throw — this status check must never itself fail.
126
+ */
127
+ function countIndexWriteFailures(vaultRoot) {
128
+ const file = path.join(vaultRoot, '.heyspex', 'index-write-failures.jsonl');
129
+ try {
130
+ const raw = fs.readFileSync(file, 'utf8');
131
+ return raw.split(/\r?\n/).filter((line) => line.trim().length > 0).length;
132
+ } catch {
133
+ return 0;
134
+ }
135
+ }
136
+
112
137
  function readConsoleUrlFile(vaultRoot) {
113
- for (const name of ['console-url', 'cockpit-url']) { // legacy-cockpit fallback
114
- try {
115
- const raw = fs.readFileSync(path.join(vaultRoot, '.heyspex', name), 'utf8').trim();
116
- if (raw.length > 0) return raw.replace(/\/+$/, '');
117
- } catch {
118
- // try next candidate
119
- }
138
+ try {
139
+ const raw = fs.readFileSync(path.join(vaultRoot, '.heyspex', 'console-url'), 'utf8').trim();
140
+ if (raw.length > 0) return raw.replace(/\/+$/, '');
141
+ } catch {
142
+ // missing or unreadable discovery file
120
143
  }
121
144
  return null;
122
145
  }
@@ -135,11 +158,6 @@ export function writeConsoleUrlFile(vaultRoot, url) {
135
158
  const normalized = String(url).trim().replace(/\/+$/, '');
136
159
  fs.writeFileSync(tmp, `${normalized}\n`, 'utf8');
137
160
  fs.renameSync(tmp, file);
138
- try {
139
- fs.writeFileSync(path.join(dir, 'cockpit-url'), `${normalized}\n`, 'utf8'); // legacy-cockpit shadow
140
- } catch {
141
- // Best-effort — canonical console-url above is authoritative.
142
- }
143
161
  return normalized;
144
162
  }
145
163
 
@@ -463,7 +481,7 @@ const DISCREPANCY_GUIDANCE_TEXT = {
463
481
  port_mismatch:
464
482
  'The lockfile points at one port but the live console server is answering on a different one. A server IS running (see `servers`) — do not start a new one; use the port shown in `servers`, not the lockfile\'s port.',
465
483
  multiple_servers:
466
- 'More than one console server is answering at once — this is the corrupted-.next state two concurrent `next dev` processes cause. Do NOT start another. Stop the extras first (`hs server stop` / `npm run console:stop`).',
484
+ `More than one console server is answering at once — this is the corrupted-.next state two concurrent \`next dev\` processes cause. Do NOT start another. Stop the extras first (\`${hsCommand('server stop')}\` / \`npm run console:stop\`).`,
467
485
  page_probe_failed:
468
486
  'The API probe answered but a real page route failed to render (HS-D4O9-DIAG: e.g. a missing dependency leaves every page throwing while API handlers keep answering 200). The server is up but likely broken — check server logs / reinstall deps. Restarting the dev server will usually NOT fix this on its own, so do not treat this flag as "not running".',
469
487
  };
@@ -647,6 +665,9 @@ export async function getDevServerStatus({ vaultRoot, ports, pagePath } = {}) {
647
665
  // entry is restart-BLOCKING (evidence a server exists), never a restart
648
666
  // trigger. See DISCREPANCY_GUIDANCE_TEXT above for the per-code rationale.
649
667
  discrepancyGuidance: buildDiscrepancyGuidance([...new Set(discrepancies)]),
668
+ // Additive (QC finding, harness security epic HS-KRK5): a count, not the raw rows —
669
+ // an operator glancing at status output needs "is this happening", not a full replay.
670
+ indexWriteFailures: countIndexWriteFailures(root),
650
671
  platform: process.platform,
651
672
  checkedAt: new Date().toISOString(),
652
673
  };