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,623 @@
1
+ // Research tool handlers: heyspex_save_research, promote_research,
2
+ // promote_research_to_diagnose, split_research, complete_research, active_research
3
+ // (the execute-level logic lives in research-tools.mjs and siblings). Extracted from cli.mjs under
4
+ // [[HS-BLNF-6W61-DIAG-cli-mjs-has-grown-into-a-13-665-line]] (no behavior change).
5
+
6
+ import { executeCompleteResearch, loadResearchFocus } from './research-focus.mjs';
7
+ import { allocateRandomVaultId, collectOccupiedIdsFromSpecs } from '../../scripts/vault-id-lib.mjs';
8
+ import { findSpecFile, parseFrontmatter } from './vault-io.mjs';
9
+ import { executeSplitResearch } from './split-research.mjs';
10
+ import { executePromoteResearchToDiagnose } from './promote-research-to-diagnose.mjs';
11
+ import { executePromoteResearch } from './promote-research.mjs';
12
+ import { consoleFetch, enrichConsoleFailurePayloadAsync } from './console-transport.mjs';
13
+ import { consoleErrorCode, consoleErrorMessage } from './console-fetch.mjs';
14
+ import { randomUUID } from 'node:crypto';
15
+ import { basename, join, relative, isAbsolute, resolve, sep } from 'node:path';
16
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
17
+ import { readViaSelector, resolveReadTier, resolveVaultWriteMode } from './read-path.mjs';
18
+ import { detectAgentIdentity, getConsoleUrl } from './mcp-config.mjs';
19
+ import { collectOccupiedKeysEitherKind, mintFamilyKey, mintFileKey, parseSpecId } from './spec-id-lib.mjs';
20
+ import { yamlScalar } from './yaml-scalar.mjs';
21
+ import { measureDiskLanding } from './tool-helpers.mjs';
22
+ import { assertPathUnderRoot } from '../../scripts/lib/shared-path-helpers.mjs';
23
+
24
+
25
+ /**
26
+ * /hs-promote-research — research → milestone promotion with AI-assisted epic
27
+ * stubs. Implementation lives in `promote-research.mjs`; this wrapper supplies
28
+ * the console-fetch dep so the AI proposer call routes through console (Haiku
29
+ * 4.5 via tool-use, matching [[HS-TASK-TAOD]]'s pattern).
30
+ */
31
+ /**
32
+ * Cloud-tier reads ([[HS-KR16-CWPX-TASK-promotion-reads-through-the-cloud-tier]]):
33
+ * `tier` is resolved ONCE per call here (`resolveReadTier()`) and threaded
34
+ * into `executePromoteResearch`'s deps — never re-resolved downstream.
35
+ * `overrides` lets a test inject `{ tier, consoleUrl, consoleFetch,
36
+ * resolveReadTier }` directly so it never opens a socket; every production
37
+ * call site (tool-dispatch.mjs) passes none, so the real `resolveReadTier()`
38
+ * / `getConsoleUrl()` / `consoleFetch` run unchanged.
39
+ *
40
+ * Cloud-tier WRITES ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]):
41
+ * `writeMode` is resolved here on the same once-per-call rule —
42
+ * `resolveVaultWriteMode()` is the write-side twin of `resolveReadTier()`, and
43
+ * `handleSaveResearch` in this same file already trusts it. This is the seam
44
+ * that makes the cloud write path reachable: the execute phase defaults to
45
+ * `deps.writeMode ?? 'local'`, so without this line a cloud-reliant session
46
+ * would still mint ids locally and write milestone/epic files to a disk the
47
+ * console's index has never heard of.
48
+ */
49
+ export async function handlePromoteResearch(vaultRoot, args = {}, overrides = {}) {
50
+ try {
51
+ const doResolveReadTier = overrides.resolveReadTier ?? resolveReadTier;
52
+ const tier = overrides.tier ?? (await doResolveReadTier());
53
+ const doResolveWriteMode = overrides.resolveVaultWriteMode ?? resolveVaultWriteMode;
54
+ const writeMode = overrides.writeMode ?? (await doResolveWriteMode());
55
+ const consoleUrl = overrides.consoleUrl ?? getConsoleUrl();
56
+ const doConsoleFetch = overrides.consoleFetch ?? consoleFetch;
57
+ const result = await executePromoteResearch(vaultRoot, args, {
58
+ consoleFetch: (path, options) => doConsoleFetch(path, options),
59
+ tier,
60
+ writeMode,
61
+ consoleUrl,
62
+ });
63
+ if (result?.error) {
64
+ return {
65
+ isError: true,
66
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
67
+ };
68
+ }
69
+ return {
70
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
71
+ };
72
+ } catch (err) {
73
+ return {
74
+ isError: true,
75
+ content: [
76
+ {
77
+ type: 'text',
78
+ text: JSON.stringify(
79
+ {
80
+ error: 'PROMOTE_RESEARCH_EXCEPTION',
81
+ message: err?.message ?? String(err),
82
+ },
83
+ null,
84
+ 2,
85
+ ),
86
+ },
87
+ ],
88
+ };
89
+ }
90
+ }
91
+
92
+ /**
93
+ * heyspex_promote_research_to_diagnose — one research investigation → one
94
+ * HS-DIAG-* diagnose spec ([[HS-MSHY-TASK-research-diagnose-link-send-bugs-to-the]]).
95
+ * Research-side sibling of handlePromoteCaptureToDiagnose. Single-phase;
96
+ * writes the diagnose + the research's produces_diagnose linkage
97
+ * transactionally.
98
+ */
99
+ export async function handlePromoteResearchToDiagnose(vaultRoot, args = {}) {
100
+ try {
101
+ const result = await executePromoteResearchToDiagnose(vaultRoot, args, {});
102
+ if (result?.error) {
103
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
104
+ }
105
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
106
+ } catch (err) {
107
+ return {
108
+ isError: true,
109
+ content: [
110
+ {
111
+ type: 'text',
112
+ text: JSON.stringify(
113
+ { error: 'PROMOTE_RESEARCH_TO_DIAGNOSE_EXCEPTION', message: err?.message ?? String(err) },
114
+ null,
115
+ 2,
116
+ ),
117
+ },
118
+ ],
119
+ };
120
+ }
121
+ }
122
+
123
+ /**
124
+ * /hs-split-research — split a research doc into N child research docs.
125
+ * Implementation lives in `split-research.mjs`; single-phase tool (no AI),
126
+ * caller provides body_slice + sources_captures partitions.
127
+ *
128
+ * `tier` / `writeMode` are resolved HERE, once per call, and threaded down —
129
+ * the same rule {@link handlePromoteResearch} follows. In cloud write mode the
130
+ * children are POSTed first and every sibling cross-link is PUT against the
131
+ * basenames those POSTs returned.
132
+ */
133
+ export async function handleSplitResearch(vaultRoot, args = {}, overrides = {}) {
134
+ try {
135
+ const doResolveReadTier = overrides.resolveReadTier ?? resolveReadTier;
136
+ const tier = overrides.tier ?? (await doResolveReadTier());
137
+ const doResolveWriteMode = overrides.resolveVaultWriteMode ?? resolveVaultWriteMode;
138
+ const writeMode = overrides.writeMode ?? (await doResolveWriteMode());
139
+ const consoleUrl = overrides.consoleUrl ?? getConsoleUrl();
140
+ const doConsoleFetch = overrides.consoleFetch ?? consoleFetch;
141
+ const result = await executeSplitResearch(vaultRoot, args, {
142
+ consoleFetch: (path, options) => doConsoleFetch(path, options),
143
+ tier,
144
+ writeMode,
145
+ consoleUrl,
146
+ });
147
+ if (result?.error) {
148
+ return {
149
+ isError: true,
150
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
151
+ };
152
+ }
153
+ return {
154
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
155
+ };
156
+ } catch (err) {
157
+ return {
158
+ isError: true,
159
+ content: [
160
+ {
161
+ type: 'text',
162
+ text: JSON.stringify(
163
+ {
164
+ error: 'SPLIT_RESEARCH_EXCEPTION',
165
+ message: err?.message ?? String(err),
166
+ },
167
+ null,
168
+ 2,
169
+ ),
170
+ },
171
+ ],
172
+ };
173
+ }
174
+ }
175
+
176
+ export function handleActiveResearch(vaultRoot) {
177
+ const focus = loadResearchFocus(vaultRoot);
178
+ if (!focus?.spec_path) {
179
+ return {
180
+ content: [
181
+ {
182
+ type: 'text',
183
+ text: JSON.stringify(
184
+ {
185
+ focus: null,
186
+ message:
187
+ 'No research pinned. Open an idea in console and click Promote on the doc toolbar, or pass source_idea_id + scope to heyspex_complete_research directly.',
188
+ },
189
+ null,
190
+ 2,
191
+ ),
192
+ },
193
+ ],
194
+ };
195
+ }
196
+ return {
197
+ content: [{ type: 'text', text: JSON.stringify({ focus }, null, 2) }],
198
+ };
199
+ }
200
+
201
+ export function handleCompleteResearch(vaultRoot, args) {
202
+ const result = executeCompleteResearch(vaultRoot, args, {
203
+ allocateRandomVaultId,
204
+ findSpecFile,
205
+ });
206
+ if (result.error) {
207
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(result) }] };
208
+ }
209
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
210
+ }
211
+
212
+ // `research_type` enum — mirrors schemas/heyspex-frontmatter.schema.json and
213
+ // [[entity-model]] §research ([[HS-120I-TASK-implement-research-type-field-end-to-end]]).
214
+ export const RESEARCH_TYPE_VALUES = ['new-feature', 'enhancement', 'diagnose', 'code-audit', 'reference'];
215
+
216
+ export const DEFAULT_RESEARCH_TYPE = 'reference';
217
+
218
+ // `scope` enum — mirrors schemas/heyspex-frontmatter.schema.json and
219
+ // [[entity-model]] §research ([[HS-NGD1-TASK-consolidate-and-fix-the-research-scope-field]]).
220
+ // Same values + same default as `heyspex_promote_capture` and `heyspex_complete_research`.
221
+ export const RESEARCH_SCOPE_VALUES = ['internal', 'codebase', 'external', 'all'];
222
+
223
+ export const DEFAULT_RESEARCH_SCOPE = 'internal';
224
+
225
+ export async function handleSaveResearch(vaultRoot, args) {
226
+ const { title, summary, content, tags = [], related = [], sources = [], research_type, scope } = args;
227
+
228
+ if (!title || !summary || !content) {
229
+ return {
230
+ isError: true,
231
+ content: [{ type: 'text', text: JSON.stringify({ error: 'MISSING_REQUIRED_FIELDS', message: 'title, summary, and content are required' }) }],
232
+ };
233
+ }
234
+
235
+ // Optional, defaults to 'reference' — never written blank. Seam for a future
236
+ // kind-based suggestion is the `research_type` arg itself (a later task can
237
+ // pass a computed suggestion here); no inference is performed by this tool
238
+ // (heyspex_save_research has no source capture to derive a kind from —
239
+ // unlike heyspex_promote_capture, which does, see research-type-suggest.mjs).
240
+ if (research_type !== undefined && research_type !== null && !RESEARCH_TYPE_VALUES.includes(research_type)) {
241
+ return {
242
+ isError: true,
243
+ content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: `research_type ${JSON.stringify(research_type)} must be one of: ${RESEARCH_TYPE_VALUES.join(', ')}` }) }],
244
+ };
245
+ }
246
+ // HS-MVC1 AC3: track whether the caller supplied research_type at all, so
247
+ // the response can carry a NON-BLOCKING warning when it defaulted (save
248
+ // still proceeds — modeled on the `warning: "TARGET_RESEARCH_CLOSED"`
249
+ // pattern in promote-capture.mjs; this is not a hard gate).
250
+ const researchTypeWasOmitted = research_type === undefined || research_type === null;
251
+ const resolvedResearchType = RESEARCH_TYPE_VALUES.includes(research_type) ? research_type : DEFAULT_RESEARCH_TYPE;
252
+
253
+ // Optional, defaults to 'internal' — never written blank. NOT required (consistency
254
+ // fix only, no grace window) — [[HS-NGD1-TASK-consolidate-and-fix-the-research-scope-field]].
255
+ if (scope !== undefined && scope !== null && !RESEARCH_SCOPE_VALUES.includes(scope)) {
256
+ return {
257
+ isError: true,
258
+ content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: `scope ${JSON.stringify(scope)} must be one of: ${RESEARCH_SCOPE_VALUES.join(', ')}` }) }],
259
+ };
260
+ }
261
+ const resolvedScope = RESEARCH_SCOPE_VALUES.includes(scope) ? scope : DEFAULT_RESEARCH_SCOPE;
262
+
263
+ // Vault-root path guard (HS-AI1T) — in BOTH write modes, before any tier or
264
+ // network resolution: a relative / drive-relative root is refused before
265
+ // anything touches disk. The directory itself is created only on the local
266
+ // write path below (a cloud-reliant machine may have no vault clone at all,
267
+ // and must not grow a phantom `specs/research/` on every save).
268
+ const researchDir = join(vaultRoot, 'specs', 'research');
269
+ if (!existsSync(researchDir)) {
270
+ try {
271
+ assertPathUnderRoot(vaultRoot, researchDir, 'research-handlers write');
272
+ } catch (err) {
273
+ return {
274
+ isError: true,
275
+ content: [{ type: 'text', text: JSON.stringify({ error: 'BAD_VAULT_ROOT', message: err.message }) }],
276
+ };
277
+ }
278
+ }
279
+
280
+ // Shared between the two write paths: the acting agent (`assisted_by`
281
+ // auto-stamp, [[HS-EPIC-IKIM-authorship-provenance-frontmatter-owner]] AC #2),
282
+ // the cleaned `related` ids, and the markdown body.
283
+ const actingAgent = detectAgentIdentity(args.agent, args.session_id).agent;
284
+ const relatedIds = related.map((r) => String(r).replace(/^\[\[|\]\]$/g, ''));
285
+ const bodyMarkdown = buildResearchBodyMarkdown({ title, summary, content, sources });
286
+
287
+ // WHERE THE WRITE GOES ([[HS-DJWP-MCPC-TASK]] closed the read half; this is
288
+ // the write half). `vaultWriteMode` comes from the same dev-mode resolver
289
+ // every read tier trusts: `cloud` for prod and dev+cloud, `local` only for
290
+ // dev+local. A disk write in cloud mode is a split brain — a file the
291
+ // console's index has never heard of, on a machine that may not even hold a
292
+ // clone — so cloud mode files through the console exactly like
293
+ // heyspex_file_spec does (POST /api/specs, server-minted id).
294
+ const writeMode = await resolveVaultWriteMode();
295
+ if (writeMode === 'cloud') {
296
+ return saveResearchViaConsole({
297
+ vaultRoot,
298
+ title,
299
+ summary,
300
+ tags,
301
+ relatedIds,
302
+ researchType: resolvedResearchType,
303
+ scope: resolvedScope,
304
+ bodyMarkdown,
305
+ actingAgent,
306
+ researchTypeWasOmitted,
307
+ });
308
+ }
309
+
310
+ if (!existsSync(researchDir)) {
311
+ mkdirSync(researchDir, { recursive: true });
312
+ }
313
+
314
+ // [[HS-DJWP-MCPC-TASK]] "Occupied-id scans" family: id allocation needs the
315
+ // occupied set BEFORE the write — in cloud reliance that comes from `GET
316
+ // /api/vault/occupied-ids?bucket=research` instead of scanning
317
+ // `specs/research` on disk.
318
+ const tier = await resolveReadTier();
319
+ const consoleUrl = getConsoleUrl();
320
+ let occupiedResearch;
321
+ try {
322
+ occupiedResearch = await readViaSelector({
323
+ tier,
324
+ diskRead: () => {
325
+ const existingFiles = readdirSync(researchDir).filter(f => f.endsWith('.md') && !f.endsWith('.review.md'));
326
+ const scanSpecs = [];
327
+ for (const file of existingFiles) {
328
+ try {
329
+ const filePath = join(researchDir, file);
330
+ const fileContent = readFileSync(filePath, 'utf8');
331
+ const { frontmatter } = parseFrontmatter(fileContent);
332
+ scanSpecs.push({
333
+ id: String(frontmatter.id ?? ''),
334
+ basename: file.replace(/\.md$/i, ''),
335
+ type: 'research',
336
+ });
337
+ } catch {
338
+ /* skip unreadable */
339
+ }
340
+ }
341
+ return collectOccupiedIdsFromSpecs(scanSpecs).research;
342
+ },
343
+ neonRead: async () => {
344
+ const res = await consoleFetch('/api/vault/occupied-ids?bucket=research');
345
+ if (!res.ok) {
346
+ throw new Error(`GET /api/vault/occupied-ids?bucket=research returned ${res.status}: ${res.error || ''}`);
347
+ }
348
+ return new Set(Array.isArray(res.body?.ids) ? res.body.ids : []);
349
+ },
350
+ routeLabel: 'GET /api/vault/occupied-ids?bucket=research',
351
+ consoleUrl,
352
+ });
353
+ } catch (err) {
354
+ return {
355
+ isError: true,
356
+ content: [{ type: 'text', text: JSON.stringify({ error: 'CONSOLE_UNREACHABLE_CLOUD_MODE', message: String(err?.message ?? err) }) }],
357
+ };
358
+ }
359
+ // Era-3 file-key-first ([[naming-v3-7-rulings-hs-namespace-kept]]): the
360
+ // FAMILY token still comes from the legacy allocator's `HS-<family>-RES`
361
+ // draw (parentless research — no family to inherit); `id:` keys onto the
362
+ // file key alone.
363
+ const allocatedFamilyId = allocateRandomVaultId('research', occupiedResearch);
364
+ const familyKey = parseSpecId(allocatedFamilyId)?.familyKey ?? mintFamilyKey(vaultRoot);
365
+ // Per-file FILE KEY (owner ruling 2026-07-10) — filename + `file_id:` SSOT,
366
+ // and (V3) the anchor for `id:` itself.
367
+ // Disk occupancy ([[HS-FJWL]]): cloud write mode returned above (the
368
+ // `writeMode === 'cloud'` branch files through the console, which mints the
369
+ // id itself), so this line only ever runs on a disk-backed vault.
370
+ const fileKey = mintFileKey(collectOccupiedKeysEitherKind(vaultRoot));
371
+ const researchId = `HS-${fileKey}`;
372
+
373
+ const slug = researchSlug(title);
374
+
375
+ const filename = `${researchId}-${familyKey}-RES-${slug}.md`;
376
+ const filepath = join(researchDir, filename);
377
+
378
+ // Build frontmatter
379
+ const today = new Date().toISOString().split('T')[0];
380
+ const relatedLinks = relatedIds.map((cleanId) => ` - "[[${cleanId}]]"`).join('\n');
381
+ const tagsArray = tags.length > 0 ? `[${tags.join(', ')}]` : '[]';
382
+
383
+ // `assisted_by` auto-stamp: a research doc filed via MCP is agent-created, so
384
+ // stamp the acting agent. `owner` stays the vault owner (no authenticated user
385
+ // on this direct-disk path). Omit-when-empty: no line when no agent is detected.
386
+ const assistedByBlock = actingAgent ? `assisted_by:\n - ${yamlScalar(actingAgent)}\n` : '';
387
+
388
+ const fileContent = `---
389
+ id: ${researchId}
390
+ file_id: ${fileKey}
391
+ family: ${familyKey}
392
+ name: ${yamlScalar(title)}
393
+ type: research
394
+ research_type: ${resolvedResearchType}
395
+ scope: ${resolvedScope}
396
+ status: open
397
+ owner: pwher
398
+ created: ${today}
399
+ updated: ${today}
400
+ ${assistedByBlock}tags: ${tagsArray}
401
+ ${relatedLinks ? `related:\n${relatedLinks}` : 'related: []'}
402
+ ---
403
+
404
+ ${bodyMarkdown}`;
405
+
406
+ try {
407
+ writeFileSync(filepath, fileContent, 'utf8');
408
+ } catch (e) {
409
+ return {
410
+ isError: true,
411
+ content: [{ type: 'text', text: JSON.stringify({ error: 'WRITE_FAILED', message: e.message }) }],
412
+ };
413
+ }
414
+
415
+ // Report on the RESULT, not the request ([[HS-IOJW-5YDH-DIAG]] sweep): stat
416
+ // the file before claiming it saved.
417
+ const landed = measureDiskLanding(vaultRoot, filepath);
418
+ if (!landed.on_disk) {
419
+ return {
420
+ isError: true,
421
+ content: [{
422
+ type: 'text',
423
+ text: JSON.stringify({
424
+ error: 'WRITE_NOT_ON_DISK',
425
+ message: `writeFileSync reported success but no file exists at ${filepath}. Treat the research as unsaved.`,
426
+ research_id: researchId,
427
+ landed,
428
+ }, null, 2),
429
+ }],
430
+ };
431
+ }
432
+
433
+ const result = {
434
+ success: true,
435
+ research_id: researchId,
436
+ filename,
437
+ filepath: relative(vaultRoot, filepath),
438
+ landed,
439
+ message: `Research saved to specs/research/${filename}`,
440
+ next_steps: researchNextSteps(slug),
441
+ };
442
+
443
+ // HS-MVC1 AC3: non-blocking warning — the save already succeeded above
444
+ // (resolvedResearchType was written to disk either way); this only informs
445
+ // the caller that research_type was not supplied so it defaulted, rather
446
+ // than silently going unnoticed.
447
+ if (researchTypeWasOmitted) attachResearchTypeWarning(result);
448
+
449
+ return {
450
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
451
+ };
452
+ }
453
+
454
+ /** The filename slug — same rule on both write paths. */
455
+ function researchSlug(title) {
456
+ return String(title)
457
+ .toLowerCase()
458
+ .replace(/[^a-z0-9]+/g, '-')
459
+ .replace(/^-|-$/g, '')
460
+ .slice(0, 40);
461
+ }
462
+
463
+ /** Markdown after the frontmatter: title, summary, content, optional sources. */
464
+ export function buildResearchBodyMarkdown({ title, summary, content, sources = [] }) {
465
+ let sourcesSection = '';
466
+ if (sources.length > 0) {
467
+ sourcesSection = '\n## Sources\n\n';
468
+ for (const src of sources) {
469
+ if (src.url && src.title) {
470
+ sourcesSection += `- [${src.title}](${src.url})${src.description ? ` — ${src.description}` : ''}\n`;
471
+ }
472
+ }
473
+ }
474
+ return `# ${title}\n\n${summary}\n\n${content}\n${sourcesSection}\n`;
475
+ }
476
+
477
+ function researchNextSteps(linkTarget) {
478
+ return [
479
+ `Add [[${linkTarget}]] wikilinks to related specs`,
480
+ `Update the research file as implementation progresses`,
481
+ // Research is evergreen ([[research-is-evergreen]]) — it never closes.
482
+ `Add \`enhances:\` to bind to a milestone (or \`bound_epic:\` when an epic is filed) — research stays status: open`,
483
+ ];
484
+ }
485
+
486
+ // HS-MVC1 AC3: non-blocking warning — the save already succeeded; this only
487
+ // informs the caller that research_type was not supplied so it defaulted.
488
+ function attachResearchTypeWarning(result) {
489
+ result.warning = 'RESEARCH_TYPE_MISSING';
490
+ result.warning_message = `research_type was not supplied — defaulted to "${DEFAULT_RESEARCH_TYPE}". Consider passing an explicit research_type (${RESEARCH_TYPE_VALUES.join(', ')}) next time; this does not block the save.`;
491
+ }
492
+
493
+ /**
494
+ * Cloud write mode: file the research doc through the console, the same call
495
+ * heyspex_file_spec makes (filing-tools.mjs `handleFileSpec` — POST /api/specs,
496
+ * idempotency-key header, no timeout retry against a remote console per
497
+ * [[HS-2FQY]]'s double-mint guard). The console mints the id and the family
498
+ * (`createSpec`: `research` -> `specs/research/`, `RES`), so nothing is
499
+ * allocated client-side and no occupied-id scan is needed.
500
+ *
501
+ * Returns the SAME result shape as the disk path — `success`, `research_id`,
502
+ * `filename`, `filepath`, `landed`, `message`, `next_steps` (+ the
503
+ * research_type warning) — so callers and the `/hs-res` flow read one shape.
504
+ * `filepath` is the console's vault-relative path (`specs/research/...`);
505
+ * `landed.tier` is `cloud` and `landed.on_disk` is `false`, by construction:
506
+ * nothing was written to this machine.
507
+ */
508
+ async function saveResearchViaConsole({
509
+ vaultRoot,
510
+ title,
511
+ summary,
512
+ tags,
513
+ relatedIds,
514
+ researchType,
515
+ scope,
516
+ bodyMarkdown,
517
+ actingAgent,
518
+ researchTypeWasOmitted,
519
+ }) {
520
+ // No `status:` — the console seeds `open` and refuses an out-of-band status
521
+ // on create ([[HS-ISZP]]); only the disk path writes it itself.
522
+ const frontmatter = {
523
+ research_type: researchType,
524
+ scope,
525
+ ...(tags.length > 0 ? { tags } : {}),
526
+ ...(relatedIds.length > 0 ? { related: relatedIds.map((id) => `[[${id}]]`) } : {}),
527
+ };
528
+ const body = {
529
+ type: 'research',
530
+ name: title,
531
+ summary,
532
+ frontmatter,
533
+ body: bodyMarkdown,
534
+ ...(actingAgent ? { acting_agent: actingAgent } : {}),
535
+ };
536
+
537
+ let result;
538
+ try {
539
+ result = await consoleFetch('/api/specs', {
540
+ method: 'POST',
541
+ body,
542
+ headers: { 'idempotency-key': randomUUID() },
543
+ retryOnTimeout: false,
544
+ });
545
+ } catch (err) {
546
+ return {
547
+ isError: true,
548
+ content: [{ type: 'text', text: JSON.stringify({ error: 'SAVE_RESEARCH_FAILED', message: String(err?.message ?? err) }) }],
549
+ };
550
+ }
551
+
552
+ if (!result.ok) {
553
+ const payload = await enrichConsoleFailurePayloadAsync(result, {
554
+ error: consoleErrorCode(result, 'SAVE_RESEARCH_FAILED'),
555
+ message: consoleErrorMessage(result, `POST /api/specs returned ${result.status}`),
556
+ status: result.status,
557
+ url: result.url,
558
+ console_response: result.body,
559
+ attempted: { type: 'research', name: title },
560
+ write_mode: 'cloud',
561
+ ...(result.errorKind === 'timeout'
562
+ ? {
563
+ hint:
564
+ 'The create may still have completed server-side after this client timed out. '
565
+ + 'Search for a research doc with this exact name before re-saving — a blind re-save can mint a duplicate id ([[HS-2FQY]]).',
566
+ }
567
+ : {}),
568
+ });
569
+ return {
570
+ isError: true,
571
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
572
+ };
573
+ }
574
+
575
+ // POST returns { spec: { id, relPath, ... }, write, warnings? }.
576
+ const spec = result.body?.spec ?? null;
577
+ const researchId = typeof spec?.id === 'string' && spec.id ? spec.id : null;
578
+ const relPath = spec?.relPath ?? spec?.path ?? null;
579
+ if (!researchId) {
580
+ return {
581
+ isError: true,
582
+ content: [{
583
+ type: 'text',
584
+ text: JSON.stringify({
585
+ error: 'SAVE_RESEARCH_MALFORMED_RESPONSE',
586
+ message: `POST /api/specs answered ${result.status} but carried no spec.id — treat the research as unsaved and check the console before re-saving.`,
587
+ url: result.url,
588
+ console_response: result.body,
589
+ }, null, 2),
590
+ }],
591
+ };
592
+ }
593
+ const filename = relPath ? basename(String(relPath)) : null;
594
+ const consoleWrite = result.body?.write ?? null;
595
+ const consoleUrl = result.url ?? getConsoleUrl();
596
+ const landed = {
597
+ tier: 'cloud',
598
+ reported_by: consoleWrite ? 'console' : 'inferred_from_console_url',
599
+ console_url: consoleUrl,
600
+ rel_path: relPath,
601
+ abs_path: null,
602
+ on_disk: false,
603
+ in_local_vault: false,
604
+ vault_root: vaultRoot ?? null,
605
+ console_write: consoleWrite,
606
+ };
607
+ const out = {
608
+ success: true,
609
+ research_id: researchId,
610
+ filename,
611
+ filepath: relPath,
612
+ landed,
613
+ message: `Research saved through the console at ${consoleUrl}${relPath ? ` (${relPath})` : ''} — cloud write mode, nothing written to local disk`,
614
+ next_steps: researchNextSteps(filename ? filename.replace(/\.md$/i, '') : researchId),
615
+ ...(Array.isArray(result.body?.warnings) && result.body.warnings.length > 0
616
+ ? { console_warnings: result.body.warnings }
617
+ : {}),
618
+ };
619
+ if (researchTypeWasOmitted) attachResearchTypeWarning(out);
620
+ return {
621
+ content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
622
+ };
623
+ }
@@ -15,7 +15,7 @@
15
15
  * out of scope — that is [[HS-X5GJ-RES-EU6J]], a separate feature built on top of this fix).
