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,197 @@
1
+ // spexharness/src/adapters/audit.mjs
2
+ //
3
+ // Restores the `.heyspex/guardrail-history.jsonl` audit-log side effect the
4
+ // ported gates under spexharness/src/gates/ deliberately left out — each
5
+ // one's header comment says "Not ported HERE" and points here. A gate stays
6
+ // a pure decision function (no fs writes); THIS module is the front-side
7
+ // observer that turns a gate's decision back into the exact row(s) its
8
+ // pre-cutover Claude Code hook wrote for the same firing, so
9
+ // scripts/claude-hooks/<gate>.smoke.mjs — the spec of that original
10
+ // behavior — keeps passing now that the shells route through the ported
11
+ // gates instead of the original scripts.
12
+ //
13
+ // Row shape is byte-compatible with the original writer
14
+ // (scripts/claude-hooks/_guardrail-events.mjs's appendGuardrailEvent):
15
+ // `{ timestamp, event_kind: "guardrail.fired", actor, spec_id, metadata }`.
16
+ // This module calls that function directly rather than reimplementing it —
17
+ // same "import the shared, already-pure library" precedent this porting
18
+ // effort already set for _guardrail-loader.mjs / _transcript-text.mjs (see
19
+ // e.g. enforce-session-sequence.mjs's own header) — so the JSONL rows the
20
+ // console's guardrail-history reader sees are identical whichever writer
21
+ // produced them.
22
+ //
23
+ // Per-gate mapping: only the three gates whose pre-cutover hooks called
24
+ // appendGuardrailEvent AND whose smoke tests assert on the resulting log
25
+ // (capture-workflow-gate, enforce-session-sequence, naughty-list-gate) are
26
+ // mapped in {@link auditRowsFor} today. Each case reads its exact original
27
+ // call shape straight from `git show HEAD:scripts/claude-hooks/<gate>.mjs`
28
+ // (pre-cutover) and reconstructs it from the fields the ported gate's
29
+ // decision object carries forward for exactly this purpose (see each gate's
30
+ // own header comment for which fields and why). A gate/decision this module
31
+ // has no case for returns no rows — this NEVER guesses a shape for a gate it
32
+ // doesn't recognize.
33
+ //
34
+ // FAIL-SAFE (same contract as every hook and as _guardrail-events.mjs
35
+ // itself): a write failure here must never break the caller. Every row
36
+ // write is wrapped; recordGuardrailAudit() never throws.
37
+
38
+ import { appendGuardrailEvent as defaultAppendGuardrailEvent } from '../../../scripts/claude-hooks/_guardrail-events.mjs';
39
+ import { redactRecord } from '../redact.mjs';
40
+
41
+ /**
42
+ * Build the zero-or-more guardrail-history rows implied by one gate's
43
+ * decision, matching what its pre-cutover Claude Code hook wrote for the
44
+ * same firing. Pure — no I/O. Returns `[]` for a plain `allow`, for any gate
45
+ * this module has no mapping for, or for a decision shape the mapping
46
+ * doesn't recognize (fail-safe: never guess).
47
+ *
48
+ * Each returned row is an `appendGuardrailEvent()`-shaped options object
49
+ * (`{ rule, surface, severity, decision, specId?, matchContext? }`) — the
50
+ * caller still supplies `repoRoot`.
51
+ *
52
+ * @param {{ id?: string }} gate
53
+ * @param {{ decision: string, reason?: string, rewrite?: Record<string, unknown>, rule?: string, blockHits?: Array<{ word: string, context?: string }> }} decision
54
+ * @returns {Array<Record<string, unknown>>}
55
+ */
56
+ export function auditRowsFor(gate, decision) {
57
+ const kind = decision?.decision ?? 'allow';
58
+ if (kind === 'allow' || !decision) return [];
59
+
60
+ switch (gate?.id) {
61
+ // Original (scripts/claude-hooks/capture-workflow-gate.mjs, pre-cutover):
62
+ // appendGuardrailEvent({ rule: `capture-workflow-gate:${type}`,
63
+ // surface: 'pretooluse', severity: 'warn', decision: 'warn',
64
+ // specId: resolved.spec_id }, root);
65
+ // Fired on every match — this gate NEVER denies (see its header), so
66
+ // only the `rewrite.warn` shape is mapped.
67
+ case 'capture-workflow-gate': {
68
+ if (kind !== 'rewrite' || !decision.rewrite?.warn) return [];
69
+ const captureId = decision.rewrite?.captureId;
70
+ if (!captureId) return [];
71
+ const type = decision.rewrite?.type ?? '';
72
+ return [
73
+ {
74
+ rule: `capture-workflow-gate:${type}`,
75
+ surface: 'pretooluse',
76
+ severity: 'warn',
77
+ decision: 'warn',
78
+ specId: captureId,
79
+ },
80
+ ];
81
+ }
82
+
83
+ // Original (scripts/claude-hooks/enforce-session-sequence.mjs, pre-cutover)
84
+ // fireRule():
85
+ // appendGuardrailEvent({ rule: ruleId, surface: 'stop',
86
+ // severity: forReal ? (rule.severity ?? 'block') : 'warn',
87
+ // decision: forReal ? 'block' : 'warn' }, root);
88
+ // Fired exactly once per Stop — either the wip rule or the progress
89
+ // rule, whichever the gate reported (`decision.rule`, carried on both
90
+ // the `deny` and `rewrite` results — see the gate's own header). The
91
+ // registry's `session-wip-before-stop` / `session-progress-before-review`
92
+ // rules are both severity "block" today, so `rule.severity ?? 'block'`
93
+ // and the `'block'` literal here agree; this reads the literal (matching
94
+ // the fallback) rather than re-loading the registry, keeping this module
95
+ // free of any guardrail-loader dependency of its own.
96
+ case 'enforce-session-sequence': {
97
+ const ruleId = decision.rule;
98
+ if (!ruleId) return []; // no rule id carried → nothing to log (fail-safe)
99
+ if (kind === 'deny') {
100
+ return [{ rule: ruleId, surface: 'stop', severity: 'block', decision: 'block' }];
101
+ }
102
+ if (kind === 'rewrite' && decision.rewrite?.warn) {
103
+ return [{ rule: ruleId, surface: 'stop', severity: 'warn', decision: 'warn' }];
104
+ }
105
+ return [];
106
+ }
107
+
108
+ // Original (scripts/claude-hooks/naughty-list-gate.mjs, pre-cutover):
109
+ // for (const hit of blockHits) {
110
+ // appendGuardrailEvent({ rule: `naughty-list:${hit.word}`,
111
+ // surface: 'stop', severity: 'block',
112
+ // decision: warnOnly ? 'warn' : 'block', matchContext: hit.context });
113
+ // }
114
+ // One row PER block-severity hit (not deduped by word — a repeated hit
115
+ // logs a repeated row, same as the original's own loop), fired on both
116
+ // rungs; warn-severity-only hits are never logged (the original's loop
117
+ // only ever iterates `blockHits`). `decision.blockHits` carries exactly
118
+ // that array on both `deny` and `rewrite` — see the gate's own header.
119
+ case 'naughty-list-gate': {
120
+ const hits = Array.isArray(decision.blockHits) ? decision.blockHits : [];
121
+ if (!hits.length) return [];
122
+ const rowDecision = kind === 'deny' ? 'block' : 'warn';
123
+ return hits.map((hit) => ({
124
+ rule: `naughty-list:${hit?.word ?? ''}`,
125
+ surface: 'stop',
126
+ severity: 'block',
127
+ decision: rowDecision,
128
+ matchContext: hit?.context,
129
+ }));
130
+ }
131
+
132
+ // Original (scripts/claude-hooks/epic-milestone-phrasing-gate.mjs,
133
+ // pre-cutover):
134
+ // appendGuardrailEvent({ rule: RULE_ID, surface: 'stop',
135
+ // severity: 'warn', decision: warnOnly ? 'warn' : 'block',
136
+ // matchContext: hits[0].excerpt });
137
+ // Exactly ONE row per firing (the original logs the FIRST hit's excerpt,
138
+ // not one row per hit — unlike naughty-list-gate above), at the literal
139
+ // severity 'warn' on BOTH rungs, because this gate ships warn-first by
140
+ // design and the registry row's own severity is 'warn' too. `decision.hits`
141
+ // carries the excerpts on both the `deny` and `rewrite` results — see the
142
+ // gate's own header.
143
+ case 'epic-milestone-phrasing-gate': {
144
+ const hits = Array.isArray(decision.hits) ? decision.hits : [];
145
+ if (!hits.length) return [];
146
+ if (kind !== 'deny' && !(kind === 'rewrite' && decision.rewrite?.warn)) return [];
147
+ return [
148
+ {
149
+ rule: 'epic-milestone-phrasing-gate',
150
+ surface: 'stop',
151
+ severity: 'warn',
152
+ decision: kind === 'deny' ? 'block' : 'warn',
153
+ matchContext: hits[0]?.excerpt,
154
+ },
155
+ ];
156
+ }
157
+
158
+ default:
159
+ return [];
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Write every audit row {@link auditRowsFor} derives from one gate's
165
+ * decision. Injectable: pass `deps.append` to swap out the real
166
+ * `_guardrail-events.mjs` writer (e.g. so a unit test can assert the call
167
+ * without touching the real file, or so an in-process caller that wants no
168
+ * file writes passes a no-op). Never throws — a row write failure is
169
+ * swallowed per row, matching `appendGuardrailEvent`'s own fail-safe
170
+ * contract (and this module's own header).
171
+ *
172
+ * @param {{ id?: string }} gate
173
+ * @param {{ decision: string }} decision
174
+ * @param {string} repoRoot
175
+ * @param {{ append?: (row: Record<string, unknown>, repoRoot: string) => unknown }} [deps]
176
+ * @returns {number} how many rows were written (best-effort; a swallowed
177
+ * per-row failure still counts as attempted, not written — callers that
178
+ * need the true count should inspect their own `deps.append`'s return).
179
+ */
180
+ export function recordGuardrailAudit(gate, decision, repoRoot, deps = {}) {
181
+ const append = deps.append ?? defaultAppendGuardrailEvent;
182
+ const rows = auditRowsFor(gate, decision);
183
+ let written = 0;
184
+ for (const row of rows) {
185
+ try {
186
+ // Redacted before it ever reaches `.heyspex/guardrail-history.jsonl`
187
+ // (HS-Y424) — `matchContext` (naughty-list-gate) is literally a
188
+ // snippet of whatever text tripped a rule, the field most likely to
189
+ // carry ingested prose or a leaked key verbatim.
190
+ append(redactRecord(row), repoRoot);
191
+ written += 1;
192
+ } catch {
193
+ // fail-safe: observability must never break the caller.
194
+ }
195
+ }
196
+ return written;
197
+ }
@@ -0,0 +1,449 @@
1
+ // spexharness/src/adapters/claude-code.mjs
2
+ //
3
+ // The Claude Code front's translation layer (HS-IQBF, wave-5 of
4
+ // [[HS-AZ61-NYPR]]). Every one of the twelve ported gates under
5
+ // spexharness/src/gates/ is now driven through THIS module by a generated
6
+ // shell (scripts/generate-hook-shells.mjs) rather than by its own
7
+ // stdin-reading, stdout-writing main() — this file is the only place that
8
+ // speaks the Claude Code hook stdin/stdout contract.
9
+ //
10
+ // Two directions, both pure except the tiny stdin/stdout glue at the bottom:
11
+ // 1. toEngineEvent(payload) — Claude Code hook stdin JSON → an
12
+ // spexharness/src/events.mjs event.
13
+ // 2. toHookResponse(point, id, decision) — a gate's decide.mjs-shaped
14
+ // decision → { exitCode, stdout, stderr }
15
+ // in the exact shape Claude Code expects
16
+ // back, one of four answer forms:
17
+ // a. bare exit code (0 allow / 2 deny)
18
+ // plus a stderr banner — the
19
+ // LEGACY style two of the twelve
20
+ // originals used (env-vault-guard,
21
+ // enforce-vault-naming).
22
+ // b. hookSpecificOutput.permissionDecision
23
+ // allow|deny|ask (+ reason) on
24
+ // stdout, exit 0 — the MODERN style
25
+ // the other before_tool gates use.
26
+ // c. hookSpecificOutput.additionalContext
27
+ // on stdout, exit 0 — an `inject`
28
+ // decision (no built-in gate uses
29
+ // this yet; it's here so the
30
+ // adapter's own contract is complete
31
+ // per this task's AC1).
32
+ // d. { decision: "block", reason } on
33
+ // stdout, exit 0 — the Stop hook
34
+ // contract (enforce-session-sequence,
35
+ // naughty-list-gate,
36
+ // id-display-format-gate).
37
+ //
38
+ // `runGateAsHook(gate)` at the bottom is the one impure entry point: it reads
39
+ // stdin, calls the two pure functions above, restores the guardrail-history
40
+ // audit-log write each ported gate deliberately left out of its own pure
41
+ // decide() (see ./audit.mjs and each gate's "Not ported HERE" header), writes
42
+ // stdout/stderr, and sets process.exitCode. This is what every generated
43
+ // shell calls — see scripts/generate-hook-shells.mjs and PORTING.md.
44
+ //
45
+ // FAIL-SAFE (house rule every original hook already followed): any internal
46
+ // error here — bad JSON on stdin, an unknown hook_event_name, a gate that
47
+ // throws — must never deny. It falls through to allow (exit 0, no output),
48
+ // same as every original script's own top-level `.catch(() => process.exit(0))`.
49
+
50
+ import path from 'node:path';
51
+ import { fileURLToPath } from 'node:url';
52
+
53
+ import { createEvent } from '../events.mjs';
54
+ import { recordGuardrailAudit } from './audit.mjs';
55
+ import { detectHarness } from './detect-harness.mjs';
56
+ import { normalizeCopilotPayload, toCopilotResponse } from './copilot.mjs';
57
+
58
+ /** Claude Code `hook_event_name` → spexharness/src/events.mjs event point.
59
+ * Deliberately does NOT map `SubagentStop` to `stop` — SubagentStop is a
60
+ * distinct Claude Code hook event none of the twelve gates in this task are
61
+ * registered for (see claim-liveness-shared.mjs for where it IS used); an
62
+ * explicit `hook_event_name: "SubagentStop"` on stdin must read as "not this
63
+ * gate's event" (→ allow), never silently fold into `stop`. */
64
+ const HOOK_EVENT_TO_POINT = {
65
+ SessionStart: 'session_start',
66
+ UserPromptSubmit: 'prompt_submit',
67
+ PreToolUse: 'before_tool',
68
+ PostToolUse: 'after_tool',
69
+ Stop: 'stop',
70
+ SessionEnd: 'session_end',
71
+ };
72
+
73
+ /** The inverse of {@link HOOK_EVENT_TO_POINT}, for building output JSON's
74
+ * `hookEventName` field from an engine event's `point`. */
75
+ const POINT_TO_HOOK_EVENT = {
76
+ session_start: 'SessionStart',
77
+ prompt_submit: 'UserPromptSubmit',
78
+ before_tool: 'PreToolUse',
79
+ after_tool: 'PostToolUse',
80
+ stop: 'Stop',
81
+ session_end: 'SessionEnd',
82
+ };
83
+
84
+ /**
85
+ * @param {string} hookEventName - Claude Code's `hook_event_name` stdin field.
86
+ * @returns {string | null} the matching spexharness event point, or null for
87
+ * an event this adapter has no mapping for (a front-end concern, not a
88
+ * gate concern — the caller should fail open, never guess).
89
+ */
90
+ export function hookEventNameToPoint(hookEventName) {
91
+ return HOOK_EVENT_TO_POINT[hookEventName] ?? null;
92
+ }
93
+
94
+ /**
95
+ * Field mapping (Claude Code stdin → engine event fields), shared by
96
+ * {@link toEngineEvent} and the lenient per-gate resolution
97
+ * {@link runGateAsHook} uses:
98
+ * session_id → sessionId (every point)
99
+ * tool_name → toolName (before_tool / after_tool)
100
+ * tool_input → toolInput (before_tool / after_tool)
101
+ * tool_response → toolResult (after_tool)
102
+ * prompt → prompt (prompt_submit)
103
+ * cwd → cwd (session_start)
104
+ * source → agent (session_start — Claude Code's own field
105
+ * name for "how the session started")
106
+ * transcript_path → transcriptPath (stop)
107
+ * stop_hook_active → stopHookActive (stop)
108
+ * reason → reason (session_end)
109
+ *
110
+ * @param {string} point
111
+ * @param {Record<string, unknown>} payload
112
+ * @returns {Record<string, unknown>}
113
+ */
114
+ function fieldsForPoint(point, payload) {
115
+ const fields = { sessionId: payload?.session_id };
116
+ switch (point) {
117
+ case 'before_tool':
118
+ fields.toolName = payload?.tool_name;
119
+ fields.toolInput = payload?.tool_input ?? {};
120
+ // 2026-10-10: carried so a before_tool gate can scan the session so far (diagnose-capture-reminder).
121
+ if (payload?.transcript_path) fields.transcriptPath = payload.transcript_path;
122
+ break;
123
+ case 'after_tool':
124
+ fields.toolName = payload?.tool_name;
125
+ fields.toolInput = payload?.tool_input ?? {};
126
+ fields.toolResult = payload?.tool_response;
127
+ break;
128
+ case 'prompt_submit':
129
+ fields.prompt = payload?.prompt;
130
+ break;
131
+ case 'session_start':
132
+ fields.cwd = payload?.cwd;
133
+ fields.agent = payload?.source;
134
+ break;
135
+ case 'stop':
136
+ fields.transcriptPath = payload?.transcript_path;
137
+ fields.stopHookActive = payload?.stop_hook_active;
138
+ break;
139
+ case 'session_end':
140
+ fields.reason = payload?.reason;
141
+ break;
142
+ default:
143
+ break;
144
+ }
145
+ return fields;
146
+ }
147
+
148
+ /**
149
+ * Map a Claude Code hook stdin payload (already JSON.parse'd) to a
150
+ * spexharness engine event. Returns null when `hook_event_name` is missing
151
+ * or unrecognized — the caller's job to fail open on that, not this
152
+ * function's (this module has no I/O of its own).
153
+ *
154
+ * @param {Record<string, unknown>} payload
155
+ * @returns {ReturnType<typeof createEvent> | null}
156
+ */
157
+ export function toEngineEvent(payload) {
158
+ const point = hookEventNameToPoint(payload?.hook_event_name);
159
+ if (!point) return null;
160
+ return createEvent(point, fieldsForPoint(point, payload));
161
+ }
162
+
163
+ // Two of the twelve original hooks used the LEGACY "bare exit code + stderr
164
+ // banner" style (exit 2, empty stdout, a `\n[<banner>]\n<reason>\n` stderr
165
+ // message) instead of the modern permissionDecision JSON every other
166
+ // before_tool gate here uses. Keyed by gate id so a future gate defaults to
167
+ // the modern style without needing to opt out of anything.
168
+ const LEGACY_EXIT2_BANNER = {
169
+ 'env-vault-guard': 'HeySpex env-vault guard',
170
+ 'enforce-vault-naming': 'HeySpex vault-naming hook',
171
+ };
172
+
173
+ function jsonStdout(obj) {
174
+ return JSON.stringify(obj);
175
+ }
176
+
177
+ /**
178
+ * Map one gate's decide.mjs-shaped result — `{ decision: 'allow'|'deny'|
179
+ * 'rewrite'|'inject', reason?, rewrite?, inject? }` — onto the Claude Code
180
+ * hook contract for the event it ran against.
181
+ *
182
+ * @param {{ id: string, event: string }} gate - the gate's own `id`/`event`
183
+ * (used for the legacy-banner lookup and as a fallback hookEventName).
184
+ * @param {{ point: string }} engineEvent - the event the gate ran against
185
+ * (its `point` decides which of the four answer shapes applies).
186
+ * @param {{ decision: string, reason?: string, rewrite?: object, inject?: object|string }} decision
187
+ * @returns {{ exitCode: number, stdout: string, stderr: string }}
188
+ */
189
+ export function toHookResponse(gate, engineEvent, decision) {
190
+ const point = engineEvent?.point ?? gate?.event;
191
+ const hookEventName = POINT_TO_HOOK_EVENT[point] ?? 'PreToolUse';
192
+ const kind = decision?.decision ?? 'allow';
193
+
194
+ // ── Stop hook contract: { decision: "block", reason } or nothing. ────────
195
+ if (point === 'stop') {
196
+ if (kind === 'deny') {
197
+ return { exitCode: 0, stdout: jsonStdout({ decision: 'block', reason: decision.reason }), stderr: '' };
198
+ }
199
+ if (kind === 'rewrite' && decision.rewrite?.warn) {
200
+ const text = decision.rewrite.stderrText ?? warnStderrText(decision.reason ?? decision.rewrite.reason);
201
+ return { exitCode: 0, stdout: '', stderr: text };
202
+ }
203
+ return { exitCode: 0, stdout: '', stderr: '' };
204
+ }
205
+
206
+ // ── before_tool / after_tool: permissionDecision, or the legacy exit-2 style. ──
207
+ if (kind === 'deny') {
208
+ const banner = LEGACY_EXIT2_BANNER[gate?.id];
209
+ if (banner) {
210
+ return { exitCode: 2, stdout: '', stderr: `\n[${banner}]\n${decision.reason}\n` };
211
+ }
212
+ return {
213
+ exitCode: 0,
214
+ stdout: jsonStdout({
215
+ hookSpecificOutput: { hookEventName, permissionDecision: 'deny', permissionDecisionReason: decision.reason },
216
+ }),
217
+ stderr: '',
218
+ };
219
+ }
220
+
221
+ if (kind === 'rewrite') {
222
+ if (decision.rewrite?.ask) {
223
+ return {
224
+ exitCode: 0,
225
+ stdout: jsonStdout({
226
+ hookSpecificOutput: { hookEventName, permissionDecision: 'ask', permissionDecisionReason: decision.reason },
227
+ }),
228
+ stderr: '',
229
+ };
230
+ }
231
+ if (decision.rewrite?.warn) {
232
+ const text = decision.rewrite.stderrText ?? warnStderrText(decision.reason ?? decision.rewrite.reason);
233
+ return { exitCode: 0, stdout: '', stderr: text };
234
+ }
235
+ // A rewrite with neither ask nor warn (not produced by any of the twelve
236
+ // gates today) — fail open rather than guess at a shape.
237
+ return { exitCode: 0, stdout: '', stderr: '' };
238
+ }
239
+
240
+ if (kind === 'inject') {
241
+ const additionalContext =
242
+ typeof decision.inject === 'string' ? decision.inject : decision.inject?.additionalContext;
243
+ if (!additionalContext) return { exitCode: 0, stdout: '', stderr: '' };
244
+ return {
245
+ exitCode: 0,
246
+ stdout: jsonStdout({ hookSpecificOutput: { hookEventName, additionalContext } }),
247
+ stderr: '',
248
+ };
249
+ }
250
+
251
+ // 'allow' (and any unrecognized decision — fail open, never guess a deny).
252
+ return { exitCode: 0, stdout: '', stderr: '' };
253
+ }
254
+
255
+ /** Default stderr formatter for a `rewrite.warn` decision that carries no
256
+ * explicit `rewrite.stderrText` override: append exactly one trailing
257
+ * newline, unless `reason` already ends with one (several gates' own warn
258
+ * text already carries its own trailing newline — see naughty-list-gate.mjs
259
+ * / id-display-format-gate.mjs's `warnReason()`). */
260
+ function warnStderrText(reason) {
261
+ const text = String(reason ?? '');
262
+ return text.endsWith('\n') ? text : `${text}\n`;
263
+ }
264
+
265
+ // ── stdin / process glue — the only impure code in this module ─────────────
266
+
267
+ /** Read and JSON.parse Claude Code's hook stdin payload. Mirrors every
268
+ * original hook's own `readStdinJson()`: a TTY (no piped stdin) or
269
+ * unparseable input both resolve to null, never throw. */
270
+ async function readStdinJson() {
271
+ if (process.stdin.isTTY) return null;
272
+ let buf = '';
273
+ process.stdin.setEncoding('utf8');
274
+ for await (const chunk of process.stdin) buf += chunk;
275
+ if (!buf.trim()) return null;
276
+ try {
277
+ return JSON.parse(buf);
278
+ } catch {
279
+ return null;
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Resolve the engine event a gate should run against, for one gate's shell
285
+ * specifically — LENIENT where {@link toEngineEvent} is strict: when
286
+ * `hook_event_name` is present, it must map to this gate's OWN `event`
287
+ * (anything else, including a recognized-but-different event like
288
+ * `SubagentStop` or `PostToolUse`, is "not this gate's turn" → null, allow).
289
+ * When `hook_event_name` is absent altogether (no key on the payload at
290
+ * all — every real Claude Code hook call always includes it; only a
291
+ * hand-built test payload omits it, e.g. `{ tool_name, tool_input }` with no
292
+ * envelope), this assumes the payload is already shaped for THIS gate's own
293
+ * registered event and maps its fields accordingly — a shell is always
294
+ * wired to exactly one event, so there is nothing to guess here.
295
+ *
296
+ * @param {Record<string, unknown>} payload
297
+ * @param {{ event: string }} gate
298
+ * @returns {ReturnType<typeof createEvent> | null}
299
+ */
300
+ function resolveEventForGate(payload, gate) {
301
+ const hasHookEventName = payload?.hook_event_name !== undefined;
302
+ // A hook script registered on SEVERAL Claude Code events (coo-guard-check is
303
+ // registered on both SessionStart and PreToolUse in .claude/settings.json)
304
+ // hands this function a gate whose `event` is the ARRAY of points it answers
305
+ // for — see spexharness/src/gates/coo-guard-gate.mjs's `hookGate`. Only this
306
+ // adapter understands that form; registry.mjs stays one-event-per-gate by
307
+ // construction, so such a hook registers its rungs there as separate gates.
308
+ const multi = Array.isArray(gate?.event);
309
+ // With no `hook_event_name` at all (a hand-built test payload), a single-event
310
+ // gate's own event is the only sensible reading; a multi-event gate has
311
+ // nothing to guess from, so it declines rather than picking one.
312
+ const point = hasHookEventName ? hookEventNameToPoint(payload.hook_event_name) : multi ? null : gate.event;
313
+ if (!point) return null;
314
+ if (multi ? !gate.event.includes(point) : point !== gate.event) return null;
315
+ return createEvent(point, fieldsForPoint(point, payload));
316
+ }
317
+
318
+ /** This adapter's own repo root (spexharness/src/adapters/claude-code.mjs is
319
+ * three levels below it) — the same depth every ported gate's own REPO_ROOT
320
+ * constant resolves from, so {@link auditRepoRoot}'s env-first fallback
321
+ * agrees with whatever root a gate itself used to resolve its decision. */
322
+ const ADAPTER_REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
323
+
324
+ /** Same env-first resolution every pre-cutover hook's own `repoRoot()`
325
+ * helper used (CLAUDE_PROJECT_DIR, trailing slash stripped, else the
326
+ * repo-relative fallback) — kept here rather than imported so this module
327
+ * has no dependency on any one gate's own copy of the same three lines. */
328
+ function auditRepoRoot() {
329
+ const fromEnv = process.env.CLAUDE_PROJECT_DIR?.trim();
330
+ if (fromEnv) return fromEnv.replace(/[\\/]+$/, '');
331
+ return ADAPTER_REPO_ROOT;
332
+ }
333
+
334
+ /**
335
+ * Run ONE gate as a Claude Code hook process: read stdin, map it to an
336
+ * engine event, run the gate, restore the guardrail-history audit-log side
337
+ * effect the gate itself deliberately left out (see audit.mjs and each
338
+ * ported gate's own "Not ported HERE" header), map the decision back to the
339
+ * hook contract, write stdout/stderr, and set `process.exitCode`. This is
340
+ * the whole body of every generated shell under scripts/claude-hooks/ — see
341
+ * scripts/generate-hook-shells.mjs.
342
+ *
343
+ * Fail-safe throughout: any error anywhere in this pipeline is swallowed and
344
+ * treated as allow (exit 0, no output) — matching every original hook's own
345
+ * `.catch(() => process.exit(0))`. The audit write is additionally never
346
+ * allowed to affect the hook response either way — see
347
+ * `recordGuardrailAudit`'s own fail-safe contract.
348
+ *
349
+ * @param {{ id: string, event: string, run: (event: object, ctx: object) => unknown }} gate
350
+ * @param {{ audit?: false | ((gate: object, decision: object, repoRoot: string) => unknown),
351
+ * append?: (row: Record<string, unknown>, repoRoot: string) => unknown,
352
+ * payload?: Record<string, unknown> }} [deps]
353
+ * `deps.payload` — an ALREADY-READ stdin payload. Only {@link runGatesAsHook}
354
+ * passes it: stdin can be consumed exactly once, so a shell wired to several
355
+ * gates reads it, picks the gate whose event matches, and hands the payload
356
+ * on rather than letting this function read an empty stream.
357
+ * `deps.audit` — full override of the audit step; pass `false` to disable
358
+ * it entirely (an in-process caller that wants no `.heyspex/guardrail-
359
+ * history.jsonl` writes at all). `deps.append` — override just the
360
+ * low-level row writer (recordGuardrailAudit's own `deps.append`), while
361
+ * keeping this module's row-shape mapping — what the adapter's own unit
362
+ * test uses to assert the call without touching the real file. Neither
363
+ * set → the real audit trail is written, restoring the pre-cutover
364
+ * behavior unconditionally (every generated shell calls `runGateAsHook(gate)`
365
+ * with no `deps`, so this is what a live Claude Code session gets).
366
+ * @returns {Promise<void>}
367
+ */
368
+ /**
369
+ * Run the ONE gate out of `gates` that is registered for the incoming hook
370
+ * event, for a Claude Code hook script wired to MORE THAN ONE event.
371
+ *
372
+ * Why it exists (wave 6 of [[HS-AZ61-NYPR]]): `.claude/settings.json` registers
373
+ * `git-reflog-audit` on BOTH SessionStart and Stop, and the original script
374
+ * branched on `hook_event_name` inside its own `main()`. The engine registry
375
+ * keys a gate on exactly one event point (registry.mjs's `assertValidGate`), so
376
+ * that rule ships as a PAIR of gates and its shell needs to pick between them.
377
+ * `runGateAsHook` cannot simply be called twice: stdin can be consumed once, so
378
+ * the second call would read an empty stream. This reads it once and hands it
379
+ * on via `deps.payload`.
380
+ *
381
+ * Fail-safe throughout, exactly like {@link runGateAsHook}: a payload that maps
382
+ * to none of these gates' events writes nothing and exits 0.
383
+ *
384
+ * @param {Array<{ id: string, event: string, run: Function }>|{ id: string, event: string, run: Function }} gates
385
+ * @param {object} [deps] - forwarded to {@link runGateAsHook}.
386
+ * @returns {Promise<void>}
387
+ */
388
+ export async function runGatesAsHook(gates, deps = {}) {
389
+ let payload = null;
390
+ try {
391
+ payload = await readStdinJson();
392
+ } catch {
393
+ return; // fail-open, same as runGateAsHook's own catch
394
+ }
395
+ if (!payload) return;
396
+
397
+ const list = Array.isArray(gates) ? gates : [gates];
398
+ const match = list.find((gate) => {
399
+ try {
400
+ return resolveEventForGate(payload, gate) !== null;
401
+ } catch {
402
+ return false;
403
+ }
404
+ });
405
+ if (!match) return; // none of these gates runs on this event — allow
406
+
407
+ await runGateAsHook(match, { ...deps, payload });
408
+ }
409
+
410
+ export async function runGateAsHook(gate, deps = {}) {
411
+ try {
412
+ const rawPayload = deps.payload ?? (await readStdinJson());
413
+ if (!rawPayload) return;
414
+ // GitHub Copilot runs these same hooks (HS-20SW): positive-signature detection, then the Copilot dialect
415
+ // (top-level permissionDecision JSON, exit 0 - never exit 2, Copilot would drop the reason).
416
+ const harness = detectHarness(rawPayload, process.env);
417
+ const payload = harness === 'copilot' ? normalizeCopilotPayload(rawPayload) : rawPayload;
418
+
419
+ const engineEvent = resolveEventForGate(payload, gate);
420
+ if (!engineEvent) return; // not this gate's event — allow
421
+
422
+ const raw = await gate.run(engineEvent, {});
423
+ const decision = typeof raw === 'string' ? { decision: raw } : raw ?? { decision: 'allow' };
424
+
425
+ if (deps.audit === false) {
426
+ // in-process caller opt-out — no audit write at all.
427
+ } else if (typeof deps.audit === 'function') {
428
+ try {
429
+ await deps.audit(gate, decision, auditRepoRoot());
430
+ } catch {
431
+ /* fail-safe: the audit step must never affect the hook response */
432
+ }
433
+ } else {
434
+ try {
435
+ recordGuardrailAudit(gate, decision, auditRepoRoot(), deps.append ? { append: deps.append } : {});
436
+ } catch {
437
+ /* fail-safe: the audit step must never affect the hook response */
438
+ }
439
+ }
440
+
441
+ const { exitCode, stdout, stderr } = (harness === 'copilot' ? toCopilotResponse : toHookResponse)(gate, engineEvent, decision);
442
+ if (stdout) process.stdout.write(stdout);
443
+ if (stderr) process.stderr.write(stderr);
444
+ process.exitCode = exitCode;
445
+ } catch {
446
+ // Fail-open: never strand the agent on an adapter/gate crash.
447
+ process.exitCode = 0;
448
+ }
449
+ }