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
@@ -42,8 +42,27 @@ import {
42
42
  mkdirSync,
43
43
  unlinkSync,
44
44
  } from 'node:fs';
45
- import { join } from 'node:path';
46
- import { deriveSlug } from './promotion-helpers.mjs';
45
+ import { join, isAbsolute, resolve, sep } from 'node:path';
46
+ import {
47
+ deriveSlug,
48
+ fetchSpecRecordViaConsole,
49
+ resolveWriteModeFromDeps,
50
+ splitRawSpecForConsole,
51
+ cloudWriteFailureResult,
52
+ } from './promotion-helpers.mjs';
53
+ // Cloud-mode vault WRITES + the cloud-tier read selector
54
+ // ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]], HS-KR16).
55
+ // Split has the same create-first, link-second shape as the other promotion
56
+ // flows: each child's "Split-from" callout names its SIBLINGS, and the
57
+ // original's `split_into:` names every child — basenames the console only
58
+ // mints when the POST lands.
59
+ import {
60
+ createSpecViaConsole,
61
+ updateSpecViaConsole,
62
+ stripServerMintedFields,
63
+ } from './cloud-vault-writes.mjs';
64
+ import { readViaSelector } from './read-path.mjs';
65
+
47
66
  import { yamlScalar } from './yaml-scalar.mjs';
