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
@@ -36,9 +36,20 @@
36
36
  */
37
37
  import { readFileSync, writeFileSync } from 'node:fs';
38
38
  import { join } from 'node:path';
39
- import { collectVaultSpecs, validateLinkage } from './validate-linkage.mjs';
39
+ import {
40
+ collectVaultSpecs,
41
+ validateLinkage,
42
+ buildSpecIndex,
43
+ resolveSpecRef,
44
+ refNamesSpec,
45
+ refsNameSameSpec,
46
+ } from './validate-linkage.mjs';
40
47
  import { yamlScalar } from './yaml-scalar.mjs';
41
- import { toCanonicalFamilyFirstId } from './spec-id-lib.mjs';
48
+ import { toCanonicalFamilyFirstId, lookupFileKeyOf, fileKeyFromBasename } from './spec-id-lib.mjs';
49
+ import { consoleFetch as realConsoleFetch } from './console-transport.mjs';
50
+ import { getConsoleUrl } from './mcp-config.mjs';
51
+ import { resolveVaultWriteMode } from './read-path.mjs';
52
+ import { splitRawSpec, updateSpecViaConsole } from './cloud-vault-writes.mjs';
42
53
 
43
54
  const SUBJECT_TYPES = new Set(['milestone', 'epic', 'task']);
44
55
 
@@ -48,9 +59,15 @@ const SUBJECT_TYPES = new Set(['milestone', 'epic', 'task']);
48
59
  // ---------------------------------------------------------------------------
49
60
 
50
61
  // Canonical, ordering-independent core of a spec id (dual-read: a type-first id
