heyspex 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (352) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/README.md +21 -0
  3. package/bin/hs.mjs +1089 -104
  4. package/bin/spexcode.mjs +262 -0
  5. package/dev-factory/engine/worktree.mjs +740 -0
  6. package/dev-factory/lib/code-repo-root.mjs +67 -0
  7. package/dev-factory/lib/context-budget.mjs +84 -0
  8. package/dev-factory/lib/epic-detect.mjs +190 -0
  9. package/dev-factory/lib/fleet-queue-file.mjs +846 -0
  10. package/dev-factory/lib/fleet-queue.mjs +426 -0
  11. package/dev-factory/lib/fleet-state.mjs +14 -18
  12. package/dev-factory/lib/model-catalog.mjs +338 -0
  13. package/dev-factory/lib/seat-context.mjs +199 -0
  14. package/dev-factory/lib/spec-io.mjs +93 -0
  15. package/dev-factory/lib/sufficiency-score.mjs +8 -2
  16. package/dev-factory/lib/worktree-allocator.mjs +744 -0
  17. package/dev-factory/model-catalog.json +222 -0
  18. package/dist/cli/hs-check.mjs +1127 -103
  19. package/dist/cli/hs-claim.mjs +981 -63
  20. package/dist/cli/hs-connect.mjs +10059 -1729
  21. package/dist/cli/hs-defer.mjs +5507 -3072
  22. package/dist/cli/hs-dev.mjs +576 -0
  23. package/dist/cli/hs-env.mjs +416 -302
  24. package/dist/cli/hs-index-rebuild.mjs +7334 -0
  25. package/dist/cli/hs-index-reconcile.mjs +8138 -0
  26. package/dist/cli/hs-init.mjs +2840 -149
  27. package/dist/cli/hs-install.mjs +7214 -594
  28. package/dist/cli/hs-join.mjs +10640 -2421
  29. package/dist/cli/hs-vault-pull.mjs +8157 -6370
  30. package/dist/cli/hs-vault-push.mjs +6433 -0
  31. package/dist/cli/hs-vault.mjs +4029 -1555
  32. package/dist/cli/hs-workspace.mjs +2702 -538
  33. package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
  34. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
  35. package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
  36. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
  37. package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
  38. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
  39. package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
  40. package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
  41. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
  42. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
  43. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
  44. package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
  45. package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
  46. package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -0
  47. package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
  48. package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
  49. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
  50. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
  51. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
  52. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
  53. package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
  54. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
  55. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
  56. package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -0
  57. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
  58. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
  59. package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
  60. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
  61. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
  62. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
  63. package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
  64. package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
  65. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +33 -0
  66. package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
  67. package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
  68. package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
  69. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
  70. package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
  71. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
  72. package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
  73. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
  74. package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
  75. package/dist/cli-templates/harness-commands/cursor/hs-view.md +32 -0
  76. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
  77. package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
  78. package/docs/copilot-plugin-install.md +162 -0
  79. package/docs/driving-heyspex-from-copilot.md +165 -0
  80. package/docs/mcp-client-config.md +259 -6
  81. package/package.json +249 -124
  82. package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
  83. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
  84. package/plugins/heyspex/.mcp.json +12 -0
  85. package/plugins/heyspex/README.md +309 -0
  86. package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
  87. package/plugins/heyspex/bin/run-hook.mjs +80 -0
  88. package/plugins/heyspex/hooks/cursor-hooks.json +308 -0
  89. package/plugins/heyspex/hooks/hooks.json +418 -0
  90. package/plugins/heyspex/mcp.json +13 -0
  91. package/scripts/check-mcp-freshness.mjs +17 -9
  92. package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
  93. package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
  94. package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
  95. package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -0
  96. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +356 -0
  97. package/scripts/claude-hooks/_run.mjs +413 -0
  98. package/scripts/claude-hooks/_transcript-text.mjs +60 -0
  99. package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
  100. package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
  101. package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
  102. package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
  103. package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
  104. package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
  105. package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
  106. package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
  107. package/scripts/claude-hooks/claim-liveness-shared.mjs +577 -0
  108. package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
  109. package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
  110. package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
  111. package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
  112. package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
  113. package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
  114. package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
  115. package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
  116. package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
  117. package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
  118. package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
  119. package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
  120. package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
  121. package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
  122. package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
  123. package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
  124. package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
  125. package/scripts/claude-hooks/inject-communication-style.mjs +86 -0
  126. package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
  127. package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
  128. package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
  129. package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
  130. package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -0
  131. package/scripts/claude-hooks/log-session-activity.mjs +341 -0
  132. package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
  133. package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
  134. package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
  135. package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
  136. package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
  137. package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
  138. package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
  139. package/scripts/claude-hooks/research-method-guard.mjs +398 -0
  140. package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
  141. package/scripts/claude-hooks/session-end.mjs +451 -0
  142. package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
  143. package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
  144. package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
  145. package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
  146. package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
  147. package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
  148. package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
  149. package/scripts/claude-hooks/wip-checkpoint.mjs +302 -0
  150. package/scripts/cursor-hooks/manifest.mjs +281 -0
  151. package/scripts/dev-server-status.mjs +34 -13
  152. package/scripts/dev-server-stop.mjs +313 -0
  153. package/scripts/heyspex-mode.mjs +288 -0
  154. package/scripts/hs-auto-resolve.mjs +240 -0
  155. package/scripts/hs-auto-source.mjs +398 -0
  156. package/scripts/hs-auto.mjs +134 -0
  157. package/scripts/hs-capture-cli.mjs +326 -0
  158. package/scripts/hs-classifier.mjs +136 -0
  159. package/scripts/hs-companion.mjs +88 -0
  160. package/scripts/hs-gateway.mjs +115 -0
  161. package/scripts/hs-mode.mjs +125 -0
  162. package/scripts/hs-next.mjs +441 -0
  163. package/scripts/hs-settings.mjs +338 -0
  164. package/scripts/hs-view.mjs +496 -0
  165. package/scripts/hs-worktree.mjs +201 -0
  166. package/scripts/install-claude-code-hooks.mjs +272 -0
  167. package/scripts/install-cursor-hooks.mjs +471 -0
  168. package/scripts/lib/agent-env-markers.mjs +38 -0
  169. package/scripts/lib/console-url-resolver.mjs +199 -61
  170. package/scripts/lib/customer-kit.mjs +1203 -0
  171. package/scripts/lib/dev-mode.mjs +249 -32
  172. package/scripts/lib/directive-obligations.mjs +349 -0
  173. package/scripts/lib/heyspex-home.mjs +72 -0
  174. package/scripts/lib/hook-dispositions.mjs +210 -0
  175. package/scripts/lib/hook-vault-header.mjs +30 -0
  176. package/scripts/lib/hs-command.mjs +277 -0
  177. package/scripts/lib/hs-companion-settings.mjs +472 -0
  178. package/scripts/lib/hs-source-branch.mjs +196 -0
  179. package/scripts/lib/hs-source-commit.mjs +59 -5
  180. package/scripts/lib/hs-source-identity.mjs +526 -0
  181. package/scripts/lib/hs-source-log.mjs +13 -22
  182. package/scripts/lib/hs-source-preflight.mjs +18 -4
  183. package/scripts/lib/hs-source-push.mjs +386 -307
  184. package/scripts/lib/hs-worktree-args.mjs +115 -0
  185. package/scripts/lib/shared-path-helpers.mjs +134 -0
  186. package/scripts/lib/standalone-doctor.mjs +540 -3
  187. package/scripts/lib/subagent-git-lock.mjs +109 -0
  188. package/scripts/lib/template-pin.mjs +34 -0
  189. package/scripts/lib/window-placement.mjs +109 -0
  190. package/scripts/provenance-lib.mjs +91 -0
  191. package/scripts/stamp-provenance-on-edit.mjs +222 -0
  192. package/scripts/wip-ledger-health.mjs +653 -0
  193. package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
  194. package/spexharness/src/adapters/audit.mjs +197 -0
  195. package/spexharness/src/adapters/claude-code.mjs +449 -0
  196. package/spexharness/src/adapters/copilot.mjs +236 -0
  197. package/spexharness/src/adapters/detect-harness.mjs +34 -0
  198. package/spexharness/src/chat/tui/ansi.mjs +448 -0
  199. package/spexharness/src/chat/tui/palette.mjs +22 -0
  200. package/spexharness/src/chat/tui/select.mjs +208 -0
  201. package/spexharness/src/config-root.mjs +54 -0
  202. package/spexharness/src/context-budget.mjs +141 -0
  203. package/spexharness/src/dev-server-control.mjs +389 -0
  204. package/spexharness/src/doctrine/behaviour.mjs +371 -0
  205. package/spexharness/src/doctrine/corpus.mjs +733 -0
  206. package/spexharness/src/doctrine/inject.mjs +338 -0
  207. package/spexharness/src/doctrine/merge.mjs +184 -0
  208. package/spexharness/src/doctrine/sources.mjs +252 -0
  209. package/spexharness/src/doctrine/synthesize.mjs +401 -0
  210. package/spexharness/src/doctrine/vault-source.mjs +574 -0
  211. package/spexharness/src/doctrine/writer.mjs +176 -0
  212. package/spexharness/src/events.mjs +84 -0
  213. package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
  214. package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
  215. package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
  216. package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
  217. package/spexharness/src/gates/deep-research-cost-gate.mjs +275 -0
  218. package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
  219. package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
  220. package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
  221. package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
  222. package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
  223. package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
  224. package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
  225. package/spexharness/src/gates/git-identity-guard.mjs +61 -0
  226. package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
  227. package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
  228. package/spexharness/src/gates/git-stash-gate.mjs +159 -0
  229. package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
  230. package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
  231. package/spexharness/src/gates/inject-vault-context.mjs +239 -0
  232. package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
  233. package/spexharness/src/gates/plan-review-gate.mjs +233 -0
  234. package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
  235. package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
  236. package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
  237. package/spexharness/src/gates/settings-write-gate.mjs +104 -0
  238. package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
  239. package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
  240. package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
  241. package/spexharness/src/git-reflog-audit.mjs +294 -0
  242. package/spexharness/src/intent-classifier.mjs +196 -0
  243. package/spexharness/src/intent-directive.mjs +626 -0
  244. package/spexharness/src/profile/learn.mjs +551 -0
  245. package/spexharness/src/profile/proposals.mjs +357 -0
  246. package/spexharness/src/profile/vault-inclusion.mjs +386 -0
  247. package/spexharness/src/redact.mjs +151 -0
  248. package/spexharness/src/rule-table.mjs +849 -0
  249. package/spexharness/src/seat-context.mjs +122 -0
  250. package/spexharness/src/session-bootstrap.mjs +512 -0
  251. package/spexharness/src/session-record.mjs +815 -0
  252. package/spexharness/src/token-inventory.mjs +207 -0
  253. package/vendor/heyspex-mcp/account-switch.mjs +96 -0
  254. package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
  255. package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
  256. package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
  257. package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
  258. package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
  259. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +77 -8
  260. package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
  261. package/vendor/heyspex-mcp/capture-console-client.mjs +117 -3
  262. package/vendor/heyspex-mcp/capture-focus.mjs +1061 -31
  263. package/vendor/heyspex-mcp/capture-outbox.mjs +15 -5
  264. package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
  265. package/vendor/heyspex-mcp/capture-tools.mjs +1264 -0
  266. package/vendor/heyspex-mcp/claim-tools.mjs +1147 -0
  267. package/vendor/heyspex-mcp/cli-verbs.mjs +2074 -0
  268. package/vendor/heyspex-mcp/cli.mjs +207 -15524
  269. package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
  270. package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
  271. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +602 -0
  272. package/vendor/heyspex-mcp/config-seed.yaml +15 -0
  273. package/vendor/heyspex-mcp/config-settings.mjs +290 -7
  274. package/vendor/heyspex-mcp/console-fetch.mjs +822 -17
  275. package/vendor/heyspex-mcp/console-transport.mjs +172 -0
  276. package/vendor/heyspex-mcp/credentials.mjs +585 -0
  277. package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
  278. package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
  279. package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
  280. package/vendor/heyspex-mcp/design-tools.mjs +4 -1
  281. package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
  282. package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
  283. package/vendor/heyspex-mcp/dev-tools.mjs +131 -0
  284. package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
  285. package/vendor/heyspex-mcp/events-tools.mjs +207 -0
  286. package/vendor/heyspex-mcp/filing-tools.mjs +723 -0
  287. package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
  288. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +71 -15
  289. package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
  290. package/vendor/heyspex-mcp/folder-trust-store.mjs +60 -0
  291. package/vendor/heyspex-mcp/http-transport.mjs +302 -0
  292. package/vendor/heyspex-mcp/list-tools.mjs +257 -0
  293. package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
  294. package/vendor/heyspex-mcp/mcp-config.mjs +107 -0
  295. package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
  296. package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
  297. package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
  298. package/vendor/heyspex-mcp/occupied-keys.mjs +142 -0
  299. package/vendor/heyspex-mcp/on-deck-brief.mjs +3377 -0
  300. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
  301. package/vendor/heyspex-mcp/package-root.mjs +32 -0
  302. package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
  303. package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
  304. package/vendor/heyspex-mcp/plan-push.mjs +908 -22
  305. package/vendor/heyspex-mcp/plan-review.mjs +124 -30
  306. package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
  307. package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -0
  308. package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -970
  309. package/vendor/heyspex-mcp/promote-capture.mjs +388 -69
  310. package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
  311. package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
  312. package/vendor/heyspex-mcp/promote-research.mjs +666 -53
  313. package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
  314. package/vendor/heyspex-mcp/prompts.mjs +1460 -0
  315. package/vendor/heyspex-mcp/read-path.mjs +191 -15
  316. package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
  317. package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
  318. package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
  319. package/vendor/heyspex-mcp/register-spec.mjs +266 -54
  320. package/vendor/heyspex-mcp/request-context.mjs +83 -0
  321. package/vendor/heyspex-mcp/research-focus.mjs +207 -4
  322. package/vendor/heyspex-mcp/research-handlers.mjs +625 -0
  323. package/vendor/heyspex-mcp/research-tools.mjs +4 -4
  324. package/vendor/heyspex-mcp/response-budget.mjs +348 -0
  325. package/vendor/heyspex-mcp/review-focus.mjs +25 -1
  326. package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
  327. package/vendor/heyspex-mcp/search-specs.mjs +280 -0
  328. package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
  329. package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
  330. package/vendor/heyspex-mcp/session-owner.mjs +109 -0
  331. package/vendor/heyspex-mcp/session-tools.mjs +162 -0
  332. package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
  333. package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
  334. package/vendor/heyspex-mcp/spec-edit-tools.mjs +453 -0
  335. package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
  336. package/vendor/heyspex-mcp/split-research.mjs +304 -56
  337. package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
  338. package/vendor/heyspex-mcp/submit-for-review.mjs +150 -7
  339. package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
  340. package/vendor/heyspex-mcp/summary-write-tools.mjs +228 -0
  341. package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
  342. package/vendor/heyspex-mcp/test-helpers.mjs +123 -1
  343. package/vendor/heyspex-mcp/tool-catalog.mjs +2819 -0
  344. package/vendor/heyspex-mcp/tool-dispatch.mjs +657 -0
  345. package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
  346. package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
  347. package/vendor/heyspex-mcp/vault-io.mjs +720 -0
  348. package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
  349. package/vendor/heyspex-mcp/vault-resolve.mjs +554 -68
  350. package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
  351. package/vendor/heyspex-mcp/whoami.mjs +92 -0
  352. package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
