heyspex 0.1.0 → 0.2.0

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 (322) hide show
  1. package/README.md +21 -0
  2. package/bin/hs.mjs +931 -98
  3. package/bin/spexcode.mjs +262 -0
  4. package/dev-factory/engine/worktree.mjs +740 -0
  5. package/dev-factory/lib/code-repo-root.mjs +67 -0
  6. package/dev-factory/lib/context-budget.mjs +84 -0
  7. package/dev-factory/lib/epic-detect.mjs +190 -0
  8. package/dev-factory/lib/fleet-queue-file.mjs +846 -0
  9. package/dev-factory/lib/fleet-queue.mjs +426 -0
  10. package/dev-factory/lib/fleet-state.mjs +14 -18
  11. package/dev-factory/lib/model-catalog.mjs +338 -0
  12. package/dev-factory/lib/seat-context.mjs +199 -0
  13. package/dev-factory/lib/spec-io.mjs +93 -0
  14. package/dev-factory/lib/sufficiency-score.mjs +8 -2
  15. package/dev-factory/lib/worktree-allocator.mjs +744 -0
  16. package/dev-factory/model-catalog.json +222 -0
  17. package/dist/cli/hs-check.mjs +1085 -103
  18. package/dist/cli/hs-claim.mjs +936 -63
  19. package/dist/cli/hs-connect.mjs +9786 -1910
  20. package/dist/cli/hs-defer.mjs +5267 -3053
  21. package/dist/cli/hs-dev.mjs +564 -0
  22. package/dist/cli/hs-env.mjs +385 -299
  23. package/dist/cli/hs-index-rebuild.mjs +7129 -0
  24. package/dist/cli/hs-index-reconcile.mjs +7852 -0
  25. package/dist/cli/hs-init.mjs +777 -138
  26. package/dist/cli/hs-install.mjs +6583 -485
  27. package/dist/cli/hs-join.mjs +10137 -2368
  28. package/dist/cli/hs-vault-pull.mjs +4659 -5882
  29. package/dist/cli/hs-vault.mjs +3793 -1537
  30. package/dist/cli/hs-workspace.mjs +2550 -524
  31. package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
  32. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
  33. package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
  34. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
  35. package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
  36. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
  37. package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
  38. package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
  39. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
  40. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
  41. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
  42. package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
  43. package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
  44. package/dist/cli-templates/harness-commands/claude/hs-ground.md +28 -0
  45. package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
  46. package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
  47. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
  48. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
  49. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
  50. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
  51. package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
  52. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
  53. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
  54. package/dist/cli-templates/harness-commands/claude/hs-view.md +26 -0
  55. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
  56. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
  57. package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
  58. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
  59. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
  60. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
  61. package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
  62. package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
  63. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +31 -0
  64. package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
  65. package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
  66. package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
  67. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
  68. package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
  69. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
  70. package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
  71. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
  72. package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
  73. package/dist/cli-templates/harness-commands/cursor/hs-view.md +30 -0
  74. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
  75. package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
  76. package/docs/mcp-client-config.md +259 -6
  77. package/package.json +231 -124
  78. package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
  79. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
  80. package/plugins/heyspex/.mcp.json +12 -0
  81. package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
  82. package/plugins/heyspex/bin/run-hook.mjs +62 -0
  83. package/plugins/heyspex/hooks/cursor-hooks.json +227 -0
  84. package/plugins/heyspex/hooks/hooks.json +361 -0
  85. package/plugins/heyspex/mcp.json +13 -0
  86. package/scripts/check-mcp-freshness.mjs +17 -9
  87. package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
  88. package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
  89. package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
  90. package/scripts/claude-hooks/_intent-classifier-router.mjs +410 -0
  91. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +351 -0
  92. package/scripts/claude-hooks/_run.mjs +120 -0
  93. package/scripts/claude-hooks/_transcript-text.mjs +60 -0
  94. package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
  95. package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
  96. package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
  97. package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
  98. package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
  99. package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
  100. package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
  101. package/scripts/claude-hooks/claim-liveness-shared.mjs +575 -0
  102. package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
  103. package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
  104. package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
  105. package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
  106. package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
  107. package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
  108. package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
  109. package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
  110. package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
  111. package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
  112. package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
  113. package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
  114. package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
  115. package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
  116. package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
  117. package/scripts/claude-hooks/inject-communication-style.mjs +66 -0
  118. package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
  119. package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
  120. package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
  121. package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
  122. package/scripts/claude-hooks/intent-classifier-trigger.mjs +247 -0
  123. package/scripts/claude-hooks/log-session-activity.mjs +341 -0
  124. package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
  125. package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
  126. package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
  127. package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
  128. package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
  129. package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
  130. package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
  131. package/scripts/claude-hooks/research-method-guard.mjs +398 -0
  132. package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
  133. package/scripts/claude-hooks/session-end.mjs +449 -0
  134. package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
  135. package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
  136. package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
  137. package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
  138. package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
  139. package/scripts/claude-hooks/wip-checkpoint.mjs +298 -0
  140. package/scripts/cursor-hooks/manifest.mjs +235 -0
  141. package/scripts/dev-server-status.mjs +34 -13
  142. package/scripts/dev-server-stop.mjs +313 -0
  143. package/scripts/heyspex-mode.mjs +268 -0
  144. package/scripts/hs-auto-resolve.mjs +240 -0
  145. package/scripts/hs-auto-source.mjs +398 -0
  146. package/scripts/hs-auto.mjs +134 -0
  147. package/scripts/hs-capture-cli.mjs +326 -0
  148. package/scripts/hs-classifier.mjs +136 -0
  149. package/scripts/hs-companion.mjs +88 -0
  150. package/scripts/hs-gateway.mjs +115 -0
  151. package/scripts/hs-mode.mjs +125 -0
  152. package/scripts/hs-next.mjs +441 -0
  153. package/scripts/hs-settings.mjs +338 -0
  154. package/scripts/hs-view.mjs +333 -0
  155. package/scripts/hs-worktree.mjs +201 -0
  156. package/scripts/install-claude-code-hooks.mjs +270 -0
  157. package/scripts/install-cursor-hooks.mjs +471 -0
  158. package/scripts/lib/agent-env-markers.mjs +38 -0
  159. package/scripts/lib/console-url-resolver.mjs +197 -60
  160. package/scripts/lib/customer-kit.mjs +886 -0
  161. package/scripts/lib/dev-mode.mjs +244 -28
  162. package/scripts/lib/heyspex-home.mjs +72 -0
  163. package/scripts/lib/hs-command.mjs +277 -0
  164. package/scripts/lib/hs-companion-settings.mjs +472 -0
  165. package/scripts/lib/hs-source-commit.mjs +59 -5
  166. package/scripts/lib/hs-source-identity.mjs +526 -0
  167. package/scripts/lib/hs-source-log.mjs +11 -21
  168. package/scripts/lib/hs-source-push.mjs +107 -58
  169. package/scripts/lib/hs-worktree-args.mjs +115 -0
  170. package/scripts/lib/shared-path-helpers.mjs +134 -0
  171. package/scripts/lib/standalone-doctor.mjs +540 -3
  172. package/scripts/lib/subagent-git-lock.mjs +109 -0
  173. package/scripts/lib/template-pin.mjs +34 -0
  174. package/scripts/provenance-lib.mjs +91 -0
  175. package/scripts/stamp-provenance-on-edit.mjs +222 -0
  176. package/scripts/wip-ledger-health.mjs +653 -0
  177. package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
  178. package/spexharness/src/adapters/audit.mjs +197 -0
  179. package/spexharness/src/adapters/claude-code.mjs +441 -0
  180. package/spexharness/src/chat/tui/ansi.mjs +448 -0
  181. package/spexharness/src/chat/tui/palette.mjs +22 -0
  182. package/spexharness/src/chat/tui/select.mjs +208 -0
  183. package/spexharness/src/config-root.mjs +54 -0
  184. package/spexharness/src/context-budget.mjs +141 -0
  185. package/spexharness/src/dev-server-control.mjs +389 -0
  186. package/spexharness/src/doctrine/behaviour.mjs +371 -0
  187. package/spexharness/src/doctrine/corpus.mjs +733 -0
  188. package/spexharness/src/doctrine/inject.mjs +338 -0
  189. package/spexharness/src/doctrine/merge.mjs +184 -0
  190. package/spexharness/src/doctrine/sources.mjs +252 -0
  191. package/spexharness/src/doctrine/synthesize.mjs +401 -0
  192. package/spexharness/src/doctrine/vault-source.mjs +574 -0
  193. package/spexharness/src/doctrine/writer.mjs +176 -0
  194. package/spexharness/src/events.mjs +84 -0
  195. package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
  196. package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
  197. package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
  198. package/spexharness/src/gates/deep-research-cost-gate.mjs +274 -0
  199. package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
  200. package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
  201. package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
  202. package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
  203. package/spexharness/src/gates/git-identity-guard.mjs +61 -0
  204. package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
  205. package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
  206. package/spexharness/src/gates/git-stash-gate.mjs +159 -0
  207. package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
  208. package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
  209. package/spexharness/src/gates/inject-vault-context.mjs +239 -0
  210. package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
  211. package/spexharness/src/gates/plan-review-gate.mjs +233 -0
  212. package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
  213. package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
  214. package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
  215. package/spexharness/src/gates/settings-write-gate.mjs +104 -0
  216. package/spexharness/src/git-reflog-audit.mjs +294 -0
  217. package/spexharness/src/intent-classifier.mjs +194 -0
  218. package/spexharness/src/intent-directive.mjs +541 -0
  219. package/spexharness/src/profile/learn.mjs +551 -0
  220. package/spexharness/src/profile/proposals.mjs +357 -0
  221. package/spexharness/src/profile/vault-inclusion.mjs +386 -0
  222. package/spexharness/src/redact.mjs +151 -0
  223. package/spexharness/src/rule-table.mjs +819 -0
  224. package/spexharness/src/seat-context.mjs +122 -0
  225. package/spexharness/src/session-bootstrap.mjs +512 -0
  226. package/spexharness/src/session-record.mjs +815 -0
  227. package/spexharness/src/token-inventory.mjs +207 -0
  228. package/vendor/heyspex-mcp/account-switch.mjs +95 -0
  229. package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
  230. package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
  231. package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
  232. package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
  233. package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
  234. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +46 -5
  235. package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
  236. package/vendor/heyspex-mcp/capture-console-client.mjs +80 -1
  237. package/vendor/heyspex-mcp/capture-focus.mjs +1035 -29
  238. package/vendor/heyspex-mcp/capture-outbox.mjs +5 -1
  239. package/vendor/heyspex-mcp/capture-tools.mjs +1219 -0
  240. package/vendor/heyspex-mcp/claim-tools.mjs +1124 -0
  241. package/vendor/heyspex-mcp/cli-verbs.mjs +2076 -0
  242. package/vendor/heyspex-mcp/cli.mjs +201 -15524
  243. package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
  244. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +412 -0
  245. package/vendor/heyspex-mcp/config-seed.yaml +15 -0
  246. package/vendor/heyspex-mcp/config-settings.mjs +290 -7
  247. package/vendor/heyspex-mcp/console-fetch.mjs +657 -16
  248. package/vendor/heyspex-mcp/console-transport.mjs +166 -0
  249. package/vendor/heyspex-mcp/credentials.mjs +584 -0
  250. package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
  251. package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
  252. package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
  253. package/vendor/heyspex-mcp/design-tools.mjs +4 -1
  254. package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
  255. package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
  256. package/vendor/heyspex-mcp/dev-tools.mjs +124 -0
  257. package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
  258. package/vendor/heyspex-mcp/events-tools.mjs +207 -0
  259. package/vendor/heyspex-mcp/filing-tools.mjs +688 -0
  260. package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
  261. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +69 -14
  262. package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
  263. package/vendor/heyspex-mcp/folder-trust-store.mjs +59 -0
  264. package/vendor/heyspex-mcp/http-transport.mjs +300 -0
  265. package/vendor/heyspex-mcp/list-tools.mjs +257 -0
  266. package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
  267. package/vendor/heyspex-mcp/mcp-config.mjs +94 -0
  268. package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
  269. package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
  270. package/vendor/heyspex-mcp/occupied-keys.mjs +141 -0
  271. package/vendor/heyspex-mcp/on-deck-brief.mjs +3360 -0
  272. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
  273. package/vendor/heyspex-mcp/package-root.mjs +32 -0
  274. package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
  275. package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
  276. package/vendor/heyspex-mcp/plan-push.mjs +906 -21
  277. package/vendor/heyspex-mcp/plan-review.mjs +124 -30
  278. package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
  279. package/vendor/heyspex-mcp/procedure-handlers.mjs +325 -0
  280. package/vendor/heyspex-mcp/procedure-tools.mjs +540 -351
  281. package/vendor/heyspex-mcp/promote-capture.mjs +386 -69
  282. package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
  283. package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
  284. package/vendor/heyspex-mcp/promote-research.mjs +660 -53
  285. package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
  286. package/vendor/heyspex-mcp/prompts.mjs +1460 -0
  287. package/vendor/heyspex-mcp/read-path.mjs +29 -9
  288. package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
  289. package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
  290. package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
  291. package/vendor/heyspex-mcp/register-spec.mjs +266 -54
  292. package/vendor/heyspex-mcp/request-context.mjs +59 -0
  293. package/vendor/heyspex-mcp/research-focus.mjs +207 -4
  294. package/vendor/heyspex-mcp/research-handlers.mjs +623 -0
  295. package/vendor/heyspex-mcp/research-tools.mjs +4 -4
  296. package/vendor/heyspex-mcp/response-budget.mjs +348 -0
  297. package/vendor/heyspex-mcp/review-focus.mjs +25 -1
  298. package/vendor/heyspex-mcp/search-specs.mjs +280 -0
  299. package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
  300. package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
  301. package/vendor/heyspex-mcp/session-owner.mjs +108 -0
  302. package/vendor/heyspex-mcp/session-tools.mjs +152 -0
  303. package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
  304. package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
  305. package/vendor/heyspex-mcp/spec-edit-tools.mjs +446 -0
  306. package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
  307. package/vendor/heyspex-mcp/split-research.mjs +301 -56
  308. package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
  309. package/vendor/heyspex-mcp/submit-for-review.mjs +132 -6
  310. package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
  311. package/vendor/heyspex-mcp/summary-write-tools.mjs +224 -0
  312. package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
  313. package/vendor/heyspex-mcp/test-helpers.mjs +108 -1
  314. package/vendor/heyspex-mcp/tool-catalog.mjs +2813 -0
  315. package/vendor/heyspex-mcp/tool-dispatch.mjs +639 -0
  316. package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
  317. package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
  318. package/vendor/heyspex-mcp/vault-io.mjs +724 -0
  319. package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
  320. package/vendor/heyspex-mcp/vault-resolve.mjs +366 -68
  321. package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
  322. package/vendor/heyspex-mcp/whoami.mjs +91 -0