51
- // and its family-first form resolve to the SAME core, so byIdCore / listHasSpec
52
- // match across orderings). [[HS-EPIC-DLE3]] / [[HS-EPIC-6SNQ]]. Falls back to the
53
- // legacy anchored regex for unknown-type / hyphenated ids parseSpecId can't read.
62
+ // and its family-first form resolve to the SAME core). [[HS-EPIC-DLE3]] /
63
+ // [[HS-EPIC-6SNQ]]. Falls back to the legacy anchored regex for unknown-type /
64
+ // hyphenated ids parseSpecId can't read.
65
+ //
66
+ // NOT an identity ([[HS-J6IE-K2EL-DIAG-heyspex-register-spec-resolves-a-task]]):
67
+ // under Naming V3 the core is family+TYPE, which every task in one family
68
+ // shares. It is used here ONLY for the SPEC_NOT_FOUND suggestion list; all
69
+ // resolution and list-membership goes through the shared file-key-first
70
+ // resolver in validate-linkage.mjs (resolveSpecRef / refNamesSpec).
54
71
  function idCore(value) {
55
72
  const canonical = toCanonicalFamilyFirstId(value);
56
73
  if (canonical) return canonical.toUpperCase();
@@ -58,6 +75,14 @@ function idCore(value) {
58
75
  return m ? m[1].toUpperCase() : null;
59
76
  }
60
77
 
78
+ // File key a caller's ref carries (bare `HS-OCL4`, display `HS-OCL4-NYPR`, full
79
+ // era-3 basename, or an era-1/2 basename whose slug leads with the key).
80
+ function refFileKey(ref) {
81
+ if (typeof ref !== 'string' || !ref.trim()) return null;
82
+ const t = ref.trim().replace(/^hs-/i, 'HS-');
83
+ return lookupFileKeyOf(t) ?? fileKeyFromBasename(t) ?? null;
84
+ }
85
+
61
86
  function stripWikilink(value) {
62
87
  if (typeof value !== 'string') return null;
63
88
  const inner = value.match(/\[\[([^\]]+)\]\]/);
@@ -104,8 +129,7 @@ function lineRefMatches(line, ref) {
104
129
  const im = line.match(/^[ \t]+-[ \t]*(.+?)[ \t]*$/);
105
130
  if (!im) return false;
106
131
  const target = stripWikilink(im[1].replace(/^['"]|['"]$/g, '')) ?? '';
107
- const refCore = idCore(ref);
108
- return target === stripWikilink(ref) || (!!refCore && idCore(target) === refCore);
132
+ return refsNameSameSpec(target, ref);
109
133
  }
110
134
 
111
135
  /**
@@ -218,6 +242,53 @@ function loadFile(vaultRoot, relFile) {
218
242
  * @param {boolean} [opts.updateTimestamps=true]
219
243
  */
220
244
  export function registerSpec(vaultRoot, opts = {}) {
245
+ const planned = planRegisterSpec(vaultRoot, opts);
246
+ if (!planned.__plan) return planned;
247
+ return finalize(planned.__plan);
248
+ }
249
+
250
+ /**
251
+ * Mode-aware entry point — the one a handler should call
252
+ * ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]).
253
+ *
254
+ * {@link registerSpec} stays SYNCHRONOUS and local-only on purpose: it is
255
+ * called from sync call sites in filing-tools.mjs, cli-verbs.mjs and
256
+ * promote-research.mjs, and async-ifying it cascades through all three (the
257
+ * same bounded deferral
258
+ * [[HS-M0MK-CPFP-TASK-migrate-capture-focus-yaml-to-neon-capture-focus-pin]]
259
+ * took for the vendor capture writers). This wrapper resolves the vault write
260
+ * mode ONCE and dispatches:
261
+ *
262
+ * - `local` — delegates straight to {@link registerSpec}; byte-identical.
263
+ * - `cloud` — plans exactly the same frontmatter surgery, then sends each
264
+ * touched file through `PUT /api/specs/:id` ({@link updateSpecViaConsole})
265
+ * instead of `writeFileSync`. Nothing is written to this machine's disk.
266
+ *
267
+ * `registerSpec` only ever EDITS existing vault files (it links an already-filed
268
+ * spec into its parent's back-reference list), so there is no create branch here
269
+ * — `createSpecViaConsole` belongs to the filing tools, not to this one.
270
+ *
271
+ * @param {object} [deps] Injection seam for tests: `consoleFetch`, `consoleUrl`,
272
+ * `resolveWriteMode`.
273
+ */
274
+ export async function registerSpecViaMode(vaultRoot, opts = {}, deps = {}) {
275
+ const writeMode =
276
+ opts.writeMode ?? (await (deps.resolveWriteMode ?? resolveVaultWriteMode)()) ?? 'local';
277
+ if (writeMode !== 'cloud') return registerSpec(vaultRoot, opts);
278
+ const planned = planRegisterSpec(vaultRoot, opts);
279
+ if (!planned.__plan) return planned;
280
+ return finalizeViaConsole(planned.__plan, {
281
+ consoleFetch: deps.consoleFetch ?? realConsoleFetch,
282
+ consoleUrl: deps.consoleUrl ?? getConsoleUrl(),
283
+ });
284
+ }
285
+
286
+ /**
287
+ * Resolve + stage every edit without committing any of them. Returns either a
288
+ * failure result (the `err(...)` shape callers already read) or `{ __plan }` —
289
+ * everything {@link finalize} / {@link finalizeViaConsole} need.
290
+ */
291
+ function planRegisterSpec(vaultRoot, opts = {}) {
221
292
  const {
222
293
  spec_id,
223
294
  parent_id = null,
@@ -233,32 +304,45 @@ export function registerSpec(vaultRoot, opts = {}) {
233
304
  }
234
305
 
235
306
  const specs = collectVaultSpecs(vaultRoot);
236
- const byBasename = new Map();
237
- const byId = new Map();
238
- const byIdCore = new Map();
239
- for (const s of specs) {
240
- byBasename.set(s.basename, s);
241
- if (s.id) byId.set(s.id, s);
242
- if (s.idCore && !byIdCore.has(s.idCore)) byIdCore.set(s.idCore, s);
243
- }
244
- const resolveRef = (ref) => {
245
- if (!ref) return null;
246
- const t = stripWikilink(ref) ?? ref;
247
- return byBasename.get(t) || byId.get(t) || byIdCore.get(idCore(t)) || null;
307
+ // Shared Naming V3 resolver ([[HS-J6IE-K2EL-DIAG-heyspex-register-spec-resolves-a-task]]):
308
+ // exact basename → exact id → UNIQUE file key → idCore soft match only for a
309
+ // key-less (era-1/2) ref and only when exactly one spec matches. A ref that
310
+ // names two specs is refused with AMBIGUOUS_SPEC_ID naming the candidates;
311
+ // the old first-wins family-core map re-parented an unrelated sibling.
312
+ const index = buildSpecIndex(specs);
313
+ const resolveRef = (ref) => resolveSpecRef(index, ref).spec;
314
+ const ambiguous = (ref, role) => {
315
+ const r = resolveSpecRef(index, ref);
316
+ if (!r.ambiguous) return null;
317
+ const names = r.candidates.map((c) => c.basename).join(', ');
318
+ return {
319
+ ...err(
320
+ 'AMBIGUOUS_SPEC_ID',
321
+ `${role} "${stripWikilink(ref) ?? ref}" names ${r.candidates.length} specs (${names}). ` +
322
+ 'Under Naming V3 the file key (the token right after HS-) is the only unique key; a family key or ' +
323
+ 'family+TYPE core is shared by every spec in the family. Pass the full basename or the bare file key ' +
324
+ '(HS-<fileKey>). NOTHING WAS WRITTEN.',
325
+ ),
326
+ candidates: r.candidates,
327
+ matched_by: r.matched_by,
328
+ };
248
329
  };
249
- const listHasSpec = (list, target) =>
250
- list.some((ref) => {
251
- const t = stripWikilink(ref) ?? ref;
252
- return t === target.basename || t === target.id || (!!target.idCore && idCore(t) === target.idCore);
253
- });
330
+ const listHasSpec = (list, target) => list.some((ref) => refNamesSpec(ref, target));
254
331
 
332
+ const specAmbiguity = ambiguous(spec_id, 'spec_id');
333
+ if (specAmbiguity) return specAmbiguity;
255
334
  const spec = resolveRef(spec_id);
256
335
  if (!spec) {
257
336
  const want = idCore(spec_id);
258
337
  const suggestions = want
259
338
  ? specs.filter((s) => s.idCore && s.idCore.startsWith(want.slice(0, 7))).map((s) => s.id).slice(0, 3)
260
339
  : [];
261
- return err('SPEC_NOT_FOUND', `Spec ${spec_id} does not exist in the vault.`, suggestions);
340
+ const key = refFileKey(String(spec_id));
341
+ const hint = key
342
+ ? ` It carries file key ${key}, which no file under specs/ is stored under — a cloud-only spec with no local ` +
343
+ 'copy? Pull the vault (or run in cloud write mode) before registering; the resolver never falls through to a sibling.'
344
+ : '';
345
+ return err('SPEC_NOT_FOUND', `Spec ${spec_id} does not exist in the vault.${hint}`, suggestions);
262
346
  }
263
347
  if (!SUBJECT_TYPES.has(spec.type)) {
264
348
  return err(
@@ -275,7 +359,20 @@ export function registerSpec(vaultRoot, opts = {}) {
275
359
  const { abs, raw } = loadFile(vaultRoot, record.file);
276
360
  const block = getFrontmatter(raw);
277
361
  if (block == null) throw new Error(`No frontmatter in ${record.file}`);
278
- const e = { abs, raw, block, eol: detectEol(raw), actions: [], touched: false };
362
+ // `specId`/`type`/`file` ride along so the cloud commit can address this
363
+ // file by the id `PUT /api/specs/:id` takes, and can refuse the one
364
+ // document shape that cannot survive that route (see finalizeViaConsole).
365
+ const e = {
366
+ abs,
367
+ raw,
368
+ block,
369
+ eol: detectEol(raw),
370
+ actions: [],
371
+ touched: false,
372
+ specId: record.id ?? null,
373
+ type: record.type ?? null,
374
+ file: record.file,
375
+ };
279
376
  edits.set(record.file, e);
280
377
  return e;
281
378
  };
@@ -285,6 +382,8 @@ export function registerSpec(vaultRoot, opts = {}) {
285
382
  if (spec.type === 'milestone') {
286
383
  // Target roadmap: explicit parent_id wins, else roadmap_id default.
287
384
  const roadmapRef = parent_id || roadmap_id;
385
+ const roadmapAmbiguity = ambiguous(roadmapRef, 'roadmap');
386
+ if (roadmapAmbiguity) return roadmapAmbiguity;
288
387
  const roadmap = resolveRef(roadmapRef);
289
388
  if (!roadmap || !roadmap.manifest) {
290
389
  return err('ROADMAP_NOT_FOUND', `Roadmap ${roadmapRef} not found (or it has no roadmap_manifest).`);
@@ -298,18 +397,20 @@ export function registerSpec(vaultRoot, opts = {}) {
298
397
  e.touched = true;
299
398
  changes.push({ file: roadmap.file, action: `Added ${spec.basename} to roadmap_manifest.milestones` });
300
399
  }
301
- return finalize({
302
- spec,
303
- parent: { id: roadmap.id, type: 'roadmap' },
304
- roadmap_id: roadmap.id,
305
- already,
306
- edits,
307
- changes,
308
- vaultRoot,
309
- dryRun,
310
- updateTimestamps,
311
- now,
312
- });
400
+ return {
401
+ __plan: {
402
+ spec,
403
+ parent: { id: roadmap.id, type: 'roadmap' },
404
+ roadmap_id: roadmap.id,
405
+ already,
406
+ edits,
407
+ changes,
408
+ vaultRoot,
409
+ dryRun,
410
+ updateTimestamps,
411
+ now,
412
+ },
413
+ };
313
414
  }
314
415
 
315
416
  // epic → milestone (milestone:/epics:) or task → epic (parent:/children:)
@@ -325,6 +426,8 @@ export function registerSpec(vaultRoot, opts = {}) {
325
426
  `${spec.id} is a ${spec.type} with no ${parentType} parent. Pass parent_id (an existing HS-${parentType === 'milestone' ? 'MIL' : 'EPIC'}-* id) to register it.`,
326
427
  );
327
428
  }
429
+ const parentAmbiguity = ambiguous(targetRef, 'parent_id');
430
+ if (parentAmbiguity) return parentAmbiguity;
328
431
  const parent = resolveRef(targetRef);
329
432
  if (!parent) {
330
433
  return err('PARENT_NOT_FOUND', `Parent ${targetRef} not found in the vault.`);
@@ -337,11 +440,7 @@ export function registerSpec(vaultRoot, opts = {}) {
337
440
  }
338
441
 
339
442
  // Side 1: the child's own pointer field.
340
- const fieldPointsAtParent =
341
- existingParentRef &&
342
- (stripWikilink(existingParentRef) === parent.basename ||
343
- stripWikilink(existingParentRef) === parent.id ||
344
- (!!parent.idCore && idCore(existingParentRef) === parent.idCore));
443
+ const fieldPointsAtParent = !!existingParentRef && refNamesSpec(existingParentRef, parent);
345
444
  if (!fieldPointsAtParent) {
346
445
  const e = stage(spec);
347
446
  e.block = setScalar(e.block, childField, yamlScalar(`[[${parent.basename}]]`), e.eol);
@@ -361,17 +460,19 @@ export function registerSpec(vaultRoot, opts = {}) {
361
460
  changes.push({ file: parent.file, action: `Added [[${spec.basename}]] to ${parent.id} ${parentListField}[]` });
362
461
  }
363
462
 
364
- return finalize({
365
- spec,
366
- parent: { id: parent.id, type: parent.type },
367
- already: fieldPointsAtParent && listAlready,
368
- edits,
369
- changes,
370
- vaultRoot,
371
- dryRun,
372
- updateTimestamps,
373
- now,
374
- });
463
+ return {
464
+ __plan: {
465
+ spec,
466
+ parent: { id: parent.id, type: parent.type },
467
+ already: fieldPointsAtParent && listAlready,
468
+ edits,
469
+ changes,
470
+ vaultRoot,
471
+ dryRun,
472
+ updateTimestamps,
473
+ now,
474
+ },
475
+ };
375
476
  }
376
477
 
377
478
  function positionErrorMessage(res) {
@@ -415,3 +516,114 @@ function finalize({ spec, parent, roadmap_id, already, edits, changes, vaultRoot
415
516
  }
416
517
  return result;
417
518
  }
519
+
520
+ /**
521
+ * Cloud-write-mode twin of {@link finalize}: the SAME staged frontmatter
522
+ * surgery, committed through `PUT /api/specs/:id` instead of `writeFileSync`.
523
+ *
524
+ * Each touched file's spliced markdown is split back into the
525
+ * `{ frontmatter, body }` pair the route takes ({@link splitRawSpec}), and
526
+ * addressed by the spec id the planner already resolved for that file (falling
527
+ * back to the document's own `id:`). A file with no cloud route degrades
528
+ * explicitly as `CLOUD_MODE_UNSUPPORTED_WRITE` rather than quietly landing on a
529
+ * disk the mode forbade.
530
+ *
531
+ * Writes run IN ORDER and stop at the FIRST failure, and nothing is rolled back
532
+ * — the console has no un-write. The failure result therefore names the step
533
+ * that failed AND the steps that already landed, so a caller reports the real
534
+ * half-linked state instead of implying a clean abort (the
535
+ * `commitWritesViaConsole` contract).
536
+ *
537
+ * `validation` is deliberately ABSENT: `validateLinkage` re-reads the vault from
538
+ * local disk, which in cloud mode still holds the PRE-edit bytes (or no clone at
539
+ * all), so running it would report a confidently wrong verdict. The reason rides
540
+ * in `validation_skipped`.
541
+ */
542
+ async function finalizeViaConsole(
543
+ { spec, parent, roadmap_id, already, edits, changes, dryRun, updateTimestamps, now },
544
+ { consoleFetch, consoleUrl },
545
+ ) {
546
+ const written = [];
547
+ if (!dryRun) {
548
+ for (const e of edits.values()) {
549
+ if (!e.touched) continue;
550
+ let block = e.block;
551
+ if (updateTimestamps) block = setScalar(block, 'updated', yamlScalar(now), e.eol);
552
+ const next = replaceFrontmatter(e.raw, block);
553
+ if (next == null) throw new Error(`Failed to splice frontmatter for ${e.abs}`);
554
+ // A ROADMAP has no safe cloud route today, and the failure would be
555
+ // silent DATA LOSS rather than an error: `PUT /api/specs/:id` takes a
556
+ // PARSED frontmatter object, and the parser that builds it
557
+ // (`parseFrontmatter` → `parseRoadmapManifest`, vault-io.mjs) still reads
558
+ // the retired v2 manifest shape — it returns `{ phases, initiatives,
559
+ // horizon }` and drops `milestones[]` entirely. Sending that object back
560
+ // would erase the roadmap's whole milestone list. The disk write this
561
+ // mode forbade is NOT used as a fallback; the caller is told which file
562
+ // and why.
563
+ if (e.type === 'roadmap' || /^[ \t]*roadmap_manifest:/m.test(block)) {
564
+ return {
565
+ ...err(
566
+ 'CLOUD_MODE_UNSUPPORTED_WRITE',
567
+ `register_spec cannot write ${e.file ?? e.abs} in cloud vault-write mode: it carries a `
568
+ + '`roadmap_manifest:`, and the client-side frontmatter parser behind PUT /api/specs/:id still reads '
569
+ + 'the retired v2 manifest shape (phases/initiatives/horizon) — round-tripping it would DROP the v3 '
570
+ + '`milestones[]` list. NOTHING WAS WRITTEN. File the milestone through the console (which owns the '
571
+ + 'manifest), or run this registration in local write mode.',
572
+ ),
573
+ write_mode: 'cloud',
574
+ console_url: consoleUrl ?? null,
575
+ on_disk: false,
576
+ completed_writes: written,
577
+ };
578
+ }
579
+ const { frontmatter, body } = splitRawSpec(next);
580
+ const specId = e.specId || (typeof frontmatter?.id === 'string' ? frontmatter.id.trim() : '');
581
+ if (!specId) {
582
+ return {
583
+ ...err(
584
+ 'CLOUD_MODE_UNSUPPORTED_WRITE',
585
+ `register_spec cannot write ${e.file ?? e.abs} in cloud vault-write mode: neither the staged spec `
586
+ + "record nor the document's own frontmatter carries an `id:`, and PUT /api/specs/:id needs one. "
587
+ + 'NOTHING WAS WRITTEN — the local disk write is deliberately NOT used as a fallback.',
588
+ ),
589
+ write_mode: 'cloud',
590
+ console_url: consoleUrl ?? null,
591
+ on_disk: false,
592
+ completed_writes: written,
593
+ };
594
+ }
595
+ try {
596
+ const res = await updateSpecViaConsole({ consoleFetch, consoleUrl, specId, frontmatter, body });
597
+ written.push({ file: e.file ?? null, spec_id: res.specId, rel_path: res.relPath });
598
+ } catch (cause) {
599
+ return {
600
+ ...err('CONSOLE_UNREACHABLE_CLOUD_MODE', String(cause?.message ?? cause)),
601
+ write_mode: 'cloud',
602
+ console_url: consoleUrl ?? null,
603
+ on_disk: false,
604
+ failed_write: { file: e.file ?? null, spec_id: specId },
605
+ completed_writes: written,
606
+ ...(cause?.notFound ? { not_found: true } : {}),
607
+ };
608
+ }
609
+ }
610
+ }
611
+
612
+ const result = {
613
+ success: true,
614
+ spec_id: spec.id,
615
+ type: spec.type,
616
+ parent,
617
+ already_registered: !!already && changes.length === 0,
618
+ changes,
619
+ dry_run: !!dryRun,
620
+ write_mode: 'cloud',
621
+ on_disk: false,
622
+ console_url: consoleUrl ?? null,
623
+ console_writes: written,
624
+ validation_skipped:
625
+ 'cloud write mode — the local vault copy was not updated, so validateLinkage would re-read pre-edit bytes',
626
+ };
627
+ if (roadmap_id) result.roadmap_id = roadmap_id;
628
+ return result;
629
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * request-context.mjs — per-call overrides for the console transport.
3
+ *
4
+ * WHY: the stdio MCP server is one process per user, so the console URL and
5
+ * the outbound bearer are process-wide (`HEYSPEX_MCP_TOKEN` env, the
6
+ * credentials file, the shared console-url resolver). The remote endpoint
7
+ * (`POST /api/mcp` on the console, WP6) runs the SAME dispatch in-process on a
8
+ * shared serverless runtime, where one request's caller must never leak into
9
+ * another's — so `process.env` is off limits and the override has to travel
10
+ * with the async call chain instead.
11
+ *
12
+ * An `AsyncLocalStorage` store carries `{ consoleUrl, authHeader }` for the
13
+ * duration of one `runWithRequestContext(ctx, fn)` call, including every
14
+ * `await` beneath it. `console-transport.mjs` consults the two accessors when
15
+ * it binds `makeConsoleFetch`; with no active store they return `undefined`
16
+ * and the transport falls through to today's process-wide resolution, so the
17
+ * stdio server's behavior is unchanged byte-for-byte.
18
+ */
19
+ import { AsyncLocalStorage } from 'node:async_hooks';
20
+
21
+ /** @typedef {{ consoleUrl?: string, authHeader?: Record<string, string>, vaultId?: string }} RequestContext */
22
+
23
+ const storage = new AsyncLocalStorage();
24
+
25
+ /**
26
+ * Run `fn` with `ctx` as the active request context. Returns whatever `fn`
27
+ * returns (a Promise stays a Promise; the store follows its continuations).
28
+ *
29
+ * @template T
30
+ * @param {RequestContext} ctx
31
+ * @param {() => T} fn
32
+ * @returns {T}
33
+ */
34
+ export function runWithRequestContext(ctx, fn) {
35
+ return storage.run(ctx ?? {}, fn);
36
+ }
37
+
38
+ /** The active request context, or `undefined` outside any `runWithRequestContext`. */
39
+ export function getRequestContext() {
40
+ return storage.getStore();
41
+ }
42
+
43
+ /** Console origin override for the active request, or `undefined` (use the shared resolver). */
44
+ export function contextConsoleUrl() {
45
+ const url = storage.getStore()?.consoleUrl;
46
+ return typeof url === 'string' && url.trim() ? url.replace(/\/+$/, '') : undefined;
47
+ }
48
+
49
+ /**
50
+ * Outbound auth header(s) for the active request, or `undefined` (use the
51
+ * process-wide `getAuthHeader`). An explicit empty object is honored — it
52
+ * means "this caller presented no credential; do not fall back to the
53
+ * server's own token", which is the correct fail-closed shape on a shared
54
+ * runtime.
55
+ */
56
+ export function contextAuthHeader() {
57
+ const header = storage.getStore()?.authHeader;
58
+ return header && typeof header === 'object' ? header : undefined;
59
+ }
60
+
61
+ /**
62
+ * Outbound `x-heyspex-vault-id` for the active request, or `undefined` outside
63
+ * any request context (the stdio server then resolves the repo-bound vault
64
+ * from its cwd). INSIDE a context the answer is ALWAYS an object: the caller's
65
+ * own vault header, or `{}` when it sent none. It is never the server
66
+ * process's own vault (a shared serverless runtime has none), and never the
67
+ * account-wide active pointer: a bearer call that names no vault is refused by
68
+ * the console (`vault-scope-required`). Owner principles P1/P2/P3, 2026-10-16.
69
+ */
70
+ export function contextVaultIdHeader() {
71
+ const store = storage.getStore();
72
+ if (!store) return undefined;
73
+ const id = typeof store.vaultId === 'string' ? store.vaultId.trim() : '';
74
+ return id ? { 'x-heyspex-vault-id': id } : {};
75
+ }
76
+
77
+ /**
78
+ * Outbound `x-heyspex-account-id` for the active request: always `{}` inside a
79
+ * context (the forwarded credential IS the account), `undefined` outside.
80
+ */
81
+ export function contextAccountIdHeader() {
82
+ return storage.getStore() ? {} : undefined;
83
+ }