heyspex 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/README.md +21 -0
  2. package/bin/hs.mjs +931 -98
  3. package/bin/spexcode.mjs +262 -0
  4. package/dev-factory/engine/worktree.mjs +740 -0
  5. package/dev-factory/lib/code-repo-root.mjs +67 -0
  6. package/dev-factory/lib/context-budget.mjs +84 -0
  7. package/dev-factory/lib/epic-detect.mjs +190 -0
  8. package/dev-factory/lib/fleet-queue-file.mjs +846 -0
  9. package/dev-factory/lib/fleet-queue.mjs +426 -0
  10. package/dev-factory/lib/fleet-state.mjs +14 -18
  11. package/dev-factory/lib/model-catalog.mjs +338 -0
  12. package/dev-factory/lib/seat-context.mjs +199 -0
  13. package/dev-factory/lib/spec-io.mjs +93 -0
  14. package/dev-factory/lib/sufficiency-score.mjs +8 -2
  15. package/dev-factory/lib/worktree-allocator.mjs +744 -0
  16. package/dev-factory/model-catalog.json +222 -0
  17. package/dist/cli/hs-check.mjs +1085 -103
  18. package/dist/cli/hs-claim.mjs +936 -63
  19. package/dist/cli/hs-connect.mjs +9786 -1910
  20. package/dist/cli/hs-defer.mjs +5267 -3053
  21. package/dist/cli/hs-dev.mjs +564 -0
  22. package/dist/cli/hs-env.mjs +385 -299
  23. package/dist/cli/hs-index-rebuild.mjs +7129 -0
  24. package/dist/cli/hs-index-reconcile.mjs +7852 -0
  25. package/dist/cli/hs-init.mjs +777 -138
  26. package/dist/cli/hs-install.mjs +6583 -485
  27. package/dist/cli/hs-join.mjs +10137 -2368
  28. package/dist/cli/hs-vault-pull.mjs +4659 -5882
  29. package/dist/cli/hs-vault.mjs +3793 -1537
  30. package/dist/cli/hs-workspace.mjs +2550 -524
  31. package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
  32. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
  33. package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
  34. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
  35. package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
  36. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
  37. package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
  38. package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
  39. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
  40. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
  41. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
  42. package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
  43. package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
  44. package/dist/cli-templates/harness-commands/claude/hs-ground.md +28 -0
  45. package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
  46. package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
  47. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
  48. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
  49. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
  50. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
  51. package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
  52. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
  53. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
  54. package/dist/cli-templates/harness-commands/claude/hs-view.md +26 -0
  55. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
  56. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
  57. package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
  58. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
  59. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
  60. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
  61. package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
  62. package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
  63. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +31 -0
  64. package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
  65. package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
  66. package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
  67. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
  68. package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
  69. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
  70. package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
  71. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
  72. package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
  73. package/dist/cli-templates/harness-commands/cursor/hs-view.md +30 -0
  74. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
  75. package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
  76. package/docs/mcp-client-config.md +259 -6
  77. package/package.json +231 -124
  78. package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
  79. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
  80. package/plugins/heyspex/.mcp.json +12 -0
  81. package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
  82. package/plugins/heyspex/bin/run-hook.mjs +62 -0
  83. package/plugins/heyspex/hooks/cursor-hooks.json +227 -0
  84. package/plugins/heyspex/hooks/hooks.json +361 -0
  85. package/plugins/heyspex/mcp.json +13 -0
  86. package/scripts/check-mcp-freshness.mjs +17 -9
  87. package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
  88. package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
  89. package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
  90. package/scripts/claude-hooks/_intent-classifier-router.mjs +410 -0
  91. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +351 -0
  92. package/scripts/claude-hooks/_run.mjs +120 -0
  93. package/scripts/claude-hooks/_transcript-text.mjs +60 -0
  94. package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
  95. package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
  96. package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
  97. package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
  98. package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
  99. package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
  100. package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
  101. package/scripts/claude-hooks/claim-liveness-shared.mjs +575 -0
  102. package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
  103. package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
  104. package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
  105. package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
  106. package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
  107. package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
  108. package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
  109. package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
  110. package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
  111. package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
  112. package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
  113. package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
  114. package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
  115. package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
  116. package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
  117. package/scripts/claude-hooks/inject-communication-style.mjs +66 -0
  118. package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
  119. package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
  120. package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
  121. package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
  122. package/scripts/claude-hooks/intent-classifier-trigger.mjs +247 -0
  123. package/scripts/claude-hooks/log-session-activity.mjs +341 -0
  124. package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
  125. package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
  126. package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
  127. package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
  128. package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
  129. package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
  130. package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
  131. package/scripts/claude-hooks/research-method-guard.mjs +398 -0
  132. package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
  133. package/scripts/claude-hooks/session-end.mjs +449 -0
  134. package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
  135. package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
  136. package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
  137. package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
  138. package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
  139. package/scripts/claude-hooks/wip-checkpoint.mjs +298 -0
  140. package/scripts/cursor-hooks/manifest.mjs +235 -0
  141. package/scripts/dev-server-status.mjs +34 -13
  142. package/scripts/dev-server-stop.mjs +313 -0
  143. package/scripts/heyspex-mode.mjs +268 -0
  144. package/scripts/hs-auto-resolve.mjs +240 -0
  145. package/scripts/hs-auto-source.mjs +398 -0
  146. package/scripts/hs-auto.mjs +134 -0
  147. package/scripts/hs-capture-cli.mjs +326 -0
  148. package/scripts/hs-classifier.mjs +136 -0
  149. package/scripts/hs-companion.mjs +88 -0
  150. package/scripts/hs-gateway.mjs +115 -0
  151. package/scripts/hs-mode.mjs +125 -0
  152. package/scripts/hs-next.mjs +441 -0
  153. package/scripts/hs-settings.mjs +338 -0
  154. package/scripts/hs-view.mjs +333 -0
  155. package/scripts/hs-worktree.mjs +201 -0
  156. package/scripts/install-claude-code-hooks.mjs +270 -0
  157. package/scripts/install-cursor-hooks.mjs +471 -0
  158. package/scripts/lib/agent-env-markers.mjs +38 -0
  159. package/scripts/lib/console-url-resolver.mjs +197 -60
  160. package/scripts/lib/customer-kit.mjs +886 -0
  161. package/scripts/lib/dev-mode.mjs +244 -28
  162. package/scripts/lib/heyspex-home.mjs +72 -0
  163. package/scripts/lib/hs-command.mjs +277 -0
  164. package/scripts/lib/hs-companion-settings.mjs +472 -0
  165. package/scripts/lib/hs-source-commit.mjs +59 -5
  166. package/scripts/lib/hs-source-identity.mjs +526 -0
  167. package/scripts/lib/hs-source-log.mjs +11 -21
  168. package/scripts/lib/hs-source-push.mjs +107 -58
  169. package/scripts/lib/hs-worktree-args.mjs +115 -0
  170. package/scripts/lib/shared-path-helpers.mjs +134 -0
  171. package/scripts/lib/standalone-doctor.mjs +540 -3
  172. package/scripts/lib/subagent-git-lock.mjs +109 -0
  173. package/scripts/lib/template-pin.mjs +34 -0
  174. package/scripts/provenance-lib.mjs +91 -0
  175. package/scripts/stamp-provenance-on-edit.mjs +222 -0
  176. package/scripts/wip-ledger-health.mjs +653 -0
  177. package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
  178. package/spexharness/src/adapters/audit.mjs +197 -0
  179. package/spexharness/src/adapters/claude-code.mjs +441 -0
  180. package/spexharness/src/chat/tui/ansi.mjs +448 -0
  181. package/spexharness/src/chat/tui/palette.mjs +22 -0
  182. package/spexharness/src/chat/tui/select.mjs +208 -0
  183. package/spexharness/src/config-root.mjs +54 -0
  184. package/spexharness/src/context-budget.mjs +141 -0
  185. package/spexharness/src/dev-server-control.mjs +389 -0
  186. package/spexharness/src/doctrine/behaviour.mjs +371 -0
  187. package/spexharness/src/doctrine/corpus.mjs +733 -0
  188. package/spexharness/src/doctrine/inject.mjs +338 -0
  189. package/spexharness/src/doctrine/merge.mjs +184 -0
  190. package/spexharness/src/doctrine/sources.mjs +252 -0
  191. package/spexharness/src/doctrine/synthesize.mjs +401 -0
  192. package/spexharness/src/doctrine/vault-source.mjs +574 -0
  193. package/spexharness/src/doctrine/writer.mjs +176 -0
  194. package/spexharness/src/events.mjs +84 -0
  195. package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
  196. package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
  197. package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
  198. package/spexharness/src/gates/deep-research-cost-gate.mjs +274 -0
  199. package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
  200. package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
  201. package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
  202. package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
  203. package/spexharness/src/gates/git-identity-guard.mjs +61 -0
  204. package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
  205. package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
  206. package/spexharness/src/gates/git-stash-gate.mjs +159 -0
  207. package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
  208. package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
  209. package/spexharness/src/gates/inject-vault-context.mjs +239 -0
  210. package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
  211. package/spexharness/src/gates/plan-review-gate.mjs +233 -0
  212. package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
  213. package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
  214. package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
  215. package/spexharness/src/gates/settings-write-gate.mjs +104 -0
  216. package/spexharness/src/git-reflog-audit.mjs +294 -0
  217. package/spexharness/src/intent-classifier.mjs +194 -0
  218. package/spexharness/src/intent-directive.mjs +541 -0
  219. package/spexharness/src/profile/learn.mjs +551 -0
  220. package/spexharness/src/profile/proposals.mjs +357 -0
  221. package/spexharness/src/profile/vault-inclusion.mjs +386 -0
  222. package/spexharness/src/redact.mjs +151 -0
  223. package/spexharness/src/rule-table.mjs +819 -0
  224. package/spexharness/src/seat-context.mjs +122 -0
  225. package/spexharness/src/session-bootstrap.mjs +512 -0
  226. package/spexharness/src/session-record.mjs +815 -0
  227. package/spexharness/src/token-inventory.mjs +207 -0
  228. package/vendor/heyspex-mcp/account-switch.mjs +95 -0
  229. package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
  230. package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
  231. package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
  232. package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
  233. package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
  234. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +46 -5
  235. package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
  236. package/vendor/heyspex-mcp/capture-console-client.mjs +80 -1
  237. package/vendor/heyspex-mcp/capture-focus.mjs +1035 -29
  238. package/vendor/heyspex-mcp/capture-outbox.mjs +5 -1
  239. package/vendor/heyspex-mcp/capture-tools.mjs +1219 -0
  240. package/vendor/heyspex-mcp/claim-tools.mjs +1124 -0
  241. package/vendor/heyspex-mcp/cli-verbs.mjs +2076 -0
  242. package/vendor/heyspex-mcp/cli.mjs +201 -15524
  243. package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
  244. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +412 -0
  245. package/vendor/heyspex-mcp/config-seed.yaml +15 -0
  246. package/vendor/heyspex-mcp/config-settings.mjs +290 -7
  247. package/vendor/heyspex-mcp/console-fetch.mjs +657 -16
  248. package/vendor/heyspex-mcp/console-transport.mjs +166 -0
  249. package/vendor/heyspex-mcp/credentials.mjs +584 -0
  250. package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
  251. package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
  252. package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
  253. package/vendor/heyspex-mcp/design-tools.mjs +4 -1
  254. package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
  255. package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
  256. package/vendor/heyspex-mcp/dev-tools.mjs +124 -0
  257. package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
  258. package/vendor/heyspex-mcp/events-tools.mjs +207 -0
  259. package/vendor/heyspex-mcp/filing-tools.mjs +688 -0
  260. package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
  261. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +69 -14
  262. package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
  263. package/vendor/heyspex-mcp/folder-trust-store.mjs +59 -0
  264. package/vendor/heyspex-mcp/http-transport.mjs +300 -0
  265. package/vendor/heyspex-mcp/list-tools.mjs +257 -0
  266. package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
  267. package/vendor/heyspex-mcp/mcp-config.mjs +94 -0
  268. package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
  269. package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
  270. package/vendor/heyspex-mcp/occupied-keys.mjs +141 -0
  271. package/vendor/heyspex-mcp/on-deck-brief.mjs +3360 -0
  272. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
  273. package/vendor/heyspex-mcp/package-root.mjs +32 -0
  274. package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
  275. package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
  276. package/vendor/heyspex-mcp/plan-push.mjs +906 -21
  277. package/vendor/heyspex-mcp/plan-review.mjs +124 -30
  278. package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
  279. package/vendor/heyspex-mcp/procedure-handlers.mjs +325 -0
  280. package/vendor/heyspex-mcp/procedure-tools.mjs +540 -351
  281. package/vendor/heyspex-mcp/promote-capture.mjs +386 -69
  282. package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
  283. package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
  284. package/vendor/heyspex-mcp/promote-research.mjs +660 -53
  285. package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
  286. package/vendor/heyspex-mcp/prompts.mjs +1460 -0
  287. package/vendor/heyspex-mcp/read-path.mjs +29 -9
  288. package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
  289. package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
  290. package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
  291. package/vendor/heyspex-mcp/register-spec.mjs +266 -54
  292. package/vendor/heyspex-mcp/request-context.mjs +59 -0
  293. package/vendor/heyspex-mcp/research-focus.mjs +207 -4
  294. package/vendor/heyspex-mcp/research-handlers.mjs +623 -0
  295. package/vendor/heyspex-mcp/research-tools.mjs +4 -4
  296. package/vendor/heyspex-mcp/response-budget.mjs +348 -0
  297. package/vendor/heyspex-mcp/review-focus.mjs +25 -1
  298. package/vendor/heyspex-mcp/search-specs.mjs +280 -0
  299. package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
  300. package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
  301. package/vendor/heyspex-mcp/session-owner.mjs +108 -0
  302. package/vendor/heyspex-mcp/session-tools.mjs +152 -0
  303. package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
  304. package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
  305. package/vendor/heyspex-mcp/spec-edit-tools.mjs +446 -0
  306. package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
  307. package/vendor/heyspex-mcp/split-research.mjs +301 -56
  308. package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
  309. package/vendor/heyspex-mcp/submit-for-review.mjs +132 -6
  310. package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
  311. package/vendor/heyspex-mcp/summary-write-tools.mjs +224 -0
  312. package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
  313. package/vendor/heyspex-mcp/test-helpers.mjs +108 -1
  314. package/vendor/heyspex-mcp/tool-catalog.mjs +2813 -0
  315. package/vendor/heyspex-mcp/tool-dispatch.mjs +639 -0
  316. package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
  317. package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
  318. package/vendor/heyspex-mcp/vault-io.mjs +724 -0
  319. package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
  320. package/vendor/heyspex-mcp/vault-resolve.mjs +366 -68
  321. package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
  322. package/vendor/heyspex-mcp/whoami.mjs +91 -0