@@ -0,0 +1,426 @@
1
+ #!/usr/bin/env node
2
+ // Shared auto-close terminal helpers — ship detection, pending arm, process kill,
3
+ // debug trace. Used by close-terminal-on-ship.mjs (hooks) and terminal-close-watcher.mjs.
4
+
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { spawn } from 'node:child_process';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ import { getActiveConfig } from '../../vendor/heyspex-mcp/vault-resolve.mjs';
11
+
12
+ export const STATUS_REVIEW_RE = /^\s*status:\s*in_review\s*$/m;
13
+ export const STATUS_CLOSED_RE = /^\s*status:\s*closed\s*$/m;
14
+ export const RECORD_WIP_TOOL_RE = /heyspex_record_wip/i;
15
+ export const REVIEW_DOC_RE = /[\\/]specs[\\/]reviews[\\/]HS-REV-/i;
16
+
17
+ const EDIT_TOOL_NAMES = new Set([
18
+ 'Edit',
19
+ 'Write',
20
+ 'MultiEdit',
21
+ 'StrReplace',
22
+ 'search_replace',
23
+ 'apply_patch',
24
+ 'WriteFile',
25
+ 'Replace',
26
+ ]);
27
+
28
+ export function repoRoot() {
29
+ const fromEnv = process.env.CLAUDE_PROJECT_DIR?.trim();
30
+ if (fromEnv) return fromEnv.replace(/[\\/]+$/, '');
31
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
32
+ }
33
+
34
+ const DEBUG_LOG_NAME = 'terminal-close-debug.jsonl';
35
+ /** Opt-in only — default off so hooks never grow a multi-MB trace during normal agent work. */
36
+ const DEBUG_LOG_MAX_BYTES = 256 * 1024;
37
+
38
+ export function terminalCloseDebugEnabled() {
39
+ return process.env.HEYSPEX_TERMINAL_CLOSE_DEBUG === '1';
40
+ }
41
+
42
+ function trimDebugLogIfOversized(logPath) {
43
+ try {
44
+ const stat = fs.statSync(logPath);
45
+ if (stat.size <= DEBUG_LOG_MAX_BYTES) return;
46
+ const raw = fs.readFileSync(logPath, 'utf8');
47
+ const lines = raw.split('\n').filter(Boolean);
48
+ const keep = lines.slice(-200);
49
+ fs.writeFileSync(logPath, keep.join('\n') + (keep.length ? '\n' : ''));
50
+ } catch {
51
+ // absent or unreadable — append below will create it
52
+ }
53
+ }
54
+
55
+ export function debugLog(event, detail = {}) {
56
+ if (!terminalCloseDebugEnabled()) return;
57
+ try {
58
+ const logPath = path.join(repoRoot(), '.heyspex', DEBUG_LOG_NAME);
59
+ trimDebugLogIfOversized(logPath);
60
+ const line =
61
+ JSON.stringify({
62
+ at: new Date().toISOString(),
63
+ event,
64
+ pid: process.pid,
65
+ ppid: process.ppid,
66
+ ...detail,
67
+ }) + '\n';
68
+ fs.appendFileSync(logPath, line);
69
+ } catch {
70
+ // never block
71
+ }
72
+ }
73
+
74
+ export function resolveCloseTerminalOnTaskCompleteFromSettings() {
75
+ if (process.env.HEYSPEX_AUTO_CLOSE_TERMINAL === '0') return false;
76
+ try {
77
+ const raw = fs.readFileSync(path.join(repoRoot(), 'spex-console', 'settings.json'), 'utf8');
78
+ const parsed = JSON.parse(raw);
79
+ const launch = parsed?.launchModels;
80
+ if (typeof launch?.closeTerminalOnTaskComplete === 'boolean') {
81
+ return launch.closeTerminalOnTaskComplete;
82
+ }
83
+ if (typeof launch?.closeTerminalOnTaskCreation === 'boolean') {
84
+ return launch.closeTerminalOnTaskCreation;
85
+ }
86
+ return true;
87
+ } catch {
88
+ return true;
89
+ }
90
+ }
91
+
92
+ const PENDING_CLOSE_FILE = '.pending-terminal-close';
93
+
94
+ export function pendingTerminalClosePath() {
95
+ return path.join(repoRoot(), '.heyspex', PENDING_CLOSE_FILE);
96
+ }
97
+
98
+ export function readPendingTerminalClose() {
99
+ try {
100
+ return JSON.parse(fs.readFileSync(pendingTerminalClosePath(), 'utf8'));
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ export function armPendingTerminalClose(sessionId, reason = 'ship') {
107
+ try {
108
+ fs.mkdirSync(path.join(repoRoot(), '.heyspex'), { recursive: true });
109
+ const body = {
110
+ armedAt: new Date().toISOString(),
111
+ sessionId: typeof sessionId === 'string' ? sessionId : null,
112
+ reason,
113
+ };
114
+ fs.writeFileSync(pendingTerminalClosePath(), JSON.stringify(body));
115
+ debugLog('armed', body);
116
+ } catch {
117
+ // never block the hook
118
+ }
119
+ }
120
+
121
+ export function clearPendingTerminalClose() {
122
+ try {
123
+ fs.unlinkSync(pendingTerminalClosePath());
124
+ } catch {
125
+ // absent is fine
126
+ }
127
+ }
128
+
129
+ export function isPendingTerminalCloseArmed() {
130
+ return readPendingTerminalClose() !== null;
131
+ }
132
+
133
+ /**
134
+ * Bases that can contain spec files. Since the repo split (2026-08-31) the
135
+ * vault lives outside the code repo, so spec edits arrive as paths under the
136
+ * registry-resolved vault root, not under CLAUDE_PROJECT_DIR — test both.
137
+ */
138
+ export function specBaseRoots() {
139
+ const roots = [repoRoot()];
140
+ try {
141
+ const resolved = getActiveConfig();
142
+ if (resolved?.repoRootAbs && !roots.includes(resolved.repoRootAbs)) {
143
+ roots.push(resolved.repoRootAbs);
144
+ }
145
+ } catch {
146
+ // registry unreadable — the code-repo base still applies
147
+ }
148
+ return roots;
149
+ }
150
+
151
+ export function isUnderSpecsTaskOrEpic(filePath) {
152
+ if (typeof filePath !== 'string' || filePath.length === 0) return false;
153
+ const abs = path.resolve(filePath);
154
+ for (const root of specBaseRoots()) {
155
+ for (const sub of ['specs/tasks', 'specs/epics']) {
156
+ const base = path.join(root, sub);
157
+ const rel = path.relative(base, abs);
158
+ if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) return true;
159
+ }
160
+ }
161
+ return false;
162
+ }
163
+
164
+ export function isReviewDocPath(filePath) {
165
+ if (typeof filePath !== 'string' || filePath.length === 0) return false;
166
+ return REVIEW_DOC_RE.test(path.resolve(filePath));
167
+ }
168
+
169
+ function textFlipToShipped(prev, next) {
170
+ const wasShipped = STATUS_REVIEW_RE.test(prev) || STATUS_CLOSED_RE.test(prev);
171
+ const nowShipped = STATUS_REVIEW_RE.test(next) || STATUS_CLOSED_RE.test(next);
172
+ return nowShipped && !wasShipped;
173
+ }
174
+
175
+ export function detectShipFromEditLike(toolName, toolInput) {
176
+ if (!toolInput || typeof toolInput !== 'object') return false;
177
+ if (!isUnderSpecsTaskOrEpic(toolInput.file_path)) return false;
178
+
179
+ if (toolName === 'Edit' || toolName === 'StrReplace' || toolName === 'search_replace') {
180
+ const next = typeof toolInput.new_string === 'string' ? toolInput.new_string : '';
181
+ const prev = typeof toolInput.old_string === 'string' ? toolInput.old_string : '';
182
+ return textFlipToShipped(prev, next);
183
+ }
184
+
185
+ if (toolName === 'MultiEdit') {
186
+ const edits = Array.isArray(toolInput.edits) ? toolInput.edits : [];
187
+ return edits.some((edit) => {
188
+ const next = typeof edit?.new_string === 'string' ? edit.new_string : '';
189
+ const prev = typeof edit?.old_string === 'string' ? edit.old_string : '';
190
+ return textFlipToShipped(prev, next);
191
+ });
192
+ }
193
+
194
+ if (toolName === 'Write' || toolName === 'WriteFile') {
195
+ const content = typeof toolInput.content === 'string' ? toolInput.content : '';
196
+ return STATUS_REVIEW_RE.test(content) || STATUS_CLOSED_RE.test(content);
197
+ }
198
+
199
+ return false;
200
+ }
201
+
202
+ export function detectShipFromAfterFileEdit(payload) {
203
+ const filePath = payload?.file_path;
204
+ if (!isUnderSpecsTaskOrEpic(filePath)) return false;
205
+ const edits = Array.isArray(payload?.edits) ? payload.edits : [];
206
+ return edits.some((edit) => {
207
+ const next = typeof edit?.new_string === 'string' ? edit.new_string : '';
208
+ const prev = typeof edit?.old_string === 'string' ? edit.old_string : '';
209
+ return textFlipToShipped(prev, next);
210
+ });
211
+ }
212
+
213
+ export function normalizeHookEvent(payload) {
214
+ const raw = payload?.hook_event_name;
215
+ if (typeof raw === 'string' && raw.trim()) {
216
+ const key = raw.trim();
217
+ if (key === 'postToolUse' || key === 'PostToolUse') return 'postToolUse';
218
+ if (key === 'sessionEnd' || key === 'SessionEnd') return 'sessionEnd';
219
+ if (key === 'afterFileEdit') return 'afterFileEdit';
220
+ if (key === 'afterMCPExecution') return 'afterMCPExecution';
221
+ if (key === 'stop' || key === 'Stop') return 'stop';
222
+ return key;
223
+ }
224
+ if (Array.isArray(payload?.edits) && payload?.file_path) return 'afterFileEdit';
225
+ if (typeof payload?.tool_name === 'string') return 'postToolUse';
226
+ return null;
227
+ }
228
+
229
+ export function toolCallFailed(payload) {
230
+ const response = payload?.tool_response;
231
+ if (response && typeof response === 'object' && response.is_error === true) return true;
232
+ if (payload?.failure_type) return true;
233
+ const output = payload?.tool_output;
234
+ if (typeof output === 'string') {
235
+ try {
236
+ const parsed = JSON.parse(output);
237
+ if (parsed?.isError === true || parsed?.error) return true;
238
+ } catch {
239
+ // not json
240
+ }
241
+ }
242
+ return false;
243
+ }
244
+
245
+ export function isShipEvent(payload) {
246
+ const event = normalizeHookEvent(payload);
247
+ if (!event) return false;
248
+ if (toolCallFailed(payload)) return false;
249
+
250
+ if (event === 'afterFileEdit') {
251
+ return detectShipFromAfterFileEdit(payload);
252
+ }
253
+
254
+ if (event === 'postToolUse') {
255
+ const toolName = payload?.tool_name;
256
+ if (typeof toolName === 'string' && EDIT_TOOL_NAMES.has(toolName)) {
257
+ return detectShipFromEditLike(toolName, payload?.tool_input ?? {});
258
+ }
259
+ }
260
+
261
+ return false;
262
+ }
263
+
264
+ export function isRecordWipSuccess(payload) {
265
+ const event = normalizeHookEvent(payload);
266
+ if (event !== 'postToolUse' && event !== 'afterMCPExecution') return false;
267
+ if (toolCallFailed(payload)) return false;
268
+
269
+ const toolName = typeof payload?.tool_name === 'string' ? payload.tool_name : '';
270
+ if (!RECORD_WIP_TOOL_RE.test(toolName)) return false;
271
+ return true;
272
+ }
273
+
274
+ export function isReviewDocWriteSuccess(payload) {
275
+ const event = normalizeHookEvent(payload);
276
+ if (event !== 'postToolUse' && event !== 'afterFileEdit') return false;
277
+ if (toolCallFailed(payload)) return false;
278
+
279
+ if (event === 'afterFileEdit') {
280
+ return isReviewDocPath(payload?.file_path);
281
+ }
282
+
283
+ const toolName = payload?.tool_name;
284
+ if (toolName !== 'Write' && toolName !== 'WriteFile' && toolName !== 'Edit') return false;
285
+ return isReviewDocPath(payload?.tool_input?.file_path);
286
+ }
287
+
288
+ export function isSessionEndEvent(payload) {
289
+ return normalizeHookEvent(payload) === 'sessionEnd';
290
+ }
291
+
292
+ export function wipStateUpdatedAfter(armedAtIso) {
293
+ if (!armedAtIso) return false;
294
+ const armedMs = Date.parse(armedAtIso);
295
+ if (!Number.isFinite(armedMs)) return false;
296
+ // Since the repo split the ledger heyspex_record_wip writes lives in the VAULT.
297
+ // Watching only repoRoot() here meant a recorded WIP never satisfied the arm, so
298
+ // the ship-close never fired. Check every base that can hold a ledger.
299
+ for (const root of specBaseRoots()) {
300
+ try {
301
+ const raw = fs.readFileSync(path.join(root, '.heyspex', 'wip-state.json'), 'utf8');
302
+ const data = JSON.parse(raw);
303
+ if (!data || typeof data !== 'object') continue;
304
+ for (const entry of Object.values(data)) {
305
+ const updatedAt = entry?.updatedAt;
306
+ if (typeof updatedAt === 'string' && Date.parse(updatedAt) > armedMs) return true;
307
+ }
308
+ } catch {
309
+ // missing or unreadable at this root — try the next one
310
+ }
311
+ }
312
+ return false;
313
+ }
314
+
315
+ export function shouldCloseNow() {
316
+ const pending = readPendingTerminalClose();
317
+ if (!pending?.armedAt) return false;
318
+ return wipStateUpdatedAfter(pending.armedAt);
319
+ }
320
+
321
+ export function scheduleAgentHostExit(startPid = process.ppid) {
322
+ if (!Number.isFinite(startPid) || startPid <= 0) {
323
+ debugLog('kill_skipped', { reason: 'invalid_start_pid', startPid });
324
+ return;
325
+ }
326
+
327
+ debugLog('kill_scheduled', { startPid });
328
+
329
+ if (process.platform === 'win32') {
330
+ const ps = [
331
+ '-NoProfile',
332
+ '-WindowStyle',
333
+ 'Hidden',
334
+ '-Command',
335
+ `
336
+ $ErrorActionPreference = 'SilentlyContinue'
337
+ $current = ${startPid}
338
+ for ($i = 0; $i -lt 40; $i++) {
339
+ $p = Get-CimInstance Win32_Process -Filter "ProcessId=$current"
340
+ if (-not $p) { break }
341
+ $name = [string]$p.Name
342
+ $cmd = [string]$p.CommandLine
343
+ if ($name -ieq 'claude.exe') { Stop-Process -Id $current -Force; exit 0 }
344
+ if ($cmd -match 'cursor-agent|@anthropic-ai/claude-code|\\\\cursor-agent\\\\') { Stop-Process -Id $current -Force; exit 0 }
345
+ $parent = [int]$p.ParentProcessId
346
+ if ($parent -le 0 -or $parent -eq $current) { break }
347
+ $current = $parent
348
+ }
349
+ Stop-Process -Id ${startPid} -Force -ErrorAction SilentlyContinue
350
+ `.trim(),
351
+ ];
352
+ try {
353
+ const child = spawn('powershell.exe', ps, {
354
+ detached: true,
355
+ stdio: 'ignore',
356
+ windowsHide: true,
357
+ });
358
+ child.unref();
359
+ } catch (err) {
360
+ debugLog('kill_failed', { message: err instanceof Error ? err.message : String(err) });
361
+ }
362
+ return;
363
+ }
364
+
365
+ const sh = `pid=${startPid}; for i in $(seq 1 30); do
366
+ line=$(ps -p "$pid" -o comm= 2>/dev/null) || break
367
+ case "$line" in *claude*|*Claude*) kill -TERM "$pid" 2>/dev/null; exit 0 ;; esac
368
+ cmd=$(ps -p "$pid" -o args= 2>/dev/null || true)
369
+ case "$cmd" in *cursor-agent*) kill -TERM "$pid" 2>/dev/null; exit 0 ;; esac
370
+ ppid=$(ps -p "$pid" -o ppid= 2>/dev/null | tr -d ' ')
371
+ [ -z "$ppid" ] || [ "$ppid" = "0" ] || [ "$ppid" = "$pid" ] && break
372
+ pid=$ppid
373
+ done
374
+ kill -TERM ${startPid} 2>/dev/null || true`;
375
+ try {
376
+ const child = spawn('sh', ['-c', sh], { detached: true, stdio: 'ignore' });
377
+ child.unref();
378
+ } catch (err) {
379
+ debugLog('kill_failed', { message: err instanceof Error ? err.message : String(err) });
380
+ }
381
+ }
382
+
383
+ export function tryCloseIfReady(reason) {
384
+ if (!resolveCloseTerminalOnTaskCompleteFromSettings()) {
385
+ debugLog('close_skipped', { reason: 'setting_disabled', trigger: reason });
386
+ return false;
387
+ }
388
+ if (!isPendingTerminalCloseArmed()) {
389
+ debugLog('close_skipped', { reason: 'not_armed', trigger: reason });
390
+ return false;
391
+ }
392
+ if (!shouldCloseNow() && reason !== 'session_end' && reason !== 'record_wip') {
393
+ debugLog('close_skipped', { reason: 'wip_not_updated', trigger: reason });
394
+ return false;
395
+ }
396
+ clearPendingTerminalClose();
397
+ debugLog('closing', { trigger: reason });
398
+ scheduleAgentHostExit();
399
+ return true;
400
+ }
401
+
402
+ export async function readStdin() {
403
+ if (process.stdin.isTTY) return null;
404
+ return new Promise((resolve) => {
405
+ let buf = '';
406
+ let resolved = false;
407
+ const finish = (value) => {
408
+ if (resolved) return;
409
+ resolved = true;
410
+ resolve(value);
411
+ };
412
+ const timer = setTimeout(() => finish(buf || null), 500);
413
+ process.stdin.setEncoding('utf8');
414
+ process.stdin.on('data', (chunk) => {
415
+ buf += chunk;
416
+ });
417
+ process.stdin.on('end', () => {
418
+ clearTimeout(timer);
419
+ finish(buf || null);
420
+ });
421
+ process.stdin.on('error', () => {
422
+ clearTimeout(timer);
423
+ finish(buf || null);
424
+ });
425
+ });
426
+ }
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ // Spawn-side fallback when hooks miss ship/close events (external cursor-agent terminal).
3
+ // Polls claimed spec status + wip-state.json; kills the agent host when close-out lands.
4
+ //
5
+ // Started detached from spawnNewAgent when auto-close is enabled.
6
+ // Mute: HEYSPEX_AUTO_CLOSE_TERMINAL=0 or HEYSPEX_CLOSE_TERMINAL_ON_SHIP_OFF=1
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ import {
13
+ armPendingTerminalClose,
14
+ debugLog,
15
+ clearPendingTerminalClose,
16
+ isPendingTerminalCloseArmed,
17
+ readPendingTerminalClose,
18
+ repoRoot,
19
+ resolveCloseTerminalOnTaskCompleteFromSettings,
20
+ specBaseRoots,
21
+ scheduleAgentHostExit,
22
+ shouldCloseNow,
23
+ STATUS_CLOSED_RE,
24
+ STATUS_REVIEW_RE,
25
+ } from './terminal-close-shared.mjs';
26
+
27
+ const POLL_MS = 3000;
28
+ const MAX_MS = 8 * 60 * 60 * 1000;
29
+
30
+ function readActiveClaimSpecId() {
31
+ try {
32
+ const raw = fs.readFileSync(path.join(repoRoot(), '.heyspex', 'active-claims.json'), 'utf8');
33
+ const claims = JSON.parse(raw);
34
+ const session =
35
+ process.env.CURSOR_TRACE_ID ??
36
+ process.env.CLAUDE_CODE_SESSION_ID ??
37
+ process.env.HEYSPEX_SESSION_ID ??
38
+ '';
39
+ if (!Array.isArray(claims)) return null;
40
+ const mine = claims.find(
41
+ (c) =>
42
+ c &&
43
+ (session
44
+ ? c.sessionId === session || c.session_id === session
45
+ : c.agent === process.env.HEYSPEX_AGENT),
46
+ );
47
+ return mine?.specId ?? mine?.spec_id ?? null;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ function findSpecFile(specId) {
54
+ for (const root of specBaseRoots()) {
55
+ for (const dir of ['specs/tasks', 'specs/epics']) {
56
+ const folder = path.join(root, dir);
57
+ let names;
58
+ try {
59
+ names = fs.readdirSync(folder);
60
+ } catch {
61
+ continue;
62
+ }
63
+ const hit = names.find((n) => n.startsWith(`${specId}-`) || n.startsWith(specId));
64
+ if (hit) return path.join(folder, hit);
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+
70
+ function readSpecStatus(specPath) {
71
+ try {
72
+ const raw = fs.readFileSync(specPath, 'utf8');
73
+ const m = raw.match(/^\s*status:\s*(\S+)\s*$/m);
74
+ return m?.[1] ?? null;
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ function isShippedStatus(status) {
81
+ return status === 'in_review' || status === 'closed' || status === 'review_pending';
82
+ }
83
+
84
+ function pollOnce() {
85
+ if (!resolveCloseTerminalOnTaskCompleteFromSettings()) return false;
86
+
87
+ const specId = readActiveClaimSpecId();
88
+ if (!specId) return false;
89
+
90
+ const specPath = findSpecFile(specId);
91
+ if (!specPath) return false;
92
+
93
+ const status = readSpecStatus(specPath);
94
+ if (!isShippedStatus(status)) return false;
95
+
96
+ if (!isPendingTerminalCloseArmed()) {
97
+ armPendingTerminalClose(null, `watcher:${status}`);
98
+ }
99
+
100
+ if (shouldCloseNow()) {
101
+ debugLog('watcher_close', { specId, status });
102
+ clearPendingTerminalClose();
103
+ scheduleAgentHostExit();
104
+ return true;
105
+ }
106
+
107
+ const pending = readPendingTerminalClose();
108
+ if (pending?.armedAt) {
109
+ debugLog('watcher_waiting_wip', { specId, status, armedAt: pending.armedAt });
110
+ }
111
+ return false;
112
+ }
113
+
114
+ const started = Date.now();
115
+ debugLog('watcher_start', { pid: process.pid });
116
+
117
+ while (Date.now() - started < MAX_MS) {
118
+ if (pollOnce()) break;
119
+ await new Promise((r) => setTimeout(r, POLL_MS));
120
+ }
121
+
122
+ debugLog('watcher_exit', {});