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,265 @@
1
+ // spexharness/src/gates/preflight-reminder-gate.mjs
2
+ //
3
+ // Engine port of scripts/claude-hooks/preflight-reminder.mjs's UserPromptSubmit half — the
4
+ // registry-driven filing-intent prime ([[HS-EPIC-ZGWR]] AC#6: "file a milestone" / "create
5
+ // an epic" → the spec-creation-via-minter rule's inject) and the grounding-before-proposing
6
+ // nudge ([[HS-RES-2QX6]] / [[HS-EPIC-PLRV]]: a design/planning request → "run /hs-ground
7
+ // first"). Task [[HS-N01T-8CMW-TASK-prompt-submit-injects-reach-the-model]], epic
8
+ // [[HS-LS51-8CMW-EPIC-spexcode-plugin-parity-the-prompt]]. See ./PORTING.md for the recipe.
9
+ //
10
+ // The hook's PreToolUse half (protocol-edit, CLAUDE.md-edit and destructive-command
11
+ // reminders) is NOT ported here — it is a `before_tool` concern with its own shape, and
12
+ // this task's scope is the prompt_submit event only.
13
+ //
14
+ // PORTING.md step 1 says "import the pure decision function; never a second copy". That
15
+ // rule bends here for a documented reason: the hook's `detectRegistryPrimeReminder` was
16
+ // written against scripts/claude-hooks/_guardrail-loader.mjs (its file-scanning config
17
+ // helpers and stderr-writing loader), and the engine must not import upward into the
18
+ // plugin's scripts. So the decision function LIVES HERE, against ../rule-table.mjs and the
19
+ // injected config reader ../intent-directive.mjs provides, and the hook script imports it
20
+ // from this module — one implementation, the plugin as the thin caller, which is the same
21
+ // relationship every other ported gate ends up in. The hook's own smoke keeps exercising it
22
+ // through the hook's re-export.
23
+ //
24
+ // Event mapping: UserPromptSubmit → `prompt_submit` (../events.mjs).
25
+ //
26
+ // Decision mapping: the hook's only outcomes are "say nothing" and "emit additionalContext",
27
+ // so: `allow` and `inject` — a PRIME injection, never a deny, never a rewrite.
28
+ //
29
+ // NOT GATED ON CLAUDE_AGENT. The hook exits silently unless `CLAUDE_AGENT=true` (so a human
30
+ // typing in an interactive Claude Code session is not nudged) and keeps that gate in its
31
+ // own `main()`. A SpexCode seat IS the agent, so the engine gate runs unconditionally; the
32
+ // two kill switches port unchanged through `ctx.env`:
33
+ // HEYSPEX_PREFLIGHT_REMINDER_OFF=1 — the whole gate is silent.
34
+ // HEYSPEX_GROUNDING_NUDGE_OFF=1 — the grounding nudge alone is silent.
35
+ //
36
+ // OWNED ELSEWHERE — the one deliberate difference from the hook's sweep. The hook excludes
37
+ // only `research-before-decision` from the userpromptsubmit `prime` rules it will fire,
38
+ // because on the plugin every OTHER intent rule is ALSO fired by the (CLAUDE_AGENT-gated)
39
+ // hook — a latent double-injection that never shows because the gate is unset
40
+ // interactively. Ungated in the engine that would fire capture-on-idea's raw inject (with
41
+ // a literal `{{CONFIRM_MODE}}`) next to ../intent-classifier.mjs's finished directive, and
42
+ // would fire `communication-style-prime` (an unconditional `[\s\S]` rule, 3.7 KB) on every
43
+ // prompt. So the ENGINE default excludes every rule ../intent-classifier.mjs owns on this
44
+ // same event plus communication-style-prime (its own always-on injection is a separate
45
+ // concern — see intent-classifier.mjs's CLASSIFIER_RULE_IDS note). The hook passes its own
46
+ // narrower set explicitly (`PRIME_RULES_OWNED_ELSEWHERE`), so its emitted bytes are
47
+ // unchanged.
48
+ //
49
+ // `ctx` contract (all optional; every field defaults to a real read):
50
+ // ctx.repoRoot / ctx.vaultRoot - the app root holding `.heyspex/guardrails.json`. Defaults
51
+ // to the event's `cwd`, then CLAUDE_PROJECT_DIR, then this repo's root.
52
+ // When given it is a self-contained fixture root and each rule's
53
+ // config toggle is read from it too.
54
+ // ctx.configRoot - the directory holding `.heyspex/config.yaml` (the rules' configKey
55
+ // toggles); wins over ctx.repoRoot/ctx.vaultRoot for that read. With
56
+ // none of the three, the VAULT root (../config-root.mjs
57
+ // `defaultConfigRoot()`) — never the event's cwd or CLAUDE_PROJECT_DIR,
58
+ // which under Claude Code is the app checkout, not where the settings
59
+ // file lives ([[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]]).
60
+ // ctx.ruleTable - an injected ../rule-table.mjs table (tests).
61
+ // ctx.getSetting - an injected settings reader (see ../intent-directive.mjs's header).
62
+ // ctx.env - env var overrides (tests use this instead of mutating process.env).
63
+ // ctx.ownedElsewhere - override the excluded rule-id set (the hook passes its own).
64
+
65
+ import path from 'node:path';
66
+ import { fileURLToPath } from 'node:url';
67
+
68
+ import { loadRuleTable } from '../rule-table.mjs';
69
+ import { CLASSIFIER_RULE_IDS } from '../intent-classifier.mjs';
70
+ import { containsInjectedContent, isRuleEnabled } from '../intent-directive.mjs';
71
+ import { defaultConfigRoot } from '../config-root.mjs';
72
+
73
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
74
+ const REPO_ROOT = path.resolve(HERE, '..', '..', '..');
75
+
76
+ /** This gate's id — unique across the registry. */
77
+ export const GATE_ID = 'preflight-reminder';
78
+ /** Runs at prompt submit, same as its UserPromptSubmit hook registration. */
79
+ export const EVENT = 'prompt_submit';
80
+ /** Caller-defined severity label; a prime injection, never a block. */
81
+ export const SEVERITY = 'prime';
82
+
83
+ /** The whole-gate kill switch (ported unchanged from the hook). */
84
+ export const OFF_ENV = 'HEYSPEX_PREFLIGHT_REMINDER_OFF';
85
+ /** The grounding-nudge-only kill switch (ported unchanged from the hook). */
86
+ export const GROUNDING_OFF_ENV = 'HEYSPEX_GROUNDING_NUDGE_OFF';
87
+
88
+ /** The registry surface the filing-intent prime rules are authored on. */
89
+ export const USERPROMPT_PRIME_SURFACE = 'userpromptsubmit';
90
+
91
+ /**
92
+ * The hook's OWN exclusion set: research-before-decision has its own dedicated dispatch
93
+ * (the unified intent-classifier-trigger.mjs router) that does extra workflow
94
+ * orchestration — exclude it so it never double-injects. The hook passes this set.
95
+ */
96
+ export const PRIME_RULES_OWNED_ELSEWHERE = new Set(['research-before-decision']);
97
+
98
+ /**
99
+ * The ENGINE's exclusion set (the default here): the hook's set, plus every intent rule
100
+ * ../intent-classifier.mjs fires on this same event, plus the always-on
101
+ * communication-style prime. See the header for why.
102
+ */
103
+ export const ENGINE_PRIME_RULES_OWNED_ELSEWHERE = new Set([
104
+ ...PRIME_RULES_OWNED_ELSEWHERE,
105
+ ...CLASSIFIER_RULE_IDS,
106
+ 'communication-style-prime',
107
+ ]);
108
+
109
+ // Grounding-before-proposing nudge ([[HS-RES-2QX6]] / [[HS-EPIC-PLRV]]).
110
+ // Deterministic context injection — the research's PRIMARY lever (F1): surface
111
+ // prior decisions at the moment a design/planning request arrives, BEFORE the
112
+ // agent proposes an approach or runs an AskUserQuestion menu around its own
113
+ // abstraction (the HS-CAP-UIBF "three modes" failure). This is injected context,
114
+ // not a block; it only points the agent at /hs-ground (which now searches
115
+ // captures). Tuned for PRECISION over recall (a noisy nudge is worse than none —
116
+ // F12): fires on imperative design/build/plan requests, NOT on questions about
117
+ // existing work or on filing verbs (covered by the intent reminders above).
118
+ export const DESIGN_INTENT_PATTERNS = [
119
+ // A — design/architecture verb acting directly on a thing (verb + determiner).
120
+ /\b(?:design|architect|re-?design|re-?architect|rework|reimplement|propose|structure|plan|model)\s+(?:a|an|the|our|this|how|out)\b/i,
121
+ // B — build/finish a design-noun, EXCLUDING the filing nouns (epic/task/
122
+ // milestone) the intent reminders already handle.
123
+ /\b(?:build|implement|finish|create|introduce|spec|wire)\b[^.?!\n]{0,40}\b(?:feature|flow|system|capability|workflow|mechanism|surface|component|schema|gate|pipeline|integration|architecture|approach|modes?)\b/i,
124
+ // C — explicit approach-choosing questions.
125
+ /\b(?:how should (?:we|i)|what(?:'s| is) the best way to|which approach|what approach should)\b/i,
126
+ ];
127
+
128
+ export const GROUNDING_REMINDER =
129
+ 'Detected a design/planning request. Before you propose an approach or run an ' +
130
+ 'AskUserQuestion menu to choose between options, run `/hs-ground` (it now ' +
131
+ 'searches captures, epics, and research) to surface prior decisions and ' +
132
+ 'existing vocabulary — the user is NOT the memory of record. If a related ' +
133
+ 'capture/epic exists, reconcile with its terms instead of inventing your own. ' +
134
+ 'Skip only for a trivial/mechanical change with no design choice. ' +
135
+ '(Mute: HEYSPEX_GROUNDING_NUDGE_OFF=1.)';
136
+
137
+ /** Compile a rule's string patterns to case-insensitive RegExps; an uncompilable
138
+ * pattern is skipped (fail-safe), never a throw. */
139
+ function compilePatterns(rule) {
140
+ const patterns = rule && Array.isArray(rule.patterns) ? rule.patterns : [];
141
+ const compiled = [];
142
+ for (const p of patterns) {
143
+ try {
144
+ compiled.push(new RegExp(p, 'i'));
145
+ } catch {
146
+ // corrupt pattern — skip it.
147
+ }
148
+ }
149
+ return compiled;
150
+ }
151
+
152
+ /**
153
+ * Registry-driven filing-intent prime ([[HS-EPIC-ZGWR]] AC#6). Reads the userpromptsubmit
154
+ * `prime` rules from the engine rule table, matches the prompt against each rule's
155
+ * authored patterns, and returns the matched rules' inject text joined by a blank line.
156
+ * Honors each rule's config_key gate + ignore_injected_content, and skips rules owned by
157
+ * another gate/hook. Fail-safe: any error → null (no reminder, never throws).
158
+ *
159
+ * @param {string} prompt
160
+ * @param {string} [repoRoot] - where `.heyspex/guardrails.json` (the rule table) is read from.
161
+ * @param {{ ruleTable?: object, getSetting?: Function, ownedElsewhere?: Set<string>|string[],
162
+ * configRoot?: string }} [opts] - `configRoot` is where `.heyspex/config.yaml` (each rule's
163
+ * toggle) is read from; defaults to `repoRoot` so a self-contained fixture root — and the
164
+ * hook-parity call that passes neither — keeps working as one unit. `run()` passes the
165
+ * vault root explicitly.
166
+ * @returns {string|null}
167
+ */
168
+ export function detectRegistryPrimeReminder(prompt, repoRoot = REPO_ROOT, opts = {}) {
169
+ if (typeof prompt !== 'string' || !prompt.trim()) return null;
170
+ const configRoot =
171
+ typeof opts.configRoot === 'string' && opts.configRoot.trim() ? opts.configRoot : repoRoot;
172
+ const owned = opts.ownedElsewhere instanceof Set
173
+ ? opts.ownedElsewhere
174
+ : new Set(Array.isArray(opts.ownedElsewhere) ? opts.ownedElsewhere : ENGINE_PRIME_RULES_OWNED_ELSEWHERE);
175
+ let rules;
176
+ try {
177
+ const table = opts.ruleTable ?? loadRuleTable({ vaultRoot: repoRoot });
178
+ rules = (Array.isArray(table?.rules) ? table.rules : []).filter(
179
+ (r) => r && Array.isArray(r.surfaces) && r.surfaces.includes(USERPROMPT_PRIME_SURFACE) && r.severity === 'prime',
180
+ );
181
+ } catch {
182
+ return null;
183
+ }
184
+ const parts = [];
185
+ for (const rule of rules) {
186
+ if (!rule.inject || owned.has(rule.rule)) continue;
187
+ if (!isRuleEnabled(rule, configRoot, opts)) continue;
188
+ if (rule.ignoreInjectedContent && containsInjectedContent(prompt)) continue;
189
+ const patterns = compilePatterns(rule);
190
+ if (patterns.length && patterns.some((re) => re.test(prompt))) {
191
+ parts.push(rule.inject);
192
+ }
193
+ }
194
+ return parts.length ? parts.join('\n\n') : null;
195
+ }
196
+
197
+ /** The grounding nudge: GROUNDING_REMINDER on a design/planning prompt, else null. */
198
+ export function detectDesignIntent(prompt) {
199
+ if (typeof prompt !== 'string' || !prompt.trim()) return null;
200
+ return DESIGN_INTENT_PATTERNS.some((re) => re.test(prompt)) ? GROUNDING_REMINDER : null;
201
+ }
202
+
203
+ function resolveRoot(evt, ctx) {
204
+ for (const candidate of [ctx.repoRoot, ctx.vaultRoot, evt?.cwd]) {
205
+ if (typeof candidate === 'string' && candidate.trim()) return candidate.trim().replace(/[\\/]+$/, '');
206
+ }
207
+ const env = ctx.env ?? process.env;
208
+ const fromEnv = env.CLAUDE_PROJECT_DIR?.trim();
209
+ if (fromEnv) return fromEnv.replace(/[\\/]+$/, '');
210
+ return REPO_ROOT;
211
+ }
212
+
213
+ /**
214
+ * @param {{ point: string, prompt?: string, cwd?: string }} evt
215
+ * @param {{ repoRoot?: string, vaultRoot?: string, configRoot?: string, ruleTable?: object,
216
+ * getSetting?: Function, env?: Record<string, string | undefined>,
217
+ * ownedElsewhere?: Set<string>|string[] }} [ctx]
218
+ * @returns {{ decision: 'allow' }|{ decision: 'inject', reason: string,
219
+ * inject: { additionalContext: string, severity: string } }}
220
+ */
221
+ export function run(evt, ctx = {}) {
222
+ const env = ctx.env ?? process.env;
223
+ if (String(env[OFF_ENV] ?? '') === '1') return { decision: 'allow' };
224
+
225
+ const prompt = evt?.prompt;
226
+ if (typeof prompt !== 'string' || !prompt.trim()) return { decision: 'allow' };
227
+
228
+ const root = resolveRoot(evt, ctx);
229
+ // Only a ctx-supplied root is a fixture; the event's cwd / CLAUDE_PROJECT_DIR are the live
230
+ // app checkout and never decide where the settings file is read from.
231
+ const fixtureRoot = [ctx.repoRoot, ctx.vaultRoot].some((c) => typeof c === 'string' && c.trim());
232
+ const configRoot = ctx.configRoot ?? (fixtureRoot ? root : defaultConfigRoot());
233
+
234
+ // Combined into ONE injection — the filing-intent reminder and the grounding nudge are
235
+ // independent and can co-fire, exactly as the hook joins them into one emit.
236
+ const parts = [];
237
+ const reasons = [];
238
+ const intentReminder = detectRegistryPrimeReminder(prompt, root, {
239
+ ruleTable: ctx.ruleTable,
240
+ getSetting: ctx.getSetting,
241
+ ownedElsewhere: ctx.ownedElsewhere,
242
+ configRoot,
243
+ });
244
+ if (intentReminder) {
245
+ parts.push(intentReminder);
246
+ reasons.push('filing intent');
247
+ }
248
+ if (String(env[GROUNDING_OFF_ENV] ?? '') !== '1') {
249
+ const grounding = detectDesignIntent(prompt);
250
+ if (grounding) {
251
+ parts.push(grounding);
252
+ reasons.push('design/planning request');
253
+ }
254
+ }
255
+ if (!parts.length) return { decision: 'allow' };
256
+
257
+ return {
258
+ decision: 'inject',
259
+ reason: `preflight-reminder: ${reasons.join(' + ')}`,
260
+ inject: { additionalContext: parts.join('\n\n'), severity: SEVERITY },
261
+ };
262
+ }
263
+
264
+ const gate = { id: GATE_ID, event: EVENT, severity: SEVERITY, run };
265
+ export default gate;
@@ -0,0 +1,175 @@
1
+ // spexharness/src/gates/research-protocol-gate.mjs
2
+ //
3
+ // [[HS-K0ZP]] — the research-capture-protocol proof-of-use gate.
4
+ //
5
+ // THE HOLE THIS CLOSES: research-capture-protocol was enforced as prose. The
6
+ // only machine on the research path was ./deep-research-cost-gate.mjs, which
7
+ // sees Workflow/Skill launches and nothing else — its own protocol text named
8
+ // the bare WebSearch/WebFetch path as "out of reach". On 2026-09-11 an agent
9
+ // asked to verify one claim ran native WebSearch + WebFetch and then filed a
10
+ // full research doc through heyspex_save_research: no method triage (this was
11
+ // a known-target retrieval that should never have entered the research-doc
12
+ // pipeline), no ground-first, no offer-before-save. Nothing refused any of
13
+ // the three calls because nothing was looking at them.
14
+ //
15
+ // WHAT IT DECIDES: a `before_tool` event for WebSearch, WebFetch or
16
+ // heyspex_save_research (bare or MCP-prefixed) is DENIED unless the session
17
+ // holds a current preflight proof-of-use marker for one of
18
+ // RESEARCH_PROTOCOL_INTENTS (`research`, `file_research`, `promote_research`
19
+ // — vendor/heyspex-mcp/preflight-gate.mjs, the same store the MCP server's
20
+ // own PREFLIGHT_NOT_RUN refusals read). heyspex_preflight({ intent:
21
+ // "research" }) writes that marker; so does heyspex_get_procedure on the
22
+ // protocol itself (procedure-handlers.mjs). Every other tool is allowed —
23
+ // this gate never speaks for anything but those three names.
24
+ //
25
+ // WHY A DENY, NOT A PRIME: the owner's ruling on the incident, verbatim —
26
+ // "HeySpex plugin needs to make it impossible for you to break the rule.
27
+ // User permissions is the only way to break the rule." A prime is text the
28
+ // model can read and skip, which is exactly what failed. So: deny by default,
29
+ // the reason names the one call that unlocks it, and the ONLY bypass is the
30
+ // owner setting HEYSPEX_RESEARCH_PROTOCOL_GATE_OFF=1 in their own environment
31
+ // — an env var, never a file (.heyspex/mode.json and config.yaml are
32
+ // agent-writable; scripts/claude-hooks/_run.mjs lists this gate ALWAYS_ON so
33
+ // the mode file cannot silence it either). The agent has no lever here.
34
+ //
35
+ // WHERE IT RUNS: registered in ./index.mjs's PORTED_GATES (every engine front
36
+ // — SpexCode seats, hs chat, overnight runs) and shelled to Claude Code by
37
+ // scripts/claude-hooks/research-protocol-gate.mjs (generated) on the matcher
38
+ // `WebSearch|WebFetch|mcp__heyspex__heyspex_save_research|
39
+ // mcp__plugin_heyspex_heyspex__heyspex_save_research`, and to Cursor through
40
+ // scripts/cursor-hooks/_run.mjs's --hs-tool matcher.
41
+ //
42
+ // MARKER ROOTS: preflight markers live under the VAULT root
43
+ // (`<vault>/.heyspex/preflight/`). A hook process only knows
44
+ // CLAUDE_PROJECT_DIR (the code checkout), so the gate looks in BOTH the
45
+ // project root and the vault the MCP resolver names (vault-resolve.mjs —
46
+ // the same 5-tier resolver the server used when it wrote the marker). On a
47
+ // single-checkout install the two are one directory.
48
+ //
49
+ // `ctx` contract (all optional):
50
+ // ctx.env - env var overrides (falls back to process.env).
51
+ // ctx.cwd - project root (defaults to CLAUDE_PROJECT_DIR, else
52
+ // this repo's root).
53
+ // ctx.vaultRoot - when given, the ONLY marker root searched (tests).
54
+ // ctx.readMarkers - (root, intent, sessionId) => marker[] override
55
+ // (defaults to preflight-gate.mjs's readPreflightMarkers).
56
+ // ctx.now - clock for the session-less TTL fallback (tests).
57
+
58
+ import path from 'node:path';
59
+ import { fileURLToPath } from 'node:url';
60
+
61
+ import {
62
+ RESEARCH_PROTOCOL_GATE_OFF_ENV,
63
+ RESEARCH_PROTOCOL_INTENTS,
64
+ isMarkerCurrent,
65
+ readPreflightMarkers,
66
+ } from '../../../vendor/heyspex-mcp/preflight-gate.mjs';
67
+ import { getActiveConfig } from '../../../vendor/heyspex-mcp/vault-resolve.mjs';
68
+
69
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
70
+ const REPO_ROOT = path.resolve(HERE, '..', '..', '..');
71
+
72
+ /** The gate's id — unique across the registry; also its Claude Code hook name. */
73
+ export const id = 'research-protocol-gate';
74
+ /** Runs pre-tool: a PreToolUse hook in Claude Code, before_tool in the engine. */
75
+ export const event = 'before_tool';
76
+ /** Denies, so "block". */
77
+ export const severity = 'block';
78
+
79
+ /** The exact env var the deny reason names — exported so tests and docs cannot drift from it. */
80
+ export const OFF_ENV = RESEARCH_PROTOCOL_GATE_OFF_ENV;
81
+
82
+ /** The call the deny reason names as the unlock — one string, quoted everywhere it appears. */
83
+ export const UNLOCK_CALL = 'heyspex_preflight({ intent: "research" })';
84
+
85
+ /**
86
+ * Is this tool one of the three the protocol governs? Bare names are what the
87
+ * engine and Claude Code's native tools use; the two MCP prefixes are the
88
+ * project-scoped and plugin-scoped spellings of the same HeySpex tool.
89
+ *
90
+ * @param {unknown} toolName
91
+ * @returns {boolean}
92
+ */
93
+ export function isResearchTool(toolName) {
94
+ if (typeof toolName !== 'string') return false;
95
+ const name = toolName.trim();
96
+ if (name === 'WebSearch' || name === 'WebFetch') return true;
97
+ return /(^|__)heyspex_save_research$/.test(name);
98
+ }
99
+
100
+ /**
101
+ * The deny reason. Kept as a function of the tool name only, so the golden
102
+ * fixtures pin its exact text and a reworded reason is a deliberate edit here.
103
+ *
104
+ * @param {string} toolName
105
+ * @returns {string}
106
+ */
107
+ export function denyReason(toolName) {
108
+ return (
109
+ `Research blocked: ${toolName} was called before research-capture-protocol was consulted this session. ` +
110
+ `Call ${UNLOCK_CALL} first — it returns the protocol's method triage (known-target-retrieval vs the confidence ladder), ` +
111
+ `the ground-first rule (heyspex_recall_memories + /hs-ground) and the offer-before-save step — read it, then retry this exact call. ` +
112
+ `Reading the protocol directly with heyspex_get_procedure({ name: "research-capture-protocol" }) also counts. ` +
113
+ `This gate is deterministic and is not the agent's to bypass: only the owner can lift it, by setting ${OFF_ENV}=1 in their own environment.`
114
+ );
115
+ }
116
+
117
+ function resolvedVaultRoot() {
118
+ try {
119
+ const { repoRootAbs } = getActiveConfig();
120
+ return typeof repoRootAbs === 'string' && repoRootAbs ? repoRootAbs : null;
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * The marker roots to search, in order, de-duplicated. `ctx.vaultRoot` pins a
128
+ * single root (tests, and any front that already knows its vault); otherwise
129
+ * the project root first, then the resolver's vault.
130
+ */
131
+ function markerRoots(ctx) {
132
+ if (typeof ctx.vaultRoot === 'string' && ctx.vaultRoot) return [ctx.vaultRoot];
133
+ const roots = [ctx.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? REPO_ROOT, resolvedVaultRoot()];
134
+ const seen = new Set();
135
+ return roots.filter((r) => {
136
+ if (typeof r !== 'string' || !r) return false;
137
+ const key = path.resolve(r);
138
+ if (seen.has(key)) return false;
139
+ seen.add(key);
140
+ return true;
141
+ });
142
+ }
143
+
144
+ /**
145
+ * @param {{ point: string, sessionId?: string, toolName?: string, toolInput?: Record<string, unknown> }} evt
146
+ * @param {{ env?: Record<string, string|undefined>, cwd?: string, vaultRoot?: string,
147
+ * readMarkers?: (root: string, intent: string, sessionId?: string) => object[],
148
+ * now?: number }} [ctx]
149
+ * @returns {{ decision: 'allow'|'deny', reason?: string }}
150
+ */
151
+ export function run(evt, ctx = {}) {
152
+ const env = ctx.env ?? process.env;
153
+ if (String(env[OFF_ENV] ?? '') === '1') return { decision: 'allow' };
154
+
155
+ const toolName = evt?.toolName;
156
+ if (!isResearchTool(toolName)) return { decision: 'allow' };
157
+
158
+ const read = ctx.readMarkers ?? readPreflightMarkers;
159
+ const now = ctx.now ?? Date.now();
160
+ const sessionId = evt?.sessionId;
161
+ for (const root of markerRoots(ctx)) {
162
+ for (const intent of RESEARCH_PROTOCOL_INTENTS) {
163
+ let markers = [];
164
+ try {
165
+ markers = read(root, intent, sessionId) ?? [];
166
+ } catch {
167
+ markers = []; // an unreadable root proves nothing either way
168
+ }
169
+ if (markers.some((m) => isMarkerCurrent(m, sessionId, now))) return { decision: 'allow' };
170
+ }
171
+ }
172
+ return { decision: 'deny', reason: denyReason(String(toolName).trim()) };
173
+ }
174
+
175
+ export default { id, event, severity, run };
@@ -0,0 +1,96 @@
1
+ // spexharness/src/gates/settings-write-gate.decide.mjs
2
+ //
3
+ // Pure decision logic relocated from scripts/claude-hooks/settings-write-gate.mjs
4
+ // (HS-IQBF, per PORTING.md's "move the pure decision logic into the gate
5
+ // module itself or a sibling pure module"). The original hook's `decide()`
6
+ // had NO isDirectExecution guard around its own module-level `main()`, which
7
+ // is why ./settings-write-gate.mjs (the gate) used to import it lazily with
8
+ // an env-var dance to dodge a stdin hang on static import (see that gate
9
+ // module's git history / PORTING.md for the story). Relocating the pure
10
+ // function here removes the hazard entirely: this file has no main() at all,
11
+ // so a plain static top-level import is always safe. No stdin/stdout/
12
+ // process.exit anywhere in this file.
13
+
14
+ const CONFIG_FILE_RE = /(?:^|[\\/])\.heyspex[\\/]config\.ya?ml$/i;
15
+
16
+ /** Conservative shell write-target scanner (same approach as env-vault-guard.mjs). */
17
+ function extractShellTargets(command) {
18
+ if (!command || typeof command !== 'string') return [];
19
+ const targets = new Set();
20
+ const redirectRe = /(?:>>?|\|\s*tee(?:\s+-a)?)\s+("([^"]+)"|'([^']+)'|(\S+))/g;
21
+ for (const m of command.matchAll(redirectRe)) targets.add(m[2] ?? m[3] ?? m[4]);
22
+ const cmdRe =
23
+ /\b(?:mv|cp|touch|install|rename|tee|rm|del|Remove-Item|Move-Item|Copy-Item|New-Item|Set-Content|Out-File|Add-Content|Clear-Content|sed)\s+([^|;&\n]+)/g;
24
+ for (const m of command.matchAll(cmdRe)) {
25
+ const head = m[0].split(/\s/)[0];
26
+ const args = m[1]
27
+ .trim()
28
+ .split(/\s+/)
29
+ .filter((a) => a && !a.startsWith('-'));
30
+ if (args.length === 0) continue;
31
+ if (/^(?:mv|cp|install|rename|Move-Item|Copy-Item)$/.test(head)) {
32
+ targets.add(args[args.length - 1]);
33
+ } else {
34
+ for (const a of args) targets.add(a);
35
+ }
36
+ }
37
+ return [...targets]
38
+ .map((t) => t.replace(/^['"]|['"]$/g, ''))
39
+ .filter(Boolean)
40
+ .filter((t) => !/[$`(){}]/.test(t));
41
+ }
42
+
43
+ /**
44
+ * Pure gate decision. Returns null (pass through) or
45
+ * { ask: true, reason } — the harness then asks the USER for permission.
46
+ */
47
+ export function decide(toolName, toolInput) {
48
+ // The MCP settings-write tool: gate ONLY the applying call (confirm_token given).
49
+ if (toolName === 'mcp__heyspex__heyspex_settings_set') {
50
+ const token = toolInput?.confirm_token;
51
+ if (typeof token === 'string' && token.trim()) {
52
+ return {
53
+ ask: true,
54
+ reason:
55
+ 'Applying a HeySpex settings change (heyspex_settings_set with confirm_token). Settings writes ' +
56
+ 'always require explicit user permission (agent-settings-protocol) — approve only if you asked for ' +
57
+ 'this exact change.',
58
+ };
59
+ }
60
+ return null; // request step writes nothing
61
+ }
62
+
63
+ // Direct file writes to .heyspex/config.yaml.
64
+ if (toolName === 'Write' || toolName === 'Edit' || toolName === 'NotebookEdit') {
65
+ const filePath = String(toolInput?.file_path ?? toolInput?.notebook_path ?? '');
66
+ if (filePath && CONFIG_FILE_RE.test(filePath)) {
67
+ return {
68
+ ask: true,
69
+ reason:
70
+ `.heyspex/config.yaml is the settings control surface — agent writes require explicit user ` +
71
+ `permission (agent-settings-protocol), regardless of ask_forgiveness_not_permission. Approve only ` +
72
+ `if you asked for this change; the sanctioned agent path is heyspex_settings_set (two-step confirm) ` +
73
+ `or \`hs settings set ... --confirmed-by-user\`.`,
74
+ };
75
+ }
76
+ return null;
77
+ }
78
+
79
+ // Shell writes targeting the config file.
80
+ if (toolName === 'Bash' || toolName === 'PowerShell') {
81
+ for (const target of extractShellTargets(String(toolInput?.command ?? ''))) {
82
+ if (CONFIG_FILE_RE.test(target)) {
83
+ return {
84
+ ask: true,
85
+ reason:
86
+ 'This shell command writes to .heyspex/config.yaml — the settings control surface. Settings ' +
87
+ 'writes require explicit user permission (agent-settings-protocol). Approve only if you asked ' +
88
+ 'for this change; prefer `hs settings set` or heyspex_settings_set.',
89
+ };
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+
95
+ return null;
96
+ }
@@ -0,0 +1,104 @@
1
+ // spexharness/src/gates/settings-write-gate.mjs
2
+ //
3
+ // Engine port of scripts/claude-hooks/settings-write-gate.mjs — the
4
+ // PreToolUse gate that asks the user's explicit permission before any agent
5
+ // path writes .heyspex/config.yaml (the settings control surface;
6
+ // agent-settings-protocol). See ./PORTING.md for the recipe this follows and
7
+ // ./env-vault-guard.mjs for the reference port.
8
+ //
9
+ // This module does NOT reimplement the guard's decision logic. It imports
10
+ // `decide(toolName, toolInput)` from ./settings-write-gate.decide.mjs — the
11
+ // pure function relocated out of the original hook script (HS-IQBF; see
12
+ // PORTING.md and that sibling module's own header for why) — and adapts it
13
+ // to the engine's `{ id, event, severity, run(event, ctx) }` gate shape:
14
+ // - `event` (spexharness/src/events.mjs `before_tool`) → the legacy
15
+ // `(toolName, toolInput)` args `decide()` already expects.
16
+ // - the legacy `null | { ask: true, reason }` verdict → the engine's
17
+ // `{ decision: 'allow' | 'rewrite', reason?, rewrite? }` shape.
18
+ //
19
+ // The relocation also retires an old IMPORT HAZARD: the ORIGINAL hook
20
+ // script's `main()` used to run unconditionally at module-evaluation time
21
+ // (no isDirectExecution guard), so a plain static `import` of it hung any
22
+ // process that imported it with an open, not-yet-closed stdin. This module
23
+ // used to work around that with a lazily-imported, env-var-gated dynamic
24
+ // import. Now that the pure `decide()` lives in a sibling module with no
25
+ // main() of its own, that hazard no longer exists — a plain static
26
+ // top-level import is always safe, same as every other gate in this set.
27
+ //
28
+ // Decision mapping (a documented judgment call, the same shape as
29
+ // env-vault-guard.mjs's "ask" → rewrite mapping): the legacy verdict is
30
+ // either `null` (no opinion → allow) or `{ ask: true, reason }` (the harness
31
+ // should show the user a synchronous permission prompt). The engine's
32
+ // decide.mjs has no synchronous-approval primitive, so `ask` is carried
33
+ // forward as a `rewrite` envelope tagging `ask: true` — never silently
34
+ // downgraded to an auto-allow (unsafe: skips the required permission
35
+ // prompt) or upgraded to an auto-deny (a real behavior change; the original
36
+ // never hard-denies a settings write, it only asks). A calling front decides
37
+ // what `rewrite.ask` means for it.
38
+ //
39
+ // This gate never hard-denies (matching the original) — "deny" coverage for
40
+ // AC3 ("allow, deny and rewrite each covered by at least one test per gate",
41
+ // checked as one combined `node --test` run across all three ported gates in
42
+ // this task) comes from the sibling enforce-session-sequence gate, which is
43
+ // the only one of the three that can actually block a turn. See
44
+ // ./enforce-session-sequence.test.mjs.
45
+ //
46
+ // Env hatch (unchanged): HEYSPEX_SETTINGS_GATE_OFF=1.
47
+ // Config toggle (unchanged, full parity with the original's own opt-out, in
48
+ // addition to the env hatch): agent_behaviors.settings_write_guard.enabled
49
+ // (default ON; an explicit `false` in .heyspex/config.yaml silences the
50
+ // gate) — read via the same dependency-free `_guardrail-loader.mjs` the
51
+ // original imports.
52
+ //
53
+ // `ctx` contract (all optional):
54
+ // ctx.cwd - a self-contained fixture root: when given, the config-toggle
55
+ // read resolves from it (tests build a temp root carrying its
56
+ // own .heyspex/config.yaml and pass it here).
57
+ // ctx.configRoot - the directory holding `.heyspex/config.yaml`; wins over
58
+ // ctx.cwd for the toggle read. With neither, the VAULT root
59
+ // (../config-root.mjs `defaultConfigRoot()`) — never
60
+ // CLAUDE_PROJECT_DIR, which under Claude Code is the app
61
+ // checkout, not where the settings file lives
62
+ // ([[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]]).
63
+ // ctx.env - env var overrides (tests use this instead of mutating
64
+ // process.env).
65
+
66
+ import { isConfigKeyEnabled } from '../../../scripts/claude-hooks/_guardrail-loader.mjs';
67
+ import { defaultConfigRoot } from '../config-root.mjs';
68
+ import { decide as legacyDecide } from './settings-write-gate.decide.mjs';
69
+
70
+ const TOGGLE_KEY = 'agent_behaviors.settings_write_guard.enabled';
71
+ const OFF_ENV = 'HEYSPEX_SETTINGS_GATE_OFF';
72
+
73
+ /** The settings-write gate's id — unique across the built-in registry. */
74
+ export const id = 'settings-write-gate';
75
+ /** Runs pre-tool, same as its PreToolUse Claude Code hook registration. */
76
+ export const event = 'before_tool';
77
+ /** Caller-defined severity label; this gate only ever asks (rewrites) or allows. */
78
+ export const severity = 'ask';
79
+
80
+ /**
81
+ * @param {{ point: string, toolName?: string, toolInput?: Record<string, unknown> }} evt
82
+ * @param {{ cwd?: string, configRoot?: string, env?: Record<string, string | undefined> }} [ctx]
83
+ * @returns {{ decision: 'allow'|'rewrite', reason?: string, rewrite?: object }}
84
+ */
85
+ export function run(evt, ctx = {}) {
86
+ const env = ctx.env ?? process.env;
87
+ if (String(env[OFF_ENV] ?? '') === '1') return { decision: 'allow' };
88
+
89
+ // The ONLY root this gate reads is the config root — see the ctx contract above.
90
+ const configRoot = ctx.configRoot ?? (ctx.cwd ? ctx.cwd : defaultConfigRoot());
91
+ if (!isConfigKeyEnabled(TOGGLE_KEY, configRoot)) return { decision: 'allow' };
92
+
93
+ const verdict = legacyDecide(evt?.toolName, evt?.toolInput ?? {});
94
+
95
+ if (!verdict || !verdict.ask) return { decision: 'allow' };
96
+
97
+ return {
98
+ decision: 'rewrite',
99
+ reason: verdict.reason,
100
+ rewrite: { ask: true, reason: verdict.reason },
101
+ };
102
+ }
103
+
104
+ export default { id, event, severity, run };