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,625 @@
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 { consoleReadErrorFrom, readViaSelector, resolveReadTier, resolveVaultWriteMode, withDegradationScope } 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
+ let occupiedDegraded = null;
322
+ try {
323
+ ({ value: occupiedResearch, degraded: occupiedDegraded } = await withDegradationScope(() => readViaSelector({
324
+ tier,
325
+ diskRead: () => {
326
+ const existingFiles = readdirSync(researchDir).filter(f => f.endsWith('.md') && !f.endsWith('.review.md'));
327
+ const scanSpecs = [];
328
+ for (const file of existingFiles) {
329
+ try {
330
+ const filePath = join(researchDir, file);
331
+ const fileContent = readFileSync(filePath, 'utf8');
332
+ const { frontmatter } = parseFrontmatter(fileContent);
333
+ scanSpecs.push({
334
+ id: String(frontmatter.id ?? ''),
335
+ basename: file.replace(/\.md$/i, ''),
336
+ type: 'research',
337
+ });
338
+ } catch {
339
+ /* skip unreadable */
340
+ }
341
+ }
342
+ return collectOccupiedIdsFromSpecs(scanSpecs).research;
343
+ },
344
+ neonRead: async () => {
345
+ const res = await consoleFetch('/api/vault/occupied-ids?bucket=research');
346
+ if (!res.ok) {
347
+ throw consoleReadErrorFrom(res, `GET /api/vault/occupied-ids?bucket=research returned ${res.status}: ${res.error || ''}`, null);
348
+ }
349
+ return new Set(Array.isArray(res.body?.ids) ? res.body.ids : []);
350
+ },
351
+ routeLabel: 'GET /api/vault/occupied-ids?bucket=research',
352
+ consoleUrl,
353
+ })));
354
+ } catch (err) {
355
+ return {
356
+ isError: true,
357
+ content: [{ type: 'text', text: JSON.stringify({ error: 'CONSOLE_UNREACHABLE_CLOUD_MODE', message: String(err?.message ?? err) }) }],
358
+ };
359
+ }
360
+ // Era-3 file-key-first ([[naming-v3-7-rulings-hs-namespace-kept]]): the
361
+ // FAMILY token still comes from the legacy allocator's `HS-<family>-RES`
362
+ // draw (parentless research — no family to inherit); `id:` keys onto the
363
+ // file key alone.
364
+ const allocatedFamilyId = allocateRandomVaultId('research', occupiedResearch);
365
+ const familyKey = parseSpecId(allocatedFamilyId)?.familyKey ?? mintFamilyKey(vaultRoot);
366
+ // Per-file FILE KEY (owner ruling 2026-07-10) — filename + `file_id:` SSOT,
367
+ // and (V3) the anchor for `id:` itself.
368
+ // Disk occupancy ([[HS-FJWL]]): cloud write mode returned above (the
369
+ // `writeMode === 'cloud'` branch files through the console, which mints the
370
+ // id itself), so this line only ever runs on a disk-backed vault.
371
+ const fileKey = mintFileKey(collectOccupiedKeysEitherKind(vaultRoot));
372
+ const researchId = `HS-${fileKey}`;
373
+
374
+ const slug = researchSlug(title);
375
+
376
+ const filename = `${researchId}-${familyKey}-RES-${slug}.md`;
377
+ const filepath = join(researchDir, filename);
378
+
379
+ // Build frontmatter
380
+ const today = new Date().toISOString().split('T')[0];
381
+ const relatedLinks = relatedIds.map((cleanId) => ` - "[[${cleanId}]]"`).join('\n');
382
+ const tagsArray = tags.length > 0 ? `[${tags.join(', ')}]` : '[]';
383
+
384
+ // `assisted_by` auto-stamp: a research doc filed via MCP is agent-created, so
385
+ // stamp the acting agent. `owner` stays the vault owner (no authenticated user
386
+ // on this direct-disk path). Omit-when-empty: no line when no agent is detected.
387
+ const assistedByBlock = actingAgent ? `assisted_by:\n - ${yamlScalar(actingAgent)}\n` : '';
388
+
389
+ const fileContent = `---
390
+ id: ${researchId}
391
+ file_id: ${fileKey}
392
+ family: ${familyKey}
393
+ name: ${yamlScalar(title)}
394
+ type: research
395
+ research_type: ${resolvedResearchType}
396
+ scope: ${resolvedScope}
397
+ status: open
398
+ owner: pwher
399
+ created: ${today}
400
+ updated: ${today}
401
+ ${assistedByBlock}tags: ${tagsArray}
402
+ ${relatedLinks ? `related:\n${relatedLinks}` : 'related: []'}
403
+ ---
404
+
405
+ ${bodyMarkdown}`;
406
+
407
+ try {
408
+ writeFileSync(filepath, fileContent, 'utf8');
409
+ } catch (e) {
410
+ return {
411
+ isError: true,
412
+ content: [{ type: 'text', text: JSON.stringify({ error: 'WRITE_FAILED', message: e.message }) }],
413
+ };
414
+ }
415
+
416
+ // Report on the RESULT, not the request ([[HS-IOJW-5YDH-DIAG]] sweep): stat
417
+ // the file before claiming it saved.
418
+ const landed = measureDiskLanding(vaultRoot, filepath);
419
+ if (!landed.on_disk) {
420
+ return {
421
+ isError: true,
422
+ content: [{
423
+ type: 'text',
424
+ text: JSON.stringify({
425
+ error: 'WRITE_NOT_ON_DISK',
426
+ message: `writeFileSync reported success but no file exists at ${filepath}. Treat the research as unsaved.`,
427
+ research_id: researchId,
428
+ landed,
429
+ }, null, 2),
430
+ }],
431
+ };
432
+ }
433
+
434
+ const result = {
435
+ success: true,
436
+ research_id: researchId,
437
+ filename,
438
+ filepath: relative(vaultRoot, filepath),
439
+ landed,
440
+ message: `Research saved to specs/research/${filename}`,
441
+ next_steps: researchNextSteps(slug),
442
+ };
443
+
444
+ // HS-MVC1 AC3: non-blocking warning — the save already succeeded above
445
+ // (resolvedResearchType was written to disk either way); this only informs
446
+ // the caller that research_type was not supplied so it defaulted, rather
447
+ // than silently going unnoticed.
448
+ if (researchTypeWasOmitted) attachResearchTypeWarning(result);
449
+ if (occupiedDegraded) result.degraded_warning = occupiedDegraded.warning;
450
+
451
+ return {
452
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
453
+ };
454
+ }
455
+
456
+ /** The filename slug — same rule on both write paths. */
457
+ function researchSlug(title) {
458
+ return String(title)
459
+ .toLowerCase()
460
+ .replace(/[^a-z0-9]+/g, '-')
461
+ .replace(/^-|-$/g, '')
462
+ .slice(0, 40);
463
+ }
464
+
465
+ /** Markdown after the frontmatter: title, summary, content, optional sources. */
466
+ export function buildResearchBodyMarkdown({ title, summary, content, sources = [] }) {
467
+ let sourcesSection = '';
468
+ if (sources.length > 0) {
469
+ sourcesSection = '\n## Sources\n\n';
470
+ for (const src of sources) {
471
+ if (src.url && src.title) {
472
+ sourcesSection += `- [${src.title}](${src.url})${src.description ? ` — ${src.description}` : ''}\n`;
473
+ }
474
+ }
475
+ }
476
+ return `# ${title}\n\n${summary}\n\n${content}\n${sourcesSection}\n`;
477
+ }
478
+
479
+ function researchNextSteps(linkTarget) {
480
+ return [
481
+ `Add [[${linkTarget}]] wikilinks to related specs`,
482
+ `Update the research file as implementation progresses`,
483
+ // Research is evergreen ([[research-is-evergreen]]) — it never closes.
484
+ `Add \`enhances:\` to bind to a milestone (or \`bound_epic:\` when an epic is filed) — research stays status: open`,
485
+ ];
486
+ }
487
+
488
+ // HS-MVC1 AC3: non-blocking warning — the save already succeeded; this only
489
+ // informs the caller that research_type was not supplied so it defaulted.
490
+ function attachResearchTypeWarning(result) {
491
+ result.warning = 'RESEARCH_TYPE_MISSING';
492
+ 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.`;
493
+ }
494
+
495
+ /**
496
+ * Cloud write mode: file the research doc through the console, the same call
497
+ * heyspex_file_spec makes (filing-tools.mjs `handleFileSpec` — POST /api/specs,
498
+ * idempotency-key header, no timeout retry against a remote console per
499
+ * [[HS-2FQY]]'s double-mint guard). The console mints the id and the family
500
+ * (`createSpec`: `research` -> `specs/research/`, `RES`), so nothing is
501
+ * allocated client-side and no occupied-id scan is needed.
502
+ *
503
+ * Returns the SAME result shape as the disk path — `success`, `research_id`,
504
+ * `filename`, `filepath`, `landed`, `message`, `next_steps` (+ the
505
+ * research_type warning) — so callers and the `/hs-res` flow read one shape.
506
+ * `filepath` is the console's vault-relative path (`specs/research/...`);
507
+ * `landed.tier` is `cloud` and `landed.on_disk` is `false`, by construction:
508
+ * nothing was written to this machine.
509
+ */
510
+ async function saveResearchViaConsole({
511
+ vaultRoot,
512
+ title,
513
+ summary,
514
+ tags,
515
+ relatedIds,
516
+ researchType,
517
+ scope,
518
+ bodyMarkdown,
519
+ actingAgent,
520
+ researchTypeWasOmitted,
521
+ }) {
522
+ // No `status:` — the console seeds `open` and refuses an out-of-band status
523
+ // on create ([[HS-ISZP]]); only the disk path writes it itself.
524
+ const frontmatter = {
525
+ research_type: researchType,
526
+ scope,
527
+ ...(tags.length > 0 ? { tags } : {}),
528
+ ...(relatedIds.length > 0 ? { related: relatedIds.map((id) => `[[${id}]]`) } : {}),
529
+ };
530
+ const body = {
531
+ type: 'research',
532
+ name: title,
533
+ summary,
534
+ frontmatter,
535
+ body: bodyMarkdown,
536
+ ...(actingAgent ? { acting_agent: actingAgent } : {}),
537
+ };
538
+
539
+ let result;
540
+ try {
541
+ result = await consoleFetch('/api/specs', {
542
+ method: 'POST',
543
+ body,
544
+ headers: { 'idempotency-key': randomUUID() },
545
+ retryOnTimeout: false,
546
+ });
547
+ } catch (err) {
548
+ return {
549
+ isError: true,
550
+ content: [{ type: 'text', text: JSON.stringify({ error: 'SAVE_RESEARCH_FAILED', message: String(err?.message ?? err) }) }],
551
+ };
552
+ }
553
+
554
+ if (!result.ok) {
555
+ const payload = await enrichConsoleFailurePayloadAsync(result, {
556
+ error: consoleErrorCode(result, 'SAVE_RESEARCH_FAILED'),
557
+ message: consoleErrorMessage(result, `POST /api/specs returned ${result.status}`),
558
+ status: result.status,
559
+ url: result.url,
560
+ console_response: result.body,
561
+ attempted: { type: 'research', name: title },
562
+ write_mode: 'cloud',
563
+ ...(result.errorKind === 'timeout'
564
+ ? {
565
+ hint:
566
+ 'The create may still have completed server-side after this client timed out. '
567
+ + 'Search for a research doc with this exact name before re-saving — a blind re-save can mint a duplicate id ([[HS-2FQY]]).',
568
+ }
569
+ : {}),
570
+ });
571
+ return {
572
+ isError: true,
573
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
574
+ };
575
+ }
576
+
577
+ // POST returns { spec: { id, relPath, ... }, write, warnings? }.
578
+ const spec = result.body?.spec ?? null;
579
+ const researchId = typeof spec?.id === 'string' && spec.id ? spec.id : null;
580
+ const relPath = spec?.relPath ?? spec?.path ?? null;
581
+ if (!researchId) {
582
+ return {
583
+ isError: true,
584
+ content: [{
585
+ type: 'text',
586
+ text: JSON.stringify({
587
+ error: 'SAVE_RESEARCH_MALFORMED_RESPONSE',
588
+ message: `POST /api/specs answered ${result.status} but carried no spec.id — treat the research as unsaved and check the console before re-saving.`,
589
+ url: result.url,
590
+ console_response: result.body,
591
+ }, null, 2),
592
+ }],
593
+ };
594
+ }
595
+ const filename = relPath ? basename(String(relPath)) : null;
596
+ const consoleWrite = result.body?.write ?? null;
597
+ const consoleUrl = result.url ?? getConsoleUrl();
598
+ const landed = {
599
+ tier: 'cloud',
600
+ reported_by: consoleWrite ? 'console' : 'inferred_from_console_url',
601
+ console_url: consoleUrl,
602
+ rel_path: relPath,
603
+ abs_path: null,
604
+ on_disk: false,
605
+ in_local_vault: false,
606
+ vault_root: vaultRoot ?? null,
607
+ console_write: consoleWrite,
608
+ };
609
+ const out = {
610
+ success: true,
611
+ research_id: researchId,
612
+ filename,
613
+ filepath: relPath,
614
+ landed,
615
+ message: `Research saved through the console at ${consoleUrl}${relPath ? ` (${relPath})` : ''} — cloud write mode, nothing written to local disk`,
616
+ next_steps: researchNextSteps(filename ? filename.replace(/\.md$/i, '') : researchId),
617
+ ...(Array.isArray(result.body?.warnings) && result.body.warnings.length > 0
618
+ ? { console_warnings: result.body.warnings }
619
+ : {}),
620
+ };
621
+ if (researchTypeWasOmitted) attachResearchTypeWarning(out);
622
+ return {
623
+ content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
624
+ };
625
+ }
@@ -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 = {}) {