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,247 @@
1
+ #!/usr/bin/env node
2
+ // Claude Code hook → the HeySpex Intent Classifier's SINGLE UserPromptSubmit entry point.
3
+ //
4
+ // HS-5ISS ENGINE CUTOVER: this hook now classifies through the engine module
5
+ // (spexharness/src/intent-classifier.mjs's classifyPrompt) instead of the old
6
+ // per-prompt Layer 2 "self_check" prose block (_intent-classifier-router.mjs's
7
+ // buildSelfCheckBlock — ~4,200 tokens / ~6.5 KB, pasted into EVERY prompt that missed
8
+ // every lexical pattern, i.e. almost every ordinary work order). The engine module is
9
+ // LEXICAL ONLY: a prompt that matches none of the five classifier rules
10
+ // (capture-on-idea, research-before-decision, settings-intent, defer-intent,
11
+ // spec-reference-lookup) now injects NOTHING — under 500 bytes, usually zero — instead
12
+ // of the old self-check block (AC3, [[HS-5ISS]]).
13
+ //
14
+ // A rule the engine matches still has to clear the SAME gating the old lexical sweep
15
+ // applied (env kill switch, agent_behaviors.<key>.enabled, the injected-content guard)
16
+ // before it fires — the engine module itself knows only patterns, not config or env,
17
+ // so isBehaviorEligible() re-applies that gating here. Once eligible, the directive is
18
+ // built through the SAME per-behavior transform (capture's confirm-mode substitution +
19
+ // hints, research's source hint, settings/defer/spec-reference's raw inject) and
20
+ // wrapped in the same CONFIRMED MATCH marker as the old lexical path
21
+ // (buildConfirmedDirective) — so a gate that fires emits a byte-identical directive to
22
+ // before. The five gates' own matching patterns and inject text are untouched.
23
+ //
24
+ // CUTOVER FLAG: agent_behaviors.intent_classifier.engine (.heyspex/config.yaml, read
25
+ // from the VAULT root via the loader's default config root — this hook never joins a
26
+ // config path itself; [[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]]).
27
+ // Defaults ON (isConfigKeyEnabled semantics — absent/anything but explicit `false` is
28
+ // enabled). Set to `false` to fall back to the pre-HS-5ISS path in full — the unified
29
+ // lexical-sweep-plus-Layer-2-self-check hook, unchanged (_intent-classifier-router.mjs's
30
+ // runClassifierHook()).
31
+ //
32
+ // UNGATED by CLAUDE_AGENT — interactive human typing is the point. Per-behavior mutes
33
+ // still work: HEYSPEX_CAPTURE_TRIGGER_OFF / HEYSPEX_RESEARCH_TRIGGER_OFF /
34
+ // HEYSPEX_SETTINGS_INTENT_OFF / HEYSPEX_DEFER_INTENT_OFF / HEYSPEX_SPEC_REFERENCE_LOOKUP_OFF
35
+ // = 1. HEYSPEX_INTENT_ENGINE_OFF=1 is the cutover's own env kill switch — same
36
+ // contract as every other behavior here (a config_key AND an env var, never just
37
+ // one) — and forces the old fallback path without touching the git-tracked config
38
+ // file, which is what tests use to exercise the fallback safely. Decisions audited
39
+ // to .heyspex/intent-classifier-history.jsonl.
40
+ //
41
+ // FAIL-SAFE: exits 0 on every path. A missing/corrupt config, a missing rule table, or
42
+ // any thrown error degrades to "no injection" (engine path) or defers to the untouched
43
+ // fallback (runClassifierHook never throws either).
44
+
45
+ import { classifyPrompt } from '../../spexharness/src/intent-classifier.mjs';
46
+ import { isConfigKeyEnabled, startsWithInjectedContent } from './_guardrail-loader.mjs';
47
+ import {
48
+ appendClassifierEvent,
49
+ classifyIntent,
50
+ readIntentClassifierConfig,
51
+ } from './_intent-classifier-semantic.mjs';
52
+ import {
53
+ runClassifierHook,
54
+ readStdinJson,
55
+ behaviorKeyForRuleId,
56
+ isBehaviorEligible,
57
+ buildConfirmedDirective,
58
+ buildSelfCheckBlock,
59
+ CLASSIFIER_BEHAVIORS,
60
+ MIN_SELF_CHECK_CHARS,
61
+ } from './_intent-classifier-router.mjs';
62
+
63
+ const ENGINE_FLAG = 'agent_behaviors.intent_classifier.engine';
64
+
65
+ // THE SEMANTIC NET ON THE ENGINE PATH ([[HS-3J57-7EDT-DIAG-intent-classifier-s-semantic-fallback]],
66
+ // [[HS-8A3Y-LELB-DIAG-a-prose-feature-request-bypassed]]). The HS-5ISS cutover made a
67
+ // lexical miss a silent no-op — the ~6.5 KB Layer 2 self-check block was dropped and
68
+ // nothing took its place, so a paraphrased idea, or an owner's work-order-shaped
69
+ // feature ask ("please also add this to heyspex-app … should we create our own …"),
70
+ // reached the agent with no capture directive at all and was built straight from the
71
+ // prompt. This is the replacement: on a miss, when the semantic layer is on
72
+ // (agent_behaviors.intent_classifier.semantic_enabled, env HEYSPEX_INTENT_SEMANTIC_OFF=1
73
+ // to mute), the hook's behavior depends on the resolved mode
74
+ // (agent_behaviors.intent_classifier.mode, default self_check):
75
+ // - mode:self_check (DEFAULT) — the SAME deterministic SELF-CHECK block the legacy
76
+ // _intent-classifier-router.mjs path builds (buildSelfCheckBlock, reused verbatim
77
+ // — not a second copy of its prose) over whichever behaviors are still eligible on
78
+ // this prompt (env kill switch / config_key / injected-content guard, via
79
+ // isBehaviorEligible). This is a REAL directive, not an advisory: the agent judges
80
+ // the gates and, on a hit, fetches and follows the exact directive via
81
+ // `--directive <key>`, byte-identical to Layer 1. A prompt with zero eligible
82
+ // behaviors emits nothing (nothing to self-check).
83
+ // - mode:model (OPT-IN) — the local classifier is consulted first and a confident hit
84
+ // fires the full CONFIRMED MATCH directive, exactly as the legacy router's opt-in
85
+ // path did. When the model is silent, low-confidence, or unreachable, the hook
86
+ // falls back to the COMPACT_GATE_CHECK advisory below (under 400 bytes) rather than
87
+ // going fully silent again.
88
+ // A sub-12-char prompt ("yes", "proceed") and a harness envelope (task-notification /
89
+ // system-reminder as the prompt) never get either treatment.
90
+ export const COMPACT_GATE_CHECK =
91
+ 'HEYSPEX GATE CHECK (system note): no lexical rule matched. If this is a raw idea or a ' +
92
+ 'feature ask (please add/build X, should we build our own X), run `node ' +
93
+ 'scripts/claude-hooks/_intent-classifier-router.mjs --directive capture_on_idea` and ' +
94
+ 'follow it. Other keys: research_before_decision, settings_intent, defer_intent, ' +
95
+ 'spec_reference_lookup. Otherwise proceed silently.';
96
+
97
+ /**
98
+ * The engine-miss fallback. Returns the additionalContext to emit, or null when the
99
+ * miss must stay silent (semantic layer off, prompt too short, harness envelope).
100
+ * Never throws.
101
+ */
102
+ export async function engineMissFallback(prompt) {
103
+ try {
104
+ if (typeof prompt !== 'string' || prompt.trim().length < MIN_SELF_CHECK_CHARS) return null;
105
+ if (startsWithInjectedContent(prompt)) return null;
106
+ const cfg = readIntentClassifierConfig();
107
+ if (!cfg.semanticEnabled) return null;
108
+
109
+ if (cfg.semanticMode === 'model') {
110
+ const result = await classifyIntent(prompt);
111
+ if (
112
+ result?.behavior &&
113
+ result.confidence >= cfg.minConfidence &&
114
+ isBehaviorEligible(result.behavior, prompt)
115
+ ) {
116
+ const directive = buildConfirmedDirective(result.behavior, undefined, prompt);
117
+ if (directive) {
118
+ appendClassifierEvent({
119
+ surface: 'userpromptsubmit',
120
+ rule: result.behavior,
121
+ layer: 'semantic',
122
+ decision: 'fire',
123
+ engine: true,
124
+ confidence: result.confidence,
125
+ latency_ms: result.latencyMs,
126
+ prompt_chars: prompt.length,
127
+ });
128
+ return directive;
129
+ }
130
+ }
131
+ // model silent/low-confidence/unreachable/ineligible → fall through to the
132
+ // COMPACT_GATE_CHECK advisory below (unchanged from before this fix).
133
+ } else if (cfg.semanticMode === 'self_check') {
134
+ // THE FIX: reuse the legacy router's own self-check block builder
135
+ // (_intent-classifier-router.mjs's buildSelfCheckBlock) rather than the weak
136
+ // COMPACT_GATE_CHECK advisory — this is what closes the Layer 2 hole if
137
+ // `engine` is ever flipped back to true while mode stays self_check (the
138
+ // default and, on this machine, the only reachable mode: `model` needs an
139
+ // Ollama endpoint that is off the network here). Byte-identical to what the
140
+ // legacy path would emit for the SAME considered set, so a gate an agent
141
+ // judges to fire resolves to the exact same directive either way.
142
+ const considered = CLASSIFIER_BEHAVIORS.filter((behavior) =>
143
+ isBehaviorEligible(behavior.key, prompt),
144
+ ).map((behavior) => ({ behavior }));
145
+ if (considered.length) {
146
+ const additionalContext = buildSelfCheckBlock(considered, undefined, prompt);
147
+ if (additionalContext) {
148
+ appendClassifierEvent({
149
+ surface: 'userpromptsubmit',
150
+ rule: 'intent-classifier',
151
+ layer: 'semantic',
152
+ decision: 'self_check_injected',
153
+ engine: true,
154
+ offered: considered.map((entry) => entry.behavior.key),
155
+ prompt_chars: prompt.length,
156
+ });
157
+ return additionalContext;
158
+ }
159
+ }
160
+ // No behavior is currently eligible (every gate env/config-disabled) — mirror
161
+ // the legacy router's routePrompt(), which returns null before ever building a
162
+ // self-check block when its `eligible` set is empty. Nothing to self-check.
163
+ return null;
164
+ }
165
+
166
+ appendClassifierEvent({
167
+ surface: 'userpromptsubmit',
168
+ rule: null,
169
+ layer: 'semantic',
170
+ decision: 'compact_gate_check',
171
+ engine: true,
172
+ mode: cfg.semanticMode,
173
+ prompt_chars: prompt.length,
174
+ });
175
+ return COMPACT_GATE_CHECK;
176
+ } catch {
177
+ return null;
178
+ }
179
+ }
180
+
181
+ function emit(additionalContext) {
182
+ if (!additionalContext) return;
183
+ process.stdout.write(
184
+ JSON.stringify({
185
+ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext },
186
+ }),
187
+ );
188
+ }
189
+
190
+ /**
191
+ * The engine-cutover hook body: read the payload, classify via the engine module, and
192
+ * emit a directive ONLY when a gate both matches lexically AND is eligible (env/config/
193
+ * guard). Every other path — a full lexical miss, an unknown rule id, an ineligible
194
+ * behavior, a malformed payload — emits nothing at all, which is the point (AC3).
195
+ * Never throws (mirrors runClassifierHook's own contract).
196
+ */
197
+ export async function runEngineClassifierHook() {
198
+ try {
199
+ const raw = await readStdinJson();
200
+ if (!raw) return;
201
+ let payload;
202
+ try {
203
+ payload = JSON.parse(raw);
204
+ } catch {
205
+ return;
206
+ }
207
+ if (payload?.hook_event_name !== 'UserPromptSubmit') return;
208
+ const prompt = payload?.prompt;
209
+ if (typeof prompt !== 'string' || !prompt.trim()) return;
210
+
211
+ const hit = classifyPrompt(prompt);
212
+ if (!hit) {
213
+ // Lexical miss: the compact semantic net (see engineMissFallback) — never the
214
+ // old multi-KB self-check block, and nothing at all when the layer is off.
215
+ emit(await engineMissFallback(prompt));
216
+ return;
217
+ }
218
+
219
+ const behaviorKey = behaviorKeyForRuleId(hit.rule);
220
+ if (!behaviorKey) return; // an engine rule id this router doesn't know as a behavior
221
+
222
+ if (!isBehaviorEligible(behaviorKey, prompt)) return; // env-off / config-disabled / injected-content guard
223
+
224
+ const directive = buildConfirmedDirective(behaviorKey, undefined, prompt);
225
+ if (!directive) return;
226
+
227
+ appendClassifierEvent({
228
+ surface: 'userpromptsubmit',
229
+ rule: hit.rule,
230
+ layer: 'lexical',
231
+ decision: 'fire',
232
+ engine: true,
233
+ prompt_chars: prompt.length,
234
+ });
235
+
236
+ emit(directive);
237
+ } catch {
238
+ // Never break Claude Code on a hook failure.
239
+ }
240
+ }
241
+
242
+ const engineForcedOff = process.env.HEYSPEX_INTENT_ENGINE_OFF === '1';
243
+ if (!engineForcedOff && isConfigKeyEnabled(ENGINE_FLAG)) {
244
+ runEngineClassifierHook();
245
+ } else {
246
+ runClassifierHook();
247
+ }
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env node
2
+ // log-session-activity.mjs — HS-6QEW-TASK (epic HS-JAAB-EPIC)
3
+ // PreToolUse + PostToolUse hook (matcher .*): append one `agent.tool_called`
4
+ // event row per tool call to the git-tracked .heyspex/sesslog-history.jsonl.
5
+ //
6
+ // Contract (see the task spec's Builder brief):
7
+ // - Reuse the stdin-payload parse + resolveSessionId from claim-liveness-shared.mjs.
8
+ // - Fail-open: any error, malformed payload, or missing field exits 0 — never
9
+ // break the host session.
10
+ // - Privacy: args_hash (deterministic hash) by default; always redact keys
11
+ // matching /token|secret|password|api_key|auth/i; full args only behind the
12
+ // per-vault audit_log_full_args opt-in.
13
+ // - Event fields: kind=agent.tool_called, source=hook, transport=local,
14
+ // tool_kind (edit|command|mcp|task|read|other), tool_name, session_id,
15
+ // session_short, spec_id, args_hash, latency_ms, success, error_kind, ts.
16
+ // - `--self-test` entrypoint: exercises well-formed payload → one valid JSONL
17
+ // row (temp file), malformed payload → exit 0 no throw, api_key/token keys →
18
+ // redacted, identical args → identical args_hash. Exits 0 on pass.
19
+
20
+ import { createHash } from "node:crypto";
21
+ import fs from "node:fs";
22
+ import path from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+ import {
25
+ normalizeHookEvent,
26
+ parseArgs,
27
+ readStdin,
28
+ resolveSessionId,
29
+ } from "./claim-liveness-shared.mjs";
30
+
31
+ const REPO_ROOT = path.resolve(
32
+ path.dirname(fileURLToPath(import.meta.url)),
33
+ "..",
34
+ "..",
35
+ );
36
+
37
+ function getLogPath() {
38
+ const env = process.env.HEYSPEX_SESSLOG_PATH?.trim();
39
+ return env || path.join(REPO_ROOT, ".heyspex", "sesslog-history.jsonl");
40
+ }
41
+
42
+ function getClaimsPath() {
43
+ const env = process.env.HEYSPEX_CLAIMS_PATH?.trim();
44
+ return env || path.join(REPO_ROOT, ".heyspex", "active-claims.json");
45
+ }
46
+
47
+ function getPendingDir() {
48
+ const env = process.env.HEYSPEX_SESSLOG_PENDING_DIR?.trim();
49
+ return env || path.join(REPO_ROOT, ".heyspex", "pending");
50
+ }
51
+
52
+ function getFocusPath() {
53
+ const env = process.env.HEYSPEX_FOCUS_PATH?.trim();
54
+ return env || path.join(REPO_ROOT, ".heyspex", "focus.yaml");
55
+ }
56
+
57
+ // Tool kind mapping
58
+ function getToolKind(toolName) {
59
+ if (!toolName) return "other";
60
+ const toolMap = {
61
+ Edit: "edit",
62
+ Write: "edit",
63
+ NotebookEdit: "edit",
64
+ Bash: "command",
65
+ PowerShell: "command",
66
+ Read: "read",
67
+ Grep: "read",
68
+ Glob: "read",
69
+ Task: "task",
70
+ Agent: "task",
71
+ };
72
+ if (toolMap[toolName] !== undefined) return toolMap[toolName];
73
+ // Claude Code reports MCP tools as mcp__<server>__<tool>; Command Code's
74
+ // hooks engine reports the bare heyspex_<tool> name (seen in live rows).
75
+ if (
76
+ typeof toolName === "string" &&
77
+ (toolName.startsWith("mcp__") || toolName.startsWith("heyspex_"))
78
+ )
79
+ return "mcp";
80
+ return "other";
81
+ }
82
+
83
+ // Redact sensitive keys (shallow)
84
+ function redactSensitiveKeys(obj) {
85
+ if (!obj || typeof obj !== "object") return obj;
86
+ const redacted = {};
87
+ for (const [key, value] of Object.entries(obj)) {
88
+ if (
89
+ typeof key === "string" &&
90
+ /token|secret|password|api_key|auth/i.test(key)
91
+ ) {
92
+ redacted[key] = "[REDACTED]";
93
+ } else {
94
+ redacted[key] = value;
95
+ }
96
+ }
97
+ return redacted;
98
+ }
99
+
100
+ // Generate deterministic hash from redacted args (sha256, first 16 hex chars)
101
+ function generateArgsHash(args) {
102
+ if (!args || typeof args !== "object") return null;
103
+ const clean = redactSensitiveKeys(args);
104
+ return createHash("sha256")
105
+ .update(JSON.stringify(clean))
106
+ .digest("hex")
107
+ .slice(0, 16);
108
+ }
109
+
110
+ // Write session log entry
111
+ function writeSessionLogEntry(entry) {
112
+ try {
113
+ const logPath = getLogPath();
114
+ const dir = path.dirname(logPath);
115
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
116
+ fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
117
+ } catch {
118
+ // Fail-safe: silently ignore write errors
119
+ }
120
+ }
121
+
122
+ // Read active-claims.json and find claim for session
123
+ function resolveClaim(sessionId) {
124
+ try {
125
+ const claimsPath = getClaimsPath();
126
+ if (!fs.existsSync(claimsPath)) return null;
127
+ const raw = fs.readFileSync(claimsPath, "utf8");
128
+ const data = JSON.parse(raw);
129
+ const claims = data?.claims;
130
+ if (!claims || typeof claims !== "object") return null;
131
+ // A session may hold several claims (e.g. an epic and its task) —
132
+ // attribute to the most recently active one.
133
+ const mine = Object.values(claims)
134
+ .filter((c) => c?.sessionId === sessionId)
135
+ .sort(
136
+ (a, b) =>
137
+ Date.parse(b.lastActivityAt ?? b.claimedAt ?? 0) -
138
+ Date.parse(a.lastActivityAt ?? a.claimedAt ?? 0),
139
+ );
140
+ if (mine.length > 0) {
141
+ return { specId: mine[0].specId, actor: mine[0].agent };
142
+ }
143
+ } catch {
144
+ // ignore
145
+ }
146
+ return null;
147
+ }
148
+
149
+ // Fallback attribution: the vault-wide focus pin (.heyspex/focus.yaml).
150
+ // Coarser than a session claim — every unclaimed session inherits the pinned
151
+ // spec — but it is the epic's named v1 mechanism for spec_id attribution.
152
+ function resolveFocusSpecId() {
153
+ try {
154
+ const focusPath = getFocusPath();
155
+ if (!fs.existsSync(focusPath)) return null;
156
+ const raw = fs.readFileSync(focusPath, "utf8");
157
+ const m = /^spec_path:\s*(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/m.exec(raw);
158
+ const specPath = m?.[1] ?? m?.[2] ?? m?.[3];
159
+ if (!specPath) return null;
160
+ // basename "HS-XXXX-KIND-slug.md" → id "HS-XXXX-KIND"
161
+ const base = path.basename(specPath, ".md");
162
+ const idMatch = /^(HS-[A-Za-z0-9]+-(?:MIL|EPIC|TASK|DIAG|RES|CAP|REV))/.exec(
163
+ base,
164
+ );
165
+ return idMatch?.[1] ?? null;
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+
171
+ // Sweep orphaned pending markers. A tool call that never gets a PostToolUse
172
+ // (interrupt, denied permission, crashed session) leaks its marker forever, so
173
+ // the dir accumulates. Anything older than 1 hour is dead — a stale marker only
174
+ // costs one missing latency_ms, so deletion is always safe. Non-fatal.
175
+ const STALE_MARKER_MS = 60 * 60 * 1000;
176
+ function sweepStaleMarkers(dir) {
177
+ try {
178
+ const cutoff = Date.now() - STALE_MARKER_MS;
179
+ for (const name of fs.readdirSync(dir)) {
180
+ try {
181
+ const p = path.join(dir, name);
182
+ const st = fs.statSync(p);
183
+ if (st.isFile() && st.mtimeMs < cutoff) fs.unlinkSync(p);
184
+ } catch {
185
+ // fail-safe: skip entries we cannot stat/delete
186
+ }
187
+ }
188
+ } catch {
189
+ // fail-safe
190
+ }
191
+ }
192
+
193
+ // Write a pending marker file for PreToolUse
194
+ function writePendingMarker(toolUseId) {
195
+ try {
196
+ const dir = getPendingDir();
197
+ if (!dir) return;
198
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
199
+ sweepStaleMarkers(dir);
200
+ const marker = { startTime: Date.now() };
201
+ fs.writeFileSync(path.join(dir, toolUseId), JSON.stringify(marker));
202
+ } catch {
203
+ // fail-safe
204
+ }
205
+ }
206
+
207
+ // Read and remove pending marker for PostToolUse; returns startTime or null
208
+ function readPendingMarker(toolUseId) {
209
+ try {
210
+ const dir = getPendingDir();
211
+ if (!dir) return null;
212
+ const filePath = path.join(dir, toolUseId);
213
+ if (!fs.existsSync(filePath)) return null;
214
+ const raw = fs.readFileSync(filePath, "utf8");
215
+ fs.unlinkSync(filePath);
216
+ const data = JSON.parse(raw);
217
+ return typeof data?.startTime === "number" ? data.startTime : null;
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+
223
+ // Process hook event
224
+ async function processHookEvent() {
225
+ try {
226
+ const payload = await readStdin();
227
+ if (!payload) return;
228
+
229
+ const parsedPayload = JSON.parse(payload);
230
+ const args = parsedPayload?.tool_input;
231
+ const toolName = parsedPayload?.tool_name;
232
+ // Claude Code sends hook_event_name in the payload (PascalCase); Cursor
233
+ // passes --event=camelCase on argv instead. Normalize both.
234
+ const cliArgs = parseArgs(process.argv);
235
+ const event = normalizeHookEvent(
236
+ parsedPayload?.hook_event_name ?? cliArgs.event,
237
+ );
238
+
239
+ // Skip if not a tool call event
240
+ if (event !== "PreToolUse" && event !== "PostToolUse") return;
241
+
242
+ const sessionId = resolveSessionId(parsedPayload);
243
+ if (!sessionId) return;
244
+
245
+ const fullArgsEnabled =
246
+ process.env.HEYSPEX_AUDIT_LOG_FULL_ARGS?.trim() === "1";
247
+ const argsHash = generateArgsHash(args);
248
+
249
+ // Determine tool kind
250
+ const toolKind = getToolKind(toolName);
251
+
252
+ // Resolve spec_id and actor: session claim first, focus pin as fallback
253
+ const claim = resolveClaim(sessionId);
254
+ const specId = claim?.specId ?? resolveFocusSpecId();
255
+ const actor = claim?.actor ?? "unknown";
256
+
257
+ const toolUseId = parsedPayload?.tool_use_id;
258
+
259
+ // PreToolUse: store pending marker, do NOT write session log row.
260
+ // (No claim-liveness emit here — that is the liveness hook's own job;
261
+ // calling it from this logger would double-emit heartbeats and add an
262
+ // HTTP round-trip to every tool call.)
263
+ if (event === "PreToolUse") {
264
+ if (toolUseId) writePendingMarker(toolUseId);
265
+ return;
266
+ }
267
+
268
+ // PostToolUse: compute latency (null = no matching marker), build entry, write row
269
+ let latencyMs = null;
270
+ if (toolUseId) {
271
+ const startTime = readPendingMarker(toolUseId);
272
+ if (startTime !== null) {
273
+ latencyMs = Math.max(0, Date.now() - startTime);
274
+ }
275
+ }
276
+
277
+ const response = parsedPayload?.tool_response;
278
+ let success = true;
279
+ let errorKind = null;
280
+
281
+ if (response && typeof response === "object") {
282
+ if (response.is_error === true) {
283
+ success = false;
284
+ errorKind = "tool_error";
285
+ } else if (typeof response.error === "string" && response.error.trim()) {
286
+ success = false;
287
+ errorKind = "tool_error";
288
+ }
289
+ }
290
+
291
+ // Build metadata
292
+ const metadata = {
293
+ tool_name: toolName,
294
+ tool_kind: toolKind,
295
+ session_id: sessionId,
296
+ session_short: sessionId.slice(0, 8),
297
+ source: "hook",
298
+ transport: "local",
299
+ args_hash: argsHash,
300
+ success,
301
+ latency_ms: latencyMs,
302
+ };
303
+
304
+ if (errorKind) {
305
+ metadata.error_kind = errorKind;
306
+ }
307
+
308
+ if (fullArgsEnabled && args && typeof args === "object") {
309
+ metadata.args = redactSensitiveKeys(args);
310
+ }
311
+
312
+ const entry = {
313
+ timestamp: new Date().toISOString(),
314
+ event_kind: "agent.tool_called",
315
+ actor,
316
+ spec_id: specId,
317
+ metadata,
318
+ };
319
+
320
+ writeSessionLogEntry(entry);
321
+ } catch {
322
+ // Fail-open: silently ignore all errors
323
+ }
324
+ }
325
+
326
+ // Self-test: DELEGATES to the sibling smoke file (the deterministic gate).
327
+ // DO NOT replace this block with inline assertions — the gate contract requires
328
+ // `node <this file> --self-test` to run log-session-activity.smoke.mjs verbatim.
329
+ if (process.argv.includes("--self-test")) {
330
+ const { spawnSync } = await import("node:child_process");
331
+ const smoke = path.join(
332
+ path.dirname(fileURLToPath(import.meta.url)),
333
+ "log-session-activity.smoke.mjs",
334
+ );
335
+ const proc = spawnSync(process.execPath, [smoke], { stdio: "inherit" });
336
+ process.exit(proc.status ?? 1);
337
+ }
338
+
339
+ // Main execution
340
+ await processHookEvent();
341
+ process.exit(0);
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ // GENERATED by scripts/generate-hook-shells.mjs (HS-IQBF) — do not hand-edit.
3
+ // This file only wires Claude Code's hook stdin/stdout contract to the
4
+ // spexharness engine port of this gate. To change what this gate DECIDES,
5
+ // edit spexharness/src/gates/naughty-list-gate.mjs. To change the SHAPE of this shell,
6
+ // edit the generator, then rerun: node scripts/generate-hook-shells.mjs
7
+
8
+ import { runGateAsHook } from '../../spexharness/src/adapters/claude-code.mjs';
9
+ import gate from '../../spexharness/src/gates/naughty-list-gate.mjs';
10
+
11
+ await runGateAsHook(gate);
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ // Claude Code hook → play tada.wav when a spec flips to `status: in_review`.
3
+
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { spawn } from 'node:child_process';
7
+
8
+ import { isShipEvent, readStdin, repoRoot } from './terminal-close-shared.mjs';
9
+
10
+ const SOUND_PATH = 'C:\\Windows\\Media\\tada.wav';
11
+
12
+ function resolveSoundEnabledFromSettings() {
13
+ try {
14
+ const raw = fs.readFileSync(path.join(repoRoot(), 'spex-console', 'settings.json'), 'utf8');
15
+ const parsed = JSON.parse(raw);
16
+ return parsed?.soundEnabled === true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ function playSound() {
23
+ try {
24
+ const child = spawn(
25
+ 'powershell.exe',
26
+ [
27
+ '-NoProfile',
28
+ '-WindowStyle',
29
+ 'Hidden',
30
+ '-Command',
31
+ `[System.Media.SoundPlayer]::new('${SOUND_PATH}').PlaySync()`,
32
+ ],
33
+ { detached: true, stdio: 'ignore', windowsHide: true },
34
+ );
35
+ child.unref();
36
+ } catch {
37
+ // Silent
38
+ }
39
+ try {
40
+ fs.writeFileSync(path.join(repoRoot(), '.heyspex', '.last-success-sound-at'), String(Date.now()));
41
+ } catch {
42
+ // never block
43
+ }
44
+ }
45
+
46
+ async function main() {
47
+ const stdinRaw = await readStdin();
48
+ if (!stdinRaw) return;
49
+
50
+ let payload;
51
+ try {
52
+ payload = JSON.parse(stdinRaw);
53
+ } catch {
54
+ return;
55
+ }
56
+
57
+ if (!isShipEvent(payload)) return;
58
+ if (!resolveSoundEnabledFromSettings()) return;
59
+
60
+ const filePath = payload?.file_path ?? payload?.tool_input?.file_path;
61
+ if (typeof filePath === 'string') {
62
+ try {
63
+ fs.accessSync(filePath, fs.constants.R_OK);
64
+ } catch {
65
+ return;
66
+ }
67
+ }
68
+
69
+ playSound();
70
+ }
71
+
72
+ main().catch(() => {});