16
16
  * - `heyspex_firecrawl_research` is the on-demand execution surface that makes the configured
17
17
  * Firecrawl backend REACHABLE from an interactive turn — it is a thin MCP wrapper around
18
- * dev-factory/dream-research/interactive-firecrawl-tool.mjs, which itself wraps the
18
+ * dev-factory/spexcrawl/interactive-firecrawl-tool.mjs, which itself wraps the
19
19
  * existing, self-tested default-backends.mjs/firecrawl-adapter.mjs implementation (no
20
20
  * search/scrape logic is reimplemented here).
21
21
  *
@@ -24,7 +24,7 @@
24
24
 
25
25
  import { listConfigLeaves } from './config-settings.mjs';
26
26
  // The deep-research backend is loaded LAZILY through the bridge, never imported
27
- // statically — a static import here pulls 25 SpexFleet / dream-research files into
27
+ // statically — a static import here pulls 25 SpexFleet / spexcrawl files into
28
28
  // the MCP server's package closure. See dev-factory-bridge.mjs for the full
29
29
  // rationale ([[HS-ZJSP-0JNK-TASK-ship-the-heyspex-mcp-server-as-an]]).
30
30
  import { loadDreamResearch } from './dev-factory-bridge.mjs';
@@ -65,7 +65,7 @@ export async function executeResolveResearchPlan(vaultRoot) {
65
65
  'reports an explicit { downgraded: true, reason } if the backend is unavailable — never substitute ' +
66
66
  'web-native silently. IMPORTANT (research-routing-hard-rule-sonnet, "Sonnet is the fallback, ' +
67
67
  'Firecrawl is the default"): once the fetch succeeds, hand the gathered text to DeepSeek for ' +
68
- 'synthesis (dev-factory/dream-research/synthesize-cli.mjs, or the `hs-res` skill for anything ' +
68
+ 'synthesis (dev-factory/spexcrawl/synthesize-cli.mjs, or the `hs-res` skill for anything ' +
69
69
  'beyond a single lookup) — do NOT reason over/summarize the results yourself. Sonnet doing its ' +
70
70
  'own synthesis is the fallback branch only, used after an explicit unreachability report and ' +
71
71
  'never silently.'
@@ -85,7 +85,7 @@ export async function executeResolveResearchPlan(vaultRoot) {
85
85
  /**
86
86
  * Run one Firecrawl search or scrape for an interactive agent turn (rung 0 `sequential`) —
87
87
  * the on-demand execution surface for whatever heyspex_resolve_research_plan resolved.
88
- * Delegates entirely to dev-factory/dream-research/interactive-firecrawl-tool.mjs; this
88
+ * Delegates entirely to dev-factory/spexcrawl/interactive-firecrawl-tool.mjs; this
89
89
  * function only maps the MCP snake_case args onto that module's contract.
90
90
  */
91
91
  export async function executeFirecrawlResearch(args = {}) {