heyspex 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (352) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/README.md +21 -0
  3. package/bin/hs.mjs +1089 -104
  4. package/bin/spexcode.mjs +262 -0
  5. package/dev-factory/engine/worktree.mjs +740 -0
  6. package/dev-factory/lib/code-repo-root.mjs +67 -0
  7. package/dev-factory/lib/context-budget.mjs +84 -0
  8. package/dev-factory/lib/epic-detect.mjs +190 -0
  9. package/dev-factory/lib/fleet-queue-file.mjs +846 -0
  10. package/dev-factory/lib/fleet-queue.mjs +426 -0
  11. package/dev-factory/lib/fleet-state.mjs +14 -18
  12. package/dev-factory/lib/model-catalog.mjs +338 -0
  13. package/dev-factory/lib/seat-context.mjs +199 -0
  14. package/dev-factory/lib/spec-io.mjs +93 -0
  15. package/dev-factory/lib/sufficiency-score.mjs +8 -2
  16. package/dev-factory/lib/worktree-allocator.mjs +744 -0
  17. package/dev-factory/model-catalog.json +222 -0
  18. package/dist/cli/hs-check.mjs +1127 -103
  19. package/dist/cli/hs-claim.mjs +981 -63
  20. package/dist/cli/hs-connect.mjs +10059 -1729
  21. package/dist/cli/hs-defer.mjs +5507 -3072
  22. package/dist/cli/hs-dev.mjs +576 -0
  23. package/dist/cli/hs-env.mjs +416 -302
  24. package/dist/cli/hs-index-rebuild.mjs +7334 -0
  25. package/dist/cli/hs-index-reconcile.mjs +8138 -0
  26. package/dist/cli/hs-init.mjs +2840 -149
  27. package/dist/cli/hs-install.mjs +7214 -594
  28. package/dist/cli/hs-join.mjs +10640 -2421
  29. package/dist/cli/hs-vault-pull.mjs +8157 -6370
  30. package/dist/cli/hs-vault-push.mjs +6433 -0
  31. package/dist/cli/hs-vault.mjs +4029 -1555
  32. package/dist/cli/hs-workspace.mjs +2702 -538
  33. package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
  34. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
  35. package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
  36. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
  37. package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
  38. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
  39. package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
  40. package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
  41. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
  42. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
  43. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
  44. package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
  45. package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
  46. package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -0
  47. package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
  48. package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
  49. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
  50. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
  51. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
  52. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
  53. package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
  54. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
  55. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
  56. package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -0
  57. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
  58. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
  59. package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
  60. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
  61. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
  62. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
  63. package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
  64. package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
  65. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +33 -0
  66. package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
  67. package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
  68. package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
  69. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
  70. package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
  71. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
  72. package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
  73. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
  74. package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
  75. package/dist/cli-templates/harness-commands/cursor/hs-view.md +32 -0
  76. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
  77. package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
  78. package/docs/copilot-plugin-install.md +162 -0
  79. package/docs/driving-heyspex-from-copilot.md +165 -0
  80. package/docs/mcp-client-config.md +259 -6
  81. package/package.json +249 -124
  82. package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
  83. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
  84. package/plugins/heyspex/.mcp.json +12 -0
  85. package/plugins/heyspex/README.md +309 -0
  86. package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
  87. package/plugins/heyspex/bin/run-hook.mjs +80 -0
  88. package/plugins/heyspex/hooks/cursor-hooks.json +308 -0
  89. package/plugins/heyspex/hooks/hooks.json +418 -0
  90. package/plugins/heyspex/mcp.json +13 -0
  91. package/scripts/check-mcp-freshness.mjs +17 -9
  92. package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
  93. package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
  94. package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
  95. package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -0
  96. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +356 -0
  97. package/scripts/claude-hooks/_run.mjs +413 -0
  98. package/scripts/claude-hooks/_transcript-text.mjs +60 -0
  99. package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
  100. package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
  101. package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
  102. package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
  103. package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
  104. package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
  105. package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
  106. package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
  107. package/scripts/claude-hooks/claim-liveness-shared.mjs +577 -0
  108. package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
  109. package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
  110. package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
  111. package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
  112. package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
  113. package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
  114. package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
  115. package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
  116. package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
  117. package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
  118. package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
  119. package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
  120. package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
  121. package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
  122. package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
  123. package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
  124. package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
  125. package/scripts/claude-hooks/inject-communication-style.mjs +86 -0
  126. package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
  127. package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
  128. package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
  129. package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
  130. package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -0
  131. package/scripts/claude-hooks/log-session-activity.mjs +341 -0
  132. package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
  133. package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
  134. package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
  135. package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
  136. package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
  137. package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
  138. package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
  139. package/scripts/claude-hooks/research-method-guard.mjs +398 -0
  140. package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
  141. package/scripts/claude-hooks/session-end.mjs +451 -0
  142. package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
  143. package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
  144. package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
  145. package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
  146. package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
  147. package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
  148. package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
  149. package/scripts/claude-hooks/wip-checkpoint.mjs +302 -0
  150. package/scripts/cursor-hooks/manifest.mjs +281 -0
  151. package/scripts/dev-server-status.mjs +34 -13
  152. package/scripts/dev-server-stop.mjs +313 -0
  153. package/scripts/heyspex-mode.mjs +288 -0
  154. package/scripts/hs-auto-resolve.mjs +240 -0
  155. package/scripts/hs-auto-source.mjs +398 -0
  156. package/scripts/hs-auto.mjs +134 -0
  157. package/scripts/hs-capture-cli.mjs +326 -0
  158. package/scripts/hs-classifier.mjs +136 -0
  159. package/scripts/hs-companion.mjs +88 -0
  160. package/scripts/hs-gateway.mjs +115 -0
  161. package/scripts/hs-mode.mjs +125 -0
  162. package/scripts/hs-next.mjs +441 -0
  163. package/scripts/hs-settings.mjs +338 -0
  164. package/scripts/hs-view.mjs +496 -0
  165. package/scripts/hs-worktree.mjs +201 -0
  166. package/scripts/install-claude-code-hooks.mjs +272 -0
  167. package/scripts/install-cursor-hooks.mjs +471 -0
  168. package/scripts/lib/agent-env-markers.mjs +38 -0
  169. package/scripts/lib/console-url-resolver.mjs +199 -61
  170. package/scripts/lib/customer-kit.mjs +1203 -0
  171. package/scripts/lib/dev-mode.mjs +249 -32
  172. package/scripts/lib/directive-obligations.mjs +349 -0
  173. package/scripts/lib/heyspex-home.mjs +72 -0
  174. package/scripts/lib/hook-dispositions.mjs +210 -0
  175. package/scripts/lib/hook-vault-header.mjs +30 -0
  176. package/scripts/lib/hs-command.mjs +277 -0
  177. package/scripts/lib/hs-companion-settings.mjs +472 -0
  178. package/scripts/lib/hs-source-branch.mjs +196 -0
  179. package/scripts/lib/hs-source-commit.mjs +59 -5
  180. package/scripts/lib/hs-source-identity.mjs +526 -0
  181. package/scripts/lib/hs-source-log.mjs +13 -22
  182. package/scripts/lib/hs-source-preflight.mjs +18 -4
  183. package/scripts/lib/hs-source-push.mjs +386 -307
  184. package/scripts/lib/hs-worktree-args.mjs +115 -0
  185. package/scripts/lib/shared-path-helpers.mjs +134 -0
  186. package/scripts/lib/standalone-doctor.mjs +540 -3
  187. package/scripts/lib/subagent-git-lock.mjs +109 -0
  188. package/scripts/lib/template-pin.mjs +34 -0
  189. package/scripts/lib/window-placement.mjs +109 -0
  190. package/scripts/provenance-lib.mjs +91 -0
  191. package/scripts/stamp-provenance-on-edit.mjs +222 -0
  192. package/scripts/wip-ledger-health.mjs +653 -0
  193. package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
  194. package/spexharness/src/adapters/audit.mjs +197 -0
  195. package/spexharness/src/adapters/claude-code.mjs +449 -0
  196. package/spexharness/src/adapters/copilot.mjs +236 -0
  197. package/spexharness/src/adapters/detect-harness.mjs +34 -0
  198. package/spexharness/src/chat/tui/ansi.mjs +448 -0
  199. package/spexharness/src/chat/tui/palette.mjs +22 -0
  200. package/spexharness/src/chat/tui/select.mjs +208 -0
  201. package/spexharness/src/config-root.mjs +54 -0
  202. package/spexharness/src/context-budget.mjs +141 -0
  203. package/spexharness/src/dev-server-control.mjs +389 -0
  204. package/spexharness/src/doctrine/behaviour.mjs +371 -0
  205. package/spexharness/src/doctrine/corpus.mjs +733 -0
  206. package/spexharness/src/doctrine/inject.mjs +338 -0
  207. package/spexharness/src/doctrine/merge.mjs +184 -0
  208. package/spexharness/src/doctrine/sources.mjs +252 -0
  209. package/spexharness/src/doctrine/synthesize.mjs +401 -0
  210. package/spexharness/src/doctrine/vault-source.mjs +574 -0
  211. package/spexharness/src/doctrine/writer.mjs +176 -0
  212. package/spexharness/src/events.mjs +84 -0
  213. package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
  214. package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
  215. package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
  216. package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
  217. package/spexharness/src/gates/deep-research-cost-gate.mjs +275 -0
  218. package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
  219. package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
  220. package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
  221. package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
  222. package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
  223. package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
  224. package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
  225. package/spexharness/src/gates/git-identity-guard.mjs +61 -0
  226. package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
  227. package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
  228. package/spexharness/src/gates/git-stash-gate.mjs +159 -0
  229. package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
  230. package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
  231. package/spexharness/src/gates/inject-vault-context.mjs +239 -0
  232. package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
  233. package/spexharness/src/gates/plan-review-gate.mjs +233 -0
  234. package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
  235. package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
  236. package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
  237. package/spexharness/src/gates/settings-write-gate.mjs +104 -0
  238. package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
  239. package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
  240. package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
  241. package/spexharness/src/git-reflog-audit.mjs +294 -0
  242. package/spexharness/src/intent-classifier.mjs +196 -0
  243. package/spexharness/src/intent-directive.mjs +626 -0
  244. package/spexharness/src/profile/learn.mjs +551 -0
  245. package/spexharness/src/profile/proposals.mjs +357 -0
  246. package/spexharness/src/profile/vault-inclusion.mjs +386 -0
  247. package/spexharness/src/redact.mjs +151 -0
  248. package/spexharness/src/rule-table.mjs +849 -0
  249. package/spexharness/src/seat-context.mjs +122 -0
  250. package/spexharness/src/session-bootstrap.mjs +512 -0
  251. package/spexharness/src/session-record.mjs +815 -0
  252. package/spexharness/src/token-inventory.mjs +207 -0
  253. package/vendor/heyspex-mcp/account-switch.mjs +96 -0
  254. package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
  255. package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
  256. package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
  257. package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
  258. package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
  259. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +77 -8
  260. package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
  261. package/vendor/heyspex-mcp/capture-console-client.mjs +117 -3
  262. package/vendor/heyspex-mcp/capture-focus.mjs +1061 -31
  263. package/vendor/heyspex-mcp/capture-outbox.mjs +15 -5
  264. package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
  265. package/vendor/heyspex-mcp/capture-tools.mjs +1264 -0
  266. package/vendor/heyspex-mcp/claim-tools.mjs +1147 -0
  267. package/vendor/heyspex-mcp/cli-verbs.mjs +2074 -0
  268. package/vendor/heyspex-mcp/cli.mjs +207 -15524
  269. package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
  270. package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
  271. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +602 -0
  272. package/vendor/heyspex-mcp/config-seed.yaml +15 -0
  273. package/vendor/heyspex-mcp/config-settings.mjs +290 -7
  274. package/vendor/heyspex-mcp/console-fetch.mjs +822 -17
  275. package/vendor/heyspex-mcp/console-transport.mjs +172 -0
  276. package/vendor/heyspex-mcp/credentials.mjs +585 -0
  277. package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
  278. package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
  279. package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
  280. package/vendor/heyspex-mcp/design-tools.mjs +4 -1
  281. package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
  282. package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
  283. package/vendor/heyspex-mcp/dev-tools.mjs +131 -0
  284. package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
  285. package/vendor/heyspex-mcp/events-tools.mjs +207 -0
  286. package/vendor/heyspex-mcp/filing-tools.mjs +723 -0
  287. package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
  288. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +71 -15
  289. package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
  290. package/vendor/heyspex-mcp/folder-trust-store.mjs +60 -0
  291. package/vendor/heyspex-mcp/http-transport.mjs +302 -0
  292. package/vendor/heyspex-mcp/list-tools.mjs +257 -0
  293. package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
  294. package/vendor/heyspex-mcp/mcp-config.mjs +107 -0
  295. package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
  296. package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
  297. package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
  298. package/vendor/heyspex-mcp/occupied-keys.mjs +142 -0
  299. package/vendor/heyspex-mcp/on-deck-brief.mjs +3377 -0
  300. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
  301. package/vendor/heyspex-mcp/package-root.mjs +32 -0
  302. package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
  303. package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
  304. package/vendor/heyspex-mcp/plan-push.mjs +908 -22
  305. package/vendor/heyspex-mcp/plan-review.mjs +124 -30
  306. package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
  307. package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -0
  308. package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -970
  309. package/vendor/heyspex-mcp/promote-capture.mjs +388 -69
  310. package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
  311. package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
  312. package/vendor/heyspex-mcp/promote-research.mjs +666 -53
  313. package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
  314. package/vendor/heyspex-mcp/prompts.mjs +1460 -0
  315. package/vendor/heyspex-mcp/read-path.mjs +191 -15
  316. package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
  317. package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
  318. package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
  319. package/vendor/heyspex-mcp/register-spec.mjs +266 -54
  320. package/vendor/heyspex-mcp/request-context.mjs +83 -0
  321. package/vendor/heyspex-mcp/research-focus.mjs +207 -4
  322. package/vendor/heyspex-mcp/research-handlers.mjs +625 -0
  323. package/vendor/heyspex-mcp/research-tools.mjs +4 -4
  324. package/vendor/heyspex-mcp/response-budget.mjs +348 -0
  325. package/vendor/heyspex-mcp/review-focus.mjs +25 -1
  326. package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
  327. package/vendor/heyspex-mcp/search-specs.mjs +280 -0
  328. package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
  329. package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
  330. package/vendor/heyspex-mcp/session-owner.mjs +109 -0
  331. package/vendor/heyspex-mcp/session-tools.mjs +162 -0
  332. package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
  333. package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
  334. package/vendor/heyspex-mcp/spec-edit-tools.mjs +453 -0
  335. package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
  336. package/vendor/heyspex-mcp/split-research.mjs +304 -56
  337. package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
  338. package/vendor/heyspex-mcp/submit-for-review.mjs +150 -7
  339. package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
  340. package/vendor/heyspex-mcp/summary-write-tools.mjs +228 -0
  341. package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
  342. package/vendor/heyspex-mcp/test-helpers.mjs +123 -1
  343. package/vendor/heyspex-mcp/tool-catalog.mjs +2819 -0
  344. package/vendor/heyspex-mcp/tool-dispatch.mjs +657 -0
  345. package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
  346. package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
  347. package/vendor/heyspex-mcp/vault-io.mjs +720 -0
  348. package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
  349. package/vendor/heyspex-mcp/vault-resolve.mjs +554 -68
  350. package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
  351. package/vendor/heyspex-mcp/whoami.mjs +92 -0
  352. package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