48
67
  import {
49
68
  allocateFileKey,
@@ -60,6 +79,14 @@ import {
60
79
  // caller would only learn that reactively when heyspex_promote_research
61
80
  // later refuses a child with RESEARCH_STILL_STUB.
62
81
  import { findUnresolvedResearchPlaceholders } from './promote-research.mjs';
82
+ import { assertPathUnderRoot } from '../../scripts/lib/shared-path-helpers.mjs';
83
+
84
+ // Placeholders the CLOUD path feeds `buildChildResearchContent` in place of a
85
+ // locally-minted id / file key / family key; `stripServerMintedFields` removes
86
+ // them from the POST body (plus an explicit `delete` of an un-inheritable
87
+ // `family:`), so the console never sees them.
88
+ const CLOUD_PENDING_ID = 'HS-PENDING';
89
+ const CLOUD_PENDING_KEY = 'PENDING';
63
90
 
64
91
  // Id validation/classification is via parseSpecId (dual-read: accepts type-first
65
92
  // HS-RES-<key> / HS-MIL-<key> / HS-CAP-<key> and the family-first orderings, plus
@@ -216,7 +243,7 @@ function appendChangeLogRow(body, dateStr, author, message) {
216
243
  return body.replace(/\s*$/, '') + `\n${row}\n`;
217
244
  }
218
245
 
219
- function commitWrites(writes) {
246
+ function commitWrites(writes, vaultRoot) {
220
247
  const snapshots = new Map();
221
248
  const performed = [];
222
249
  try {
@@ -230,7 +257,10 @@ function commitWrites(writes) {
230
257
  0,
231
258
  Math.max(w.absPath.lastIndexOf('/'), w.absPath.lastIndexOf('\\')),
232
259
  );
233
- if (dir) mkdirSync(dir, { recursive: true });
260
+ if (dir) {
261
+ if (vaultRoot) assertPathUnderRoot(vaultRoot, dir, 'split-research write');
262
+ mkdirSync(dir, { recursive: true });
263
+ }
234
264
  writeFileSync(w.absPath, w.content, 'utf8');
235
265
  performed.push(w);
236
266
  } else if (w.kind === 'update') {
@@ -396,6 +426,49 @@ function findCaptureByIdOrBasename(vaultRoot, ref) {
396
426
  return null;
397
427
  }
398
428
 
429
+ // ── Cloud-tier siblings of the three lookups above (HS-KR16 pattern) ────────
430
+ // `tier` is resolved ONCE per call by `executeSplitResearch` from `deps.tier`
431
+ // and threaded down; absent it is `disk`, so every pre-existing caller reads
432
+ // exactly as before. The neon branch reuses `fetchSpecRecordViaConsole` — the
433
+ // same injected-`consoleFetch` reader promote-capture/promote-research use —
434
+ // so a test drives the whole flow with a recorder and never opens a socket.
435
+ // These exist because a cloud WRITE is pointless if the INPUTS cannot be
436
+ // found: the writes below all derive from the original research doc and the
437
+ // captures it names.
438
+
439
+ async function findResearchByIdViaTier(vaultRoot, researchId, { tier, consoleUrl, consoleFetch }) {
440
+ return readViaSelector({
441
+ allowDiskFallback: false,
442
+ tier,
443
+ diskRead: () => findResearchByIdLocal(vaultRoot, researchId),
444
+ neonRead: () => fetchSpecRecordViaConsole(consoleFetch, researchId),
445
+ routeLabel: 'GET /api/specs/[id]',
446
+ consoleUrl,
447
+ });
448
+ }
449
+
450
+ async function findMilestoneViaTier(vaultRoot, ref, { tier, consoleUrl, consoleFetch }) {
451
+ return readViaSelector({
452
+ allowDiskFallback: false,
453
+ tier,
454
+ diskRead: () => findMilestoneByIdOrBasename(vaultRoot, ref),
455
+ neonRead: () => fetchSpecRecordViaConsole(consoleFetch, ref),
456
+ routeLabel: 'GET /api/specs/[id]',
457
+ consoleUrl,
458
+ });
459
+ }
460
+
461
+ async function findCaptureViaTier(vaultRoot, ref, { tier, consoleUrl, consoleFetch }) {
462
+ return readViaSelector({
463
+ allowDiskFallback: false,
464
+ tier,
465
+ diskRead: () => findCaptureByIdOrBasename(vaultRoot, ref),
466
+ neonRead: () => fetchSpecRecordViaConsole(consoleFetch, ref),
467
+ routeLabel: 'GET /api/specs/[id]',
468
+ consoleUrl,
469
+ });
470
+ }
471
+
399
472
  function collectOccupiedResearchIds(vaultRoot) {
400
473
  const occupied = new Set();
401
474
  const dir = join(vaultRoot, 'specs', 'research');
@@ -601,7 +674,20 @@ export async function executeSplitResearch(vaultRoot, args = {}, deps = {}) {
601
674
  const validationError = validateInputShape(args);
602
675
  if (validationError) return validationError;
603
676
 
604
- const research = findResearchByIdLocal(vaultRoot, args.research_id);
677
+ // Tier + write mode are resolved ONCE, here, by the caller and threaded down
678
+ // — never re-resolved inside a helper. Both default to the disk behaviour so
679
+ // every pre-existing caller/test is byte-identical
680
+ // ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]).
681
+ const tierDeps = {
682
+ tier: deps?.tier ?? 'disk',
683
+ consoleUrl: deps?.consoleUrl,
684
+ consoleFetch: deps?.consoleFetch,
685
+ };
686
+ const writeMode = resolveWriteModeFromDeps(deps);
687
+ const cloudDeps = { consoleFetch: deps?.consoleFetch, consoleUrl: deps?.consoleUrl };
688
+ const cloudCompleted = [];
689
+
690
+ const research = await findResearchByIdViaTier(vaultRoot, args.research_id, tierDeps);
605
691
  if (!research) {
606
692
  return {
607
693
  error: 'RESEARCH_NOT_FOUND',
@@ -650,9 +736,16 @@ export async function executeSplitResearch(vaultRoot, args = {}, deps = {}) {
650
736
  // Resolve all splits up-front, before any writes.
651
737
  // One occupied set across the whole loop (owner ruling 2026-07-10) — children
652
738
  // are not on disk yet, so a fresh vault walk per split could repeat a key.
653
- const occupiedFileKeys = collectOccupiedKeysEitherKind(vaultRoot); // union: file+family keys (HS-SJOT)
739
+ //
740
+ // In CLOUD mode nothing is minted here at all: `collectOccupiedKeysEitherKind`
741
+ // is a vault SCAN, and in a file-less vault it returns an EMPTY set — a key
742
+ // drawn against it can collide with one the cloud vault already holds. The
743
+ // console mints every child's file key, family and filename.
744
+ const occupiedFileKeys =
745
+ writeMode === 'cloud' ? new Set() : collectOccupiedKeysEitherKind(vaultRoot); // union: file+family keys (HS-SJOT)
654
746
  const childRecords = [];
655
747
  const captureChildMap = new Map();
748
+ const captureRecords = new Map(); // basename -> record (raw reused by the write phase)
656
749
 
657
750
  for (let i = 0; i < args.splits.length; i++) {
658
751
  const split = args.splits[i];
@@ -661,7 +754,7 @@ export async function executeSplitResearch(vaultRoot, args = {}, deps = {}) {
661
754
  if (split.target_milestone === 'NEW') {
662
755
  targetMilestone = { absent: true, newName: split.new_milestone_name.trim() };
663
756
  } else {
664
- const ms = findMilestoneByIdOrBasename(vaultRoot, split.target_milestone);
757
+ const ms = await findMilestoneViaTier(vaultRoot, split.target_milestone, tierDeps);
665
758
  if (!ms) {
666
759
  return {
667
760
  error: 'MILESTONE_NOT_FOUND',
@@ -673,7 +766,8 @@ export async function executeSplitResearch(vaultRoot, args = {}, deps = {}) {
673
766
 
674
767
  const childCaptureBasenames = [];
675
768
  for (const captureRef of split.sources_captures) {
676
- const cap = findCaptureByIdOrBasename(vaultRoot, captureRef);
769
+ const cap = await findCaptureViaTier(vaultRoot, captureRef, tierDeps);
770
+ if (cap) captureRecords.set(cap.basename, cap);
677
771
  if (!cap) {
678
772
  return {
679
773
  error: 'CAPTURE_NOT_FOUND',
@@ -695,15 +789,18 @@ export async function executeSplitResearch(vaultRoot, args = {}, deps = {}) {
695
789
  // original itself has no derivable family (should not occur for a real
696
790
  // research doc).
697
791
  const childFamilyKey =
698
- originalFamilyKey ?? mintFamilyKey(vaultRoot, { randomUnit: deps?.randomUnit });
792
+ writeMode === 'cloud'
793
+ ? (originalFamilyKey ?? null)
794
+ : (originalFamilyKey ?? mintFamilyKey(vaultRoot, { randomUnit: deps?.randomUnit }));
699
795
  // Per-file FILE KEY (owner ruling 2026-07-10) — filename + `file_id:` SSOT,
700
796
  // and (V3) the anchor for `id:` itself
701
797
  // ([[HS-ZU1Q-TASK-PNAS-flip-new-spec-minting-to-the-file-key]]).
702
- const childFileKey = allocateFileKey(occupiedFileKeys, { randomUnit: deps?.randomUnit });
703
- occupiedFileKeys.add(childFileKey);
704
- const childId = `HS-${childFileKey}`;
798
+ const childFileKey =
799
+ writeMode === 'cloud' ? null : allocateFileKey(occupiedFileKeys, { randomUnit: deps?.randomUnit });
800
+ if (childFileKey) occupiedFileKeys.add(childFileKey);
801
+ const childId = childFileKey ? `HS-${childFileKey}` : null;
705
802
  const slug = deriveSlug(split.name);
706
- const basename = `${childId}-${childFamilyKey}-RES-${slug}`;
803
+ const basename = childFileKey ? `${childId}-${childFamilyKey}-RES-${slug}` : null;
707
804
 
708
805
  childRecords.push({
709
806
  id: childId,
@@ -716,9 +813,13 @@ export async function executeSplitResearch(vaultRoot, args = {}, deps = {}) {
716
813
  bodySlice: split.body_slice,
717
814
  });
718
815
 
816
+ // Hold the child RECORD, not its basename: in cloud mode the basename is
817
+ // still null here and only lands when that child's POST answers, so the
818
+ // capture→children map is read lazily at write time in both modes.
819
+ const childRecord = childRecords[childRecords.length - 1];
719
820
  for (const capBasename of childCaptureBasenames) {
720
821
  if (!captureChildMap.has(capBasename)) captureChildMap.set(capBasename, []);
721
- captureChildMap.get(capBasename).push(basename);
822
+ captureChildMap.get(capBasename).push(childRecord);
722
823
  }
723
824
  }
724
825
 
@@ -727,17 +828,16 @@ export async function executeSplitResearch(vaultRoot, args = {}, deps = {}) {
727
828
  const today = now.slice(0, 10);
728
829
  const author = 'split-research (agent)';
729
830
  const writes = [];
730
- const childBasenames = childRecords.map((c) => c.basename);
731
-
732
- for (const child of childRecords) {
733
- const content = buildChildResearchContent({
734
- id: child.id,
735
- fileKey: child.fileKey,
736
- familyKey: child.familyKey,
737
- basename: child.basename,
831
+
832
+ const buildChildContent = (child, siblingBasenames) =>
833
+ buildChildResearchContent({
834
+ id: child.id ?? CLOUD_PENDING_ID,
835
+ fileKey: child.fileKey ?? CLOUD_PENDING_KEY,
836
+ familyKey: child.familyKey ?? CLOUD_PENDING_KEY,
837
+ basename: child.basename ?? '',
738
838
  name: child.name,
739
839
  originalBasename: research.basename,
740
- siblingBasenames: childBasenames,
840
+ siblingBasenames,
741
841
  targetMilestone: child.targetMilestone,
742
842
  capturesBasenames: child.capturesBasenames,
743
843
  bodySlice: child.bodySlice,
@@ -745,42 +845,186 @@ export async function executeSplitResearch(vaultRoot, args = {}, deps = {}) {
745
845
  today,
746
846
  author,
747
847
  });
748
- child.unresolvedPlaceholderCount = findUnresolvedResearchPlaceholders(content);
749
- writes.push({
750
- kind: 'create',
751
- absPath: join(vaultRoot, 'specs', 'research', `${child.basename}.md`),
752
- content,
848
+
849
+ if (writeMode === 'cloud') {
850
+ // ── CREATE FIRST ────────────────────────────────────────────────────────
851
+ // Each child is POSTed with an EMPTY sibling list (a sibling's basename is
852
+ // minted by the console, so it cannot be known before that sibling's own
853
+ // POST answers), then re-PUT once every basename is in hand. The sibling
854
+ // cross-links live only in the "Split-from:" body callout, so the follow-up
855
+ // PUT replaces the BODY and keeps the frontmatter the console stored.
856
+ for (const child of childRecords) {
857
+ const content = buildChildContent(child, []);
858
+ child.unresolvedPlaceholderCount = findUnresolvedResearchPlaceholders(content);
859
+ const { frontmatter, body } = splitRawSpecForConsole(content);
860
+ const cleaned = stripServerMintedFields(frontmatter);
861
+ if (!child.familyKey) delete cleaned.family;
862
+ let created;
863
+ try {
864
+ created = await createSpecViaConsole({
865
+ ...cloudDeps,
866
+ type: 'research',
867
+ name: child.name,
868
+ frontmatter: cleaned,
869
+ body,
870
+ actingAgent: deps?.actingAgent ?? null,
871
+ });
872
+ } catch (err) {
873
+ return cloudWriteFailureResult(err, {
874
+ step: { kind: 'create', type: 'research', name: child.name },
875
+ completed: cloudCompleted,
876
+ });
877
+ }
878
+ child.id = created.specId;
879
+ child.basename = created.basename;
880
+ child.relPath = created.relPath;
881
+ child.cloudFrontmatter = created.spec?.frontmatter ?? cleaned;
882
+ cloudCompleted.push({
883
+ kind: 'create',
884
+ type: 'research',
885
+ spec_id: created.specId,
886
+ basename: created.basename,
887
+ rel_path: created.relPath,
888
+ });
889
+ }
890
+
891
+ const cloudChildBasenames = childRecords.map((c) => c.basename);
892
+
893
+ // ── …LINK SECOND: sibling callouts, then the original, then the captures.
894
+ if (childRecords.length > 1) {
895
+ for (const child of childRecords) {
896
+ const rebuilt = buildChildContent(child, cloudChildBasenames);
897
+ const { body } = splitRawSpecForConsole(rebuilt);
898
+ try {
899
+ const res = await updateSpecViaConsole({
900
+ ...cloudDeps,
901
+ specId: child.id,
902
+ frontmatter: child.cloudFrontmatter,
903
+ body,
904
+ });
905
+ cloudCompleted.push({
906
+ kind: 'update',
907
+ type: 'research.siblings',
908
+ spec_id: res.specId,
909
+ basename: res.basename ?? child.basename,
910
+ rel_path: res.relPath,
911
+ });
912
+ } catch (err) {
913
+ return cloudWriteFailureResult(err, {
914
+ step: { kind: 'update', type: 'research.siblings', basename: child.basename },
915
+ completed: cloudCompleted,
916
+ });
917
+ }
918
+ }
919
+ }
920
+
921
+ const originalUpdatedCloud = buildUpdatedOriginalContent(research.raw, {
922
+ childBasenames: cloudChildBasenames,
923
+ now,
924
+ today,
925
+ author,
753
926
  });
754
- }
927
+ {
928
+ const { frontmatter, body } = splitRawSpecForConsole(originalUpdatedCloud);
929
+ try {
930
+ const res = await updateSpecViaConsole({
931
+ ...cloudDeps,
932
+ specId: originalId,
933
+ frontmatter,
934
+ body,
935
+ });
936
+ cloudCompleted.push({
937
+ kind: 'update',
938
+ type: 'research.original',
939
+ spec_id: res.specId,
940
+ basename: res.basename ?? research.basename,
941
+ rel_path: res.relPath,
942
+ });
943
+ } catch (err) {
944
+ return cloudWriteFailureResult(err, {
945
+ step: { kind: 'update', type: 'research.original', basename: research.basename },
946
+ completed: cloudCompleted,
947
+ });
948
+ }
949
+ }
755
950
 
756
- const originalUpdated = buildUpdatedOriginalContent(research.raw, {
757
- childBasenames,
758
- now,
759
- today,
760
- author,
761
- });
762
- writes.push({
763
- kind: 'update',
764
- absPath: research.absPath,
765
- content: originalUpdated,
766
- });
951
+ for (const [capBasename, childList] of captureChildMap.entries()) {
952
+ const capRecord = captureRecords.get(capBasename);
953
+ if (!capRecord) continue;
954
+ const capUpdated = buildUpdatedCaptureContent(
955
+ capRecord.raw,
956
+ childList.map((c) => c.basename),
957
+ now,
958
+ );
959
+ if (capUpdated === capRecord.raw) continue;
960
+ const { frontmatter, body } = splitRawSpecForConsole(capUpdated);
961
+ try {
962
+ const res = await updateSpecViaConsole({
963
+ ...cloudDeps,
964
+ specId: getScalarField(getFrontmatterBlock(capRecord.raw)?.block ?? '', 'id') ?? capBasename,
965
+ frontmatter,
966
+ body,
967
+ });
968
+ cloudCompleted.push({
969
+ kind: 'update',
970
+ type: 'capture',
971
+ spec_id: res.specId,
972
+ basename: res.basename ?? capBasename,
973
+ rel_path: res.relPath,
974
+ });
975
+ } catch (err) {
976
+ return cloudWriteFailureResult(err, {
977
+ step: { kind: 'update', type: 'capture', basename: capBasename },
978
+ completed: cloudCompleted,
979
+ });
980
+ }
981
+ }
982
+ } else {
983
+ const childBasenames = childRecords.map((c) => c.basename);
984
+
985
+ for (const child of childRecords) {
986
+ const content = buildChildContent(child, childBasenames);
987
+ child.unresolvedPlaceholderCount = findUnresolvedResearchPlaceholders(content);
988
+ writes.push({
989
+ kind: 'create',
990
+ absPath: join(vaultRoot, 'specs', 'research', `${child.basename}.md`),
991
+ content,
992
+ });
993
+ }
767
994
 
768
- for (const [capBasename, childList] of captureChildMap.entries()) {
769
- const capAbs = join(vaultRoot, 'specs', 'captures', `${capBasename}.md`);
770
- const capRaw = readFileSync(capAbs, 'utf8');
771
- const capUpdated = buildUpdatedCaptureContent(capRaw, childList, now);
772
- if (capUpdated !== capRaw) {
773
- writes.push({ kind: 'update', absPath: capAbs, content: capUpdated });
995
+ const originalUpdated = buildUpdatedOriginalContent(research.raw, {
996
+ childBasenames,
997
+ now,
998
+ today,
999
+ author,
1000
+ });
1001
+ writes.push({
1002
+ kind: 'update',
1003
+ absPath: research.absPath,
1004
+ content: originalUpdated,
1005
+ });
1006
+
1007
+ for (const [capBasename, childList] of captureChildMap.entries()) {
1008
+ const capAbs = join(vaultRoot, 'specs', 'captures', `${capBasename}.md`);
1009
+ const capRaw = readFileSync(capAbs, 'utf8');
1010
+ const capUpdated = buildUpdatedCaptureContent(
1011
+ capRaw,
1012
+ childList.map((c) => c.basename),
1013
+ now,
1014
+ );
1015
+ if (capUpdated !== capRaw) {
1016
+ writes.push({ kind: 'update', absPath: capAbs, content: capUpdated });
1017
+ }
774
1018
  }
775
- }
776
1019
 
777
- const commitResult = commitWrites(writes);
778
- if (!commitResult.ok) {
779
- return {
780
- error: 'WRITE_FAILED',
781
- message: commitResult.error?.message ?? 'unknown write failure',
782
- rollback_error: commitResult.error?.rollbackError,
783
- };
1020
+ const commitResult = commitWrites(writes, vaultRoot);
1021
+ if (!commitResult.ok) {
1022
+ return {
1023
+ error: 'WRITE_FAILED',
1024
+ message: commitResult.error?.message ?? 'unknown write failure',
1025
+ rollback_error: commitResult.error?.rollbackError,
1026
+ };
1027
+ }
784
1028
  }
785
1029
 
786
1030
  const anyChildStillStub = childRecords.some((c) => c.unresolvedPlaceholderCount > 0);
@@ -790,10 +1034,11 @@ export async function executeSplitResearch(vaultRoot, args = {}, deps = {}) {
790
1034
  archive_basename: research.basename,
791
1035
  archive_path: research.relPath,
792
1036
  archive_status_after: getScalarField(fm.block, 'status') ?? 'open',
1037
+ ...(writeMode === 'cloud' ? { write_mode: 'cloud' } : {}),
793
1038
  children: childRecords.map((c) => ({
794
1039
  id: c.id,
795
1040
  basename: c.basename,
796
- path: join('specs', 'research', `${c.basename}.md`),
1041
+ path: c.relPath ?? join('specs', 'research', `${c.basename}.md`),
797
1042
  target_milestone: c.targetMilestone.absent ? 'NEW' : c.targetMilestone.basename,
798
1043
  sources_captures: c.capturesBasenames,
799
1044
  // Same placeholder count RESEARCH_STILL_STUB would later refuse
@@ -802,7 +1047,10 @@ export async function executeSplitResearch(vaultRoot, args = {}, deps = {}) {
802
1047
  research_still_stub: c.unresolvedPlaceholderCount > 0,
803
1048
  unresolved_placeholder_count: c.unresolvedPlaceholderCount,
804
1049
  })),
805
- writes: writes.map((w) => ({ kind: w.kind, path: w.absPath })),
1050
+ writes:
1051
+ writeMode === 'cloud'
1052
+ ? cloudCompleted.map((w) => ({ kind: w.kind, path: w.rel_path ?? null, spec_id: w.spec_id }))
1053
+ : writes.map((w) => ({ kind: w.kind, path: w.absPath })),
806
1054
  // No fallback warning: every child unconditionally inherits the
807
1055
  // original's family key (V3 shared-surname model — see the block comment
808
1056
  // above inheritedFamilyKey).
@@ -0,0 +1,22 @@
1
+ // JSON-RPC writers for the stdio transport — the only place the MCP server
2
+ // writes to stdout. Extracted from cli.mjs under
3
+ // [[HS-BLNF-6W61-DIAG-cli-mjs-has-grown-into-a-13-665-line]] (no behavior change).
4
+
5
+ export function sendResponse(id, result) {
6
+ const response = { jsonrpc: '2.0', id, result };
7
+ process.stdout.write(JSON.stringify(response) + '\n');
8
+ }
9
+
10
+ export function sendError(id, code, message, data) {
11
+ const response = {
12
+ jsonrpc: '2.0',
13
+ id,
14
+ error: { code, message, ...(data && { data }) },
15
+ };
16
+ process.stdout.write(JSON.stringify(response) + '\n');
17
+ }
18
+
19
+ export function sendNotification(method, params) {
20
+ const notification = { jsonrpc: '2.0', method, params };
21
+ process.stdout.write(JSON.stringify(notification) + '\n');
22
+ }