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
@@ -52,6 +52,7 @@ import { scoreSufficiency } from '../../dev-factory/lib/sufficiency-score.mjs';
52
52
  // capture-focus.mjs deep-dive reader rather than a third regex copy.
53
53
  import { extractCaptureExpandedConceptFromBody } from './capture-focus.mjs';
54
54
  import {
55
+ collectOccupiedKeysEitherKind,
55
56
  mintFileKey,
56
57
  mintFamilyKey,
57
58
  parseSpecId,
@@ -82,7 +83,27 @@ import {
82
83
  bumpUpdated,
83
84
  appendChangeLogRow,
84
85
  commitWrites,
86
+ fetchSpecRecordViaConsole,
87
+ resolveWriteModeFromDeps,
88
+ splitRawSpecForConsole,
89
+ cloudWriteFailureResult,
85
90
  } from './promotion-helpers.mjs';
91
+ // Cloud-mode vault WRITES ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]).
92
+ // The write-side twin of read-path.mjs: in `cloud` write mode a create becomes
93
+ // POST /api/specs (server-minted id + family + filename) and an edit becomes
94
+ // PUT /api/specs/:id. Nothing here opens a socket itself — `consoleFetch` is
95
+ // the same injected dep the cloud-tier READS already use.
96
+ import {
97
+ createSpecViaConsole,
98
+ updateSpecViaConsole,
99
+ stripServerMintedFields,
100
+ } from './cloud-vault-writes.mjs';
101
+ // Cloud-tier read selector ([[HS-KR16-CWPX-TASK-promotion-reads-through-the-cloud-tier]]) —
102
+ // reused verbatim, never copied. `readViaSelector` picks disk vs neon; the
103
+ // neon branch below fetches through `fetchSpecRecordViaConsole` (an injected
104
+ // consoleFetch, not read-path.mjs's own transport) so tests never open a
105
+ // socket, matching this file's existing deps-injection convention.
106
+ import { readViaSelector } from './read-path.mjs';
86
107
  // Reuse the exact same placeholder-count logic RESEARCH_STILL_STUB uses to
87
108
  // refuse `heyspex_promote_research` on a stub research doc — proactively
88
109
  // surfaced here at creation/extension time instead of only reactively at
@@ -99,6 +120,15 @@ import { findUnresolvedResearchPlaceholders } from './promote-research.mjs';
99
120
  // vendor/heyspex-mcp/spec-id-lib.mjs (`parseSpecId`).
100
121
  // ─────────────────────────────────────────────────────────────────────────────
101
122
 
123
+ // Placeholders the CLOUD path feeds the shared content builders in place of a
124
+ // locally-minted id / file key / family key. They never reach the console:
125
+ // `stripServerMintedFields` removes `id:` and `file_id:` from the request, and
126
+ // an un-inheritable `family:` is deleted alongside them. Their only job is to
127
+ // keep the builder's template well-formed so its BODY is byte-for-byte the
128
+ // markdown the disk path would have written.
129
+ const CLOUD_PENDING_ID = 'HS-PENDING';
130
+ const CLOUD_PENDING_KEY = 'PENDING';
131
+
102
132
  /** True when `id` (either ordering) parses to a spec with the given type label. */
103
133
  function hasTypeLabel(id, label) {
104
134
  const p = parseSpecId(id);
@@ -295,6 +325,38 @@ function findResearchByIdLocal(vaultRoot, researchId) {
295
325
  return null;
296
326
  }
297
327
 
328
+ /**
329
+ * Cloud-tier sibling of {@link findCaptureByIdLocal} (HS-KR16). `tier`,
330
+ * `consoleUrl` and `consoleFetch` are resolved ONCE per handler call
331
+ * (`handlePromoteCapture` in capture-tools.mjs) and threaded down here —
332
+ * never re-resolved inside this helper.
333
+ */
334
+ async function findCaptureByIdViaTier(vaultRoot, captureId, { tier, consoleUrl, consoleFetch }) {
335
+ return readViaSelector({
336
+ allowDiskFallback: false,
337
+ tier,
338
+ diskRead: () => findCaptureByIdLocal(vaultRoot, captureId),
339
+ neonRead: () => fetchSpecRecordViaConsole(consoleFetch, captureId),
340
+ routeLabel: 'GET /api/specs/[id]',
341
+ consoleUrl,
342
+ });
343
+ }
344
+
345
+ /**
346
+ * Cloud-tier sibling of {@link findResearchByIdLocal} (HS-KR16). Same
347
+ * contract: a bare record, or `null` on a genuine not-found.
348
+ */
349
+ async function findResearchByIdViaTier(vaultRoot, researchId, { tier, consoleUrl, consoleFetch }) {
350
+ return readViaSelector({
351
+ allowDiskFallback: false,
352
+ tier,
353
+ diskRead: () => findResearchByIdLocal(vaultRoot, researchId),
354
+ neonRead: () => fetchSpecRecordViaConsole(consoleFetch, researchId),
355
+ routeLabel: 'GET /api/specs/[id]',
356
+ consoleUrl,
357
+ });
358
+ }
359
+
298
360
  function findResearchByBasename(vaultRoot, basename) {
299
361
  const absPath = join(vaultRoot, 'specs', 'research', `${basename}.md`);
300
362
  if (!existsSync(absPath)) return null;
@@ -652,6 +714,26 @@ async function runExecutePhase(args, ctx, deps) {
652
714
  const now = (deps?.clock ?? isoNow)();
653
715
  const today = now.slice(0, 10);
654
716
  const author = 'promote-capture (agent)';
717
+ // Vault WRITE mode ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]).
718
+ // Resolved ONCE by the caller (`resolveVaultWriteMode()`) and threaded in on
719
+ // `deps`; absent (every pre-existing caller and test) it is `local`, so the
720
+ // disk path below is byte-identical to before this task.
721
+ //
722
+ // CREATE FIRST, LINK SECOND: on disk the flow stages every write and commits
723
+ // them as one transaction, because it mints the research filename itself. In
724
+ // cloud mode the console mints the id, the family and the filename, so the
725
+ // basename each capture's `promoted_to:` / `informs_research:` links to is
726
+ // only knowable AFTER `POST /api/specs` returns. The order therefore
727
+ // inverts: POST the research doc, take its basename, then PUT each capture.
728
+ const writeMode = resolveWriteModeFromDeps(deps);
729
+ const cloudDeps = { consoleFetch: deps?.consoleFetch, consoleUrl: deps?.consoleUrl };
730
+ // Steps that have ALREADY landed in the cloud vault, in order. There is no
731
+ // rollback (the console has no un-create) — on a mid-sequence failure this
732
+ // list is what the caller is told actually happened.
733
+ const cloudCompleted = [];
734
+ // PUTs queued while the primary create is being resolved; flushed at the
735
+ // commit point below, in order.
736
+ const cloudUpdates = [];
655
737
 
656
738
  // ── Resolve / allocate target research basename + path ────────────────────
657
739
  let researchBasename;
@@ -672,28 +754,49 @@ async function runExecutePhase(args, ctx, deps) {
672
754
  // (ctx.captures[0]) just below. No fallback: era-3 `id:` keys onto the
673
755
  // file key alone, so a shared family can never collide.
674
756
  const primaryCapture = ctx.captures[0] ?? null;
675
- researchFamilyKey =
676
- (primaryCapture
677
- ? inheritedFamilyKey(
678
- primaryCapture.frontmatter?.id ?? null,
679
- primaryCapture.frontmatter,
680
- primaryCapture.basename,
681
- )
682
- : null) ?? mintFamilyKey(ctx.vaultRoot, { randomUnit: deps?.randomUnit });
683
- // Per-file FILE KEY (owner ruling 2026-07-10) — filename + `file_id:` SSOT,
684
- // and (V3) the anchor for `id:` itself ([[HS-ZU1Q-TASK-PNAS-flip-new-spec-minting-to-the-file-key]]).
685
- researchFileKey = mintFileKey(ctx.vaultRoot, { randomUnit: deps?.randomUnit });
686
- researchId = `HS-${researchFileKey}`;
687
- const outlineName = args.accepted_outline?.name ?? args.new_research_name;
688
- const slug = deriveSlug(outlineName);
689
- researchBasename = `${researchId}-${researchFamilyKey}-RES-${slug}`;
690
- researchAbsPath = join(
691
- ctx.vaultRoot,
692
- 'specs',
693
- 'research',
694
- `${researchBasename}.md`,
695
- );
696
- researchRelPath = join('specs', 'research', `${researchBasename}.md`);
757
+ const inherited = primaryCapture
758
+ ? inheritedFamilyKey(
759
+ primaryCapture.frontmatter?.id ?? null,
760
+ primaryCapture.frontmatter,
761
+ primaryCapture.basename,
762
+ )
763
+ : null;
764
+ if (writeMode === 'cloud') {
765
+ // NOTHING is minted locally in cloud mode (AC3): `mintFileKey` /
766
+ // `mintFamilyKey` both derive their answer by scanning the vault for
767
+ // occupied keys, and in a file-less vault that scan sees an EMPTY set —
768
+ // so a key minted here can collide with one the cloud vault already
769
+ // holds. The console mints the file key, the family and the filename
770
+ // inside `createSpec`. An INHERITED family is still passed through (it
771
+ // is read off the source capture, not minted, and `createSpec` honours
772
+ // an explicit `family:` over parent inheritance); with no inheritable
773
+ // family the field is simply omitted and the server picks one.
774
+ researchFamilyKey = inherited ?? null;
775
+ researchFileKey = null;
776
+ researchId = null;
777
+ researchBasename = null;
778
+ researchAbsPath = null;
779
+ researchRelPath = null;
780
+ } else {
781
+ researchFamilyKey =
782
+ inherited ?? mintFamilyKey(ctx.vaultRoot, { randomUnit: deps?.randomUnit });
783
+ // Per-file FILE KEY (owner ruling 2026-07-10) — filename + `file_id:` SSOT,
784
+ // and (V3) the anchor for `id:` itself ([[HS-ZU1Q-TASK-PNAS-flip-new-spec-minting-to-the-file-key]]).
785
+ // Disk occupancy ([[HS-FJWL]]): the `writeMode === 'cloud'` branch above
786
+ // mints NOTHING locally, so this is the local-vault branch only.
787
+ researchFileKey = mintFileKey(collectOccupiedKeysEitherKind(ctx.vaultRoot), { randomUnit: deps?.randomUnit });
788
+ researchId = `HS-${researchFileKey}`;
789
+ const outlineName = args.accepted_outline?.name ?? args.new_research_name;
790
+ const slug = deriveSlug(outlineName);
791
+ researchBasename = `${researchId}-${researchFamilyKey}-RES-${slug}`;
792
+ researchAbsPath = join(
793
+ ctx.vaultRoot,
794
+ 'specs',
795
+ 'research',
796
+ `${researchBasename}.md`,
797
+ );
798
+ researchRelPath = join('specs', 'research', `${researchBasename}.md`);
799
+ }
697
800
  researchCreated = true;
698
801
 
699
802
  // Sanity: accepted_outline is required to actually create the file.
@@ -775,10 +878,14 @@ async function runExecutePhase(args, ctx, deps) {
775
878
  // interim `JTBD (job-to-be-done):` leading block, when present.
776
879
  const jtbdSeed =
777
880
  ctx.captures.length > 0 ? extractCaptureJtbd(ctx.captures[0].raw) : '';
881
+ // In cloud mode the id/file key/family placeholders below are stripped
882
+ // before the request goes out (`stripServerMintedFields`) — the builder is
883
+ // reused verbatim rather than forked so the BODY the console stores is the
884
+ // same markdown the disk path writes.
778
885
  const newContent = buildResearchStubContent(
779
- researchId,
780
- researchFileKey,
781
- researchFamilyKey,
886
+ researchId ?? CLOUD_PENDING_ID,
887
+ researchFileKey ?? CLOUD_PENDING_KEY,
888
+ researchFamilyKey ?? CLOUD_PENDING_KEY,
782
889
  outlineForCreate,
783
890
  captureBasenames,
784
891
  enhancesBasename,
@@ -786,7 +893,42 @@ async function runExecutePhase(args, ctx, deps) {
786
893
  suggestedType,
787
894
  jtbdSeed,
788
895
  );
789
- writes.push({ kind: 'create', absPath: researchAbsPath, content: newContent });
896
+ if (writeMode === 'cloud') {
897
+ // ── CREATE FIRST ────────────────────────────────────────────────────
898
+ const { frontmatter, body } = splitRawSpecForConsole(newContent);
899
+ const cleaned = stripServerMintedFields(frontmatter);
900
+ if (!researchFamilyKey) delete cleaned.family;
901
+ let created;
902
+ try {
903
+ created = await createSpecViaConsole({
904
+ ...cloudDeps,
905
+ type: 'research',
906
+ name: outlineForCreate.name,
907
+ frontmatter: cleaned,
908
+ body,
909
+ actingAgent: deps?.actingAgent ?? null,
910
+ });
911
+ } catch (err) {
912
+ return cloudWriteFailureResult(err, {
913
+ step: { kind: 'create', type: 'research', name: outlineForCreate.name },
914
+ completed: cloudCompleted,
915
+ });
916
+ }
917
+ // ── …LINK SECOND: every sibling write below now has a real basename.
918
+ researchId = created.specId;
919
+ researchBasename = created.basename;
920
+ researchRelPath = created.relPath;
921
+ researchAbsPath = null;
922
+ cloudCompleted.push({
923
+ kind: 'create',
924
+ type: 'research',
925
+ spec_id: created.specId,
926
+ basename: created.basename,
927
+ rel_path: created.relPath,
928
+ });
929
+ } else {
930
+ writes.push({ kind: 'create', absPath: researchAbsPath, content: newContent });
931
+ }
790
932
  finalResearchContent = newContent;
791
933
  } else {
792
934
  const updated = buildUpdatedResearchContent(
@@ -800,11 +942,23 @@ async function runExecutePhase(args, ctx, deps) {
800
942
  },
801
943
  );
802
944
  if (updated.changed) {
803
- writes.push({
804
- kind: 'update',
805
- absPath: researchAbsPath,
806
- content: updated.content,
807
- });
945
+ if (writeMode === 'cloud') {
946
+ // The target research already exists, so its basename is already
947
+ // known — this PUT carries no create-first dependency and is simply
948
+ // queued with the capture PUTs below.
949
+ cloudUpdates.push({
950
+ label: 'research',
951
+ specId: researchId,
952
+ basename: researchBasename,
953
+ content: updated.content,
954
+ });
955
+ } else {
956
+ writes.push({
957
+ kind: 'update',
958
+ absPath: researchAbsPath,
959
+ content: updated.content,
960
+ });
961
+ }
808
962
  }
809
963
  finalResearchContent = updated.changed ? updated.content : ctx.targetResolution.research.raw;
810
964
  }
@@ -842,15 +996,28 @@ async function runExecutePhase(args, ctx, deps) {
842
996
  promoted_to_set: updated.setPromotedTo,
843
997
  });
844
998
  if (updated.changed) {
845
- writes.push({
846
- kind: 'update',
847
- absPath: c.absPath,
848
- content: updated.content,
849
- });
999
+ if (writeMode === 'cloud') {
1000
+ cloudUpdates.push({
1001
+ label: 'capture',
1002
+ specId: c.frontmatter?.id ?? c.basename,
1003
+ basename: c.basename,
1004
+ content: updated.content,
1005
+ });
1006
+ } else {
1007
+ writes.push({
1008
+ kind: 'update',
1009
+ absPath: c.absPath,
1010
+ content: updated.content,
1011
+ });
1012
+ }
850
1013
  }
851
1014
  }
852
1015
 
853
- if (writes.length === 0) {
1016
+ const nothingToWrite =
1017
+ writeMode === 'cloud'
1018
+ ? cloudCompleted.length === 0 && cloudUpdates.length === 0
1019
+ : writes.length === 0;
1020
+ if (nothingToWrite) {
854
1021
  // Idempotent no-op: research already references every input capture and
855
1022
  // each capture already references the research. Surface this clearly.
856
1023
  return {
@@ -871,13 +1038,41 @@ async function runExecutePhase(args, ctx, deps) {
871
1038
  }
872
1039
 
873
1040
  // ── Commit ────────────────────────────────────────────────────────────────
874
- const commitResult = commitWrites(writes);
875
- if (!commitResult.ok) {
876
- return {
877
- error: 'WRITE_FAILED',
878
- message: commitResult.error?.message ?? 'unknown write failure',
879
- rollback_error: commitResult.error?.rollbackError,
880
- };
1041
+ if (writeMode === 'cloud') {
1042
+ // Flush the queued PUTs IN ORDER, stopping at the first failure. No
1043
+ // rollback: the error result names the failing step and lists what landed.
1044
+ for (const u of cloudUpdates) {
1045
+ const { frontmatter, body } = splitRawSpecForConsole(u.content);
1046
+ try {
1047
+ const res = await updateSpecViaConsole({
1048
+ ...cloudDeps,
1049
+ specId: u.specId,
1050
+ frontmatter,
1051
+ body,
1052
+ });
1053
+ cloudCompleted.push({
1054
+ kind: 'update',
1055
+ type: u.label,
1056
+ spec_id: res.specId,
1057
+ basename: res.basename ?? u.basename,
1058
+ rel_path: res.relPath,
1059
+ });
1060
+ } catch (err) {
1061
+ return cloudWriteFailureResult(err, {
1062
+ step: { kind: 'update', type: u.label, spec_id: u.specId, basename: u.basename },
1063
+ completed: cloudCompleted,
1064
+ });
1065
+ }
1066
+ }
1067
+ } else {
1068
+ const commitResult = commitWrites(writes);
1069
+ if (!commitResult.ok) {
1070
+ return {
1071
+ error: 'WRITE_FAILED',
1072
+ message: commitResult.error?.message ?? 'unknown write failure',
1073
+ rollback_error: commitResult.error?.rollbackError,
1074
+ };
1075
+ }
881
1076
  }
882
1077
 
883
1078
  return {
@@ -887,7 +1082,11 @@ async function runExecutePhase(args, ctx, deps) {
887
1082
  research_path: researchRelPath,
888
1083
  research_created: researchCreated,
889
1084
  captures: capturePromotionResults,
890
- writes: writes.map((w) => ({ kind: w.kind, path: w.absPath })),
1085
+ ...(writeMode === 'cloud' ? { write_mode: 'cloud' } : {}),
1086
+ writes:
1087
+ writeMode === 'cloud'
1088
+ ? cloudCompleted.map((w) => ({ kind: w.kind, path: w.rel_path ?? null, spec_id: w.spec_id }))
1089
+ : writes.map((w) => ({ kind: w.kind, path: w.absPath })),
891
1090
  // TARGET_RESEARCH_CLOSED only occurs on the existing-research branch
892
1091
  // (researchCreated === false). Family inheritance on the NEW-research
893
1092
  // branch no longer has a fallback case to warn about (era-3 minting —
@@ -919,6 +1118,19 @@ export async function executePromoteCapture(vaultRoot, args = {}, deps = {}) {
919
1118
  const validationError = validateInputShape(args);
920
1119
  if (validationError) return validationError;
921
1120
 
1121
+ // Cloud-tier reads ([[HS-KR16-CWPX-TASK-promotion-reads-through-the-cloud-tier]]):
1122
+ // `deps.tier` is resolved ONCE by the caller (`handlePromoteCapture` in
1123
+ // capture-tools.mjs, via `resolveReadTier()`) and threaded down — never
1124
+ // re-resolved here. Absent (every pre-existing caller/test) defaults to
1125
+ // `disk`, so disk-tier behavior is byte-identical to before this task.
1126
+ // Cloud-mode vault WRITES ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]):
1127
+ // `deps.writeMode` is resolved ONCE by the caller (`resolveVaultWriteMode()`)
1128
+ // exactly like `deps.tier`, and defaults to `local` so every pre-existing
1129
+ // caller/test writes to disk unchanged. It is consumed in `runExecutePhase`.
1130
+ const tier = deps.tier ?? 'disk';
1131
+ const consoleUrl = deps.consoleUrl;
1132
+ const consoleFetchDep = deps.consoleFetch;
1133
+
922
1134
  // 1. Load every input capture; fail on first miss. Ids collapse to the
923
1135
  // short stored form first (display long `HS-XV4W-HV6H` → `HS-XV4W`) so the
924
1136
  // basename-prefix and frontmatter-id matches inside the loader hit for any
@@ -926,7 +1138,11 @@ export async function executePromoteCapture(vaultRoot, args = {}, deps = {}) {
926
1138
  const captures = [];
927
1139
  for (const rawCaptureId of args.capture_ids) {
928
1140
  const captureId = shortLookupIdOf(rawCaptureId) ?? rawCaptureId;
929
- const capture = findCaptureByIdLocal(vaultRoot, captureId);
1141
+ const capture = await findCaptureByIdViaTier(vaultRoot, captureId, {
1142
+ tier,
1143
+ consoleUrl,
1144
+ consoleFetch: consoleFetchDep,
1145
+ });
930
1146
  if (!capture) {
931
1147
  return {
932
1148
  error: 'CAPTURE_NOT_FOUND',
@@ -1008,9 +1224,10 @@ export async function executePromoteCapture(vaultRoot, args = {}, deps = {}) {
1008
1224
  if (args.target_research === 'NEW') {
1009
1225
  targetResolution = { mode: 'new' };
1010
1226
  } else {
1011
- const research = findResearchByIdLocal(
1227
+ const research = await findResearchByIdViaTier(
1012
1228
  vaultRoot,
1013
1229
  shortLookupIdOf(args.target_research) ?? args.target_research,
1230
+ { tier, consoleUrl, consoleFetch: consoleFetchDep },
1014
1231
  );
1015
1232
  if (!research) {
1016
1233
  return {
@@ -1190,7 +1407,19 @@ export async function executePromoteCaptureToDiagnose(vaultRoot, args = {}, deps
1190
1407
  const severityRaw = typeof args.severity === 'string' ? args.severity.trim().toLowerCase() : 'warning';
1191
1408
  const severity = VALID_DIAGNOSE_SEVERITIES.has(severityRaw) ? severityRaw : 'warning';
1192
1409
 
1193
- const capture = findCaptureByIdLocal(vaultRoot, captureId);
1410
+ // Same once-per-call resolution rule as executePromoteCapture: `tier` and
1411
+ // `writeMode` come in on `deps`, already resolved, and default to
1412
+ // `disk`/`local` so every pre-existing caller is byte-identical.
1413
+ const tier = deps?.tier ?? 'disk';
1414
+ const writeMode = resolveWriteModeFromDeps(deps);
1415
+ const cloudDeps = { consoleFetch: deps?.consoleFetch, consoleUrl: deps?.consoleUrl };
1416
+ const cloudCompleted = [];
1417
+
1418
+ const capture = await findCaptureByIdViaTier(vaultRoot, captureId, {
1419
+ tier,
1420
+ consoleUrl: deps?.consoleUrl,
1421
+ consoleFetch: deps?.consoleFetch,
1422
+ });
1194
1423
  if (!capture) {
1195
1424
  return { error: 'CAPTURE_NOT_FOUND', message: `capture ${captureId} not found under specs/captures/` };
1196
1425
  }
@@ -1207,23 +1436,73 @@ export async function executePromoteCaptureToDiagnose(vaultRoot, args = {}, deps
1207
1436
 
1208
1437
  // A capture→diagnose promotion has no documented family-inheritance hop
1209
1438
  // (unlike capture→research) — mints a fresh family, same as any other
1210
- // parentless spec.
1211
- const familyKey = mintFamilyKey(vaultRoot, { randomUnit: deps?.randomUnit });
1439
+ // parentless spec. In CLOUD mode nothing is minted here at all: the console
1440
+ // mints the file key, the family and the filename (AC3).
1441
+ const familyKey =
1442
+ writeMode === 'cloud' ? null : mintFamilyKey(vaultRoot, { randomUnit: deps?.randomUnit });
1212
1443
  // Per-file FILE KEY (owner ruling 2026-07-10) — filename + `file_id:` SSOT,
1213
1444
  // and (V3) the anchor for `id:` itself.
1214
- const fileKey = mintFileKey(vaultRoot, { randomUnit: deps?.randomUnit });
1215
- const diagnoseId = `HS-${fileKey}`;
1445
+ // Disk occupancy ([[HS-FJWL]]): guarded by the same `writeMode === 'cloud'`
1446
+ // ternary as the family above — in cloud mode nothing is minted here at all.
1447
+ const fileKey =
1448
+ writeMode === 'cloud'
1449
+ ? null
1450
+ : mintFileKey(collectOccupiedKeysEitherKind(vaultRoot), { randomUnit: deps?.randomUnit });
1216
1451
  const slug = deriveSlug(name);
1217
- const diagnoseBasename = `${diagnoseId}-${familyKey}-DIAG-${slug}`;
1218
- const diagnoseRelPath = `specs/diagnoses/${diagnoseBasename}.md`;
1219
- const diagnoseAbsPath = join(vaultRoot, 'specs', 'diagnoses', `${diagnoseBasename}.md`);
1452
+ let diagnoseId = fileKey ? `HS-${fileKey}` : null;
1453
+ let diagnoseBasename = fileKey ? `${diagnoseId}-${familyKey}-DIAG-${slug}` : null;
1454
+ let diagnoseRelPath = fileKey ? `specs/diagnoses/${diagnoseBasename}.md` : null;
1455
+ const diagnoseAbsPath = fileKey
1456
+ ? join(vaultRoot, 'specs', 'diagnoses', `${diagnoseBasename}.md`)
1457
+ : null;
1458
+
1459
+ const diagnoseContent = buildDiagnoseContent(
1460
+ diagnoseId ?? CLOUD_PENDING_ID,
1461
+ fileKey ?? CLOUD_PENDING_KEY,
1462
+ familyKey ?? CLOUD_PENDING_KEY,
1463
+ name,
1464
+ severity,
1465
+ capture.basename,
1466
+ coreIdea,
1467
+ now,
1468
+ );
1220
1469
 
1221
1470
  const writes = [];
1222
- writes.push({
1223
- kind: 'create',
1224
- absPath: diagnoseAbsPath,
1225
- content: buildDiagnoseContent(diagnoseId, fileKey, familyKey, name, severity, capture.basename, coreIdea, now),
1226
- });
1471
+ if (writeMode === 'cloud') {
1472
+ // CREATE FIRST — the capture's `promoted_to:` links to the basename this
1473
+ // POST returns, so it cannot be built until the POST has answered.
1474
+ const { frontmatter, body } = splitRawSpecForConsole(diagnoseContent);
1475
+ const cleaned = stripServerMintedFields(frontmatter);
1476
+ delete cleaned.family; // never inherited on this hop; let the server mint it
1477
+ let created;
1478
+ try {
1479
+ created = await createSpecViaConsole({
1480
+ ...cloudDeps,
1481
+ type: 'diagnose',
1482
+ name,
1483
+ frontmatter: cleaned,
1484
+ body,
1485
+ actingAgent: deps?.actingAgent ?? null,
1486
+ });
1487
+ } catch (err) {
1488
+ return cloudWriteFailureResult(err, {
1489
+ step: { kind: 'create', type: 'diagnose', name },
1490
+ completed: cloudCompleted,
1491
+ });
1492
+ }
1493
+ diagnoseId = created.specId;
1494
+ diagnoseBasename = created.basename;
1495
+ diagnoseRelPath = created.relPath;
1496
+ cloudCompleted.push({
1497
+ kind: 'create',
1498
+ type: 'diagnose',
1499
+ spec_id: created.specId,
1500
+ basename: created.basename,
1501
+ rel_path: created.relPath,
1502
+ });
1503
+ } else {
1504
+ writes.push({ kind: 'create', absPath: diagnoseAbsPath, content: diagnoseContent });
1505
+ }
1227
1506
 
1228
1507
  const captureUpdate = buildUpdatedCaptureContentForDiagnose(capture.raw, {
1229
1508
  diagnoseBasename,
@@ -1232,16 +1511,50 @@ export async function executePromoteCaptureToDiagnose(vaultRoot, args = {}, deps
1232
1511
  author: 'promote-capture (agent)',
1233
1512
  });
1234
1513
  if (captureUpdate.changed) {
1235
- writes.push({ kind: 'update', absPath: capture.absPath, content: captureUpdate.content });
1514
+ if (writeMode === 'cloud') {
1515
+ // …LINK SECOND.
1516
+ const { frontmatter, body } = splitRawSpecForConsole(captureUpdate.content);
1517
+ try {
1518
+ const res = await updateSpecViaConsole({
1519
+ ...cloudDeps,
1520
+ specId: getScalarField(getFrontmatterBlock(capture.raw)?.block ?? '', 'id') ?? capture.basename,
1521
+ frontmatter,
1522
+ body,
1523
+ });
1524
+ cloudCompleted.push({
1525
+ kind: 'update',
1526
+ type: 'capture',
1527
+ spec_id: res.specId,
1528
+ basename: res.basename ?? capture.basename,
1529
+ rel_path: res.relPath,
1530
+ });
1531
+ } catch (err) {
1532
+ return cloudWriteFailureResult(err, {
1533
+ step: { kind: 'update', type: 'capture', basename: capture.basename },
1534
+ completed: cloudCompleted,
1535
+ });
1536
+ }
1537
+ } else {
1538
+ writes.push({ kind: 'update', absPath: capture.absPath, content: captureUpdate.content });
1539
+ }
1236
1540
  }
1237
1541
 
1238
- const commitResult = commitWrites(writes);
1239
- if (!commitResult.ok) {
1240
- return {
1241
- error: 'WRITE_FAILED',
1242
- message: commitResult.error?.message ?? 'unknown write failure',
1243
- rollback_error: commitResult.error?.rollbackError,
1244
- };
1542
+ if (writeMode !== 'cloud') {
1543
+ const commitResult = commitWrites(writes);
1544
+ if (!commitResult.ok) {
1545
+ return {
1546
+ error: 'WRITE_FAILED',
1547
+ message: commitResult.error?.message ?? 'unknown write failure',
1548
+ rollback_error: commitResult.error?.rollbackError,
1549
+ };
1550
+ }
1551
+ }
1552
+
1553
+ // By this point every path (local mint or cloud create-first) has replaced
1554
+ // the transitional fileKey-pending null with a real id — narrow it here so
1555
+ // callers (e.g. `promote-diagnose.ts`) see `string`, not `string | null`.
1556
+ if (!diagnoseId || !diagnoseBasename || !diagnoseRelPath) {
1557
+ throw new Error('promoteCaptureToDiagnose: missing diagnose id after promotion (unexpected)');
1245
1558
  }
1246
1559
 
1247
1560
  return {
@@ -1257,7 +1570,11 @@ export async function executePromoteCaptureToDiagnose(vaultRoot, args = {}, deps
1257
1570
  informs_diagnose_added: captureUpdate.changed,
1258
1571
  promoted_to_set: captureUpdate.setPromotedTo,
1259
1572
  },
1260
- writes: writes.map((w) => ({ kind: w.kind, path: w.absPath })),
1573
+ ...(writeMode === 'cloud' ? { write_mode: 'cloud' } : {}),
1574
+ writes:
1575
+ writeMode === 'cloud'
1576
+ ? cloudCompleted.map((w) => ({ kind: w.kind, path: w.rel_path ?? null, spec_id: w.spec_id }))
1577
+ : writes.map((w) => ({ kind: w.kind, path: w.absPath })),
1261
1578
  human_review_required: false,
1262
1579
  };
1263
1580
  }
@@ -1266,6 +1583,8 @@ export const __testables = {
1266
1583
  validateInputShape,
1267
1584
  findCaptureByIdLocal,
1268
1585
  findResearchByIdLocal,
1586
+ findCaptureByIdViaTier,
1587
+ findResearchByIdViaTier,
1269
1588
  findResearchByBasename,
1270
1589
  collectOccupiedResearchIds,
1271
1590
  collectOccupiedDiagnoseIds,