heyspex 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (352) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/README.md +21 -0
  3. package/bin/hs.mjs +1089 -104
  4. package/bin/spexcode.mjs +262 -0
  5. package/dev-factory/engine/worktree.mjs +740 -0
  6. package/dev-factory/lib/code-repo-root.mjs +67 -0
  7. package/dev-factory/lib/context-budget.mjs +84 -0
  8. package/dev-factory/lib/epic-detect.mjs +190 -0
  9. package/dev-factory/lib/fleet-queue-file.mjs +846 -0
  10. package/dev-factory/lib/fleet-queue.mjs +426 -0
  11. package/dev-factory/lib/fleet-state.mjs +14 -18
  12. package/dev-factory/lib/model-catalog.mjs +338 -0
  13. package/dev-factory/lib/seat-context.mjs +199 -0
  14. package/dev-factory/lib/spec-io.mjs +93 -0
  15. package/dev-factory/lib/sufficiency-score.mjs +8 -2
  16. package/dev-factory/lib/worktree-allocator.mjs +744 -0
  17. package/dev-factory/model-catalog.json +222 -0
  18. package/dist/cli/hs-check.mjs +1127 -103
  19. package/dist/cli/hs-claim.mjs +981 -63
  20. package/dist/cli/hs-connect.mjs +10059 -1729
  21. package/dist/cli/hs-defer.mjs +5507 -3072
  22. package/dist/cli/hs-dev.mjs +576 -0
  23. package/dist/cli/hs-env.mjs +416 -302
  24. package/dist/cli/hs-index-rebuild.mjs +7334 -0
  25. package/dist/cli/hs-index-reconcile.mjs +8138 -0
  26. package/dist/cli/hs-init.mjs +2840 -149
  27. package/dist/cli/hs-install.mjs +7214 -594
  28. package/dist/cli/hs-join.mjs +10640 -2421
  29. package/dist/cli/hs-vault-pull.mjs +8157 -6370
  30. package/dist/cli/hs-vault-push.mjs +6433 -0
  31. package/dist/cli/hs-vault.mjs +4029 -1555
  32. package/dist/cli/hs-workspace.mjs +2702 -538
  33. package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
  34. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
  35. package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
  36. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
  37. package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
  38. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
  39. package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
  40. package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
  41. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
  42. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
  43. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
  44. package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
  45. package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
  46. package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -0
  47. package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
  48. package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
  49. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
  50. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
  51. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
  52. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
  53. package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
  54. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
  55. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
  56. package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -0
  57. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
  58. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
  59. package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
  60. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
  61. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
  62. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
  63. package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
  64. package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
  65. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +33 -0
  66. package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
  67. package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
  68. package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
  69. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
  70. package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
  71. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
  72. package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
  73. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
  74. package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
  75. package/dist/cli-templates/harness-commands/cursor/hs-view.md +32 -0
  76. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
  77. package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
  78. package/docs/copilot-plugin-install.md +162 -0
  79. package/docs/driving-heyspex-from-copilot.md +165 -0
  80. package/docs/mcp-client-config.md +259 -6
  81. package/package.json +249 -124
  82. package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
  83. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
  84. package/plugins/heyspex/.mcp.json +12 -0
  85. package/plugins/heyspex/README.md +309 -0
  86. package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
  87. package/plugins/heyspex/bin/run-hook.mjs +80 -0
  88. package/plugins/heyspex/hooks/cursor-hooks.json +308 -0
  89. package/plugins/heyspex/hooks/hooks.json +418 -0
  90. package/plugins/heyspex/mcp.json +13 -0
  91. package/scripts/check-mcp-freshness.mjs +17 -9
  92. package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
  93. package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
  94. package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
  95. package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -0
  96. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +356 -0
  97. package/scripts/claude-hooks/_run.mjs +413 -0
  98. package/scripts/claude-hooks/_transcript-text.mjs +60 -0
  99. package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
  100. package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
  101. package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
  102. package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
  103. package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
  104. package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
  105. package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
  106. package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
  107. package/scripts/claude-hooks/claim-liveness-shared.mjs +577 -0
  108. package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
  109. package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
  110. package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
  111. package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
  112. package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
  113. package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
  114. package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
  115. package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
  116. package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
  117. package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
  118. package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
  119. package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
  120. package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
  121. package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
  122. package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
  123. package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
  124. package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
  125. package/scripts/claude-hooks/inject-communication-style.mjs +86 -0
  126. package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
  127. package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
  128. package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
  129. package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
  130. package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -0
  131. package/scripts/claude-hooks/log-session-activity.mjs +341 -0
  132. package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
  133. package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
  134. package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
  135. package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
  136. package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
  137. package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
  138. package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
  139. package/scripts/claude-hooks/research-method-guard.mjs +398 -0
  140. package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
  141. package/scripts/claude-hooks/session-end.mjs +451 -0
  142. package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
  143. package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
  144. package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
  145. package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
  146. package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
  147. package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
  148. package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
  149. package/scripts/claude-hooks/wip-checkpoint.mjs +302 -0
  150. package/scripts/cursor-hooks/manifest.mjs +281 -0
  151. package/scripts/dev-server-status.mjs +34 -13
  152. package/scripts/dev-server-stop.mjs +313 -0
  153. package/scripts/heyspex-mode.mjs +288 -0
  154. package/scripts/hs-auto-resolve.mjs +240 -0
  155. package/scripts/hs-auto-source.mjs +398 -0
  156. package/scripts/hs-auto.mjs +134 -0
  157. package/scripts/hs-capture-cli.mjs +326 -0
  158. package/scripts/hs-classifier.mjs +136 -0
  159. package/scripts/hs-companion.mjs +88 -0
  160. package/scripts/hs-gateway.mjs +115 -0
  161. package/scripts/hs-mode.mjs +125 -0
  162. package/scripts/hs-next.mjs +441 -0
  163. package/scripts/hs-settings.mjs +338 -0
  164. package/scripts/hs-view.mjs +496 -0
  165. package/scripts/hs-worktree.mjs +201 -0
  166. package/scripts/install-claude-code-hooks.mjs +272 -0
  167. package/scripts/install-cursor-hooks.mjs +471 -0
  168. package/scripts/lib/agent-env-markers.mjs +38 -0
  169. package/scripts/lib/console-url-resolver.mjs +199 -61
  170. package/scripts/lib/customer-kit.mjs +1203 -0
  171. package/scripts/lib/dev-mode.mjs +249 -32
  172. package/scripts/lib/directive-obligations.mjs +349 -0
  173. package/scripts/lib/heyspex-home.mjs +72 -0
  174. package/scripts/lib/hook-dispositions.mjs +210 -0
  175. package/scripts/lib/hook-vault-header.mjs +30 -0
  176. package/scripts/lib/hs-command.mjs +277 -0
  177. package/scripts/lib/hs-companion-settings.mjs +472 -0
  178. package/scripts/lib/hs-source-branch.mjs +196 -0
  179. package/scripts/lib/hs-source-commit.mjs +59 -5
  180. package/scripts/lib/hs-source-identity.mjs +526 -0
  181. package/scripts/lib/hs-source-log.mjs +13 -22
  182. package/scripts/lib/hs-source-preflight.mjs +18 -4
  183. package/scripts/lib/hs-source-push.mjs +386 -307
  184. package/scripts/lib/hs-worktree-args.mjs +115 -0
  185. package/scripts/lib/shared-path-helpers.mjs +134 -0
  186. package/scripts/lib/standalone-doctor.mjs +540 -3
  187. package/scripts/lib/subagent-git-lock.mjs +109 -0
  188. package/scripts/lib/template-pin.mjs +34 -0
  189. package/scripts/lib/window-placement.mjs +109 -0
  190. package/scripts/provenance-lib.mjs +91 -0
  191. package/scripts/stamp-provenance-on-edit.mjs +222 -0
  192. package/scripts/wip-ledger-health.mjs +653 -0
  193. package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
  194. package/spexharness/src/adapters/audit.mjs +197 -0
  195. package/spexharness/src/adapters/claude-code.mjs +449 -0
  196. package/spexharness/src/adapters/copilot.mjs +236 -0
  197. package/spexharness/src/adapters/detect-harness.mjs +34 -0
  198. package/spexharness/src/chat/tui/ansi.mjs +448 -0
  199. package/spexharness/src/chat/tui/palette.mjs +22 -0
  200. package/spexharness/src/chat/tui/select.mjs +208 -0
  201. package/spexharness/src/config-root.mjs +54 -0
  202. package/spexharness/src/context-budget.mjs +141 -0
  203. package/spexharness/src/dev-server-control.mjs +389 -0
  204. package/spexharness/src/doctrine/behaviour.mjs +371 -0
  205. package/spexharness/src/doctrine/corpus.mjs +733 -0
  206. package/spexharness/src/doctrine/inject.mjs +338 -0
  207. package/spexharness/src/doctrine/merge.mjs +184 -0
  208. package/spexharness/src/doctrine/sources.mjs +252 -0
  209. package/spexharness/src/doctrine/synthesize.mjs +401 -0
  210. package/spexharness/src/doctrine/vault-source.mjs +574 -0
  211. package/spexharness/src/doctrine/writer.mjs +176 -0
  212. package/spexharness/src/events.mjs +84 -0
  213. package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
  214. package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
  215. package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
  216. package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
  217. package/spexharness/src/gates/deep-research-cost-gate.mjs +275 -0
  218. package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
  219. package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
  220. package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
  221. package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
  222. package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
  223. package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
  224. package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
  225. package/spexharness/src/gates/git-identity-guard.mjs +61 -0
  226. package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
  227. package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
  228. package/spexharness/src/gates/git-stash-gate.mjs +159 -0
  229. package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
  230. package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
  231. package/spexharness/src/gates/inject-vault-context.mjs +239 -0
  232. package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
  233. package/spexharness/src/gates/plan-review-gate.mjs +233 -0
  234. package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
  235. package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
  236. package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
  237. package/spexharness/src/gates/settings-write-gate.mjs +104 -0
  238. package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
  239. package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
  240. package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
  241. package/spexharness/src/git-reflog-audit.mjs +294 -0
  242. package/spexharness/src/intent-classifier.mjs +196 -0
  243. package/spexharness/src/intent-directive.mjs +626 -0
  244. package/spexharness/src/profile/learn.mjs +551 -0
  245. package/spexharness/src/profile/proposals.mjs +357 -0
  246. package/spexharness/src/profile/vault-inclusion.mjs +386 -0
  247. package/spexharness/src/redact.mjs +151 -0
  248. package/spexharness/src/rule-table.mjs +849 -0
  249. package/spexharness/src/seat-context.mjs +122 -0
  250. package/spexharness/src/session-bootstrap.mjs +512 -0
  251. package/spexharness/src/session-record.mjs +815 -0
  252. package/spexharness/src/token-inventory.mjs +207 -0
  253. package/vendor/heyspex-mcp/account-switch.mjs +96 -0
  254. package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
  255. package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
  256. package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
  257. package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
  258. package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
  259. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +77 -8
  260. package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
  261. package/vendor/heyspex-mcp/capture-console-client.mjs +117 -3
  262. package/vendor/heyspex-mcp/capture-focus.mjs +1061 -31
  263. package/vendor/heyspex-mcp/capture-outbox.mjs +15 -5
  264. package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
  265. package/vendor/heyspex-mcp/capture-tools.mjs +1264 -0
  266. package/vendor/heyspex-mcp/claim-tools.mjs +1147 -0
  267. package/vendor/heyspex-mcp/cli-verbs.mjs +2074 -0
  268. package/vendor/heyspex-mcp/cli.mjs +207 -15524
  269. package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
  270. package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
  271. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +602 -0
  272. package/vendor/heyspex-mcp/config-seed.yaml +15 -0
  273. package/vendor/heyspex-mcp/config-settings.mjs +290 -7
  274. package/vendor/heyspex-mcp/console-fetch.mjs +822 -17
  275. package/vendor/heyspex-mcp/console-transport.mjs +172 -0
  276. package/vendor/heyspex-mcp/credentials.mjs +585 -0
  277. package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
  278. package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
  279. package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
  280. package/vendor/heyspex-mcp/design-tools.mjs +4 -1
  281. package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
  282. package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
  283. package/vendor/heyspex-mcp/dev-tools.mjs +131 -0
  284. package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
  285. package/vendor/heyspex-mcp/events-tools.mjs +207 -0
  286. package/vendor/heyspex-mcp/filing-tools.mjs +723 -0
  287. package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
  288. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +71 -15
  289. package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
  290. package/vendor/heyspex-mcp/folder-trust-store.mjs +60 -0
  291. package/vendor/heyspex-mcp/http-transport.mjs +302 -0
  292. package/vendor/heyspex-mcp/list-tools.mjs +257 -0
  293. package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
  294. package/vendor/heyspex-mcp/mcp-config.mjs +107 -0
  295. package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
  296. package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
  297. package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
  298. package/vendor/heyspex-mcp/occupied-keys.mjs +142 -0
  299. package/vendor/heyspex-mcp/on-deck-brief.mjs +3377 -0
  300. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
  301. package/vendor/heyspex-mcp/package-root.mjs +32 -0
  302. package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
  303. package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
  304. package/vendor/heyspex-mcp/plan-push.mjs +908 -22
  305. package/vendor/heyspex-mcp/plan-review.mjs +124 -30
  306. package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
  307. package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -0
  308. package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -970
  309. package/vendor/heyspex-mcp/promote-capture.mjs +388 -69
  310. package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
  311. package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
  312. package/vendor/heyspex-mcp/promote-research.mjs +666 -53
  313. package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
  314. package/vendor/heyspex-mcp/prompts.mjs +1460 -0
  315. package/vendor/heyspex-mcp/read-path.mjs +191 -15
  316. package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
  317. package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
  318. package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
  319. package/vendor/heyspex-mcp/register-spec.mjs +266 -54
  320. package/vendor/heyspex-mcp/request-context.mjs +83 -0
  321. package/vendor/heyspex-mcp/research-focus.mjs +207 -4
  322. package/vendor/heyspex-mcp/research-handlers.mjs +625 -0
  323. package/vendor/heyspex-mcp/research-tools.mjs +4 -4
  324. package/vendor/heyspex-mcp/response-budget.mjs +348 -0
  325. package/vendor/heyspex-mcp/review-focus.mjs +25 -1
  326. package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
  327. package/vendor/heyspex-mcp/search-specs.mjs +280 -0
  328. package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
  329. package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
  330. package/vendor/heyspex-mcp/session-owner.mjs +109 -0
  331. package/vendor/heyspex-mcp/session-tools.mjs +162 -0
  332. package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
  333. package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
  334. package/vendor/heyspex-mcp/spec-edit-tools.mjs +453 -0
  335. package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
  336. package/vendor/heyspex-mcp/split-research.mjs +304 -56
  337. package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
  338. package/vendor/heyspex-mcp/submit-for-review.mjs +150 -7
  339. package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
  340. package/vendor/heyspex-mcp/summary-write-tools.mjs +228 -0
  341. package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
  342. package/vendor/heyspex-mcp/test-helpers.mjs +123 -1
  343. package/vendor/heyspex-mcp/tool-catalog.mjs +2819 -0
  344. package/vendor/heyspex-mcp/tool-dispatch.mjs +657 -0
  345. package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
  346. package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
  347. package/vendor/heyspex-mcp/vault-io.mjs +720 -0
  348. package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
  349. package/vendor/heyspex-mcp/vault-resolve.mjs +554 -68
  350. package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
  351. package/vendor/heyspex-mcp/whoami.mjs +92 -0
  352. package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
