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,176 @@
1
+ // spexharness/src/doctrine/writer.mjs
2
+ //
3
+ // DR2 (epic [[HS-KNHZ-8CMW-EPIC-spexcode-doctrine-v0-the-foundational]], task
4
+ // [[HS-YMPY-8CMW-TASK-dr2-doctrine-synthesis-a-routed]]) — renders the merged
5
+ // principle set (merge.mjs's output) into the two committed product artifacts:
6
+ // `spexharness/doctrine/SPEXCODE-DOCTRINE.json` (schema-versioned, machine-readable,
7
+ // cited) and `SPEXCODE-DOCTRINE.md` (the human-readable, sectioned document). Pure
8
+ // rendering — {@link renderDoctrineMarkdown}/{@link buildDoctrineJson} do no I/O, so
9
+ // the golden Markdown test never touches disk; {@link writeDoctrineFiles} is the one
10
+ // function that does.
11
+
12
+ import { mkdirSync, writeFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+
15
+ export const DOCTRINE_SCHEMA_VERSION = 1;
16
+
17
+ /** Section order the Markdown writer renders in — matches the epic's own category list. */
18
+ export const CATEGORY_ORDER = [
19
+ 'delegation',
20
+ 'verification',
21
+ 'communication',
22
+ 'naming',
23
+ 'safety-permissions',
24
+ 'research-grounding',
25
+ 'session-hygiene',
26
+ 'product-taste',
27
+ ];
28
+
29
+ export const CATEGORY_LABELS = {
30
+ delegation: 'Delegation',
31
+ verification: 'Verification',
32
+ communication: 'Communication',
33
+ naming: 'Naming',
34
+ 'safety-permissions': 'Safety & Permissions',
35
+ 'research-grounding': 'Research & Grounding',
36
+ 'session-hygiene': 'Session Hygiene',
37
+ 'product-taste': 'Product Taste',
38
+ };
39
+
40
+ const DOCTRINE_HEADER =
41
+ 'This is SpexCode Doctrine — the foundational operating principles distilled from the owner\'s ' +
42
+ 'Claude Code and Command Code sessions and the Command Code taste profile. It is layer 2 of three: ' +
43
+ '1) SpexDoctrine (the enforced gates in spexharness/src/gates/, never learned), 2) SpexCode Doctrine ' +
44
+ '(this document — the base every developer profile is built on), 3) SpexCode Profile (the per-' +
45
+ 'developer layer on top, which may refine a principle here but may not silently contradict one).';
46
+
47
+ /**
48
+ * Assemble the schema-versioned JSON document. Pure — `learnedAt` must be supplied by
49
+ * the caller (never `Date.now()` internally) so the golden test is deterministic.
50
+ *
51
+ * @param {{ principles: object[], conflicts?: object[], sources?: object, learnedAt: string }} input
52
+ * @returns {{ schemaVersion: number, learnedAt: string, sources: object, principles: object[], conflicts: object[] }}
53
+ */
54
+ export function buildDoctrineJson({ principles, conflicts = [], sources = {}, learnedAt }) {
55
+ return {
56
+ schemaVersion: DOCTRINE_SCHEMA_VERSION,
57
+ learnedAt,
58
+ sources,
59
+ principles,
60
+ conflicts,
61
+ };
62
+ }
63
+
64
+ /** `claude-code ×5, taste ×1, command-code ×1` — counts an evidence-id list by its `<source>:` prefix. */
65
+ function evidenceBreakdown(evidence) {
66
+ const counts = new Map();
67
+ for (const id of evidence ?? []) {
68
+ const src = typeof id === 'string' && id.includes(':') ? id.slice(0, id.indexOf(':')) : 'unknown';
69
+ counts.set(src, (counts.get(src) ?? 0) + 1);
70
+ }
71
+ return [...counts.entries()].map(([k, v]) => `${k} ×${v}`).join(', ');
72
+ }
73
+
74
+ function renderPrincipleLine(p) {
75
+ const conf = typeof p.confidence === 'number' ? p.confidence.toFixed(2) : '0.00';
76
+ const evidence = Array.isArray(p.evidence) ? p.evidence : [];
77
+ return `- ${p.text} — confidence ${conf} · evidence ${evidence.length} (${evidenceBreakdown(evidence)})`;
78
+ }
79
+
80
+ /**
81
+ * Render the sectioned Markdown document from a JSON-shaped doctrine object
82
+ * ({@link buildDoctrineJson}'s output, or anything with the same shape). Deterministic:
83
+ * the same `doc` always renders the same string (no clock reads, no random order —
84
+ * principles within a category render in their given array order).
85
+ *
86
+ * @param {{ learnedAt: string, principles: object[], conflicts?: object[], sources?: object }} doc
87
+ * @returns {string}
88
+ */
89
+ export function renderDoctrineMarkdown(doc) {
90
+ const lines = [];
91
+ lines.push('# SpexCode Doctrine');
92
+ lines.push('');
93
+ lines.push(`_Learned ${doc.learnedAt}_`);
94
+ lines.push('');
95
+ lines.push(DOCTRINE_HEADER);
96
+ lines.push('');
97
+
98
+ const byCategory = new Map();
99
+ for (const p of doc.principles ?? []) {
100
+ const cat = p.category ?? 'product-taste';
101
+ const list = byCategory.get(cat) ?? [];
102
+ list.push(p);
103
+ byCategory.set(cat, list);
104
+ }
105
+
106
+ const renderSection = (cat, list) => {
107
+ lines.push(`## ${CATEGORY_LABELS[cat] ?? cat}`);
108
+ lines.push('');
109
+ for (const p of list) lines.push(renderPrincipleLine(p));
110
+ lines.push('');
111
+ };
112
+
113
+ for (const cat of CATEGORY_ORDER) {
114
+ const list = byCategory.get(cat);
115
+ if (list && list.length) renderSection(cat, list);
116
+ byCategory.delete(cat);
117
+ }
118
+ // Defensive: any category the seat invented outside CATEGORY_ORDER still renders,
119
+ // in the stable order it was first seen, rather than being silently dropped.
120
+ for (const [cat, list] of byCategory) renderSection(cat, list);
121
+
122
+ lines.push('## Conflicts');
123
+ lines.push('');
124
+ const conflicts = doc.conflicts ?? [];
125
+ if (!conflicts.length) {
126
+ lines.push('_None recorded._');
127
+ } else {
128
+ for (const c of conflicts) lines.push(`- **${c.category}**: "${c.a}" vs "${c.b}" — headline: ${c.headline}`);
129
+ }
130
+ lines.push('');
131
+
132
+ lines.push('## Sources');
133
+ lines.push('');
134
+ const sources = doc.sources ?? {};
135
+ lines.push(`- Observations: ${sources.totalObservations ?? 0}`);
136
+ const bySource = sources.bySource ?? {};
137
+ for (const [k, v] of Object.entries(bySource)) lines.push(` - ${k}: ${v}`);
138
+ // DR5 ([[HS-7Z9E-8CMW-TASK-dr5-vault-knowledge-as-a-doctrine]]) — the vault sets
139
+ // (decisions, lessons, protocol enforcement, ADRs, the naughty list, CLAUDE.md, ...)
140
+ // that fed the `vault` row above, broken out with their own per-set counts.
141
+ const vaultSets = sources.vaultSets ?? {};
142
+ if (Object.keys(vaultSets).length) {
143
+ lines.push(' - vault sets:');
144
+ for (const [k, v] of Object.entries(vaultSets)) lines.push(` - ${k}: ${v}`);
145
+ }
146
+ lines.push('');
147
+
148
+ return lines.join('\n');
149
+ }
150
+
151
+ export function defaultDoctrineOutDir(repoRoot) {
152
+ return join(repoRoot, 'spexharness', 'doctrine');
153
+ }
154
+
155
+ export function defaultDoctrineMdPath(repoRoot) {
156
+ return join(defaultDoctrineOutDir(repoRoot), 'SPEXCODE-DOCTRINE.md');
157
+ }
158
+
159
+ export function defaultDoctrineJsonPath(repoRoot) {
160
+ return join(defaultDoctrineOutDir(repoRoot), 'SPEXCODE-DOCTRINE.json');
161
+ }
162
+
163
+ /**
164
+ * Write both product artifacts for `repoRoot`. The only I/O in this module.
165
+ * @param {string} repoRoot
166
+ * @param {ReturnType<typeof buildDoctrineJson>} doc
167
+ * @returns {{ jsonPath: string, mdPath: string }}
168
+ */
169
+ export function writeDoctrineFiles(repoRoot, doc) {
170
+ mkdirSync(defaultDoctrineOutDir(repoRoot), { recursive: true });
171
+ const jsonPath = defaultDoctrineJsonPath(repoRoot);
172
+ const mdPath = defaultDoctrineMdPath(repoRoot);
173
+ writeFileSync(jsonPath, `${JSON.stringify(doc, null, 2)}\n`);
174
+ writeFileSync(mdPath, `${renderDoctrineMarkdown(doc)}\n`);
175
+ return { jsonPath, mdPath };
176
+ }
@@ -0,0 +1,84 @@
1
+ // spexharness/src/events.mjs
2
+ //
3
+ // The harness's normalized event model. Every front (Claude Code plugin, `hs
4
+ // chat`, a headless seat) translates whatever its own runtime hands it into
5
+ // one of these six shapes before calling into the gate registry / decider —
6
+ // so a gate is written once against this shape and never against a
7
+ // front-specific payload.
8
+ //
9
+ // This module is pure: no I/O, no process.stdin/stdout, no imports outside
10
+ // node:* stdlib.
11
+
12
+ /**
13
+ * The six event points a gate may run on, in the order they occur during a
14
+ * session:
15
+ * 1. session_start — a session begins.
16
+ * 2. prompt_submit — the user (or caller) submits a prompt.
17
+ * 3. before_tool — a tool is about to run (pre-flight; may deny/rewrite).
18
+ * 4. after_tool — a tool finished running (post-flight; may inject).
19
+ * 5. stop — the agent is about to stop / hand control back.
20
+ * 6. session_end — a session ends.
21
+ * @type {ReadonlyArray<string>}
22
+ */
23
+ export const EVENT_POINTS = Object.freeze([
24
+ 'session_start',
25
+ 'prompt_submit',
26
+ 'before_tool',
27
+ 'after_tool',
28
+ 'stop',
29
+ 'session_end',
30
+ ]);
31
+
32
+ const EVENT_POINT_SET = new Set(EVENT_POINTS);
33
+
34
+ /**
35
+ * Documented field set per event point. `sessionId` and `ts` are common to
36
+ * every event (added by {@link createEvent} itself); the arrays below are
37
+ * the additional fields each event point's `fields` argument is expected to
38
+ * carry. This is documentation, not a runtime schema — createEvent does not
39
+ * reject extra or missing fields, so a front can pass whatever its own
40
+ * runtime already has without a translation step per gate.
41
+ *
42
+ * - session_start: cwd, agent
43
+ * - prompt_submit: prompt
44
+ * - before_tool: toolName, toolInput
45
+ * - after_tool: toolName, toolInput, toolResult
46
+ * - stop: (none beyond the common fields)
47
+ * - session_end: reason
48
+ * @type {Readonly<Record<string, ReadonlyArray<string>>>}
49
+ */
50
+ export const EVENT_FIELDS = Object.freeze({
51
+ session_start: Object.freeze(['cwd', 'agent']),
52
+ prompt_submit: Object.freeze(['prompt']),
53
+ before_tool: Object.freeze(['toolName', 'toolInput']),
54
+ after_tool: Object.freeze(['toolName', 'toolInput', 'toolResult']),
55
+ stop: Object.freeze([]),
56
+ session_end: Object.freeze(['reason']),
57
+ });
58
+
59
+ /**
60
+ * Build one normalized event object for a gate to run against.
61
+ *
62
+ * @param {string} point - one of {@link EVENT_POINTS}. Throws on any other
63
+ * value, including a Claude-Code-shaped name like `"PreToolUse"` — callers
64
+ * translate into this vocabulary at the edge, not inside a gate.
65
+ * @param {Record<string, unknown>} [fields] - the event point's documented
66
+ * fields (see {@link EVENT_FIELDS}), plus any extra context a front wants
67
+ * to carry through to its own gates. Not validated against the field list;
68
+ * the list is documentation for gate authors, not a runtime schema.
69
+ * @returns {Readonly<{ point: string, ts: number } & Record<string, unknown>>}
70
+ * A frozen, normalized event object. `ts` defaults to `Date.now()` when
71
+ * `fields.ts` is not supplied.
72
+ */
73
+ export function createEvent(point, fields = {}) {
74
+ if (!EVENT_POINT_SET.has(point)) {
75
+ throw new Error(
76
+ `spexharness: unknown event point "${point}" — expected one of: ${EVENT_POINTS.join(', ')}`,
77
+ );
78
+ }
79
+ return Object.freeze({
80
+ ts: Date.now(),
81
+ ...fields,
82
+ point,
83
+ });
84
+ }
@@ -0,0 +1,77 @@
1
+ // spexharness/src/gates/_shell-write-targets.mjs
2
+ //
3
+ // Shared conservative shell-write scanner. Extracted from
4
+ // env-vault-guard.decide.mjs (originally the same scanner as
5
+ // guard-auto-memory-writes.mjs) so env-vault-guard and vault-write-tier-guard
6
+ // (HS-FCNH) import ONE implementation instead of drifting copies.
7
+ //
8
+ // Pure: no fs, no process. It only recognises write-SHAPED commands whose
9
+ // targets it can positively identify; callers pair it with a fail-closed
10
+ // text scan (see `mentionsWriteVerb`) for everything it cannot parse
11
+ // (heredocs, `python -c`, `node -e`, variable-expanded targets).
12
+
13
+ const REDIRECT_RE = /(?:>>?|\|\s*tee(?:\s+-a)?)\s+("([^"]+)"|'([^']+)'|(\S+))/g;
14
+ const CMD_RE =
15
+ /\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)\s+([^|;&\n]+)/g;
16
+ // In-place editors: `sed -i`, `sed -ibak`, `sed --in-place`, `perl -pi -e`, `perl -i.bak`.
17
+ const INPLACE_RE = /\b(?:sed|perl)\b([^|;&\n]*)/g;
18
+
19
+ function isInPlaceFlag(token) {
20
+ return (
21
+ token === '--in-place' ||
22
+ token.startsWith('--in-place=') ||
23
+ /^-[A-Za-z]*i[A-Za-z0-9.]*$/.test(token)
24
+ );
25
+ }
26
+
27
+ /** Same conservative shell scanner as guard-auto-memory-writes.mjs, plus in-place editors. */
28
+ export function extractShellTargets(command) {
29
+ if (!command || typeof command !== 'string') return [];
30
+ const targets = new Set();
31
+ for (const m of command.matchAll(REDIRECT_RE)) targets.add(m[2] ?? m[3] ?? m[4]);
32
+ for (const m of command.matchAll(CMD_RE)) {
33
+ const head = m[0].split(/\s/)[0];
34
+ const args = m[1]
35
+ .trim()
36
+ .split(/\s+/)
37
+ .filter((a) => a && !a.startsWith('-'));
38
+ if (args.length === 0) continue;
39
+ if (/^(?:mv|cp|install|rename|Move-Item|Copy-Item)$/.test(head)) {
40
+ targets.add(args[args.length - 1]);
41
+ } else {
42
+ for (const a of args) targets.add(a);
43
+ }
44
+ }
45
+ // sed -i / perl -i / --in-place: every non-flag operand is a candidate
46
+ // (the script operand rides along harmlessly — callers match by path).
47
+ for (const m of command.matchAll(INPLACE_RE)) {
48
+ const tokens = m[1].trim().split(/\s+/).filter(Boolean);
49
+ if (!tokens.some(isInPlaceFlag)) continue;
50
+ for (const t of tokens) if (!t.startsWith('-')) targets.add(t);
51
+ }
52
+ // PowerShell: -Path / -FilePath / -LiteralPath <target> on a write cmdlet.
53
+ if (/\b(?:New-Item|Set-Content|Out-File|Add-Content|Clear-Content)\b/i.test(command)) {
54
+ for (const m of command.matchAll(/-(?:Path|FilePath|LiteralPath)\s+("([^"]+)"|'([^']+)'|(\S+))/gi))
55
+ targets.add(m[2] ?? m[3] ?? m[4]);
56
+ }
57
+ for (const t of extractInterpreterWriteTargets(command)) targets.add(t);
58
+ return [...targets]
59
+ .map((t) => t.replace(/^['"]|['"]$/g, ''))
60
+ .filter(Boolean)
61
+ .filter((t) => !/[$`(){}]/.test(t));
62
+ }
63
+
64
+ /**
65
+ * Literal-path writes inside inline interpreter bodies: python `open('p','w')`,
66
+ * node `writeFileSync('p', ...)`. Only a literal path that is the write call's
67
+ * FIRST argument counts; a body that merely reads or prints is not a write.
68
+ */
69
+ export function extractInterpreterWriteTargets(command) {
70
+ if (!command || typeof command !== 'string') return [];
71
+ const out = [];
72
+ const pyOpen = /\bopen\s*\(\s*(['"])([^'"]+)\1\s*,\s*['"][wax]/g;
73
+ const nodeWrite = /\b(?:writeFile|writeFileSync|appendFile|appendFileSync)\s*\(\s*(['"`])([^'"`]+)\1/g;
74
+ for (const m of command.matchAll(pyOpen)) out.push(m[2]);
75
+ for (const m of command.matchAll(nodeWrite)) out.push(m[2]);
76
+ return out;
77
+ }
@@ -0,0 +1,202 @@
1
+ // spexharness/src/gates/capture-workflow-gate.mjs
2
+ //
3
+ // Engine port of scripts/claude-hooks/capture-workflow-gate.mjs — the
4
+ // PreToolUse EARLY-WARNING gate for filing an implementation spec
5
+ // (epic/task/milestone) that references the capture currently on deck. See
6
+ // ./PORTING.md for the recipe this follows and ./env-vault-guard.mjs for the
7
+ // reference port.
8
+ //
9
+ // THIS HOOK NEVER DENIES (by design — see the original's header). The real
10
+ // refusal lives in heyspex_file_spec / vendor/heyspex-mcp/capture-stage-gate.mjs,
11
+ // which reads the LIVE capture-on-deck pin (the console / Neon control
12
+ // plane); this hook can only see the OFFLINE file pin
13
+ // (.heyspex/capture-focus.yaml), so it is kept as a narrow, always-silent-
14
+ // unless-matched early warning, never a blocker.
15
+ //
16
+ // The original script does NOT export a pure decision function — its logic
17
+ // is tangled inside `main()` (stdin read, env checks, then the match logic).
18
+ // Per PORTING.md step 1's "otherwise" branch that logic would normally be
19
+ // extracted into the original script itself, but this task's Do-nots forbid
20
+ // touching anything under scripts/claude-hooks/. Rather than hand-copy the
21
+ // original's rule/regex logic (the thing PORTING.md warns will drift), this
22
+ // port imports the SAME shared, already-pure library modules the original
23
+ // hook itself imports for the actual decision surface:
24
+ // - vendor/heyspex-mcp/capture-stage-gate.mjs — BLOCKED_FILE_SPEC_TYPES,
25
+ // ALLOWED_FILE_SPEC_TYPES, referencesCapture(). This is the canonical
26
+ // module the original hook's header says it shares "so the two can
27
+ // never disagree about which filings count" — importing it here keeps
28
+ // that same guarantee for the port.
29
+ // - vendor/heyspex-mcp/capture-focus.mjs — resolveActiveCapture(), which
30
+ // reads the offline pin + the pinned capture's frontmatter/body.
31
+ // The only code re-derived here is the original's small, non-security
32
+ // glue: stripping the MCP tool-name prefix (bareToolName), and the tiny
33
+ // offline frontmatter scanner + spec loader the original defines locally
34
+ // (parseFrontmatter / loadSpec) purely to satisfy resolveActiveCapture's
35
+ // callback-shaped contract — neither is a rule/regex guard, both are
36
+ // deterministic parsing utilities with no security surface of their own.
37
+ //
38
+ // Decision mapping (documented judgment call, same shape as
39
+ // env-vault-guard.mjs's "ask" → rewrite mapping): a match produces a WARNING
40
+ // carrying the capture id and the sanctioned-exit message — richer than a
41
+ // plain allow, and the original never denies, so silently collapsing this to
42
+ // `allow` would drop information a caller might want to surface (e.g. show
43
+ // the same warning text the Claude Code hook would have written to stderr).
44
+ // Mapped to `rewrite` with `{ warn: true, reason, captureId, type }` rather
45
+ // than `deny` (the hook's own header is explicit: "it never denies") or a
46
+ // silent `allow` (loses the warning). No match, or any early-out (kill
47
+ // switch, wrong tool, allowed/unmatched type, no capture on deck, unrelated
48
+ // filing) → plain `allow`.
49
+ //
50
+ // This gate never hard-denies (matching the original) — "deny" coverage for
51
+ // AC3 ("allow, deny and rewrite each covered by at least one test per gate",
52
+ // checked as one combined `node --test` run across all three ported gates in
53
+ // this task) comes from the sibling enforce-session-sequence gate. See
54
+ // ./enforce-session-sequence.test.mjs.
55
+ //
56
+ // Env hatches (unchanged, either one silences the warning — matching the
57
+ // original, which honors the tool's own break-glass too so one variable
58
+ // covers both layers):
59
+ // HEYSPEX_CAPTURE_WORKFLOW_GATE_OFF=1
60
+ // HEYSPEX_CAPTURE_STAGE_GATE_OFF=1
61
+ //
62
+ // Not ported HERE: the original's `appendGuardrailEvent(...)` audit-log
63
+ // write on a match. A gate module stays a pure decision function (AC1: no
64
+ // stdout writes; by the same spirit, no other observability side effects
65
+ // either) — matching env-vault-guard.mjs's port. The `rewrite.captureId` /
66
+ // `rewrite.type` fields below already carry everything a front needs to
67
+ // rebuild that same audit row — see spexharness/src/adapters/audit.mjs,
68
+ // called from spexharness/src/adapters/claude-code.mjs's runGateAsHook.
69
+ //
70
+ // `ctx` contract (all optional; every field defaults to a real fs read, same
71
+ // as the original hook and as spexharness/fixtures/lib/setups.mjs's
72
+ // captureActiveVault — the golden/regression tests below point ctx.cwd at a
73
+ // real scratch vault rather than injecting a fake fs):
74
+ // ctx.cwd - repo/vault root to resolve the offline capture pin from.
75
+ // Defaults to CLAUDE_PROJECT_DIR, else this repo's root.
76
+ // ctx.env - env var overrides (tests use this instead of mutating
77
+ // process.env).
78
+
79
+ import fs from 'node:fs';
80
+ import path from 'node:path';
81
+ import { fileURLToPath } from 'node:url';
82
+
83
+ import { resolveActiveCapture } from '../../../vendor/heyspex-mcp/capture-focus.mjs';
84
+ import {
85
+ ALLOWED_FILE_SPEC_TYPES,
86
+ BLOCKED_FILE_SPEC_TYPES,
87
+ referencesCapture,
88
+ } from '../../../vendor/heyspex-mcp/capture-stage-gate.mjs';
89
+
90
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
91
+ const REPO_ROOT = path.resolve(HERE, '..', '..', '..');
92
+
93
+ // The bare (server-stripped) MCP tool name this gate acts on.
94
+ const FILE_SPEC_TOOL = 'heyspex_file_spec';
95
+ const OFF_ENV_WORKFLOW = 'HEYSPEX_CAPTURE_WORKFLOW_GATE_OFF';
96
+ const OFF_ENV_STAGE = 'HEYSPEX_CAPTURE_STAGE_GATE_OFF';
97
+
98
+ // Minimal, sync, offline frontmatter parser — mirrors the original hook's
99
+ // local helper of the same name byte-for-byte. Enough for
100
+ // resolveActiveCapture's callbacks (it only reads frontmatter.id / status /
101
+ // promoted_to and the body). Not a full YAML parser; capture frontmatter is
102
+ // flat scalars + simple arrays.
103
+ function parseFrontmatter(content) {
104
+ const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
105
+ if (!m) return { frontmatter: {}, body: content };
106
+ const frontmatter = {};
107
+ for (const line of m[1].split(/\r?\n/)) {
108
+ const trimmed = line.trim();
109
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('- ')) continue;
110
+ const colon = trimmed.indexOf(':');
111
+ if (colon === -1) continue;
112
+ const key = trimmed.slice(0, colon).trim();
113
+ let val = trimmed.slice(colon + 1).trim();
114
+ val = val.replace(/^["']|["']$/g, '');
115
+ frontmatter[key] = val;
116
+ }
117
+ return { frontmatter, body: m[2] ?? '' };
118
+ }
119
+
120
+ // readFileSync loader for resolveActiveCapture — returns { frontmatter, body }
121
+ // or null when the pinned capture file is missing/unreadable. Mirrors the
122
+ // original hook's local helper of the same name.
123
+ function loadSpec(vaultRoot, specPath) {
124
+ try {
125
+ const full = path.join(vaultRoot, specPath);
126
+ const content = fs.readFileSync(full, 'utf8');
127
+ return parseFrontmatter(content);
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ // Strip the Claude Code MCP tool prefix (mcp__<server>__<tool>) → bare tool
134
+ // name. Mirrors the original hook's local helper of the same name.
135
+ function bareToolName(name) {
136
+ if (typeof name !== 'string') return '';
137
+ const m = name.match(/^mcp__[^_]+(?:_[^_]+)*?__(.+)$/);
138
+ if (m) return m[1];
139
+ const idx = name.lastIndexOf('__');
140
+ return idx === -1 ? name : name.slice(idx + 2);
141
+ }
142
+
143
+ function warnText(captureId) {
144
+ return (
145
+ `[capture-workflow-gate] warning — this filing links the open capture ${captureId}, ` +
146
+ `which a capture must not become. The sanctioned exit is promote-to-research (\`/hs-res\`). ` +
147
+ `This hook only warns. heyspex_file_spec is where the rule is enforced, and it ` +
148
+ `reads the live capture pin rather than the offline file this hook can see.`
149
+ );
150
+ }
151
+
152
+ /** The capture-workflow gate's id — unique across the built-in registry. */
153
+ export const id = 'capture-workflow-gate';
154
+ /** Runs pre-tool, same as its PreToolUse Claude Code hook registration. */
155
+ export const event = 'before_tool';
156
+ /** Caller-defined severity label; this gate only ever warns (rewrites) or allows. */
157
+ export const severity = 'warn';
158
+
159
+ /**
160
+ * @param {{ point: string, toolName?: string, toolInput?: Record<string, unknown> }} evt
161
+ * @param {{ cwd?: string, env?: Record<string, string | undefined> }} [ctx]
162
+ * @returns {{ decision: 'allow'|'rewrite', reason?: string, rewrite?: object }}
163
+ */
164
+ export function run(evt, ctx = {}) {
165
+ const env = ctx.env ?? process.env;
166
+ if (String(env[OFF_ENV_WORKFLOW] ?? '') === '1') return { decision: 'allow' };
167
+ if (String(env[OFF_ENV_STAGE] ?? '') === '1') return { decision: 'allow' };
168
+
169
+ const tool = bareToolName(evt?.toolName);
170
+ if (tool !== FILE_SPEC_TOOL) return { decision: 'allow' };
171
+
172
+ const toolInput = evt?.toolInput ?? {};
173
+ const type = String(toolInput?.type ?? '').trim().toLowerCase();
174
+
175
+ // Only implementation specs are the premature-promotion case. Research /
176
+ // diagnose / memory / pillar / review filings are sanctioned exits or
177
+ // unrelated — never warned about.
178
+ if (!BLOCKED_FILE_SPEC_TYPES.has(type) || ALLOWED_FILE_SPEC_TYPES.has(type)) {
179
+ return { decision: 'allow' };
180
+ }
181
+
182
+ const root = ctx.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? REPO_ROOT;
183
+ let resolved;
184
+ try {
185
+ resolved = resolveActiveCapture(root, parseFrontmatter, loadSpec);
186
+ } catch {
187
+ return { decision: 'allow' }; // discovery failure → stay quiet, fail-safe
188
+ }
189
+ if (!resolved || resolved.error || !resolved.spec_id) return { decision: 'allow' };
190
+
191
+ // Arg-targeted: only warn when the spec being filed references THIS capture.
192
+ if (!referencesCapture(toolInput, resolved.spec_id)) return { decision: 'allow' };
193
+
194
+ const reason = warnText(resolved.spec_id);
195
+ return {
196
+ decision: 'rewrite',
197
+ reason,
198
+ rewrite: { warn: true, reason, captureId: resolved.spec_id, type },
199
+ };
200
+ }
201
+
202
+ export default { id, event, severity, run };
@@ -0,0 +1,33 @@
1
+ // spexharness/src/gates/claim-before-code-gate.decide.mjs
2
+ //
3
+ // Pure decision logic relocated from scripts/claude-hooks/claim-before-code-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 now delegates
6
+ // to this file too, so ./claim-before-code-gate.mjs (the gate) and the
7
+ // original hook's own smoke test both import the SAME code; nothing here is
8
+ // a second copy that can drift). No stdin/stdout/process.exit anywhere in
9
+ // this file.
10
+
11
+ const EDIT_TOOL_NAMES = new Set(['Edit', 'Write', 'MultiEdit']);
12
+
13
+ // Paths this gate never blocks: the vault itself (filing the epic IS the
14
+ // required step), hook/state machinery, and anything outside the repo tree.
15
+ // Everything else counts as "code" for this gate's purposes.
16
+ const EXEMPT_PATH_RE = /(?:^|[\\/])(?:specs|\.heyspex|\.claude|\.git)(?:[\\/]|$)/;
17
+
18
+ function isCodeEditPath(filePath) {
19
+ if (typeof filePath !== 'string' || !filePath.trim()) return false;
20
+ const normalized = filePath.replace(/\\/g, '/');
21
+ return !EXEMPT_PATH_RE.test(normalized);
22
+ }
23
+
24
+ /**
25
+ * Pure decision function (exported for the smoke test and the gate): given
26
+ * the resolved inputs, should this edit be denied?
27
+ */
28
+ export function shouldDenyEdit({ toolName, filePath, hasMarker, hasNoClaimSignal }) {
29
+ if (!EDIT_TOOL_NAMES.has(toolName)) return false;
30
+ if (!isCodeEditPath(filePath)) return false;
31
+ if (!hasMarker) return false;
32
+ return hasNoClaimSignal;
33
+ }