@@ -0,0 +1,128 @@
1
+ /**
2
+ * cloud-tool-surface.mjs — the tools the REMOTE MCP endpoint serves.
3
+ *
4
+ * `POST /api/mcp` on the console runs `dispatchHeySpexTool` in-process on a
5
+ * shared serverless runtime with NO vault checkout: `vaultRoot` points at a
6
+ * directory that does not exist, there is no git, and `~/.heyspex/*` is the
7
+ * lambda's own (empty) home. Only handlers that do their work through the
8
+ * console HTTP API in the neon read tier are correct there. This module is
9
+ * the explicit allowlist; `tools/list` on the remote endpoint returns exactly
10
+ * these, and `tools/call` on anything else answers "needs the local server".
11
+ *
12
+ * Every entry was verified by reading its handler (audit 2026-09-14, WP6):
13
+ *
14
+ * PURE console-API handlers (no vault disk, no git, no home):
15
+ * recall_memories (memory-tools.mjs neon branch), list_procedures
16
+ * (procedure-handlers.mjs loadCatalogViaTier), handoff_record/load
17
+ * (session-tools.mjs), claim_spec / release_spec / report_progress /
18
+ * record_wip / defer_spec / claim_list / claim_acquire / claim_release
19
+ * (claim-tools.mjs, all postClaimEndpoint/consoleFetch), pin_get / pin_set
20
+ * / get_focus_areas / set_focus_areas (focus-tools.mjs), milestone_show /
21
+ * events_show (events-tools.mjs), list_known_tags (spec-edit-tools.mjs).
22
+ *
23
+ * Console-API handlers whose optional disk touches are read-only or fail
24
+ * open (result still correct, some enrichment degrades):
25
+ * get_procedure (marker write try/catch'd), preflight (marker write →
26
+ * `marker_warning`; recipes readdir guarded → []), submit_for_review
27
+ * (`.heyspex/config.yaml` review grain → default), file_spec (POST
28
+ * /api/specs; landing probe defaults expectTier=cloud in neon),
29
+ * next_on_deck (scheduler cohort + direct spec are tier-split to the
30
+ * console; the synchronous enrichers — plans, amendments, WIP fallback,
31
+ * memory scoring — are existsSync-guarded and silently empty),
32
+ * active_capture (DB-first via the console; disk fallbacks guarded).
33
+ *
34
+ * Deliberately EXCLUDED (disk/git-bound or process-global in a shared runtime):
35
+ * heyspex_update_capture – stashes to `<vaultRoot>/.heyspex/capture-outbox`
36
+ * on transport failure with no try/catch
37
+ * (capture-outbox.mjs) — a lambda outbox is a
38
+ * false "your words will sync" promise.
39
+ * heyspex_validate_linkage – full readdir walk of `<vaultRoot>/specs` (validate-linkage.mjs).
40
+ * heyspex_next_review – `git status`/`git show` via execSync + disk-only queue (next-review.mjs).
41
+ * heyspex_dev_mode – `set` writes `~/.heyspex/dev-mode.json` + credentials.yaml;
42
+ * `get` would report the LAMBDA's mode, not the caller's.
43
+ * heyspex_load_tools – mutates the process-global `loadedToolNames`
44
+ * (tool-catalog.mjs) shared by every tenant in the
45
+ * container; the remote surface is fixed anyway.
46
+ * heyspex_settings_list/get – read `<vaultRoot>/.heyspex/config.yaml` → misleading defaults.
47
+ * heyspex_design_* – read `<vaultRoot>/spex-console/public/design-system/*` and DESIGN.md.
48
+ * heyspex_list_recipes – readdir `<vaultRoot>/specs/recipes`.
49
+ * save_research, push_plan, backfill_epic, promote_*, register_spec,
50
+ * edit_file, record_alignment (git shelling), regen_*, ground_plan,
51
+ * plan_review/plan_qc, canvas_inbox, session_end/hooks_*, dev_server_status,
52
+ * fleet_queue_*, firecrawl/research-plan, import_agent, start_recipe,
53
+ * verify_reproduction, split_research, active/complete_research, pin_capture,
54
+ * suggest_pillar, validate_user_skills, next_resolution, resolve_*,
55
+ * claim_file/release_file_claim, evidence_show/export_events, propose_metadata
56
+ * – vault-disk, git, or local-process bound per
57
+ * the cloud-first audit; not re-verified here.
58
+ */
59
+ import { TOOLS } from './tool-catalog.mjs';
60
+
61
+ /** Ordered allowlist — order is the order `tools/list` advertises them. */
62
+ export const CLOUD_TOOL_NAMES = Object.freeze([
63
+ // session + procedure discovery
64
+ 'heyspex_recall_memories',
65
+ 'heyspex_list_procedures',
66
+ 'heyspex_get_procedure',
67
+ 'heyspex_preflight',
68
+ 'heyspex_handoff_load',
69
+ 'heyspex_handoff_record',
70
+ // the claim contract
71
+ 'heyspex_next_on_deck',
72
+ 'heyspex_claim_spec',
73
+ 'heyspex_release_spec',
74
+ 'heyspex_report_progress',
75
+ 'heyspex_submit_for_review',
76
+ 'heyspex_record_wip',
77
+ // author_summary_then_claim completion ([[HS-T41O-NZ8S-DIAG]]): in cloud
78
+ // write mode the handler is GET + PUT /api/specs/:id only — no disk. The
79
+ // lambda's own home resolves to prod/cloud, so the local branch never runs.
80
+ 'heyspex_author_summary',
81
+ 'heyspex_defer_spec',
82
+ 'heyspex_claim_list',
83
+ 'heyspex_claim_acquire',
84
+ 'heyspex_claim_release',
85
+ // filing + read-side context
86
+ 'heyspex_file_spec',
87
+ 'heyspex_active_capture',
88
+ 'heyspex_milestone_show',
89
+ 'heyspex_events_show',
90
+ // [[HS-YFLS]] fix 2: the duplicate check before filing. `handleSearchSpecs`
91
+ // is PURE consoleFetch (GET /api/search only) — no vault disk, no git, no
92
+ // home — so it is correct on the shared runtime, and it is exactly the tool a
93
+ // remote agent needs before POST /api/specs.
94
+ 'heyspex_search_specs',
95
+ 'heyspex_list_known_tags',
96
+ // focus / pin control plane
97
+ 'heyspex_pin_get',
98
+ 'heyspex_pin_set',
99
+ 'heyspex_get_focus_areas',
100
+ 'heyspex_set_focus_areas',
101
+ ]);
102
+
103
+ const CLOUD_TOOL_NAME_SET = new Set(CLOUD_TOOL_NAMES);
104
+ const CATALOG_BY_NAME = new Map(TOOLS.map((t) => [t.name, t]));
105
+
106
+ /** True when `name` is served by the remote endpoint. */
107
+ export function isCloudSafeTool(name) {
108
+ return CLOUD_TOOL_NAME_SET.has(name);
109
+ }
110
+
111
+ /** True when `name` exists in the full catalog (local server surface). */
112
+ export function isCatalogTool(name) {
113
+ return CATALOG_BY_NAME.has(name);
114
+ }
115
+
116
+ /**
117
+ * The catalog entries for the remote surface, in allowlist order. A name the
118
+ * catalog no longer carries is dropped (never a hole in `tools/list`); the
119
+ * unit test asserts the two stay in sync so a drift fails CI, not a client.
120
+ */
121
+ export function cloudToolSurface() {
122
+ return CLOUD_TOOL_NAMES.map((name) => CATALOG_BY_NAME.get(name)).filter(Boolean);
123
+ }
124
+
125
+ /** Allowlisted names missing from the catalog — expected empty; surfaced for the test + doctor. */
126
+ export function cloudSurfaceDrift() {
127
+ return CLOUD_TOOL_NAMES.filter((name) => !CATALOG_BY_NAME.has(name));
128
+ }
@@ -0,0 +1,412 @@
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
+
43
+ /**
44
+ * Raised when a `cloud`-mode write fails. Mirrors
45
+ * `ConsoleUnreachableCloudModeError` (read-path.mjs) exactly — same `name`,
46
+ * same message prefix — so callers that already render one render both.
47
+ */
48
+ export class CloudWriteUnreachableError extends Error {
49
+ constructor(message, details = {}) {
50
+ super(message);
51
+ this.name = 'CONSOLE_UNREACHABLE_CLOUD_MODE';
52
+ Object.assign(this, details);
53
+ }
54
+ }
55
+
56
+ /** Build the canonical failure error for a route + console + detail. */
57
+ export function cloudWriteFailure(routeLabel, consoleUrl, detail, details = {}) {
58
+ return new CloudWriteUnreachableError(
59
+ `CONSOLE_UNREACHABLE_CLOUD_MODE: route "${routeLabel}" failed against console ${consoleUrl ?? '<unknown console>'} — ${detail}`,
60
+ { route: routeLabel, console_url: consoleUrl ?? null, ...details },
61
+ );
62
+ }
63
+
64
+ /** One-line summary of a consoleFetch failure result, for the message above. */
65
+ function describeResult(result) {
66
+ if (!result) return 'no response';
67
+ const bodyText =
68
+ result.error ||
69
+ (result.body ? JSON.stringify(result.body).slice(0, 400) : '') ||
70
+ '';
71
+ return `${result.status ?? '?'}${bodyText ? ` ${bodyText}` : ''}`;
72
+ }
73
+
74
+ /**
75
+ * Fields the SERVER mints and a create request must therefore never carry.
76
+ * `id`/`file_id` are the file key itself (server-minted since 2026-07-10 —
77
+ * a locally minted key in a file-less vault collides with a cloud-side one);
78
+ * `created`/`updated`/`created_tz`/`updated_tz` are stamped by `createSpec`.
79
+ * `family` is NOT stripped: an explicit family is a legal, meaningful
80
+ * override (`createSpec` honours it over parent inheritance).
81
+ */
82
+ export const SERVER_MINTED_FIELDS = Object.freeze([
83
+ 'id',
84
+ 'file_id',
85
+ 'created',
86
+ 'updated',
87
+ 'created_tz',
88
+ 'updated_tz',
89
+ ]);
90
+
91
+ /**
92
+ * The status the console seeds on every non-ADR create (`createSpec` in
93
+ * spex-console/src/lib/vault.ts). Sending it on a POST asks for nothing.
94
+ */
95
+ export const DEFAULT_INITIAL_STATUS = 'open';
96
+
97
+ /** True when `value` is the default initial status (case/whitespace-insensitive). */
98
+ export function isDefaultInitialStatus(value) {
99
+ return typeof value === 'string' && value.trim().toLowerCase() === DEFAULT_INITIAL_STATUS;
100
+ }
101
+
102
+ /** Copy of `frontmatter` with every server-minted field removed. */
103
+ export function stripServerMintedFields(frontmatter = {}) {
104
+ const out = {};
105
+ for (const [key, value] of Object.entries(frontmatter ?? {})) {
106
+ if (SERVER_MINTED_FIELDS.includes(key)) continue;
107
+ out[key] = value;
108
+ }
109
+ return out;
110
+ }
111
+
112
+ /**
113
+ * Split a raw `.md` file (the shape every disk writer already builds) into the
114
+ * `{ frontmatter, body }` pair both console write routes take. Reuses
115
+ * `parseFrontmatter` from vault-io.mjs so the YAML dialect the vault writes is
116
+ * the YAML dialect this reads — no second parser to drift.
117
+ */
118
+ export function splitRawSpec(raw) {
119
+ const { frontmatter, body } = parseFrontmatter(String(raw ?? ''));
120
+ return { frontmatter: frontmatter ?? {}, body: body ?? '' };
121
+ }
122
+
123
+ /** Basename (no extension) from a console-returned relPath. */
124
+ export function basenameFromRelPath(relPath) {
125
+ if (!relPath) return null;
126
+ return String(relPath)
127
+ .split(/[\\/]/)
128
+ .pop()
129
+ .replace(/\.md$/i, '');
130
+ }
131
+
132
+ /**
133
+ * `POST /api/specs` — create one spec through the console.
134
+ *
135
+ * The request shape is `buildFileSpecPostBody`'s (filing-tools.mjs), down to
136
+ * the per-call `idempotency-key` header and `retryOnTimeout: false` (the
137
+ * [[HS-2FQY]] double-mint guard: a blind retry against a REMOTE console can
138
+ * land on a different serverless instance whose in-memory idempotency
139
+ * registry cannot replay the first create, minting a second id for one spec).
140
+ *
141
+ * Returns `{ specId, relPath, basename, spec, consoleWrite, warnings }`. The
142
+ * BASENAME is the load-bearing return value for promotion flows: every
143
+ * sibling write references the new document by `[[basename]]`, and in cloud
144
+ * mode that basename is only knowable after this call returns — which is why
145
+ * cloud-mode promotion is create-first, link-second.
146
+ */
147
+ export async function createSpecViaConsole({
148
+ consoleFetch,
149
+ consoleUrl = null,
150
+ type,
151
+ name,
152
+ summary = null,
153
+ frontmatter = null,
154
+ body = null,
155
+ actingAgent = null,
156
+ idempotencyKey = null,
157
+ routeLabel = 'POST /api/specs',
158
+ }) {
159
+ if (typeof consoleFetch !== 'function') {
160
+ throw cloudWriteFailure(routeLabel, consoleUrl, 'no consoleFetch dependency was injected');
161
+ }
162
+ if (typeof type !== 'string' || !type.trim()) {
163
+ throw cloudWriteFailure(routeLabel, consoleUrl, 'a spec `type` is required to create through the console');
164
+ }
165
+ if (typeof name !== 'string' || !name.trim()) {
166
+ throw cloudWriteFailure(routeLabel, consoleUrl, 'a spec `name` is required to create through the console');
167
+ }
168
+ const cleanedFm = stripServerMintedFields(frontmatter ?? {});
169
+ // `name`/`type`/`summary` ride at the TOP level of the request (the route's
170
+ // contract); leaving duplicates inside `frontmatter` lets the two disagree.
171
+ delete cleanedFm.name;
172
+ delete cleanedFm.type;
173
+ // [[HS-ISZP]]: `status:` is owned by the status route — the create route
174
+ // refuses it out of band (`out_of_band_status_write`). The DEFAULT initial
175
+ // value is not a transition (the server seeds `open` itself), so it is
176
+ // dropped here for every disk-template-derived body (research stub,
177
+ // diagnose, epic/task stubs). Any OTHER value is deliberately left in so the
178
+ // server refuses it loudly rather than this client silently downgrading it.
179
+ if (isDefaultInitialStatus(cleanedFm.status)) delete cleanedFm.status;
180
+ const requestBody = {
181
+ type: type.trim(),
182
+ name: name.trim(),
183
+ ...(summary ? { summary } : {}),
184
+ ...(Object.keys(cleanedFm).length > 0 ? { frontmatter: cleanedFm } : {}),
185
+ ...(typeof body === 'string' ? { body } : {}),
186
+ ...(actingAgent ? { acting_agent: actingAgent } : {}),
187
+ };
188
+
189
+ let result;
190
+ try {
191
+ result = await consoleFetch('/api/specs', {
192
+ method: 'POST',
193
+ body: requestBody,
194
+ headers: { 'idempotency-key': idempotencyKey ?? randomUUID() },
195
+ retryOnTimeout: false,
196
+ });
197
+ } catch (err) {
198
+ throw cloudWriteFailure(routeLabel, consoleUrl, String(err?.message ?? err), {
199
+ attempted: { type: requestBody.type, name: requestBody.name },
200
+ });
201
+ }
202
+ if (!result?.ok) {
203
+ throw cloudWriteFailure(routeLabel, result?.url ?? consoleUrl, describeResult(result), {
204
+ status: result?.status ?? null,
205
+ console_response: result?.body ?? null,
206
+ attempted: { type: requestBody.type, name: requestBody.name },
207
+ ...(result?.errorKind === 'timeout'
208
+ ? {
209
+ hint:
210
+ 'The create may still have completed server-side after this client timed out. '
211
+ + 'Search for a spec with this exact name before retrying — a blind retry can mint a duplicate id ([[HS-2FQY]]).',
212
+ }
213
+ : {}),
214
+ });
215
+ }
216
+ const spec = result.body?.spec ?? null;
217
+ const specId = typeof spec?.id === 'string' && spec.id ? spec.id : null;
218
+ const relPath = spec?.relPath ?? spec?.path ?? null;
219
+ if (!specId) {
220
+ throw cloudWriteFailure(
221
+ routeLabel,
222
+ result.url ?? consoleUrl,
223
+ `answered ${result.status} but carried no spec.id — treat the spec as UNCREATED and check the console before retrying`,
224
+ { status: result.status, console_response: result.body ?? null },
225
+ );
226
+ }
227
+ return {
228
+ specId,
229
+ relPath,
230
+ basename: basenameFromRelPath(relPath) ?? specId,
231
+ spec,
232
+ consoleWrite: result.body?.write ?? null,
233
+ warnings: Array.isArray(result.body?.warnings) ? result.body.warnings : [],
234
+ consoleUrl: result.url ?? consoleUrl,
235
+ };
236
+ }
237
+
238
+ /**
239
+ * `PUT /api/specs/:id` — replace an existing spec's frontmatter + body.
240
+ *
241
+ * The route takes `{ frontmatter: object, body: string }` and answers 404 for
242
+ * an id it cannot resolve (a real answer: the spec is not in this caller's
243
+ * vault), which is surfaced here as a distinct `notFound: true` failure so a
244
+ * caller can tell "unreachable console" from "no such spec".
245
+ */
246
+ export async function updateSpecViaConsole({
247
+ consoleFetch,
248
+ consoleUrl = null,
249
+ specId,
250
+ frontmatter,
251
+ body,
252
+ routeLabel = null,
253
+ }) {
254
+ const route = routeLabel ?? `PUT /api/specs/${specId}`;
255
+ if (typeof consoleFetch !== 'function') {
256
+ throw cloudWriteFailure(route, consoleUrl, 'no consoleFetch dependency was injected');
257
+ }
258
+ if (typeof specId !== 'string' || !specId.trim()) {
259
+ throw cloudWriteFailure(route, consoleUrl, 'a spec id is required to update through the console');
260
+ }
261
+ if (typeof frontmatter !== 'object' || frontmatter === null) {
262
+ throw cloudWriteFailure(route, consoleUrl, 'PUT /api/specs/:id requires a frontmatter object');
263
+ }
264
+ let result;
265
+ try {
266
+ result = await consoleFetch(`/api/specs/${encodeURIComponent(specId)}`, {
267
+ method: 'PUT',
268
+ body: { frontmatter, body: typeof body === 'string' ? body : '' },
269
+ });
270
+ } catch (err) {
271
+ throw cloudWriteFailure(route, consoleUrl, String(err?.message ?? err), { spec_id: specId });
272
+ }
273
+ if (!result?.ok) {
274
+ throw cloudWriteFailure(route, result?.url ?? consoleUrl, describeResult(result), {
275
+ status: result?.status ?? null,
276
+ console_response: result?.body ?? null,
277
+ spec_id: specId,
278
+ ...(result?.status === 404 ? { notFound: true } : {}),
279
+ });
280
+ }
281
+ const spec = result.body?.spec ?? null;
282
+ const relPath = spec?.relPath ?? spec?.path ?? null;
283
+ return {
284
+ specId: spec?.id ?? specId,
285
+ relPath,
286
+ basename: basenameFromRelPath(relPath) ?? spec?.id ?? specId,
287
+ spec,
288
+ consoleUrl: result.url ?? consoleUrl,
289
+ };
290
+ }
291
+
292
+ /**
293
+ * `GET /api/specs?type=…` — the cloud twin of a `readdirSync(specs/<folder>)`
294
+ * scan. The keyset walk itself is {@link walkSpecListPages} (list-tools.mjs),
295
+ * the dependency-free primitive every other cloud listing in this package
296
+ * already uses — reused rather than re-implemented so there is exactly one
297
+ * place that knows the `next_cursor` contract. This wrapper only adds the
298
+ * `type`/`status`/`tag` filter and the shared failure envelope, so a listing
299
+ * that fails reads identically to a create or an edit that fails.
300
+ *
301
+ * Returns the raw summary rows (`{ id, basename, type, status, … }`) the route
302
+ * already emits — no reshaping, so callers keep whatever fields they read.
303
+ */
304
+ export async function listSpecsViaConsole({
305
+ consoleFetch,
306
+ consoleUrl = null,
307
+ type = null,
308
+ status = null,
309
+ tag = null,
310
+ limit = 500,
311
+ maxPages = 50,
312
+ }) {
313
+ const routeLabel = `GET /api/specs${type ? `?type=${type}` : ''}`;
314
+ if (typeof consoleFetch !== 'function') {
315
+ throw cloudWriteFailure(routeLabel, consoleUrl, 'no consoleFetch dependency was injected');
316
+ }
317
+ const params = new URLSearchParams();
318
+ if (type) params.set('type', type);
319
+ if (status) params.set('status', status);
320
+ if (tag) params.set('tag', tag);
321
+ const query = params.toString();
322
+ try {
323
+ return await walkSpecListPages(consoleFetch, `/api/specs${query ? `?${query}` : ''}`, {
324
+ limit,
325
+ maxPages,
326
+ });
327
+ } catch (err) {
328
+ throw cloudWriteFailure(routeLabel, consoleUrl, String(err?.message ?? err));
329
+ }
330
+ }
331
+
332
+ /**
333
+ * The cloud twin of `commitWrites` (promotion-helpers.mjs).
334
+ *
335
+ * Takes the SAME `{ kind: 'create' | 'update', absPath, content }` descriptors
336
+ * the disk path takes, so a caller switches paths with one `if` and never
337
+ * builds two different write plans. Cloud metadata that cannot be derived from
338
+ * the markdown may ride along on an optional `cloud` property:
339
+ * `{ cloud: { type, name, summary, specId } }`.
340
+ * When absent it is derived from the content's own frontmatter (`type:`,
341
+ * `name:`, `summary:`, `id:`) — which every vault document already carries.
342
+ *
343
+ * Writes run IN ORDER and stop at the first failure. Nothing is rolled back
344
+ * (the console has no un-create); the returned error names the failing step
345
+ * and lists the ones that already landed, so the caller reports the real state
346
+ * rather than implying a clean abort.
347
+ *
348
+ * Returns `{ ok: true, results }` where each result is
349
+ * `{ kind, absPath, specId, basename, relPath }`, or
350
+ * `{ ok: false, error, completed }` — the same `{ ok }`-shaped verdict
351
+ * `commitWrites` returns, so callers branch identically.
352
+ */
353
+ export async function commitWritesViaConsole(writes, { consoleFetch, consoleUrl = null, actingAgent = null } = {}) {
354
+ const results = [];
355
+ for (let i = 0; i < (writes ?? []).length; i++) {
356
+ const w = writes[i];
357
+ const { frontmatter, body } = splitRawSpec(w.content);
358
+ const meta = w.cloud ?? {};
359
+ try {
360
+ if (w.kind === 'create') {
361
+ const type = meta.type ?? frontmatter.type ?? null;
362
+ const name = meta.name ?? frontmatter.name ?? null;
363
+ const summary = meta.summary ?? frontmatter.summary ?? null;
364
+ const created = await createSpecViaConsole({
365
+ consoleFetch,
366
+ consoleUrl,
367
+ type,
368
+ name,
369
+ summary,
370
+ frontmatter,
371
+ body,
372
+ actingAgent: meta.actingAgent ?? actingAgent,
373
+ });
374
+ results.push({
375
+ kind: 'create',
376
+ absPath: w.absPath ?? null,
377
+ specId: created.specId,
378
+ basename: created.basename,
379
+ relPath: created.relPath,
380
+ });
381
+ } else if (w.kind === 'update') {
382
+ const specId =
383
+ meta.specId ?? frontmatter.id ?? (w.absPath ? basenameFromRelPath(w.absPath) : null);
384
+ const updated = await updateSpecViaConsole({
385
+ consoleFetch,
386
+ consoleUrl,
387
+ specId,
388
+ frontmatter,
389
+ body,
390
+ });
391
+ results.push({
392
+ kind: 'update',
393
+ absPath: w.absPath ?? null,
394
+ specId: updated.specId,
395
+ basename: updated.basename,
396
+ relPath: updated.relPath,
397
+ });
398
+ } else {
399
+ throw cloudWriteFailure(
400
+ 'commitWritesViaConsole',
401
+ consoleUrl,
402
+ `unknown write kind "${w.kind}" (expected "create" | "update")`,
403
+ );
404
+ }
405
+ } catch (err) {
406
+ err.failed_step = { index: i, kind: w.kind, abs_path: w.absPath ?? null };
407
+ err.completed_steps = results;
408
+ return { ok: false, error: err, completed: results };
409
+ }
410
+ }
411
+ return { ok: true, results };
412
+ }
@@ -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