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,351 @@
1
+ #!/usr/bin/env node
2
+ // HeySpex Intent Classifier — SEMANTIC layer.
3
+ //
4
+ // The Intent Classifier has two layers ([[HS-UJVA-EPIC-KKMO-autonomous-skill-routing-context]]):
5
+ // 1. LEXICAL (fast path) — the per-rule regex patterns in the guardrail registry.
6
+ // A regex hit fires instantly and never waits on this module.
7
+ // 2. SEMANTIC — on a regex MISS, resolve intent WITHOUT a second, separate model.
8
+ // DEFAULT (mode: self_check, [[HS-M4PN-TASK]]): the router deterministically
9
+ // injects a SELF-CHECK instruction and lets the agent already running the session
10
+ // judge intent as part of its own turn — no network, no fleet machine, works for
11
+ // every user. The self-check block is built in _intent-classifier-router.mjs;
12
+ // this module is the OPT-IN model fallback for anyone who still wants it.
13
+ // Why the semantic layer exists at all (regex alone is not enough):
14
+ // [[intent-classifier-injected-content]] — 9/10 paraphrased improvement-idea
15
+ // phrasings missed before the patterns were broadened, and paraphrase drift
16
+ // never ends.
17
+ //
18
+ // OPT-IN MODEL FALLBACK (mode: model): classifyIntent() below classifies the prompt
19
+ // with a fast LOCAL model (Ollama) and returns { behavior, confidence } so the caller
20
+ // can fire above a threshold. This is the ORIGINAL mechanism, now DEMOTED to an
21
+ // explicit opt-in — it is NEVER on the default path, so no reachable endpoint is
22
+ // required for the semantic layer to function ([[HS-M4PN-TASK]] AC2).
23
+ //
24
+ // HARD FAIL-SAFE CONTRACT (hook house rules): this module NEVER throws, NEVER
25
+ // hangs past its timeout, and on ANY problem (endpoint down, timeout, bad JSON,
26
+ // unexpected shape) returns { behavior: null, confidence: 0 } so the calling hook
27
+ // proceeds exactly as if the semantic layer did not exist. Dependency-free:
28
+ // Node stdlib + global fetch only (hooks run on a fresh clone with no npm install).
29
+ //
30
+ // Config (.heyspex/config.yaml → agent_behaviors.intent_classifier.*), all optional.
31
+ // The file is read from the VAULT root (the loader's `defaultConfigRoot()`), not this
32
+ // checkout — [[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]]:
33
+ // semantic_enabled: true|false (default true — turns Layer 2 on/off entirely)
34
+ // mode: self_check|model (default self_check — deterministic injection;
35
+ // `model` opts into the Ollama fallback below)
36
+ // endpoint: Ollama base URL (default http://192.168.1.252:11434 — mode:model only)
37
+ // model: model name (default qwen3:8b — mode:model only)
38
+ // timeout_ms: request budget (default 2500 — the hook's overall budget is 5s)
39
+ // min_confidence: fire threshold (default 0.7 — consumed by the calling hook)
40
+ // Env overrides (kill switch first):
41
+ // HEYSPEX_INTENT_SEMANTIC_OFF=1 hard off (disables Layer 2 entirely)
42
+ // HEYSPEX_INTENT_SEMANTIC_MODE=self_check|model
43
+ // HEYSPEX_INTENT_ENDPOINT / HEYSPEX_INTENT_MODEL /
44
+ // HEYSPEX_INTENT_TIMEOUT_MS / HEYSPEX_INTENT_MIN_CONFIDENCE
45
+ //
46
+ // Self-test without a session (exercises the opt-in model fallback):
47
+ // node scripts/claude-hooks/_intent-classifier-semantic.mjs --ping
48
+ // node scripts/claude-hooks/_intent-classifier-semantic.mjs --classify "some prompt"
49
+
50
+ import fs from 'node:fs';
51
+ import path from 'node:path';
52
+ import { fileURLToPath } from 'node:url';
53
+
54
+ import { readConfigLeafRaw } from './_guardrail-loader.mjs';
55
+ import { defaultConfigRoot } from '../../spexharness/src/config-root.mjs';
56
+
57
+ // The CODE checkout — where `.heyspex/intent-classifier-history.jsonl` (an event
58
+ // log, a code-repo artifact) is appended. NOT where config.yaml is read from: the
59
+ // config reads below default to the vault root via `defaultConfigRoot()`
60
+ // ([[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]]).
61
+ export const DEFAULT_REPO_ROOT = path.resolve(
62
+ path.dirname(fileURLToPath(import.meta.url)),
63
+ '..',
64
+ '..',
65
+ );
66
+
67
+ // Semantic-layer modes ([[HS-M4PN-TASK]]):
68
+ // 'self_check' — DEFAULT. On a regex miss the router injects a deterministic
69
+ // self-check instruction; the session's own agent judges intent.
70
+ // No network, no fleet machine.
71
+ // 'model' — OPT-IN fallback. classifyIntent() calls the local Ollama endpoint.
72
+ export const SEMANTIC_MODES = Object.freeze(['self_check', 'model']);
73
+
74
+ export const DEFAULTS = Object.freeze({
75
+ semanticEnabled: true,
76
+ semanticMode: 'self_check',
77
+ endpoint: 'http://192.168.1.252:11434',
78
+ model: 'qwen3:8b',
79
+ timeoutMs: 2500,
80
+ minConfidence: 0.7,
81
+ });
82
+
83
+ // The behavior catalog the classifier chooses from. Descriptions are the actual
84
+ // classification instructions, so keep them about the USER'S INTENT, not the
85
+ // system's response. Any hook can pass its own list; this shared catalog keeps
86
+ // capture-trigger and research-trigger consistent if both adopt the semantic layer.
87
+ export const KNOWN_BEHAVIORS = Object.freeze([
88
+ {
89
+ key: 'capture_on_idea',
90
+ description:
91
+ 'The user is sharing a raw idea, suggestion, observation, or improvement thought — a brain-dump about something that could exist or be better — rather than giving a direct work order, asking a question about existing work, or continuing work already in progress.',
92
+ },
93
+ {
94
+ key: 'research_before_decision',
95
+ description:
96
+ 'The user is declining to make a design or architecture decision themselves and is asking for research, best practices, or an expert recommendation instead (e.g. "not a decision I should have to make", "don\'t reinvent the wheel").',
97
+ },
98
+ {
99
+ key: 'settings_intent',
100
+ description:
101
+ 'The user wants to READ or CHANGE a HeySpex workflow setting / agent behavior — enable or disable a behavior (auto-capture, the research trigger, the intent classifier), turn something on or off, set/lower/raise a threshold or value (confidence, timeout), or ask what a setting is currently. About the configuration itself, NOT a request to do the underlying work. (e.g. "turn off auto-capture", "lower the classifier confidence to 0.6", "is auto-capture on", "make it stop capturing everything").',
102
+ },
103
+ {
104
+ key: 'defer_intent',
105
+ description:
106
+ 'The user wants to DEFER, POSTPONE, or SCHEDULE a follow-up to a later time — "come back to this later", "remind me in a week", "park/table this until X is done", "schedule this for next week", "not now — later, after the migration". About WHEN to revisit something, not a request to do it now.',
107
+ },
108
+ ]);
109
+
110
+ function num(raw, fallback) {
111
+ const n = Number(raw);
112
+ return Number.isFinite(n) && n > 0 ? n : fallback;
113
+ }
114
+
115
+ function clamp01(n) {
116
+ const v = Number(n);
117
+ if (!Number.isFinite(v)) return 0;
118
+ return Math.max(0, Math.min(1, v));
119
+ }
120
+
121
+ /**
122
+ * Resolve the semantic-layer config: env override → .heyspex/config.yaml →
123
+ * shipped defaults. FAIL-SAFE: any unreadable value falls back to its default.
124
+ * `repoRoot` is the directory holding `.heyspex/config.yaml` — defaults to the
125
+ * vault root; an explicit argument (tests, fixtures) always wins.
126
+ */
127
+ export function readIntentClassifierConfig(repoRoot = defaultConfigRoot()) {
128
+ const cfg = { ...DEFAULTS };
129
+ try {
130
+ if (process.env.HEYSPEX_INTENT_SEMANTIC_OFF === '1') cfg.semanticEnabled = false;
131
+ else {
132
+ const enabled = readConfigLeafRaw('agent_behaviors.intent_classifier.semantic_enabled', repoRoot);
133
+ if (enabled !== undefined) cfg.semanticEnabled = enabled.trim().toLowerCase() !== 'false';
134
+ }
135
+ // Mode: env override → config → default 'self_check'. Any unrecognized value
136
+ // falls back to the deterministic default rather than silently enabling the model.
137
+ const modeRaw =
138
+ process.env.HEYSPEX_INTENT_SEMANTIC_MODE ||
139
+ unquote(readConfigLeafRaw('agent_behaviors.intent_classifier.mode', repoRoot));
140
+ if (typeof modeRaw === 'string') {
141
+ const m = modeRaw.trim().toLowerCase();
142
+ if (SEMANTIC_MODES.includes(m)) cfg.semanticMode = m;
143
+ }
144
+ const endpoint =
145
+ process.env.HEYSPEX_INTENT_ENDPOINT ||
146
+ unquote(readConfigLeafRaw('agent_behaviors.intent_classifier.endpoint', repoRoot));
147
+ if (endpoint && /^https?:\/\//.test(endpoint)) cfg.endpoint = endpoint.replace(/\/+$/, '');
148
+ const model =
149
+ process.env.HEYSPEX_INTENT_MODEL ||
150
+ unquote(readConfigLeafRaw('agent_behaviors.intent_classifier.model', repoRoot));
151
+ if (model) cfg.model = model;
152
+ cfg.timeoutMs = num(
153
+ process.env.HEYSPEX_INTENT_TIMEOUT_MS ??
154
+ readConfigLeafRaw('agent_behaviors.intent_classifier.timeout_ms', repoRoot),
155
+ DEFAULTS.timeoutMs,
156
+ );
157
+ const minConf = Number(
158
+ process.env.HEYSPEX_INTENT_MIN_CONFIDENCE ??
159
+ readConfigLeafRaw('agent_behaviors.intent_classifier.min_confidence', repoRoot),
160
+ );
161
+ if (Number.isFinite(minConf) && minConf > 0 && minConf <= 1) cfg.minConfidence = minConf;
162
+ } catch {
163
+ return { ...DEFAULTS };
164
+ }
165
+ return cfg;
166
+ }
167
+
168
+ function unquote(raw) {
169
+ if (typeof raw !== 'string') return undefined;
170
+ const v = raw.trim();
171
+ const m = v.match(/^"(.*)"$/) || v.match(/^'(.*)'$/);
172
+ return m ? m[1] : v || undefined;
173
+ }
174
+
175
+ function buildSystemPrompt(behaviors) {
176
+ const rows = behaviors
177
+ .map((b) => `- ${b.key}: ${b.description}`)
178
+ .join('\n');
179
+ return (
180
+ 'You classify ONE user message from a coding-agent session into exactly one behavior key, or "none" ' +
181
+ 'when no behavior applies. Behaviors:\n' +
182
+ rows +
183
+ '\n- none: a direct work order, a question about existing work, a bug report to fix now, small talk, or anything else.\n' +
184
+ 'Reply with STRICT JSON only, no prose: {"behavior":"<one key above or none>","confidence":<number 0..1>}'
185
+ );
186
+ }
187
+
188
+ /**
189
+ * Classify a prompt into one of the given behaviors via the local Ollama endpoint.
190
+ *
191
+ * @param {string} prompt the user's message
192
+ * @param {{key:string, description:string}[]} behaviors candidate behaviors (defaults to KNOWN_BEHAVIORS)
193
+ * @param {object} [opts] { endpoint, model, timeoutMs, repoRoot } — omitted values come from config
194
+ * @returns {Promise<{behavior: string|null, confidence: number, latencyMs?: number, error?: string}>}
195
+ * behavior is null when nothing matched OR anything failed (fail-safe).
196
+ */
197
+ export async function classifyIntent(prompt, behaviors = KNOWN_BEHAVIORS, opts = {}) {
198
+ const none = { behavior: null, confidence: 0 };
199
+ try {
200
+ if (typeof prompt !== 'string' || !prompt.trim()) return none;
201
+ const list = Array.isArray(behaviors) && behaviors.length ? behaviors : KNOWN_BEHAVIORS;
202
+ const cfg = readIntentClassifierConfig(opts.repoRoot ?? defaultConfigRoot());
203
+ const endpoint = opts.endpoint ?? cfg.endpoint;
204
+ const model = opts.model ?? cfg.model;
205
+ const timeoutMs = num(opts.timeoutMs, cfg.timeoutMs);
206
+
207
+ // Very short prompts carry no classifiable intent; very long ones are truncated
208
+ // to keep the local model inside the latency budget.
209
+ const text = prompt.trim().slice(0, 2000);
210
+ if (text.length < 12) return none;
211
+
212
+ // Optional call counter (proof/observability): when HEYSPEX_INTENT_CALL_LOG names
213
+ // a file, append one byte per model call so a test can assert exactly ≤1 call per
214
+ // prompt. Fail-safe: never let instrumentation break the classifier.
215
+ if (process.env.HEYSPEX_INTENT_CALL_LOG) {
216
+ try {
217
+ fs.appendFileSync(process.env.HEYSPEX_INTENT_CALL_LOG, `${new Date().toISOString()} ${model}\n`);
218
+ } catch {
219
+ /* ignore */
220
+ }
221
+ }
222
+
223
+ const controller = new AbortController();
224
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
225
+ const t0 = Date.now();
226
+ let res;
227
+ try {
228
+ res = await fetch(`${endpoint}/api/chat`, {
229
+ method: 'POST',
230
+ headers: { 'content-type': 'application/json' },
231
+ signal: controller.signal,
232
+ body: JSON.stringify({
233
+ model,
234
+ stream: false,
235
+ format: 'json',
236
+ think: false, // qwen3: skip the reasoning preamble; harmless no-op elsewhere
237
+ keep_alive: '30m', // keep the classifier warm between prompts
238
+ options: { temperature: 0, num_predict: 120 },
239
+ messages: [
240
+ { role: 'system', content: buildSystemPrompt(list) },
241
+ { role: 'user', content: text },
242
+ ],
243
+ }),
244
+ });
245
+ } finally {
246
+ clearTimeout(timer);
247
+ }
248
+ const latencyMs = Date.now() - t0;
249
+ if (!res || !res.ok) return { ...none, latencyMs, error: `http_${res?.status ?? 'no_response'}` };
250
+
251
+ const payload = await res.json().catch(() => null);
252
+ const content = payload?.message?.content;
253
+ if (typeof content !== 'string' || !content.trim()) return { ...none, latencyMs, error: 'empty_response' };
254
+
255
+ let parsed;
256
+ try {
257
+ parsed = JSON.parse(content);
258
+ } catch {
259
+ // Defensive: some models wrap JSON in text — try the first {...} span.
260
+ const m = content.match(/\{[\s\S]*\}/);
261
+ if (!m) return { ...none, latencyMs, error: 'unparseable_response' };
262
+ try {
263
+ parsed = JSON.parse(m[0]);
264
+ } catch {
265
+ return { ...none, latencyMs, error: 'unparseable_response' };
266
+ }
267
+ }
268
+
269
+ const rawBehavior = typeof parsed?.behavior === 'string' ? parsed.behavior.trim() : '';
270
+ const confidence = clamp01(parsed?.confidence);
271
+ const valid = list.some((b) => b.key === rawBehavior);
272
+ if (!valid || rawBehavior === 'none') return { behavior: null, confidence, latencyMs };
273
+ return { behavior: rawBehavior, confidence, latencyMs };
274
+ } catch (err) {
275
+ const error = err?.name === 'AbortError' ? 'timeout' : 'unreachable';
276
+ return { ...none, error };
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Append one classifier-decision row to .heyspex/intent-classifier-history.jsonl.
282
+ * Called by the hooks for FIRES (either layer), semantic NEAR-MISSES (matched
283
+ * behavior below threshold), and semantic ERRORS — never for the ordinary
284
+ * regex-miss/no-match case, so the log stays small. FAIL-SAFE: never throws.
285
+ */
286
+ export function appendClassifierEvent(row, repoRoot = DEFAULT_REPO_ROOT) {
287
+ try {
288
+ const full = {
289
+ timestamp: new Date().toISOString(),
290
+ event_kind: 'intent_classifier.decision',
291
+ ...row,
292
+ };
293
+ fs.appendFileSync(
294
+ path.join(repoRoot, '.heyspex', 'intent-classifier-history.jsonl'),
295
+ JSON.stringify(full) + '\n',
296
+ );
297
+ return true;
298
+ } catch {
299
+ return false;
300
+ }
301
+ }
302
+
303
+ // ── self-test CLI (no session required) ─────────────────────────────────────
304
+
305
+ async function ping() {
306
+ const cfg = readIntentClassifierConfig();
307
+ const out = { config: cfg, reachable: false, version: null, classify: null };
308
+ try {
309
+ const controller = new AbortController();
310
+ const timer = setTimeout(() => controller.abort(), cfg.timeoutMs);
311
+ const res = await fetch(`${cfg.endpoint}/api/version`, { signal: controller.signal });
312
+ clearTimeout(timer);
313
+ if (res.ok) {
314
+ out.reachable = true;
315
+ out.version = (await res.json().catch(() => null))?.version ?? null;
316
+ }
317
+ } catch {
318
+ /* stays unreachable */
319
+ }
320
+ if (out.reachable) {
321
+ out.classify = await classifyIntent(
322
+ "it'd be smart to let the app notice ideas on its own",
323
+ KNOWN_BEHAVIORS,
324
+ );
325
+ }
326
+ process.stdout.write(JSON.stringify(out, null, 2) + '\n');
327
+ process.exit(out.reachable && out.classify?.behavior === 'capture_on_idea' ? 0 : 1);
328
+ }
329
+
330
+ async function classifyCli(text) {
331
+ const result = await classifyIntent(text, KNOWN_BEHAVIORS);
332
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
333
+ process.exit(0);
334
+ }
335
+
336
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
337
+ if (isMain) {
338
+ if (process.argv.includes('--ping')) {
339
+ ping();
340
+ } else {
341
+ const idx = process.argv.indexOf('--classify');
342
+ if (idx !== -1 && process.argv[idx + 1]) {
343
+ classifyCli(process.argv[idx + 1]);
344
+ } else if (process.argv.length > 2) {
345
+ process.stdout.write(
346
+ 'Usage: node _intent-classifier-semantic.mjs --ping | --classify "<text>"\n',
347
+ );
348
+ process.exit(2);
349
+ }
350
+ }
351
+ }
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ // _run.mjs — the single chokepoint every HeySpex Claude Code hook runs through.
3
+ //
4
+ // `.claude/settings.json` never names a hook script directly. It names this
5
+ // shim plus the hook to run:
6
+ //
7
+ // node "$CLAUDE_PROJECT_DIR/scripts/claude-hooks/_run.mjs" wip-checkpoint
8
+ // node "$CLAUDE_PROJECT_DIR/scripts/claude-hooks/_run.mjs" emit-claim-status --event=Stop
9
+ //
10
+ // The shim asks one question — is HeySpex on? — and either exits 0 immediately
11
+ // or hands off to the real hook. One place to reason about, and a hook added
12
+ // tomorrow inherits the off-switch for free.
13
+ //
14
+ // Delegation is a dynamic `import()` in THIS process, not a subprocess, so the
15
+ // hook still owns stdin (Claude Code pipes the event payload there) and still
16
+ // sets this process's exit code. `process.argv` is rewritten first so a hook
17
+ // reading `--event=Stop` sees exactly what it saw before the shim existed.
18
+ //
19
+ // Everything here is fail-open. A shim that throws must never take down a
20
+ // Claude Code session, and it must never be the reason a guardrail silently
21
+ // stops firing — so an unresolvable mode reads as ON, and a genuinely broken
22
+ // hook path reports on stderr and exits 0.
23
+
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+ import { pathToFileURL, fileURLToPath } from 'node:url';
27
+
28
+ const HOOK_DIR = path.dirname(fileURLToPath(import.meta.url));
29
+ const REPO_ROOT = path.resolve(HOOK_DIR, '..', '..');
30
+
31
+ const [target, ...rest] = process.argv.slice(2);
32
+
33
+ if (!target) {
34
+ process.stderr.write('[heyspex] _run.mjs: no hook name given\n');
35
+ process.exit(0);
36
+ }
37
+
38
+ /**
39
+ * Resolve the hook to run.
40
+ * `wip-checkpoint` → scripts/claude-hooks/wip-checkpoint.mjs
41
+ * `scripts/stamp-provenance.mjs` → <repo>/scripts/stamp-provenance.mjs
42
+ *
43
+ * A bare name is constrained to a safe charset and must land inside the hook
44
+ * directory; a path form is resolved from the repo root and must stay inside
45
+ * it. Both guards exist so a mangled settings.json can't turn the shim into an
46
+ * arbitrary-script runner.
47
+ */
48
+ function resolveHook(name) {
49
+ const abs = name.includes('/') || name.includes('\\')
50
+ ? path.resolve(REPO_ROOT, name)
51
+ : /^[A-Za-z0-9._-]+$/.test(name)
52
+ ? path.join(HOOK_DIR, name.endsWith('.mjs') ? name : `${name}.mjs`)
53
+ : null;
54
+ if (!abs) return null;
55
+ const inRepo = path.relative(REPO_ROOT, abs);
56
+ if (inRepo.startsWith('..') || path.isAbsolute(inRepo)) return null;
57
+ return abs;
58
+ }
59
+
60
+ // --- the off-switch -------------------------------------------------------
61
+ // Inlined rather than imported from scripts/heyspex-mode.mjs: this runs on
62
+ // every tool call in every session, and a resolve+parse of another module is
63
+ // real latency for a question answered by one small file read. The two must
64
+ // agree — see the resolution order documented in heyspex-mode.mjs.
65
+ function heyspexIsOff() {
66
+ try {
67
+ const raw = String(process.env.HEYSPEX_MODE ?? '').trim().toLowerCase();
68
+ if (raw) return ['off', '0', 'false', 'disabled', 'disable', 'no'].includes(raw);
69
+ const state = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, '.heyspex', 'mode.json'), 'utf8'));
70
+ return state?.mode === 'off';
71
+ } catch {
72
+ return false; // fail-open: HeySpex is on unless something says otherwise
73
+ }
74
+ }
75
+
76
+ // --- hooks the off-switch may NOT silence ---------------------------------
77
+ // The productivity hooks (reminders, injections, checkpoints) exist to help,
78
+ // so a global "HeySpex is off" is the right answer for them. A SECURITY
79
+ // control is different: the mode is decided by a repo file (.heyspex/mode.json)
80
+ // that any agent can write, and nothing guards that file — so sharing the
81
+ // off-switch would mean "write one JSON file, disarm the guard forever"
82
+ // (the disarm chain, C8). Anything named here runs regardless of mode.
83
+ // Each always-on hook keeps its OWN documented escape hatch, and it must be
84
+ // an ENV VAR (HEYSPEX_ENV_VAULT_GUARD_OFF=1 for env-vault-guard;
85
+ // HEYSPEX_RESEARCH_GUARD_OFF=1 for research-method-guard) — a human setting a
86
+ // variable for their session, not a file an agent can create.
87
+ // research-protocol-gate ([[HS-K0ZP]]) joins the set for the same reason as
88
+ // research-method-guard: its whole point is that the agent cannot switch it
89
+ // off. Escape hatch: HEYSPEX_RESEARCH_PROTOCOL_GATE_OFF=1, owner-set, env only.
90
+ // git-identity-guard (owner ruling 2026-09-19, vault lesson
91
+ // when-git-says-author-identity-unknown) has NO escape hatch at all — not
92
+ // even an env var: the owner's own path is `hs source identity set`, typed by
93
+ // a human, so there is nothing a hatch would enable that the sanctioned form
94
+ // does not already do. Agents read git identity; they never write it.
95
+ const ALWAYS_ON = new Set(['env-vault-guard', 'research-method-guard', 'research-protocol-gate', 'git-identity-guard']);
96
+
97
+ function isAlwaysOn(name) {
98
+ const base = String(name)
99
+ .replace(/^.*[\\/]/, '')
100
+ .replace(/\.mjs$/i, '');
101
+ return ALWAYS_ON.has(base);
102
+ }
103
+
104
+ if (!isAlwaysOn(target) && heyspexIsOff()) process.exit(0);
105
+
106
+ const hookPath = resolveHook(target);
107
+ if (!hookPath || !fs.existsSync(hookPath)) {
108
+ process.stderr.write(`[heyspex] _run.mjs: hook not found: ${target}\n`);
109
+ process.exit(0);
110
+ }
111
+
112
+ // Make the hook believe it was invoked directly.
113
+ process.argv = [process.argv[0], hookPath, ...rest];
114
+
115
+ try {
116
+ await import(pathToFileURL(hookPath).href);
117
+ } catch (err) {
118
+ process.stderr.write(`[heyspex] hook ${target} failed: ${err?.message ?? err}\n`);
119
+ process.exit(0);
120
+ }
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ // Shared Stop-hook input helpers: read the hook's stdin JSON payload and pull the
3
+ // agent's own last message out of the transcript JSONL. Split out of
4
+ // naughty-list-gate.mjs (its original sole owner) so a second Stop-hook scanner
5
+ // (id-display-format-gate.mjs) doesn't fork a copy of the same 40 lines.
6
+ //
7
+ // DEPENDENCY-FREE, same contract as _guardrail-loader.mjs: every function degrades
8
+ // to null/'' on any read/parse failure rather than throwing, so a caller's own
9
+ // fail-safe wrapper never has to guard against this module misbehaving.
10
+
11
+ import fs from 'node:fs';
12
+
13
+ /** Read and parse the hook's stdin JSON payload. Resolves to null on any failure. */
14
+ export async function readStdinJson() {
15
+ if (process.stdin.isTTY) return null;
16
+ return new Promise((resolve) => {
17
+ let buf = '';
18
+ let settled = false;
19
+ const finish = (value) => {
20
+ if (settled) return;
21
+ settled = true;
22
+ resolve(value);
23
+ };
24
+ const timer = setTimeout(() => finish(buf || ''), 200);
25
+ process.stdin.setEncoding('utf8');
26
+ process.stdin.on('data', (chunk) => { buf += chunk; });
27
+ process.stdin.on('end', () => { clearTimeout(timer); finish(buf); });
28
+ process.stdin.on('error', () => { clearTimeout(timer); finish(buf); });
29
+ });
30
+ }
31
+
32
+ /** Read the last assistant message's text out of a transcript JSONL file. */
33
+ export function lastAssistantText(transcriptPath) {
34
+ let raw;
35
+ try {
36
+ raw = fs.readFileSync(transcriptPath, 'utf8');
37
+ } catch {
38
+ return null;
39
+ }
40
+ const lines = raw.split(/\r?\n/).filter(Boolean);
41
+ for (let i = lines.length - 1; i >= 0; i--) {
42
+ let evt;
43
+ try { evt = JSON.parse(lines[i]); } catch { continue; }
44
+ const msg = evt?.message;
45
+ const role = msg?.role ?? evt?.role;
46
+ if (role !== 'assistant') continue;
47
+ const content = msg?.content;
48
+ if (typeof content === 'string') return content;
49
+ if (Array.isArray(content)) {
50
+ const text = content
51
+ .filter((b) => b && b.type === 'text' && typeof b.text === 'string')
52
+ .map((b) => b.text)
53
+ .join('\n')
54
+ .trim();
55
+ return text || null;
56
+ }
57
+ return null;
58
+ }
59
+ return null;
60
+ }