@@ -0,0 +1,302 @@
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 { hookVaultHeaders } from '../lib/hook-vault-header.mjs';
53
+ import { resolveConsoleUrl as resolveConsoleUrlShared } from '../lib/console-url-resolver.mjs';
54
+ import { readCredentialsMcpToken, resolveDevMode } from '../lib/dev-mode.mjs';
55
+
56
+ const NOTES_EXCERPT_MAX = 320;
57
+ const REMAINING_ITEM_MAX = 200;
58
+ const REMAINING_ITEMS_MAX = 3;
59
+ const LEDGER_REL = ['.heyspex', 'wip-state.json'];
60
+ const WIP_FETCH_TIMEOUT_MS = 1500;
61
+
62
+ function repoRoot() {
63
+ // scripts/claude-hooks/ → up two levels.
64
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
65
+ }
66
+
67
+ // Since the repo split (2026-08-31) the WIP ledger lives in the VAULT, not the
68
+ // code checkout. Reading repoRoot() here previewed a frozen pre-split snapshot to
69
+ // every new session — the HS-OR8H repo-split "quiet window OPEN" notice kept
70
+ // re-appearing for days after the window closed, because the live ledger had moved
71
+ // and this copy never advanced. Resolve the vault first; fall back to the code repo
72
+ // so a pre-split or standalone checkout still previews its own ledger.
73
+ function vaultRoot() {
74
+ try {
75
+ return getActiveConfig()?.repoRootAbs ?? null;
76
+ } catch {
77
+ return null; // registry unreadable — the code-repo base still applies
78
+ }
79
+ }
80
+
81
+ // Ledger roots in precedence order: vault first, code repo second.
82
+ function ledgerRoots() {
83
+ const roots = [];
84
+ const vault = vaultRoot();
85
+ if (vault) roots.push(vault);
86
+ const repo = repoRoot();
87
+ if (!roots.includes(repo)) roots.push(repo);
88
+ return roots;
89
+ }
90
+
91
+ async function readStdinJson() {
92
+ if (process.stdin.isTTY) return null;
93
+ return new Promise((resolve) => {
94
+ let buf = '';
95
+ let settled = false;
96
+ const finish = (value) => {
97
+ if (settled) return;
98
+ settled = true;
99
+ resolve(value);
100
+ };
101
+ const timer = setTimeout(() => finish(buf || ''), 200);
102
+ process.stdin.setEncoding('utf8');
103
+ process.stdin.on('data', (chunk) => {
104
+ buf += chunk;
105
+ });
106
+ process.stdin.on('end', () => {
107
+ clearTimeout(timer);
108
+ finish(buf);
109
+ });
110
+ process.stdin.on('error', () => {
111
+ clearTimeout(timer);
112
+ finish(buf);
113
+ });
114
+ });
115
+ }
116
+
117
+ function emit(eventName, additionalContext) {
118
+ if (!additionalContext) return;
119
+ process.stdout.write(
120
+ JSON.stringify({
121
+ hookSpecificOutput: { hookEventName: eventName, additionalContext },
122
+ }),
123
+ );
124
+ }
125
+
126
+ function truncate(value, limit) {
127
+ if (typeof value !== 'string') return '';
128
+ const trimmed = value.trim();
129
+ if (!trimmed) return '';
130
+ return trimmed.length > limit ? `${trimmed.slice(0, limit)}…` : trimmed;
131
+ }
132
+
133
+ // Read the file ledger and return the single most-recently-updated entry, or
134
+ // null when the ledger is missing/empty/malformed. The file ledger runs
135
+ // alongside the Neon handoff_context lane until the file→Neon cutover
136
+ // (HS-EPIC-PFMN); reading the file keeps SessionStart robust when console/Neon
137
+ // is offline.
138
+ function mostRecentLedgerEntry() {
139
+ let parsed = null;
140
+ for (const root of ledgerRoots()) {
141
+ try {
142
+ const candidate = JSON.parse(fs.readFileSync(path.join(root, ...LEDGER_REL), 'utf8'));
143
+ if (candidate && typeof candidate === 'object') {
144
+ parsed = candidate;
145
+ break;
146
+ }
147
+ } catch {
148
+ // missing or unreadable at this root — try the next one
149
+ }
150
+ }
151
+ if (!parsed || typeof parsed !== 'object') return null;
152
+ let best = null;
153
+ for (const [epicId, entry] of Object.entries(parsed)) {
154
+ if (!entry || typeof entry !== 'object') continue;
155
+ const updatedAt = typeof entry.updatedAt === 'string' ? entry.updatedAt : '';
156
+ if (!best || updatedAt > best.updatedAt) {
157
+ best = { epicId, updatedAt, entry };
158
+ }
159
+ }
160
+ return best;
161
+ }
162
+
163
+ function ledgerPreview(best, source = 'disk') {
164
+ if (!best) return '';
165
+ const { epicId, entry } = best;
166
+ const lines = [`Most recent WIP ledger entry — ${epicId} (updated ${best.updatedAt || 'unknown'}):`];
167
+ const notes = truncate(entry.notes, NOTES_EXCERPT_MAX);
168
+ if (notes) lines.push(` notes: ${notes}`);
169
+ if (Array.isArray(entry.remaining) && entry.remaining.length) {
170
+ lines.push(' remaining:');
171
+ for (const item of entry.remaining.slice(0, REMAINING_ITEMS_MAX)) {
172
+ const text = truncate(item, REMAINING_ITEM_MAX);
173
+ if (text) lines.push(` • ${text}`);
174
+ }
175
+ }
176
+ lines.push(
177
+ source === 'cloud'
178
+ ? ' (This is a cloud WIP preview from /api/wip — trust it over reconstructing state from scratch.)'
179
+ : ' (This is a disk preview — the vault file on disk is the source of truth; ' +
180
+ 'do not reconstruct state by re-reading every child spec + git diff.)',
181
+ );
182
+ return lines.join('\n');
183
+ }
184
+
185
+ /**
186
+ * Cloud-reliance SessionStart preview: `GET {console}/api/wip` (bearer-gated)
187
+ * returns `{ wip_state: { "<SPEC-ID>": { sessionId, agent, updatedAt, notes,
188
+ * remaining, filesTouched, ... } } }` — the same shape `heyspex_record_wip`
189
+ * writes. Only consulted when `resolveDevMode()` reports cloud reliance; falls
190
+ * back to disk on ANY failure (unreachable console, bad token, malformed
191
+ * body) and never throws — this hook must stay fail-open and silent.
192
+ */
193
+ async function fetchCloudLedgerEntry(root) {
194
+ let mode;
195
+ try {
196
+ mode = resolveDevMode({});
197
+ } catch {
198
+ return null;
199
+ }
200
+ if (mode.reliance !== 'cloud') return null;
201
+
202
+ let consoleUrl;
203
+ try {
204
+ consoleUrl = resolveConsoleUrlShared({
205
+ repoRoot: root,
206
+ devMode: { reliance: mode.reliance, consoleUrl: mode.consoleUrl },
207
+ });
208
+ } catch {
209
+ return null;
210
+ }
211
+
212
+ const token = readCredentialsMcpToken();
213
+ const controller = new AbortController();
214
+ const timer = setTimeout(() => controller.abort(), WIP_FETCH_TIMEOUT_MS);
215
+ timer.unref?.();
216
+ try {
217
+ const res = await fetch(`${consoleUrl}/api/wip`, {
218
+ // Repo-bound vault id (P1): without it the console refuses a bearer
219
+ // caller (409 vault-scope-required) and this preview silently fell back
220
+ // to a stale disk ledger (diag HS-5M0G).
221
+ headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), ...hookVaultHeaders() },
222
+ signal: controller.signal,
223
+ });
224
+ if (!res.ok) return null;
225
+ const body = await res.json();
226
+ const wipState = body?.wip_state;
227
+ if (!wipState || typeof wipState !== 'object') return null;
228
+ let best = null;
229
+ for (const [specId, entry] of Object.entries(wipState)) {
230
+ if (!entry || typeof entry !== 'object') continue;
231
+ const updatedAt = typeof entry.updatedAt === 'string' ? entry.updatedAt : '';
232
+ if (!best || updatedAt > best.updatedAt) {
233
+ best = { epicId: specId, updatedAt, entry };
234
+ }
235
+ }
236
+ return best;
237
+ } catch {
238
+ return null;
239
+ } finally {
240
+ clearTimeout(timer);
241
+ }
242
+ }
243
+
244
+ const BEFORE_REMINDER =
245
+ 'Session start. Before you claim or continue work, load the prior handoff: ' +
246
+ 'call `heyspex_handoff_load({})` for your Neon lane context, and read the ' +
247
+ '`/hs-next` WIP block (file ledger at `.heyspex/wip-state.json`). Trust the ' +
248
+ 'structured handoff over reconstructing state from scratch.';
249
+
250
+ const COMPACT_REMINDER =
251
+ 'Context was just compacted mid-session. This is a ROLLOVER, not a fresh ' +
252
+ 'start — work is continuing. Re-orient before your next action: run ' +
253
+ '`heyspex_handoff_load({})` and re-read the WIP ledger below. Do NOT treat ' +
254
+ 'this as session close — do not flip status or write an HS-REV; the work is ' +
255
+ 'not done.';
256
+
257
+ const PRECOMPACT_REMINDER =
258
+ '⚠️ Context is about to be compacted. CHECKPOINT NOW while the full context ' +
259
+ 'is still present, if you are mid-implementation: (1) `heyspex_record_wip` ' +
260
+ 'for the worked epic (filesTouched / untrackedArtifacts / specsFiled / ' +
261
+ 'verified / remaining / notes), and (2) `heyspex_handoff_record` to the ' +
262
+ 'self-purging Neon lane. This is a context-rollover CHECKPOINT, not session ' +
263
+ 'close — do NOT flip status or write an HS-REV; work is continuing.';
264
+
265
+ async function main() {
266
+ if (process.env.HEYSPEX_WIP_CHECKPOINT_OFF === '1') return;
267
+
268
+ const raw = await readStdinJson();
269
+ if (!raw) return;
270
+ let payload;
271
+ try {
272
+ payload = JSON.parse(raw);
273
+ } catch {
274
+ return;
275
+ }
276
+
277
+ const event = payload?.hook_event_name;
278
+ if (!event) return;
279
+
280
+ if (event === 'SessionStart') {
281
+ const source = payload?.source;
282
+ let best = await fetchCloudLedgerEntry(repoRoot());
283
+ let ledgerSource = 'cloud';
284
+ if (!best) {
285
+ best = mostRecentLedgerEntry();
286
+ ledgerSource = 'disk';
287
+ }
288
+ const preview = ledgerPreview(best, ledgerSource);
289
+ const head = source === 'compact' ? COMPACT_REMINDER : BEFORE_REMINDER;
290
+ emit(event, preview ? `${head}\n\n${preview}` : head);
291
+ return;
292
+ }
293
+
294
+ if (event === 'PreCompact') {
295
+ emit(event, PRECOMPACT_REMINDER);
296
+ return;
297
+ }
298
+ }
299
+
300
+ main().catch(() => {
301
+ // Never break Claude Code on a hook failure.
302
+ });
@@ -0,0 +1,281 @@
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: 'inject-doctrine', why: 'Inject the baseline SpexCode Doctrine edition at session start.' },
59
+ { event: 'sessionStart', hook: 'coo-guard-check', why: 'Warn at session start when a breakdown has not been delegated.' },
60
+ { event: 'sessionStart', hook: 'check-auto-memory-budget', why: 'Self-heal and budget-check the auto-memory index.' },
61
+ { event: 'sessionStart', hook: 'check-claude-md-budget', why: 'Advisory budget check on the always-loaded agent contract.' },
62
+ { event: 'sessionStart', hook: 'check-mcp-freshness-hook', why: 'Warn when the running MCP server is stale versus vendor.' },
63
+ { event: 'sessionStart', hook: 'git-reflog-audit', why: 'Session-start git audit (stash/reset/rebase witness).' },
64
+ { event: 'sessionStart', hook: 'vault-mirror-catchup', why: 'Cloud write mode: detached hs pull --apply closes a stale disk mirror (HS-XJFQ).' },
65
+
66
+ // ── Compaction ──────────────────────────────────────────────────────────
67
+ // Claude re-runs SessionStart(source: compact) after compaction; Cursor fires
68
+ // preCompact BEFORE it, so the shim relays this one as SessionStart(compact)
69
+ // to keep wip-checkpoint's "rollover, not fresh start" framing.
70
+ { event: 'preCompact', hook: 'wip-checkpoint', claudeEvent: 'SessionStart', why: 'Carry the WIP handoff across a context compaction.' },
71
+
72
+ // ── Every user turn ─────────────────────────────────────────────────────
73
+ // beforeSubmitPrompt is Cursor's UserPromptSubmit. It honors `user_message`
74
+ // but not `additional_context`, so these three surface in the transcript
75
+ // rather than as invisible context — the routing still reaches the agent,
76
+ // and the user can see why it fired.
77
+ { event: 'beforeSubmitPrompt', hook: 'preflight-reminder', why: 'Name the required procedure before a filing/destructive/protocol turn.' },
78
+ { event: 'beforeSubmitPrompt', hook: 'intent-classifier-trigger', why: 'Route capture / research / settings / defer intents to the right skill.' },
79
+ { event: 'beforeSubmitPrompt', hook: 'inject-communication-style', why: 'Re-assert the plain-language communication rules each turn.' },
80
+ { event: 'beforeSubmitPrompt', hook: 'inject-vault-context', timeout: 10, why: 'Lexical vault retrieval for the prompt (surfaced as a user message).' },
81
+
82
+ // ── Before every tool call (any tool) ───────────────────────────────────
83
+ { event: 'preToolUse', matcher: '.*', hook: 'emit-claim-status', args: ['--event=PreToolUse'], why: 'Claim liveness heartbeat to the console.' },
84
+ { event: 'preToolUse', matcher: '.*', hook: 'coo-denylist-gate', why: 'Hard deny the COO-window deny list.' },
85
+ { event: 'preToolUse', matcher: '.*', hook: 'log-session-activity', args: ['--event=PreToolUse'], why: 'Append agent.tool_called rows to the session log.' },
86
+
87
+ // ── Before a file write / delete ─────────────────────────────────────────
88
+ // Cursor's `Write` covers create AND patch (Claude's Write|Edit|MultiEdit);
89
+ // the shim re-derives which one from the input shape. `Delete` is included on
90
+ // the vault guard because removing a profile file is the loss it exists to
91
+ // prevent, and Claude had no separate delete tool to list.
92
+ { event: 'preToolUse', matcher: 'Write|Delete', hook: 'env-vault-guard', why: 'Block destructive writes to the encrypted env vault (ALWAYS ON).' },
93
+ { event: 'preToolUse', matcher: 'Write', hook: 'vault-write-tier-guard', why: 'Deny disk writes to vault specs in cloud write mode (HS-FCNH).' },
94
+ { event: 'preToolUse', matcher: 'Write', hook: 'settings-write-gate', why: 'Require approval before .heyspex/config.yaml changes.' },
95
+ { event: 'preToolUse', matcher: 'Write', hook: 'enforce-vault-naming', why: 'Block vault filenames that violate the naming registry.' },
96
+ { event: 'preToolUse', matcher: 'Write', hook: 'guard-auto-memory-writes', why: 'Block new topic files in the auto-memory directory.' },
97
+ { event: 'preToolUse', matcher: 'Write', hook: 'preflight-reminder', why: 'Preflight reminder on the edit path itself.' },
98
+ { event: 'preToolUse', matcher: 'Write', hook: 'claim-before-code-gate', why: 'Deny the first code edit after plan exit with no claim.' },
99
+ { event: 'preToolUse', matcher: 'Write', hook: 'directive-obligation-gate', why: 'Deny product-code writes while a capture_on_idea obligation is unmet, and (claim-before-build) with no claim/capture (HS-20SW).' },
100
+
101
+ // ── Before a shell command ──────────────────────────────────────────────
102
+ // beforeShellExecution, not preToolUse/Shell: it is the only event guaranteed
103
+ // to carry the command string, and Cursor honors allow/deny/ask on it.
104
+ { event: 'beforeShellExecution', hook: 'env-vault-guard', why: 'Block shell writes/exfiltration targeting the env vault (ALWAYS ON).' },
105
+ { event: 'beforeShellExecution', hook: 'vault-write-tier-guard', why: 'Deny shell writes to vault specs in cloud write mode (HS-FCNH).' },
106
+ { event: 'beforeShellExecution', hook: 'settings-write-gate', why: 'Catch shell writes to .heyspex/config.yaml.' },
107
+ { event: 'beforeShellExecution', hook: 'enforce-vault-naming', why: 'Catch vault files created via the shell.' },
108
+ { event: 'beforeShellExecution', hook: 'guard-auto-memory-writes', why: 'Catch auto-memory topic files created via the shell.' },
109
+ { event: 'beforeShellExecution', hook: 'preflight-reminder', why: 'Preflight reminder on rm -rf / force-push shaped commands.' },
110
+ { event: 'beforeShellExecution', hook: 'dev-server-task-reminder', timeout: 10, why: 'Dev-server start/stop hard gate plus DB-safety reminder.' },
111
+ { event: 'beforeShellExecution', hook: 'git-stash-gate', timeout: 10, why: 'Always ask before a mutating git stash.' },
112
+ { event: 'beforeShellExecution', hook: 'git-identity-guard', why: 'Agents never write git identity (owner ruling 2026-09-19, ALWAYS ON).' },
113
+ { event: 'beforeShellExecution', hook: 'capture-execution-gate', why: 'Block dev-factory research/paid-model/routing shell commands past an ungrounded capture (HS-KNJ8, platform).' },
114
+ { event: 'beforeShellExecution', hook: 'directive-obligation-gate', why: 'Deny build commands and product-code shell writes while a capture obligation is unmet (HS-20SW).' },
115
+
116
+ // ── Before specific MCP tools ───────────────────────────────────────────
117
+ // Cursor fires preToolUse for MCP tools too (matcher `MCP:<name>`), so these
118
+ // stay on preToolUse rather than beforeMCPExecution — registering both would
119
+ // fire each gate twice. Scoping is the shim's `tool` regex because a Cursor
120
+ // matcher cannot name one MCP tool per entry portably.
121
+ { event: 'preToolUse', matcher: '.*', tool: 'heyspex_claim_spec$', hook: 'coo-guard-check', why: 'Block a claim whose breakdown was never delegated.' },
122
+ { 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.' },
123
+ { event: 'preToolUse', matcher: '.*', tool: 'heyspex_settings_set$', hook: 'settings-write-gate', why: 'Require approval for settings changes made through MCP.' },
124
+ { 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.' },
125
+ { event: 'preToolUse', matcher: '.*', tool: 'heyspex_file_spec$', hook: 'diagnose-capture-reminder', why: 'Warn when a diagnose is filed with no capture holding the owner words (HS-IK2D).' },
126
+ { event: 'preToolUse', matcher: '.*', tool: '^(Workflow|Skill)$', hook: 'research-method-guard', why: 'Research method guard on Skill/Workflow launches (HS-EN4F, ALWAYS ON).' },
127
+ { event: 'preToolUse', matcher: '.*', tool: '^(Workflow|Skill)$', hook: 'deep-research-cost-gate', why: 'Cost confirmation before a paid deep-research harness launch.' },
128
+ { 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).' },
129
+
130
+ // ── Before a subagent spawn ─────────────────────────────────────────────
131
+ // subagentStart over preToolUse/Task: it carries subagent_type and the model,
132
+ // which is exactly what the routing gate decides on, and it can deny.
133
+ { event: 'subagentStart', hook: 'agent-spawn-delegation-gate', why: 'Block a build/breakdown/critic subagent on an off-routing model.' },
134
+ { event: 'subagentStart', hook: 'capture-execution-gate', why: 'Block a dev-factory subagent spawn past an ungrounded capture (HS-KNJ8, platform).' },
135
+ { event: 'subagentStart', hook: 'directive-obligation-gate', why: 'Deny a build subagent while a capture obligation is unmet or the session has no claim/capture (HS-20SW).' },
136
+
137
+ // ── After every tool call ───────────────────────────────────────────────
138
+ { event: 'postToolUse', matcher: '.*', hook: 'emit-claim-status', args: ['--event=PostToolUse'], why: 'Claim liveness heartbeat after the call resolves.' },
139
+ { event: 'postToolUse', matcher: '.*', hook: 'log-session-activity', args: ['--event=PostToolUse'], why: 'Append the tool result row to the session log.' },
140
+ { event: 'postToolUse', matcher: '.*', hook: 'directive-obligation-gate', why: 'Discharge the capture obligation on a capture write; track claim acquire/release (HS-20SW).' },
141
+ { event: 'postToolUse', matcher: '.*', hook: 'close-terminal-on-ship', why: 'Close the Launch terminal once the spec ships.' },
142
+ { event: 'postToolUse', matcher: 'Write', hook: 'notify-status-flip', why: 'Sound on an in_review status flip.' },
143
+ { event: 'postToolUse', matcher: 'Write', hook: 'scripts/stamp-provenance-on-edit.mjs', why: 'Stamp assisted_by provenance on direct spec edits.' },
144
+
145
+ // afterFileEdit is a distinct signal, not a duplicate: it reports the edit
146
+ // that actually landed (with old/new strings) even when the write came from a
147
+ // path postToolUse did not describe as an edit.
148
+ { event: 'afterFileEdit', matcher: '.*', hook: 'close-terminal-on-ship', why: 'Ship detection from the concrete file edit.' },
149
+
150
+ // ── End of the agent turn ───────────────────────────────────────────────
151
+ // Cursor's `stop` honors `followup_message`, which is how a blocking Stop
152
+ // hook re-drives the agent — the same effect as Claude's exit-2 block.
153
+ { event: 'stop', hook: 'emit-claim-status', args: ['--event=Stop'], why: 'Mark the claim pending-on-response.' },
154
+ { event: 'stop', hook: 'naughty-list-gate', why: 'Re-drive the turn when the final message uses banned jargon.' },
155
+ { event: 'stop', hook: 'id-display-format-gate', why: 'Re-drive the turn when a reply breaks the Naming V3 display-id format.' },
156
+ { event: 'stop', hook: 'epic-milestone-phrasing-gate', why: 'Re-drive the turn when a reply breaks the epic/milestone phrasing rules.' },
157
+ { event: 'stop', hook: 'git-reflog-audit', why: 'Session-close git audit backstop (stash/reset/rebase witness).' },
158
+ { event: 'stop', hook: 'enforce-session-sequence', why: 'Re-drive the turn when work shipped without record_wip.' },
159
+ { event: 'stop', hook: 'directive-obligation-gate', why: 'Re-drive the turn ONCE (followup_message) when a capture obligation is still unmet (HS-20SW).' },
160
+
161
+ // ── Session end ─────────────────────────────────────────────────────────
162
+ { event: 'sessionEnd', hook: 'session-end', why: 'Release the claim and notify the console.' },
163
+ { event: 'sessionEnd', hook: 'close-terminal-on-ship', why: 'Final terminal cleanup.' },
164
+ ];
165
+
166
+ /**
167
+ * Claude registrations with no Cursor trigger. Kept in code, not in a comment,
168
+ * so the doctor can report the gap and the smoke test can assert the list has
169
+ * not quietly grown.
170
+ */
171
+ export const UNPORTABLE = [
172
+ {
173
+ hook: 'plan-review-gate',
174
+ claudeEvent: 'PreToolUse/ExitPlanMode',
175
+ reason: 'Cursor exposes no ExitPlanMode tool call to hooks.',
176
+ coveredBy: '.cursor/rules/plan-review-required.mdc (always-on rule)',
177
+ },
178
+ {
179
+ hook: 'plan-exit-ingest-gate',
180
+ claudeEvent: 'PreToolUse/ExitPlanMode',
181
+ reason: 'Same missing trigger; it is also what arms claim-before-code-gate.',
182
+ coveredBy: '.cursor/rules/plan-review-required.mdc + spec-creation-linkage.mdc',
183
+ },
184
+ ];
185
+
186
+ /**
187
+ * Hooks that ARE registered above but run weaker than on Claude Code. Kept in code (like UNPORTABLE)
188
+ * so scripts/lib/hook-dispositions.test.mjs can hold the three-host table to it.
189
+ */
190
+ export const DEGRADED = [
191
+ {
192
+ hook: 'preflight-reminder',
193
+ reason: 'beforeSubmitPrompt honors user_message only: the reminder surfaces in the transcript, not as invisible context.',
194
+ },
195
+ {
196
+ hook: 'intent-classifier-trigger',
197
+ reason: 'Same channel limit; the routing directive reaches the agent as a visible user message.',
198
+ },
199
+ {
200
+ hook: 'inject-communication-style',
201
+ reason: 'Same channel limit; re-asserted each turn as a visible user message.',
202
+ },
203
+ {
204
+ hook: 'inject-vault-context',
205
+ reason: 'Same channel limit; retrieved vault context appears as a visible user message.',
206
+ },
207
+ {
208
+ hook: 'claim-before-code-gate',
209
+ reason: 'Armed by plan-exit-ingest-gate, which has no Cursor trigger (UNPORTABLE), so it stays dormant.',
210
+ },
211
+ ];
212
+
213
+ /**
214
+ * The command string Cursor runs for one entry.
215
+ *
216
+ * The tool filter is a REGEX, and its metacharacters are also shell
217
+ * metacharacters — `[Nn]eon__(a|b)$` unquoted is a syntax error in bash, not a
218
+ * pattern. It is therefore always double-quoted. A trailing `$` inside double
219
+ * quotes is literal in both bash and PowerShell, and the patterns contain no
220
+ * whitespace, so quoting is sufficient without any escaping.
221
+ */
222
+ export function commandFor(entry) {
223
+ const parts = ['node', SHIM, entry.hook];
224
+ if (entry.tool) parts.push(`--hs-tool="${entry.tool}"`);
225
+ if (entry.claudeEvent) parts.push(`--hs-claude-event=${entry.claudeEvent}`);
226
+ for (const arg of entry.args ?? []) parts.push(arg);
227
+ return parts.join(' ');
228
+ }
229
+
230
+ /**
231
+ * Same command, addressed from a PLUGIN install.
232
+ *
233
+ * A project hook runs with the repo as its working directory, so a relative
234
+ * path resolves. A plugin hook runs from the plugin's own cache directory,
235
+ * where `scripts/cursor-hooks/_run.mjs` does not exist — the shim and the hook
236
+ * bodies live in the host vault checkout. `$CURSOR_PROJECT_DIR` is how the
237
+ * plugin reaches back into it (the same reason the plugin's MCP entry goes
238
+ * through bin/launch-mcp.mjs instead of naming cli.mjs directly).
239
+ */
240
+ export function commandForPlugin(entry) {
241
+ return commandFor(entry).replace(`node ${SHIM}`, `node "$CURSOR_PROJECT_DIR/${SHIM}"`);
242
+ }
243
+
244
+ /**
245
+ * The committed launcher a CUSTOMER repo carries for Cursor, repo-relative.
246
+ * Written by `hs install` / `scripts/install-cursor-hooks.mjs` from
247
+ * `renderCustomerLauncher('cursor')` in scripts/lib/customer-kit.mjs; it finds
248
+ * the `heyspex` package (node_modules/heyspex) and hands off to this shim.
249
+ */
250
+ export const CUSTOMER_LAUNCHER = '.cursor/heyspex/run-hook.mjs';
251
+
252
+ /**
253
+ * Same command, addressed from a CUSTOMER repo: the committed launcher by
254
+ * `$CURSOR_PROJECT_DIR` — never this machine's path. A customer repo has no
255
+ * `scripts/cursor-hooks/` of its own, so the relative form failed there with
256
+ * MODULE_NOT_FOUND ([[HS-EN4F-F4V2-DIAG-a-repo-set-up-with-hs-install-gets-no]]);
257
+ * the 2026-10-02 fix wrote the platform checkout's ABSOLUTE path, which the
258
+ * customer committed and which existed on no other machine
259
+ * ([[HS-0AF8-WW9R-TASK-portable-hook-and-command-paths-in]]).
260
+ */
261
+ export function commandForCustomer(entry) {
262
+ return commandFor(entry).replace(`node ${SHIM}`, `node "$CURSOR_PROJECT_DIR/${CUSTOMER_LAUNCHER}"`);
263
+ }
264
+
265
+ /** Identity of an entry inside `.cursor/hooks.json` — what "already installed"
266
+ * means, and what a repair replaces. Two entries for the same hook on the same
267
+ * event are distinguished by their scoping, so the tool filter is part of it. */
268
+ export function identityFor(entry) {
269
+ return `${entry.hook}::${entry.tool ?? ''}::${(entry.args ?? []).join(' ')}`;
270
+ }
271
+
272
+ export function timeoutFor(entry) {
273
+ return entry.timeout ?? DEFAULT_TIMEOUT_SECONDS;
274
+ }
275
+
276
+ /** Every Cursor event this manifest registers, in a stable order. */
277
+ export function events() {
278
+ const seen = [];
279
+ for (const entry of HOOKS) if (!seen.includes(entry.event)) seen.push(entry.event);
280
+ return seen;
281
+ }