@@ -0,0 +1,602 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cloud-vault-writes.mjs — the WRITE-side twin of `read-path.mjs`.
4
+ *
5
+ * `read-path.mjs` decides `disk` vs `neon` for every vault READ the MCP
6
+ * server makes. This module is the single place every vault WRITE goes when
7
+ * `resolveVaultWriteMode()` says `cloud`: a create becomes
8
+ * `POST /api/specs` (the console mints the id, the family and the filename),
9
+ * an edit of an existing spec becomes `PUT /api/specs/:id`, and a listing
10
+ * needed to build either becomes `GET /api/specs?type=…`.
11
+ *
12
+ * Why this exists: a disk write in cloud mode is a SPLIT BRAIN — a markdown
13
+ * file on a machine the console's index has never heard of, on a machine that
14
+ * may not even hold a clone of the vault. `handleSaveResearch`
15
+ * (research-handlers.mjs) and `handleFileSpec` (filing-tools.mjs) already
16
+ * file through the console; this module gives every OTHER writer (the
17
+ * promotion helpers, plan-push, register-spec, spec_edit, capture-focus) the
18
+ * same two verbs without each re-deriving the request shape, the
19
+ * idempotency-key rule, or the failure envelope.
20
+ *
21
+ * Transport-free by design, like read-path.mjs: callers inject
22
+ * `consoleFetch`. The module never imports console-transport.mjs, so every
23
+ * test drives it with a recorder and never opens a socket.
24
+ *
25
+ * Failure contract: every failure throws {@link CloudWriteUnreachableError},
26
+ * whose `message` STARTS WITH `CONSOLE_UNREACHABLE_CLOUD_MODE` and names the
27
+ * route and the console — the same string shape `readViaSelector` produces on
28
+ * the read side, so one `catch` in a handler can render either. There is no
29
+ * disk fallback: a cloud-reliant session that cannot reach its console fails
30
+ * loudly rather than leaving bytes behind that nothing indexes.
31
+ *
32
+ * There is deliberately NO rollback. `commitWrites`'s disk transaction can
33
+ * restore a snapshot; the console has no "un-create". Instead
34
+ * {@link commitWritesViaConsole} stops at the FIRST failure and reports which
35
+ * step failed and which steps already landed, so the caller can say exactly
36
+ * what state the vault is in ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]] AC5).
37
+ */
38
+
39
+ import { randomUUID } from 'node:crypto';
40
+ import { walkSpecListPages } from './list-tools.mjs';
41
+ import { parseFrontmatter } from './vault-io.mjs';
42
+ import {
43
+ buildDeferredPayload,
44
+ drainAndSummarize,
45
+ getWriteOutboxRoot,
46
+ isOutageClassResult,
47
+ stashIfOutage,
48
+ stashWrite,
49
+ failureOf,
50
+ DEFERRED_RECEIPT,
51
+ } from './write-outbox.mjs';
52
+
53
+ /**
54
+ * Raised when a `cloud`-mode write fails. Mirrors
55
+ * `ConsoleUnreachableCloudModeError` (read-path.mjs) exactly — same `name`,
56
+ * same message prefix — so callers that already render one render both.
57
+ */
58
+ export class CloudWriteUnreachableError extends Error {
59
+ constructor(message, details = {}) {
60
+ super(message);
61
+ this.name = 'CONSOLE_UNREACHABLE_CLOUD_MODE';
62
+ Object.assign(this, details);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Raised INSTEAD of a plain {@link CloudWriteUnreachableError} when the write hit
68
+ * an outage (status 0 / timeout / 5xx) and was queued to the write outbox
69
+ * ([[heyspex-is-online-only-no-offline]] 2026-08-20 amendment). It extends the
70
+ * unreachable error so every existing `catch` still works, but it carries
71
+ * `deferred: true` + the truthful receipt: the write did NOT land yet, and it is
72
+ * NOT lost. Callers that can return a tool response render
73
+ * {@link deferredPayloadFromError}; callers that need the new spec's id/basename
74
+ * (promotion chains) cannot continue and surface it as before.
75
+ */
76
+ export class CloudWriteDeferredError extends CloudWriteUnreachableError {
77
+ constructor(message, details = {}) {
78
+ super(message, details);
79
+ this.deferred = true;
80
+ }
81
+ }
82
+
83
+ /** The `{ status: 'deferred', … }` tool payload for a deferred error, or null. */
84
+ export function deferredPayloadFromError(err) {
85
+ if (!err?.deferred || !err.outbox_id) return null;
86
+ return {
87
+ status: 'deferred',
88
+ outbox_id: err.outbox_id,
89
+ pending_sync: err.pending_sync ?? 1,
90
+ receipt: err.receipt ?? DEFERRED_RECEIPT,
91
+ ...(err.completed_steps ? { completed_steps: err.completed_steps } : {}),
92
+ };
93
+ }
94
+
95
+ /** Details carrying the replayable request for a no-stash caller (commitWritesViaConsole). */
96
+ function outageRequest(noStash, result, entry) {
97
+ return noStash && isOutageClassResult(result)
98
+ ? { outage_request: { ...entry, failure: failureOf(result) } }
99
+ : {};
100
+ }
101
+
102
+ function deferredError(routeLabel, consoleUrl, stashed, vaultRoot, details = {}) {
103
+ const payload = buildDeferredPayload(stashed, vaultRoot);
104
+ return new CloudWriteDeferredError(
105
+ `CONSOLE_UNREACHABLE_CLOUD_MODE: route "${routeLabel}" failed against console ${consoleUrl ?? '<unknown console>'} — ${DEFERRED_RECEIPT} (outbox_id ${payload.outbox_id})`,
106
+ { route: routeLabel, console_url: consoleUrl ?? null, ...details, outbox_id: payload.outbox_id, pending_sync: payload.pending_sync, receipt: payload.receipt },
107
+ );
108
+ }
109
+
110
+ /** Build the canonical failure error for a route + console + detail. */
111
+ export function cloudWriteFailure(routeLabel, consoleUrl, detail, details = {}) {
112
+ return new CloudWriteUnreachableError(
113
+ `CONSOLE_UNREACHABLE_CLOUD_MODE: route "${routeLabel}" failed against console ${consoleUrl ?? '<unknown console>'} — ${detail}`,
114
+ { route: routeLabel, console_url: consoleUrl ?? null, ...details },
115
+ );
116
+ }
117
+
118
+ /** One-line summary of a consoleFetch failure result, for the message above. */
119
+ function describeResult(result) {
120
+ if (!result) return 'no response';
121
+ const bodyText =
122
+ result.error ||
123
+ (result.body ? JSON.stringify(result.body).slice(0, 400) : '') ||
124
+ '';
125
+ return `${result.status ?? '?'}${bodyText ? ` ${bodyText}` : ''}`;
126
+ }
127
+
128
+ /**
129
+ * Fields the SERVER mints and a create request must therefore never carry.
130
+ * `id`/`file_id` are the file key itself (server-minted since 2026-07-10 —
131
+ * a locally minted key in a file-less vault collides with a cloud-side one);
132
+ * `created`/`updated`/`created_tz`/`updated_tz` are stamped by `createSpec`.
133
+ * `family` is NOT stripped: an explicit family is a legal, meaningful
134
+ * override (`createSpec` honours it over parent inheritance).
135
+ */
136
+ export const SERVER_MINTED_FIELDS = Object.freeze([
137
+ 'id',
138
+ 'file_id',
139
+ 'created',
140
+ 'updated',
141
+ 'created_tz',
142
+ 'updated_tz',
143
+ ]);
144
+
145
+ /**
146
+ * The status the console seeds on every non-ADR create (`createSpec` in
147
+ * spex-console/src/lib/vault.ts). Sending it on a POST asks for nothing.
148
+ */
149
+ export const DEFAULT_INITIAL_STATUS = 'open';
150
+
151
+ /** True when `value` is the default initial status (case/whitespace-insensitive). */
152
+ export function isDefaultInitialStatus(value) {
153
+ return typeof value === 'string' && value.trim().toLowerCase() === DEFAULT_INITIAL_STATUS;
154
+ }
155
+
156
+ /** Copy of `frontmatter` with every server-minted field removed. */
157
+ export function stripServerMintedFields(frontmatter = {}) {
158
+ const out = {};
159
+ for (const [key, value] of Object.entries(frontmatter ?? {})) {
160
+ if (SERVER_MINTED_FIELDS.includes(key)) continue;
161
+ out[key] = value;
162
+ }
163
+ return out;
164
+ }
165
+
166
+ /**
167
+ * Split a raw `.md` file (the shape every disk writer already builds) into the
168
+ * `{ frontmatter, body }` pair both console write routes take. Reuses
169
+ * `parseFrontmatter` from vault-io.mjs so the YAML dialect the vault writes is
170
+ * the YAML dialect this reads — no second parser to drift.
171
+ */
172
+ export function splitRawSpec(raw) {
173
+ const { frontmatter, body } = parseFrontmatter(String(raw ?? ''));
174
+ return { frontmatter: frontmatter ?? {}, body: body ?? '' };
175
+ }
176
+
177
+ /** Basename (no extension) from a console-returned relPath. */
178
+ export function basenameFromRelPath(relPath) {
179
+ if (!relPath) return null;
180
+ return String(relPath)
181
+ .split(/[\\/]/)
182
+ .pop()
183
+ .replace(/\.md$/i, '');
184
+ }
185
+
186
+ /**
187
+ * `POST /api/specs` — create one spec through the console.
188
+ *
189
+ * The request shape is `buildFileSpecPostBody`'s (filing-tools.mjs), down to
190
+ * the per-call `idempotency-key` header and `retryOnTimeout: false` (the
191
+ * [[HS-2FQY]] double-mint guard: a blind retry against a REMOTE console can
192
+ * land on a different serverless instance whose in-memory idempotency
193
+ * registry cannot replay the first create, minting a second id for one spec).
194
+ *
195
+ * Returns `{ specId, relPath, basename, spec, consoleWrite, warnings }`. The
196
+ * BASENAME is the load-bearing return value for promotion flows: every
197
+ * sibling write references the new document by `[[basename]]`, and in cloud
198
+ * mode that basename is only knowable after this call returns — which is why
199
+ * cloud-mode promotion is create-first, link-second.
200
+ */
201
+ export async function createSpecViaConsole({
202
+ consoleFetch,
203
+ consoleUrl = null,
204
+ type,
205
+ name,
206
+ summary = null,
207
+ frontmatter = null,
208
+ body = null,
209
+ actingAgent = null,
210
+ idempotencyKey = null,
211
+ routeLabel = 'POST /api/specs',
212
+ vaultRoot = null,
213
+ noStash = false,
214
+ }) {
215
+ const outboxRoot = noStash ? null : vaultRoot ?? getWriteOutboxRoot();
216
+ if (typeof consoleFetch !== 'function') {
217
+ throw cloudWriteFailure(routeLabel, consoleUrl, 'no consoleFetch dependency was injected');
218
+ }
219
+ if (typeof type !== 'string' || !type.trim()) {
220
+ throw cloudWriteFailure(routeLabel, consoleUrl, 'a spec `type` is required to create through the console');
221
+ }
222
+ if (typeof name !== 'string' || !name.trim()) {
223
+ throw cloudWriteFailure(routeLabel, consoleUrl, 'a spec `name` is required to create through the console');
224
+ }
225
+ const cleanedFm = stripServerMintedFields(frontmatter ?? {});
226
+ // `name`/`type`/`summary` ride at the TOP level of the request (the route's
227
+ // contract); leaving duplicates inside `frontmatter` lets the two disagree.
228
+ delete cleanedFm.name;
229
+ delete cleanedFm.type;
230
+ // [[HS-ISZP]]: `status:` is owned by the status route — the create route
231
+ // refuses it out of band (`out_of_band_status_write`). The DEFAULT initial
232
+ // value is not a transition (the server seeds `open` itself), so it is
233
+ // dropped here for every disk-template-derived body (research stub,
234
+ // diagnose, epic/task stubs). Any OTHER value is deliberately left in so the
235
+ // server refuses it loudly rather than this client silently downgrading it.
236
+ if (isDefaultInitialStatus(cleanedFm.status)) delete cleanedFm.status;
237
+ const requestBody = {
238
+ type: type.trim(),
239
+ name: name.trim(),
240
+ ...(summary ? { summary } : {}),
241
+ ...(Object.keys(cleanedFm).length > 0 ? { frontmatter: cleanedFm } : {}),
242
+ ...(typeof body === 'string' ? { body } : {}),
243
+ ...(actingAgent ? { acting_agent: actingAgent } : {}),
244
+ };
245
+
246
+ // One key per LOGICAL call: the stash below stores THIS key so a replay is the
247
+ // same create, never a second mint ([[HS-2FQY]]).
248
+ const createKey = idempotencyKey ?? randomUUID();
249
+ const createEntry = {
250
+ op: 'create',
251
+ kind: 'create',
252
+ route: '/api/specs',
253
+ method: 'POST',
254
+ request_body: requestBody,
255
+ idempotency_key: createKey,
256
+ acting_agent: actingAgent,
257
+ };
258
+ if (outboxRoot) await drainAndSummarize(outboxRoot, consoleFetch);
259
+ let result;
260
+ try {
261
+ result = await consoleFetch('/api/specs', {
262
+ method: 'POST',
263
+ body: requestBody,
264
+ headers: { 'idempotency-key': createKey },
265
+ retryOnTimeout: false,
266
+ });
267
+ } catch (err) {
268
+ if (outboxRoot) {
269
+ const stashed = stashWrite(outboxRoot, {
270
+ ...createEntry,
271
+ failure: { status: 0, errorKind: 'network', message: String(err?.message ?? err).slice(0, 500) },
272
+ });
273
+ if (stashed) throw deferredError(routeLabel, consoleUrl, stashed, outboxRoot, { attempted: { type: requestBody.type, name: requestBody.name } });
274
+ }
275
+ throw cloudWriteFailure(routeLabel, consoleUrl, String(err?.message ?? err), {
276
+ attempted: { type: requestBody.type, name: requestBody.name },
277
+ ...outageRequest(noStash, { ok: false, status: 0, errorKind: 'network', error: String(err?.message ?? err) }, createEntry),
278
+ });
279
+ }
280
+ if (!result?.ok) {
281
+ const stashed = outboxRoot
282
+ ? stashIfOutage(result, createEntry, outboxRoot)
283
+ : null;
284
+ if (stashed) {
285
+ throw deferredError(routeLabel, result?.url ?? consoleUrl, stashed, outboxRoot, {
286
+ status: result?.status ?? null,
287
+ attempted: { type: requestBody.type, name: requestBody.name },
288
+ });
289
+ }
290
+ throw cloudWriteFailure(routeLabel, result?.url ?? consoleUrl, describeResult(result), {
291
+ status: result?.status ?? null,
292
+ console_response: result?.body ?? null,
293
+ attempted: { type: requestBody.type, name: requestBody.name },
294
+ ...outageRequest(noStash, result, createEntry),
295
+ ...(result?.errorKind === 'timeout'
296
+ ? {
297
+ hint:
298
+ 'The create may still have completed server-side after this client timed out. '
299
+ + 'Search for a spec with this exact name before retrying — a blind retry can mint a duplicate id ([[HS-2FQY]]).',
300
+ }
301
+ : {}),
302
+ });
303
+ }
304
+ const spec = result.body?.spec ?? null;
305
+ const specId = typeof spec?.id === 'string' && spec.id ? spec.id : null;
306
+ const relPath = spec?.relPath ?? spec?.path ?? null;
307
+ if (!specId) {
308
+ throw cloudWriteFailure(
309
+ routeLabel,
310
+ result.url ?? consoleUrl,
311
+ `answered ${result.status} but carried no spec.id — treat the spec as UNCREATED and check the console before retrying`,
312
+ { status: result.status, console_response: result.body ?? null },
313
+ );
314
+ }
315
+ return {
316
+ specId,
317
+ relPath,
318
+ basename: basenameFromRelPath(relPath) ?? specId,
319
+ spec,
320
+ consoleWrite: result.body?.write ?? null,
321
+ warnings: Array.isArray(result.body?.warnings) ? result.body.warnings : [],
322
+ consoleUrl: result.url ?? consoleUrl,
323
+ };
324
+ }
325
+
326
+ /**
327
+ * `PUT /api/specs/:id` — replace an existing spec's frontmatter + body.
328
+ *
329
+ * The route takes `{ frontmatter: object, body: string }` and answers 404 for
330
+ * an id it cannot resolve (a real answer: the spec is not in this caller's
331
+ * vault), which is surfaced here as a distinct `notFound: true` failure so a
332
+ * caller can tell "unreachable console" from "no such spec".
333
+ */
334
+ export async function updateSpecViaConsole({
335
+ consoleFetch,
336
+ consoleUrl = null,
337
+ specId,
338
+ frontmatter,
339
+ body,
340
+ routeLabel = null,
341
+ vaultRoot = null,
342
+ noStash = false,
343
+ }) {
344
+ const route = routeLabel ?? `PUT /api/specs/${specId}`;
345
+ const outboxRoot = noStash ? null : vaultRoot ?? getWriteOutboxRoot();
346
+ if (typeof consoleFetch !== 'function') {
347
+ throw cloudWriteFailure(route, consoleUrl, 'no consoleFetch dependency was injected');
348
+ }
349
+ if (typeof specId !== 'string' || !specId.trim()) {
350
+ throw cloudWriteFailure(route, consoleUrl, 'a spec id is required to update through the console');
351
+ }
352
+ if (typeof frontmatter !== 'object' || frontmatter === null) {
353
+ throw cloudWriteFailure(route, consoleUrl, 'PUT /api/specs/:id requires a frontmatter object');
354
+ }
355
+ const putPath = `/api/specs/${encodeURIComponent(specId)}`;
356
+ const putBody = { frontmatter, body: typeof body === 'string' ? body : '' };
357
+ const updateEntry = {
358
+ op: 'update',
359
+ kind: 'update',
360
+ route: putPath,
361
+ method: 'PUT',
362
+ request_body: putBody,
363
+ spec_id: specId,
364
+ // The cloud copy this edit was based on; replay refuses (cloud_newer)
365
+ // when the cloud has moved on since.
366
+ pre_image_updated: frontmatter?.updated ?? null,
367
+ };
368
+ const stashUpdate = (result) => (outboxRoot ? stashIfOutage(result, updateEntry, outboxRoot) : null);
369
+ if (outboxRoot) await drainAndSummarize(outboxRoot, consoleFetch);
370
+ let result;
371
+ try {
372
+ result = await consoleFetch(putPath, { method: 'PUT', body: putBody });
373
+ } catch (err) {
374
+ const stashed = stashUpdate({ ok: false, status: 0, errorKind: 'network', error: String(err?.message ?? err) });
375
+ if (stashed) throw deferredError(route, consoleUrl, stashed, outboxRoot, { spec_id: specId });
376
+ throw cloudWriteFailure(route, consoleUrl, String(err?.message ?? err), {
377
+ spec_id: specId,
378
+ ...outageRequest(noStash, { ok: false, status: 0, errorKind: 'network', error: String(err?.message ?? err) }, updateEntry),
379
+ });
380
+ }
381
+ if (!result?.ok) {
382
+ const stashed = stashUpdate(result);
383
+ if (stashed) throw deferredError(route, result?.url ?? consoleUrl, stashed, outboxRoot, { status: result?.status ?? null, spec_id: specId });
384
+ throw cloudWriteFailure(route, result?.url ?? consoleUrl, describeResult(result), {
385
+ status: result?.status ?? null,
386
+ console_response: result?.body ?? null,
387
+ spec_id: specId,
388
+ ...outageRequest(noStash, result, updateEntry),
389
+ ...(result?.status === 404 ? { notFound: true } : {}),
390
+ });
391
+ }
392
+ const spec = result.body?.spec ?? null;
393
+ const relPath = spec?.relPath ?? spec?.path ?? null;
394
+ return {
395
+ specId: spec?.id ?? specId,
396
+ relPath,
397
+ basename: basenameFromRelPath(relPath) ?? spec?.id ?? specId,
398
+ spec,
399
+ consoleUrl: result.url ?? consoleUrl,
400
+ };
401
+ }
402
+
403
+ /**
404
+ * `GET /api/specs?type=…` — the cloud twin of a `readdirSync(specs/<folder>)`
405
+ * scan. The keyset walk itself is {@link walkSpecListPages} (list-tools.mjs),
406
+ * the dependency-free primitive every other cloud listing in this package
407
+ * already uses — reused rather than re-implemented so there is exactly one
408
+ * place that knows the `next_cursor` contract. This wrapper only adds the
409
+ * `type`/`status`/`tag` filter and the shared failure envelope, so a listing
410
+ * that fails reads identically to a create or an edit that fails.
411
+ *
412
+ * Returns the raw summary rows (`{ id, basename, type, status, … }`) the route
413
+ * already emits — no reshaping, so callers keep whatever fields they read.
414
+ */
415
+ export async function listSpecsViaConsole({
416
+ consoleFetch,
417
+ consoleUrl = null,
418
+ type = null,
419
+ status = null,
420
+ tag = null,
421
+ limit = 500,
422
+ maxPages = 50,
423
+ }) {
424
+ const routeLabel = `GET /api/specs${type ? `?type=${type}` : ''}`;
425
+ if (typeof consoleFetch !== 'function') {
426
+ throw cloudWriteFailure(routeLabel, consoleUrl, 'no consoleFetch dependency was injected');
427
+ }
428
+ const params = new URLSearchParams();
429
+ if (type) params.set('type', type);
430
+ if (status) params.set('status', status);
431
+ if (tag) params.set('tag', tag);
432
+ const query = params.toString();
433
+ try {
434
+ return await walkSpecListPages(consoleFetch, `/api/specs${query ? `?${query}` : ''}`, {
435
+ limit,
436
+ maxPages,
437
+ });
438
+ } catch (err) {
439
+ throw cloudWriteFailure(routeLabel, consoleUrl, String(err?.message ?? err));
440
+ }
441
+ }
442
+
443
+ /**
444
+ * The cloud twin of `commitWrites` (promotion-helpers.mjs).
445
+ *
446
+ * Takes the SAME `{ kind: 'create' | 'update', absPath, content }` descriptors
447
+ * the disk path takes, so a caller switches paths with one `if` and never
448
+ * builds two different write plans. Cloud metadata that cannot be derived from
449
+ * the markdown may ride along on an optional `cloud` property:
450
+ * `{ cloud: { type, name, summary, specId } }`.
451
+ * When absent it is derived from the content's own frontmatter (`type:`,
452
+ * `name:`, `summary:`, `id:`) — which every vault document already carries.
453
+ *
454
+ * Writes run IN ORDER and stop at the first failure. Nothing is rolled back
455
+ * (the console has no un-create); the returned error names the failing step
456
+ * and lists the ones that already landed, so the caller reports the real state
457
+ * rather than implying a clean abort.
458
+ *
459
+ * Returns `{ ok: true, results }` where each result is
460
+ * `{ kind, absPath, specId, basename, relPath }`, or
461
+ * `{ ok: false, error, completed }` — the same `{ ok }`-shaped verdict
462
+ * `commitWrites` returns, so callers branch identically.
463
+ */
464
+ export async function commitWritesViaConsole(writes, { consoleFetch, consoleUrl = null, actingAgent = null, vaultRoot = null } = {}) {
465
+ const results = [];
466
+ const outboxRoot = vaultRoot ?? getWriteOutboxRoot();
467
+ if (outboxRoot && typeof consoleFetch === 'function') await drainAndSummarize(outboxRoot, consoleFetch);
468
+ for (let i = 0; i < (writes ?? []).length; i++) {
469
+ const w = writes[i];
470
+ const { frontmatter, body } = splitRawSpec(w.content);
471
+ const meta = w.cloud ?? {};
472
+ try {
473
+ if (w.kind === 'create') {
474
+ const type = meta.type ?? frontmatter.type ?? null;
475
+ const name = meta.name ?? frontmatter.name ?? null;
476
+ const summary = meta.summary ?? frontmatter.summary ?? null;
477
+ const created = await createSpecViaConsole({
478
+ consoleFetch,
479
+ consoleUrl,
480
+ type,
481
+ name,
482
+ summary,
483
+ frontmatter,
484
+ body,
485
+ actingAgent: meta.actingAgent ?? actingAgent,
486
+ noStash: true,
487
+ });
488
+ results.push({
489
+ kind: 'create',
490
+ absPath: w.absPath ?? null,
491
+ specId: created.specId,
492
+ basename: created.basename,
493
+ relPath: created.relPath,
494
+ });
495
+ } else if (w.kind === 'update') {
496
+ const specId =
497
+ meta.specId ?? frontmatter.id ?? (w.absPath ? basenameFromRelPath(w.absPath) : null);
498
+ const updated = await updateSpecViaConsole({
499
+ consoleFetch,
500
+ consoleUrl,
501
+ specId,
502
+ frontmatter,
503
+ body,
504
+ noStash: true,
505
+ });
506
+ results.push({
507
+ kind: 'update',
508
+ absPath: w.absPath ?? null,
509
+ specId: updated.specId,
510
+ basename: updated.basename,
511
+ relPath: updated.relPath,
512
+ });
513
+ } else {
514
+ throw cloudWriteFailure(
515
+ 'commitWritesViaConsole',
516
+ consoleUrl,
517
+ `unknown write kind "${w.kind}" (expected "create" | "update")`,
518
+ );
519
+ }
520
+ } catch (err) {
521
+ err.failed_step = { index: i, kind: w.kind, abs_path: w.absPath ?? null };
522
+ err.completed_steps = results;
523
+ // Outage on step i: queue steps i..n-1 as ONE ordered batch (each step keeps
524
+ // its own idempotency key) so the remainder replays in order.
525
+ const failedReq = err.outage_request;
526
+ if (outboxRoot && failedReq) {
527
+ const steps = [failedReq];
528
+ for (let j = i + 1; j < writes.length; j++) {
529
+ const planned = planBatchStep(writes[j], actingAgent);
530
+ if (!planned) break;
531
+ steps.push(planned);
532
+ }
533
+ if (steps.length === writes.length - i) {
534
+ const stashed = stashWrite(outboxRoot, {
535
+ op: 'batch',
536
+ route: failedReq.route,
537
+ method: failedReq.method,
538
+ request_body: null,
539
+ acting_agent: actingAgent,
540
+ failure: failedReq.failure,
541
+ steps,
542
+ completed_steps: results,
543
+ });
544
+ if (stashed) {
545
+ const deferred = deferredError('commitWritesViaConsole', consoleUrl, stashed, outboxRoot, {
546
+ status: err.status ?? null,
547
+ failed_step: err.failed_step,
548
+ completed_steps: results,
549
+ });
550
+ return { ok: false, error: deferred, completed: results, deferred: true };
551
+ }
552
+ }
553
+ }
554
+ return { ok: false, error: err, completed: results };
555
+ }
556
+ }
557
+ return { ok: true, results };
558
+ }
559
+
560
+ /** Build a replayable batch step for a write that was never attempted. */
561
+ function planBatchStep(w, actingAgent) {
562
+ const { frontmatter, body } = splitRawSpec(w.content);
563
+ const meta = w.cloud ?? {};
564
+ if (w.kind === 'create') {
565
+ const type = meta.type ?? frontmatter.type ?? null;
566
+ const name = meta.name ?? frontmatter.name ?? null;
567
+ if (!type || !name) return null;
568
+ const cleaned = stripServerMintedFields(frontmatter);
569
+ delete cleaned.name;
570
+ delete cleaned.type;
571
+ if (isDefaultInitialStatus(cleaned.status)) delete cleaned.status;
572
+ const summary = meta.summary ?? frontmatter.summary ?? null;
573
+ const agent = meta.actingAgent ?? actingAgent;
574
+ return {
575
+ kind: 'create',
576
+ route: '/api/specs',
577
+ method: 'POST',
578
+ request_body: {
579
+ type: String(type).trim(),
580
+ name: String(name).trim(),
581
+ ...(summary ? { summary } : {}),
582
+ ...(Object.keys(cleaned).length > 0 ? { frontmatter: cleaned } : {}),
583
+ body,
584
+ ...(agent ? { acting_agent: agent } : {}),
585
+ },
586
+ idempotency_key: randomUUID(),
587
+ };
588
+ }
589
+ if (w.kind === 'update') {
590
+ const specId = meta.specId ?? frontmatter.id ?? (w.absPath ? basenameFromRelPath(w.absPath) : null);
591
+ if (!specId) return null;
592
+ return {
593
+ kind: 'update',
594
+ route: `/api/specs/${encodeURIComponent(specId)}`,
595
+ method: 'PUT',
596
+ request_body: { frontmatter, body },
597
+ spec_id: specId,
598
+ pre_image_updated: frontmatter?.updated ?? null,
599
+ };
600
+ }
601
+ return null;
602
+ }
@@ -0,0 +1,15 @@
1
+ # .heyspex/config.yaml — HeySpex workflow settings for this vault.
2
+ # Seeded by `hs init` (ADR-0009 §1: the file lives at the VAULT root and is git-tracked).
3
+ # Field reference: specs/protocols/agent-settings-protocol.md. Change values with
4
+ # `hs settings set <key> <value>` or heyspex_settings_set — never by hand.
5
+
6
+ mvp_mode:
7
+ enabled: true
8
+ review_grain: epic
9
+ review_backlog_never_blocks: true
10
+
11
+ pricing_tier:
12
+ tier: free
13
+
14
+ agent_behaviors:
15
+ ask_forgiveness_not_permission: true