@@ -0,0 +1,279 @@
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
+ // HS-KNJ8: deterministic verbatim capture. After the classifier has run, an idea-bearing prompt
66
+ // (a capture_on_idea / research_before_decision match, or the semantic-miss rung firing) is appended
67
+ // VERBATIM to the capture on deck for THIS session, and the model is told it was done. Also refreshes
68
+ // the PreToolUse capture-execution-gate's state file. Returns the note to append to the emitted context.
69
+ // Body moved to scripts/lib/capture-verbatim-append.mjs (runVerbatimStageForHook) so the legacy router shares it.
70
+ // Dynamic + variable specifier on purpose: platform-only module (customer-kit scope 'platform'), NOT in the
71
+ // published npm closure; a customer install simply has no verbatim stage (caught -> null).
72
+ //
73
+ // HS-20SW: BOTH UserPromptSubmit entry points (this engine path AND the legacy router) now pass the same
74
+ // options, so engine:true and engine:false behave identically ([[HS-LES-a-safety-net-wired-into-only-one-of-two]]):
75
+ // `directiveIfNoCapture` on the semantic-miss rung (the hard directive when nothing is on deck, exactly what the
76
+ // legacy self_check rung does) and `armObligation` on every capture-bearing path, which records the outstanding
77
+ // capture obligation the directive-obligation-gate enforces.
78
+ async function verbatimStage(payload, prompt, append, options = {}) {
79
+ try {
80
+ const mod = '../lib/capture-verbatim-append.mjs';
81
+ const { runVerbatimStageForHook } = await import(mod);
82
+ return await runVerbatimStageForHook(payload, prompt, append, options);
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ const NOTE_SEP = '\n\n';
89
+ const IDEA_BEARING_BEHAVIORS =new Set(['capture_on_idea', 'research_before_decision']);
90
+
91
+ // THE SEMANTIC NET ON THE ENGINE PATH ([[HS-3J57-7EDT-DIAG-intent-classifier-s-semantic-fallback]],
92
+ // [[HS-8A3Y-LELB-DIAG-a-prose-feature-request-bypassed]]). The HS-5ISS cutover made a
93
+ // lexical miss a silent no-op — the ~6.5 KB Layer 2 self-check block was dropped and
94
+ // nothing took its place, so a paraphrased idea, or an owner's work-order-shaped
95
+ // feature ask ("please also add this to heyspex-app … should we create our own …"),
96
+ // reached the agent with no capture directive at all and was built straight from the
97
+ // prompt. This is the replacement: on a miss, when the semantic layer is on
98
+ // (agent_behaviors.intent_classifier.semantic_enabled, env HEYSPEX_INTENT_SEMANTIC_OFF=1
99
+ // to mute), the hook's behavior depends on the resolved mode
100
+ // (agent_behaviors.intent_classifier.mode, default self_check):
101
+ // - mode:self_check (DEFAULT) — the SAME deterministic SELF-CHECK block the legacy
102
+ // _intent-classifier-router.mjs path builds (buildSelfCheckBlock, reused verbatim
103
+ // — not a second copy of its prose) over whichever behaviors are still eligible on
104
+ // this prompt (env kill switch / config_key / injected-content guard, via
105
+ // isBehaviorEligible). This is a REAL directive, not an advisory: the agent judges
106
+ // the gates and, on a hit, fetches and follows the exact directive via
107
+ // `--directive <key>`, byte-identical to Layer 1. A prompt with zero eligible
108
+ // behaviors emits nothing (nothing to self-check).
109
+ // - mode:model (OPT-IN) — the local classifier is consulted first and a confident hit
110
+ // fires the full CONFIRMED MATCH directive, exactly as the legacy router's opt-in
111
+ // path did. When the model is silent, low-confidence, or unreachable, the hook
112
+ // falls back to the COMPACT_GATE_CHECK advisory below (under 400 bytes) rather than
113
+ // going fully silent again.
114
+ // A sub-12-char prompt ("yes", "proceed") and a harness envelope (task-notification /
115
+ // system-reminder as the prompt) never get either treatment.
116
+ export const COMPACT_GATE_CHECK =
117
+ 'HEYSPEX GATE CHECK (system note): no lexical rule matched. If this is a raw idea or a ' +
118
+ 'feature ask (please add/build X, should we build our own X), run `node ' +
119
+ 'scripts/claude-hooks/_intent-classifier-router.mjs --directive capture_on_idea` and ' +
120
+ 'follow it. Other keys: research_before_decision, settings_intent, defer_intent, ' +
121
+ 'spec_reference_lookup. Otherwise proceed silently.';
122
+
123
+ /**
124
+ * The engine-miss fallback. Returns the additionalContext to emit, or null when the
125
+ * miss must stay silent (semantic layer off, prompt too short, harness envelope).
126
+ * Never throws.
127
+ */
128
+ export async function engineMissFallback(prompt) {
129
+ try {
130
+ if (typeof prompt !== 'string' || prompt.trim().length < MIN_SELF_CHECK_CHARS) return null;
131
+ if (startsWithInjectedContent(prompt)) return null;
132
+ const cfg = readIntentClassifierConfig();
133
+ if (!cfg.semanticEnabled) return null;
134
+
135
+ if (cfg.semanticMode === 'model') {
136
+ const result = await classifyIntent(prompt);
137
+ if (
138
+ result?.behavior &&
139
+ result.confidence >= cfg.minConfidence &&
140
+ isBehaviorEligible(result.behavior, prompt)
141
+ ) {
142
+ const directive = buildConfirmedDirective(result.behavior, undefined, prompt);
143
+ if (directive) {
144
+ appendClassifierEvent({
145
+ surface: 'userpromptsubmit',
146
+ rule: result.behavior,
147
+ layer: 'semantic',
148
+ decision: 'fire',
149
+ engine: true,
150
+ confidence: result.confidence,
151
+ latency_ms: result.latencyMs,
152
+ prompt_chars: prompt.length,
153
+ });
154
+ return directive;
155
+ }
156
+ }
157
+ // model silent/low-confidence/unreachable/ineligible → fall through to the
158
+ // COMPACT_GATE_CHECK advisory below (unchanged from before this fix).
159
+ } else if (cfg.semanticMode === 'self_check') {
160
+ // THE FIX: reuse the legacy router's own self-check block builder
161
+ // (_intent-classifier-router.mjs's buildSelfCheckBlock) rather than the weak
162
+ // COMPACT_GATE_CHECK advisory — this is what closes the Layer 2 hole if
163
+ // `engine` is ever flipped back to true while mode stays self_check (the
164
+ // default and, on this machine, the only reachable mode: `model` needs an
165
+ // Ollama endpoint that is off the network here). Byte-identical to what the
166
+ // legacy path would emit for the SAME considered set, so a gate an agent
167
+ // judges to fire resolves to the exact same directive either way.
168
+ const considered = CLASSIFIER_BEHAVIORS.filter((behavior) =>
169
+ isBehaviorEligible(behavior.key, prompt),
170
+ ).map((behavior) => ({ behavior }));
171
+ if (considered.length) {
172
+ const additionalContext = buildSelfCheckBlock(considered, undefined, prompt);
173
+ if (additionalContext) {
174
+ appendClassifierEvent({
175
+ surface: 'userpromptsubmit',
176
+ rule: 'intent-classifier',
177
+ layer: 'semantic',
178
+ decision: 'self_check_injected',
179
+ engine: true,
180
+ offered: considered.map((entry) => entry.behavior.key),
181
+ prompt_chars: prompt.length,
182
+ });
183
+ return additionalContext;
184
+ }
185
+ }
186
+ // No behavior is currently eligible (every gate env/config-disabled) — mirror
187
+ // the legacy router's routePrompt(), which returns null before ever building a
188
+ // self-check block when its `eligible` set is empty. Nothing to self-check.
189
+ return null;
190
+ }
191
+
192
+ appendClassifierEvent({
193
+ surface: 'userpromptsubmit',
194
+ rule: null,
195
+ layer: 'semantic',
196
+ decision: 'compact_gate_check',
197
+ engine: true,
198
+ mode: cfg.semanticMode,
199
+ prompt_chars: prompt.length,
200
+ });
201
+ return COMPACT_GATE_CHECK;
202
+ } catch {
203
+ return null;
204
+ }
205
+ }
206
+
207
+ function emit(additionalContext) {
208
+ if (!additionalContext) return;
209
+ process.stdout.write(
210
+ JSON.stringify({
211
+ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext },
212
+ }),
213
+ );
214
+ }
215
+
216
+ /**
217
+ * The engine-cutover hook body: read the payload, classify via the engine module, and
218
+ * emit a directive ONLY when a gate both matches lexically AND is eligible (env/config/
219
+ * guard). Every other path — a full lexical miss, an unknown rule id, an ineligible
220
+ * behavior, a malformed payload — emits nothing at all, which is the point (AC3).
221
+ * Never throws (mirrors runClassifierHook's own contract).
222
+ */
223
+ export async function runEngineClassifierHook() {
224
+ try {
225
+ const raw = await readStdinJson();
226
+ if (!raw) return;
227
+ let payload;
228
+ try {
229
+ payload = JSON.parse(raw);
230
+ } catch {
231
+ return;
232
+ }
233
+ if (payload?.hook_event_name !== 'UserPromptSubmit') return;
234
+ const prompt = payload?.prompt;
235
+ if (typeof prompt !== 'string' || !prompt.trim()) return;
236
+
237
+ const hit = classifyPrompt(prompt);
238
+ if (!hit) {
239
+ // Lexical miss: the compact semantic net (see engineMissFallback) — never the
240
+ // old multi-KB self-check block, and nothing at all when the layer is off.
241
+ const missContext = await engineMissFallback(prompt);
242
+ // The semantic-miss rung fired (substantive, non-envelope prompt, layer on) = idea-bearing.
243
+ const missNote = await verbatimStage(payload, prompt, Boolean(missContext), { directiveIfNoCapture: Boolean(missContext), armObligation: Boolean(missContext) });
244
+ emit([missContext, missNote].filter(Boolean).join(NOTE_SEP));
245
+ return;
246
+ }
247
+
248
+ const behaviorKey = behaviorKeyForRuleId(hit.rule);
249
+ if (!behaviorKey) return; // an engine rule id this router doesn't know as a behavior
250
+
251
+ if (!isBehaviorEligible(behaviorKey, prompt)) return; // env-off / config-disabled / injected-content guard
252
+
253
+ const directive = buildConfirmedDirective(behaviorKey, undefined, prompt);
254
+ if (!directive) return;
255
+
256
+ appendClassifierEvent({
257
+ surface: 'userpromptsubmit',
258
+ rule: hit.rule,
259
+ layer: 'lexical',
260
+ decision: 'fire',
261
+ engine: true,
262
+ prompt_chars: prompt.length,
263
+ });
264
+
265
+ const hitNote = await verbatimStage(payload, prompt, IDEA_BEARING_BEHAVIORS.has(behaviorKey), {
266
+ armObligation: behaviorKey === 'capture_on_idea',
267
+ });
268
+ emit([directive, hitNote].filter(Boolean).join(NOTE_SEP));
269
+ } catch {
270
+ // Never break Claude Code on a hook failure.
271
+ }
272
+ }
273
+
274
+ const engineForcedOff = process.env.HEYSPEX_INTENT_ENGINE_OFF === '1';
275
+ if (!engineForcedOff && isConfigKeyEnabled(ENGINE_FLAG)) {
276
+ runEngineClassifierHook();
277
+ } else {
278
+ runClassifierHook();
279
+ }
@@ -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);