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,413 @@
1
+ #!/usr/bin/env node
2
+ // _run.mjs — the single chokepoint every HeySpex Claude Code hook runs through.
3
+ //
4
+ // `.claude/settings.json` never names a hook script directly. It names this
5
+ // shim plus the hook to run:
6
+ //
7
+ // node "$CLAUDE_PROJECT_DIR/scripts/claude-hooks/_run.mjs" wip-checkpoint
8
+ // node "$CLAUDE_PROJECT_DIR/scripts/claude-hooks/_run.mjs" emit-claim-status --event=Stop
9
+ //
10
+ // The shim asks one question — is HeySpex on? — and either exits 0 immediately
11
+ // or hands off to the real hook. One place to reason about, and a hook added
12
+ // tomorrow inherits the off-switch for free.
13
+ //
14
+ // Delegation is a dynamic `import()` in THIS process, not a subprocess, so the
15
+ // hook still owns stdin (Claude Code pipes the event payload there) and still
16
+ // sets this process's exit code. `process.argv` is rewritten first so a hook
17
+ // reading `--event=Stop` sees exactly what it saw before the shim existed.
18
+ //
19
+ // Everything here is fail-open. A shim that throws must never take down a
20
+ // Claude Code session, and it must never be the reason a guardrail silently
21
+ // stops firing — so an unresolvable mode reads as ON, and a genuinely broken
22
+ // hook path reports on stderr and exits 0.
23
+
24
+ import crypto from 'node:crypto';
25
+ import fs from 'node:fs';
26
+ import os from 'node:os';
27
+ import path from 'node:path';
28
+ import { pathToFileURL, fileURLToPath } from 'node:url';
29
+
30
+ const STATE_DIR = '.' + 'heyspex';
31
+ const HOOK_DIR = path.dirname(fileURLToPath(import.meta.url));
32
+ const REPO_ROOT = path.resolve(HOOK_DIR, '..', '..');
33
+
34
+ const [target, ...rest] = process.argv.slice(2);
35
+
36
+ if (!target) {
37
+ process.stderr.write('[heyspex] _run.mjs: no hook name given\n');
38
+ process.exit(0);
39
+ }
40
+
41
+ /**
42
+ * Resolve the hook to run.
43
+ * `wip-checkpoint` → scripts/claude-hooks/wip-checkpoint.mjs
44
+ * `scripts/stamp-provenance.mjs` → <repo>/scripts/stamp-provenance.mjs
45
+ *
46
+ * A bare name is constrained to a safe charset and must land inside the hook
47
+ * directory; a path form is resolved from the repo root and must stay inside
48
+ * it. Both guards exist so a mangled settings.json can't turn the shim into an
49
+ * arbitrary-script runner.
50
+ */
51
+ function resolveHook(name) {
52
+ const abs = name.includes('/') || name.includes('\\')
53
+ ? path.resolve(REPO_ROOT, name)
54
+ : /^[A-Za-z0-9._-]+$/.test(name)
55
+ ? path.join(HOOK_DIR, name.endsWith('.mjs') ? name : `${name}.mjs`)
56
+ : null;
57
+ if (!abs) return null;
58
+ const inRepo = path.relative(REPO_ROOT, abs);
59
+ if (inRepo.startsWith('..') || path.isAbsolute(inRepo)) return null;
60
+ return abs;
61
+ }
62
+
63
+ // --- the off-switch: see modeOffFromProject() below (project-scoped; must agree with heyspex-mode.mjs resolution order:
64
+ // HEYSPEX_MODE env first, then <project>/.heyspex/mode.json).
65
+
66
+ // --- hooks the off-switch may NOT silence ---------------------------------
67
+ // The productivity hooks (reminders, injections, checkpoints) exist to help,
68
+ // so a global "HeySpex is off" is the right answer for them. A SECURITY
69
+ // control is different: the mode is decided by a repo file (.heyspex/mode.json)
70
+ // that any agent can write, and nothing guards that file — so sharing the
71
+ // off-switch would mean "write one JSON file, disarm the guard forever"
72
+ // (the disarm chain, C8). Anything named here runs regardless of mode.
73
+ // Each always-on hook keeps its OWN documented escape hatch, and it must be
74
+ // an ENV VAR (HEYSPEX_ENV_VAULT_GUARD_OFF=1 for env-vault-guard;
75
+ // HEYSPEX_RESEARCH_GUARD_OFF=1 for research-method-guard) — a human setting a
76
+ // variable for their session, not a file an agent can create.
77
+ // research-protocol-gate ([[HS-K0ZP]]) joins the set for the same reason as
78
+ // research-method-guard: its whole point is that the agent cannot switch it
79
+ // off. Escape hatch: HEYSPEX_RESEARCH_PROTOCOL_GATE_OFF=1, owner-set, env only.
80
+ // git-identity-guard (owner ruling 2026-09-19, vault lesson
81
+ // when-git-says-author-identity-unknown) has NO escape hatch at all — not
82
+ // even an env var: the owner's own path is `hs source identity set`, typed by
83
+ // a human, so there is nothing a hatch would enable that the sanctioned form
84
+ // does not already do. Agents read git identity; they never write it.
85
+ const ALWAYS_ON = new Set(['env-vault-guard', 'research-method-guard', 'research-protocol-gate', 'git-identity-guard']);
86
+
87
+ function isAlwaysOn(name) {
88
+ const base = String(name)
89
+ .replace(/^.*[\\/]/, '')
90
+ .replace(/\.mjs$/i, '');
91
+ return ALWAYS_ON.has(base);
92
+ }
93
+
94
+ // ── project roots the mode file may live in ─────────────────────────────────
95
+ // `hs mode off` writes <project>/.heyspex/mode.json. In a customer repo whose hooks come from the plugin /
96
+ // node_modules, REPO_ROOT is the package, not the project, so the dev's switch was ignored. Look in the PROJECT
97
+ // first: CLAUDE_PROJECT_DIR, then the hook payload's cwd, then walk up from process.cwd(); REPO_ROOT stays the
98
+ // last resort. HEYSPEX_MODE (env) keeps precedence over every file.
99
+ function projectRoots(payload) {
100
+ const roots = [];
101
+ const add = (r) => {
102
+ if (typeof r === 'string' && r.trim() && !roots.includes(r.trim())) roots.push(r.trim());
103
+ };
104
+ add(process.env.CLAUDE_PROJECT_DIR);
105
+ add(payload?.cwd);
106
+ try {
107
+ let d = process.cwd();
108
+ for (;;) {
109
+ if (fs.existsSync(path.join(d, '.heyspex', 'mode.json'))) {
110
+ add(d);
111
+ break;
112
+ }
113
+ const up = path.dirname(d);
114
+ if (up === d) break;
115
+ d = up;
116
+ }
117
+ } catch { /* ignore */ }
118
+ add(REPO_ROOT);
119
+ return roots;
120
+ }
121
+
122
+ function modeOffFromProject(payload) {
123
+ try {
124
+ const raw = String(process.env.HEYSPEX_MODE ?? '').trim().toLowerCase();
125
+ if (raw) return ['off', '0', 'false', 'disabled', 'disable', 'no'].includes(raw);
126
+ for (const root of projectRoots(payload)) {
127
+ const file = path.join(root, '.heyspex', 'mode.json');
128
+ if (!fs.existsSync(file)) continue;
129
+ // ANY root saying off wins: the dev's `hs mode off` lives in the project, an older switch in the platform root.
130
+ if (JSON.parse(fs.readFileSync(file, 'utf8'))?.mode === 'off') return true;
131
+ }
132
+ } catch { /* fail-open: on */ }
133
+ return false;
134
+ }
135
+
136
+ // ── fail-open, outermost ([[HS-20SW-DRSM-DIAG-heyspex-capture-on-idea-and-workflow]]) ─────────────
137
+ // GitHub Copilot turns ANY non-zero hook exit into a deny (and 2 is always a deny with the reason lost), so a
138
+ // crash anywhere - resolution, import, a hook's own async throw - must exit 0. Exit 2 survives only where a hook
139
+ // truly decided "deny" (Claude Code: stderr reason; Copilot: translated to the JSON deny below).
140
+ process.on('uncaughtException', (err) => {
141
+ try { process.stderr.write(`[heyspex] hook ${target} crashed (failing open): ${err?.message ?? err}\n`); } catch { /* ignore */ }
142
+ process.exit(0);
143
+ });
144
+ process.on('unhandledRejection', (err) => {
145
+ try { process.stderr.write(`[heyspex] hook ${target} rejected (failing open): ${err?.message ?? err}\n`); } catch { /* ignore */ }
146
+ process.exit(0);
147
+ });
148
+ process.on('exit', (code) => {
149
+ if (code !== 0 && code !== 2) process.exitCode = 0;
150
+ });
151
+
152
+ function readAll(stream, ms) {
153
+ return new Promise((resolve) => {
154
+ let buf = '';
155
+ const t = setTimeout(() => resolve(buf), ms);
156
+ t.unref?.();
157
+ stream.setEncoding('utf8');
158
+ stream.on('data', (c) => { buf += c; });
159
+ stream.on('end', () => { clearTimeout(t); resolve(buf); });
160
+ stream.on('error', () => resolve(buf));
161
+ });
162
+ }
163
+
164
+ // ── Copilot helpers (all best-effort; every failure means "run the hook normally") ─────────────────────────────
165
+ function readJson(file) {
166
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
167
+ }
168
+
169
+ // The command text after the shim: `node -e "<bootstrap>" hook [args]` or `node ".../run-hook.mjs" hook [args]`.
170
+ function rowTokens(command) {
171
+ const c = String(command ?? '');
172
+ const m = c.match(/^node -e "[^"]*"\s+([\s\S]*)$/) || c.match(/\.mjs"?\s+([\s\S]*)$/);
173
+ return m ? m[1].trim().split(/\s+/) : [];
174
+ }
175
+
176
+ /**
177
+ * The matcher(s) registered for this hook + event, read from the project's Claude settings file and the plugin's
178
+ * hooks.json. A hook may sit under several matchers; ALL are returned (an unset matcher is ''). null = the hook
179
+ * is in no row we can read, so no filtering.
180
+ */
181
+ function matchersForHook(eventName, payload) {
182
+ const files = [];
183
+ for (const r of projectRoots(payload)) files.push(path.join(r, '.claude', 'settings.json'));
184
+ for (const r of [process.env.CLAUDE_PLUGIN_ROOT, process.env.COPILOT_PLUGIN_ROOT, process.env.PLUGIN_ROOT]) {
185
+ if (r && r.trim()) files.push(path.join(r.trim(), 'hooks', 'hooks.json'));
186
+ }
187
+ files.push(path.join(REPO_ROOT, 'plugins', 'heyspex', 'hooks', 'hooks.json'));
188
+ const want = [target, ...rest].join(' ');
189
+ const matchers = [];
190
+ let found = false;
191
+ for (const file of [...new Set(files)]) {
192
+ const groups = readJson(file)?.hooks?.[eventName];
193
+ if (!Array.isArray(groups)) continue;
194
+ for (const g of groups) {
195
+ for (const h of g?.hooks ?? []) {
196
+ const t = rowTokens(h?.command);
197
+ if (t.length && t.join(' ') === want) { found = true; matchers.push(typeof g.matcher === 'string' ? g.matcher : ''); }
198
+ }
199
+ }
200
+ }
201
+ return found ? matchers : null;
202
+ }
203
+
204
+ const DEDUPE_WINDOW_MS = 2000;
205
+
206
+ /**
207
+ * Double-firing guard. The first run of a key atomically creates a stamp (flag 'wx'); a second run inside 2 s is
208
+ * a duplicate. The first run stores its final stdout in the stamp so a duplicate replays a DENY instead of
209
+ * silently allowing. Keyed by (session, event, hook+args, tool, timestamp | input hash, project).
210
+ */
211
+ function claimDedupe(payload, eventName) {
212
+ const none = { duplicate: false };
213
+ try {
214
+ if (String(process.env.HEYSPEX_COPILOT_DEDUPE_OFF ?? '').trim() === '1') return none;
215
+ const session = payload.session_id ?? payload.sessionId;
216
+ if (typeof session !== 'string' || !session) return none;
217
+ const ident = payload.timestamp !== undefined
218
+ ? String(payload.timestamp)
219
+ : crypto.createHash('sha1').update(JSON.stringify([payload.tool_input ?? null, payload.prompt ?? null])).digest('hex');
220
+ const key = crypto.createHash('sha1')
221
+ .update(JSON.stringify([session, eventName, target, rest, payload.tool_name ?? '', ident, payload.cwd ?? '']))
222
+ .digest('hex');
223
+ const dir = process.env.HEYSPEX_COPILOT_DEDUPE_DIR?.trim() || path.join(os.tmpdir(), 'heyspex-copilot-dedupe');
224
+ fs.mkdirSync(dir, { recursive: true });
225
+ const file = path.join(dir, `${key}.json`);
226
+ const now = Date.now();
227
+ try {
228
+ fs.writeFileSync(file, JSON.stringify({ t: now, out: '' }), { flag: 'wx' });
229
+ } catch (e) {
230
+ if (e?.code !== 'EEXIST') return none;
231
+ const prev = readJson(file);
232
+ if (prev && typeof prev.t === 'number' && now - prev.t >= 0 && now - prev.t < DEDUPE_WINDOW_MS) {
233
+ return { duplicate: true, replay: typeof prev.out === 'string' ? prev.out : '' };
234
+ }
235
+ fs.writeFileSync(file, JSON.stringify({ t: now, out: '' })); // stale stamp: this run owns it
236
+ }
237
+ try { // opportunistic cleanup of old stamps
238
+ for (const f of fs.readdirSync(dir)) {
239
+ const fp = path.join(dir, f);
240
+ if (now - fs.statSync(fp).mtimeMs > 60000) fs.rmSync(fp, { force: true });
241
+ }
242
+ } catch { /* ignore */ }
243
+ return { duplicate: false, store: (final) => { if (final) fs.writeFileSync(file, JSON.stringify({ t: now, out: final })); } };
244
+ } catch {
245
+ return none;
246
+ }
247
+ }
248
+
249
+ /** True exactly once per (project, session): creates <project>/<state dir>/copilot-style-<session>.stamp. */
250
+ function claimSessionStyleStamp(payload) {
251
+ try {
252
+ const session = String(payload.session_id ?? payload.sessionId ?? '').replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 80);
253
+ if (!session) return false;
254
+ const root = projectRoots(payload).find((r) => { try { return fs.statSync(r).isDirectory(); } catch { return false; } });
255
+ if (!root) return false;
256
+ const dir = path.join(root, STATE_DIR);
257
+ fs.mkdirSync(dir, { recursive: true });
258
+ try { fs.writeFileSync(path.join(dir, `copilot-style-${session}.stamp`), new Date().toISOString(), { flag: 'wx' }); } catch { return false; }
259
+ try { // drop stamps older than a week
260
+ for (const f of fs.readdirSync(dir)) {
261
+ if (f.startsWith('copilot-style-') && f.endsWith('.stamp') && Date.now() - fs.statSync(path.join(dir, f)).mtimeMs > 7 * 86400000) fs.rmSync(path.join(dir, f), { force: true });
262
+ }
263
+ } catch { /* ignore */ }
264
+ return true;
265
+ } catch {
266
+ return false;
267
+ }
268
+ }
269
+
270
+ async function main() {
271
+ // stdin is read ONCE here (harness detection and the mode file's project root both need the payload) and
272
+ // handed on to the hook as a fresh stream, so every hook still reads process.stdin exactly as before.
273
+ let text = '';
274
+ try {
275
+ if (!process.stdin.isTTY) text = await readAll(process.stdin, 4000);
276
+ } catch { /* ignore */ }
277
+ let payload = null;
278
+ try { payload = text.trim() ? JSON.parse(text) : null; } catch { payload = null; }
279
+
280
+ if (!isAlwaysOn(target) && modeOffFromProject(payload)) process.exit(0);
281
+
282
+ // ── harness detection + the Copilot dialect ──────────────────────────────
283
+ let harness = 'unknown';
284
+ let copilot = null;
285
+ try {
286
+ const { detectHarness } = await import(pathToFileURL(path.join(REPO_ROOT, 'spexharness', 'src', 'adapters', 'detect-harness.mjs')).href);
287
+ harness = detectHarness(payload, process.env);
288
+ } catch { /* no adapter in this install: behave exactly as Claude Code */ }
289
+
290
+ if (harness === 'copilot') {
291
+ try {
292
+ copilot = await import(pathToFileURL(path.join(REPO_ROOT, 'spexharness', 'src', 'adapters', 'copilot.mjs')).href);
293
+ } catch {
294
+ // Copilot denies on exit 2 and on any crash, and without the adapter we cannot speak its dialect: fail OPEN.
295
+ process.stderr.write(`[heyspex] hook ${target}: Copilot adapter unavailable (failing open)\n`);
296
+ process.exit(0);
297
+ }
298
+ }
299
+ if (copilot) await copilotBranch(copilot);
300
+
301
+ // ── the Copilot branch ([[HS-20SW-DRSM-DIAG-heyspex-capture-on-idea-and-workflow]]) ──────────────────────
302
+ // Everything below runs ONLY when the harness is GitHub Copilot. Guarantees for Copilot, whatever fails:
303
+ // * the process exits 0 (Copilot turns ANY non-zero exit into a deny, and 2 loses the reason);
304
+ // * stdout is empty (allow) or ONE valid JSON object (a gate's own decision) - never partial / malformed text;
305
+ // * a crash, a throwing hook, an unhandled rejection or a bad import is an ALLOW (the _run.mjs handlers above
306
+ // exit 0; the exit hook below translates the buffered output).
307
+ // Behaviors, in order:
308
+ // 1. opt-out: HEYSPEX_COPILOT_HOOKS_OFF=1 / config agent_behaviors.harness_hooks.copilot.enabled: false
309
+ // silences every hook except the ALWAYS_ON security gates.
310
+ // 2. normalize the payload (camelCase or PascalCase, MCP tool-name spellings -> mcp__heyspex__*).
311
+ // 3. script-side tool filter: the VS Code Local harness ignores matchers and fires every hook on every tool,
312
+ // so the matcher of the registered row (.claude/settings.json or the plugin hooks.json) is re-applied here.
313
+ // 4. double-firing guard: Copilot CLI loads hooks from the plugin AND .claude/settings.json; a duplicate
314
+ // (same session, event, hook, tool, timestamp) inside 2 s exits 0 (replaying a stored decision, if any).
315
+ // 5. prompt-time context: Copilot DROPS UserPromptSubmit output, so a context-only result prints nothing
316
+ // there. SessionStart accepts additionalContext, so ONCE PER SESSION (stamp file under the project's
317
+ // state dir) the SessionStart run also emits inject-communication-style's static text as additionalContext.
318
+ async function copilotBranch(copilot) {
319
+ if (!isAlwaysOn(target)) {
320
+ let off = String(process.env.HEYSPEX_COPILOT_HOOKS_OFF ?? '').trim() === '1';
321
+ if (!off) {
322
+ try {
323
+ const { isConfigKeyEnabled } = await import(pathToFileURL(path.join(HOOK_DIR, '_guardrail-loader.mjs')).href);
324
+ off = !isConfigKeyEnabled('agent_behaviors.harness_hooks.copilot.enabled');
325
+ } catch { /* config unreadable: stay on */ }
326
+ }
327
+ if (off) process.exit(0);
328
+ }
329
+ payload = copilot.normalizeCopilotPayload(payload);
330
+ text = JSON.stringify(payload);
331
+ const eventName = typeof payload.hook_event_name === 'string' ? payload.hook_event_name : '';
332
+
333
+ // (3) script-side tool filter
334
+ let skip = false;
335
+ try {
336
+ if (eventName && typeof payload.tool_name === 'string') {
337
+ const matchers = matchersForHook(eventName, payload);
338
+ skip = !!matchers && !copilot.toolMatchesAnyMatcher(matchers, copilot.toolNameCandidates(payload));
339
+ }
340
+ } catch { /* lookup failure: run the hook */ }
341
+ if (skip) process.exit(0);
342
+
343
+ // (4) double-firing guard
344
+ const dedupe = claimDedupe(payload, eventName);
345
+ if (dedupe.duplicate) {
346
+ if (dedupe.replay) { try { fs.writeSync(1, dedupe.replay); } catch { /* ignore */ } }
347
+ process.exit(0);
348
+ }
349
+
350
+ // (5) once-per-session static advisory at SessionStart
351
+ let extraContext = '';
352
+ if (eventName === 'SessionStart' && claimSessionStyleStamp(payload)) {
353
+ try {
354
+ const mod = await import(pathToFileURL(path.join(HOOK_DIR, 'inject-communication-style.mjs')).href);
355
+ extraContext = typeof mod.copilotSessionStartText === 'function' ? await mod.copilotSessionStartText() : '';
356
+ } catch { /* advisory only */ }
357
+ }
358
+
359
+ // Copilot reads ONE top-level {permissionDecision, permissionDecisionReason}; buffer stdout/stderr and
360
+ // translate at exit: Claude-dialect JSON -> top-level, and a legacy `exit 2 + stderr` deny -> JSON deny, exit 0.
361
+ let out = '';
362
+ let err = '';
363
+ const realStderr = process.stderr.write.bind(process.stderr);
364
+ process.stdout.write = (chunk, enc, cb) => { out += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'); if (typeof enc === 'function') enc(); else if (typeof cb === 'function') cb(); return true; };
365
+ process.stderr.write = (chunk, enc, cb) => { err += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'); if (typeof enc === 'function') enc(); else if (typeof cb === 'function') cb(); return true; };
366
+ process.on('exit', (code) => {
367
+ process.exitCode = 0; // Copilot: never non-zero, whatever happened
368
+ let final = '';
369
+ try {
370
+ final = out ? copilot.finalizeCopilotStdout(out, eventName) : '';
371
+ if (code === 2 && !final) final = copilot.denyFromLegacyExit(err);
372
+ else if (err && code !== 2) realStderr(err);
373
+ if (extraContext) final = copilot.mergeAdditionalContext(final, extraContext);
374
+ } catch { final = ''; }
375
+ try { dedupe.store?.(final); } catch { /* best effort */ }
376
+ try { if (final) fs.writeSync(1, final); } catch { /* never let the translation break the hook */ }
377
+ });
378
+ }
379
+
380
+ // hand the (already read) payload on as a fresh stdin
381
+ if (!process.stdin.isTTY) {
382
+ try {
383
+ const { Readable } = await import('node:stream');
384
+ const fresh = new Readable({ read() {} });
385
+ if (text) fresh.push(Buffer.from(text, 'utf8'));
386
+ fresh.push(null);
387
+ Object.defineProperty(process, 'stdin', { value: fresh, configurable: true });
388
+ } catch { /* hooks will see an empty stdin and fail open themselves */ }
389
+ }
390
+
391
+ const hookPath = resolveHook(target);
392
+ if (!hookPath || !fs.existsSync(hookPath)) {
393
+ process.stderr.write(`[heyspex] _run.mjs: hook not found: ${target}\n`);
394
+ process.exit(0);
395
+ }
396
+
397
+ // Make the hook believe it was invoked directly.
398
+ process.argv = [process.argv[0], hookPath, ...rest];
399
+
400
+ try {
401
+ await import(pathToFileURL(hookPath).href);
402
+ } catch (err) {
403
+ process.stderr.write(`[heyspex] hook ${target} failed: ${err?.message ?? err}\n`);
404
+ process.exit(0);
405
+ }
406
+ }
407
+
408
+ try {
409
+ await main();
410
+ } catch (err) {
411
+ process.stderr.write(`[heyspex] _run.mjs failed (failing open): ${err?.message ?? err}\n`);
412
+ process.exit(0);
413
+ }
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ // Shared Stop-hook input helpers: read the hook's stdin JSON payload and pull the
3
+ // agent's own last message out of the transcript JSONL. Split out of
4
+ // naughty-list-gate.mjs (its original sole owner) so a second Stop-hook scanner
5
+ // (id-display-format-gate.mjs) doesn't fork a copy of the same 40 lines.
6
+ //
7
+ // DEPENDENCY-FREE, same contract as _guardrail-loader.mjs: every function degrades
8
+ // to null/'' on any read/parse failure rather than throwing, so a caller's own
9
+ // fail-safe wrapper never has to guard against this module misbehaving.
10
+
11
+ import fs from 'node:fs';
12
+
13
+ /** Read and parse the hook's stdin JSON payload. Resolves to null on any failure. */
14
+ export async function readStdinJson() {
15
+ if (process.stdin.isTTY) return null;
16
+ return new Promise((resolve) => {
17
+ let buf = '';
18
+ let settled = false;
19
+ const finish = (value) => {
20
+ if (settled) return;
21
+ settled = true;
22
+ resolve(value);
23
+ };
24
+ const timer = setTimeout(() => finish(buf || ''), 200);
25
+ process.stdin.setEncoding('utf8');
26
+ process.stdin.on('data', (chunk) => { buf += chunk; });
27
+ process.stdin.on('end', () => { clearTimeout(timer); finish(buf); });
28
+ process.stdin.on('error', () => { clearTimeout(timer); finish(buf); });
29
+ });
30
+ }
31
+
32
+ /** Read the last assistant message's text out of a transcript JSONL file. */
33
+ export function lastAssistantText(transcriptPath) {
34
+ let raw;
35
+ try {
36
+ raw = fs.readFileSync(transcriptPath, 'utf8');
37
+ } catch {
38
+ return null;
39
+ }
40
+ const lines = raw.split(/\r?\n/).filter(Boolean);
41
+ for (let i = lines.length - 1; i >= 0; i--) {
42
+ let evt;
43
+ try { evt = JSON.parse(lines[i]); } catch { continue; }
44
+ const msg = evt?.message;
45
+ const role = msg?.role ?? evt?.role;
46
+ if (role !== 'assistant') continue;
47
+ const content = msg?.content;
48
+ if (typeof content === 'string') return content;
49
+ if (Array.isArray(content)) {
50
+ const text = content
51
+ .filter((b) => b && b.type === 'text' && typeof b.text === 'string')
52
+ .map((b) => b.text)
53
+ .join('\n')
54
+ .trim();
55
+ return text || null;
56
+ }
57
+ return null;
58
+ }
59
+ return null;
60
+ }