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
@@ -1,9 +1,290 @@
1
1
  import { createRequire as __hsCreateRequire } from 'node:module';
2
2
  const require = __hsCreateRequire(import.meta.url);
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+
8
+ // vendor/heyspex-mcp/credentials.mjs
9
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
10
+ import os, { homedir } from "node:os";
11
+ import { dirname as dirname2, join as join2, resolve } from "node:path";
12
+ function credentialsFilePath(homeDir = homedir()) {
13
+ return join2(homeDir, ".heyspex", "credentials.yaml");
14
+ }
15
+ function parseMcpToken(raw) {
16
+ if (typeof raw !== "string" || raw.length === 0) return null;
17
+ const token = raw.match(MCP_TOKEN_LINE_RE)?.[1]?.trim();
18
+ return token ? token : null;
19
+ }
20
+ function isValidAccountId(id) {
21
+ return typeof id === "string" && ACCOUNT_ID_RE.test(id);
22
+ }
23
+ function topLevelScalar(raw, key) {
24
+ const re = new RegExp(`^${key}:[ \\t]*["']?([^"'\\r\\n#]+)`, "m");
25
+ const value = raw.match(re)?.[1]?.trim();
26
+ return value ? value : null;
27
+ }
28
+ function decodeScalar(text) {
29
+ const t = text.trim();
30
+ if (t === "" || t === "null" || t === "~") return null;
31
+ if (t.startsWith('"')) {
32
+ try {
33
+ const v = JSON.parse(t);
34
+ return typeof v === "string" && v ? v : null;
35
+ } catch {
36
+ return t.replace(/^"|"$/g, "") || null;
37
+ }
38
+ }
39
+ if (t.startsWith("'")) return t.replace(/^'|'$/g, "") || null;
40
+ const bare = t.replace(/\s+#.*$/, "").trim();
41
+ return bare || null;
42
+ }
43
+ function emptyAccount() {
44
+ return { mcp_token: null, console_url: null, email: null, paired_at: null };
45
+ }
46
+ function findAccountsBlock(lines) {
47
+ const start = lines.findIndex((l) => /^accounts:[ \t]*(#.*)?$/.test(l));
48
+ if (start < 0) return null;
49
+ let end = start + 1;
50
+ while (end < lines.length && (lines[end].trim() === "" || /^[ \t]/.test(lines[end]))) end += 1;
51
+ while (end > start + 1 && lines[end - 1].trim() === "") end -= 1;
52
+ return { start, end };
53
+ }
54
+ function parseAccountsBlock(lines) {
55
+ const accounts = {};
56
+ const block = findAccountsBlock(lines);
57
+ if (!block) return accounts;
58
+ let current = null;
59
+ for (const line of lines.slice(block.start + 1, block.end)) {
60
+ const header = line.match(/^ {2}([^\s:#][^:]*):[ \t]*(#.*)?$/);
61
+ if (header) {
62
+ const id = decodeScalar(header[1]);
63
+ current = isValidAccountId(id) ? id : null;
64
+ if (current) accounts[current] = emptyAccount();
65
+ continue;
66
+ }
67
+ const field = line.match(/^ {4}([a-z_]+):(.*)$/);
68
+ if (field && current && ACCOUNT_FIELDS.includes(field[1])) {
69
+ accounts[current][field[1]] = decodeScalar(field[2]);
70
+ }
71
+ }
72
+ return accounts;
73
+ }
74
+ function parseCredentialsStore(raw) {
75
+ const text = typeof raw === "string" ? raw : "";
76
+ if (topLevelScalar(text, "schema_version") !== String(CREDENTIALS_SCHEMA_VERSION)) {
77
+ const token = parseMcpToken(text);
78
+ if (!token) return { schemaVersion: 1, currentAccountId: null, accounts: {} };
79
+ return {
80
+ schemaVersion: 1,
81
+ currentAccountId: LEGACY_ACCOUNT_ID,
82
+ accounts: {
83
+ [LEGACY_ACCOUNT_ID]: { ...emptyAccount(), mcp_token: token, console_url: topLevelScalar(text, "console_url") }
84
+ }
85
+ };
86
+ }
87
+ const accounts = parseAccountsBlock(text.split(/\r?\n/));
88
+ const current = topLevelScalar(text, "current_account_id");
89
+ return {
90
+ schemaVersion: 2,
91
+ currentAccountId: current && accounts[current] ? current : null,
92
+ accounts
93
+ };
94
+ }
95
+ function resolveBearer({ envToken, credentialsText, accountId, legacyAccountId } = {}) {
96
+ const fromEnv = typeof envToken === "string" ? envToken.trim() : "";
97
+ if (fromEnv) return { token: fromEnv, source: "env" };
98
+ const store = parseCredentialsStore(credentialsText);
99
+ let entry;
100
+ if (typeof accountId === "string" && accountId.trim()) {
101
+ const wanted = accountId.trim();
102
+ entry = store.accounts[wanted];
103
+ if (!entry && store.schemaVersion === 1 && legacyAccountId && wanted === legacyAccountId) {
104
+ entry = store.accounts[LEGACY_ACCOUNT_ID];
105
+ }
106
+ } else {
107
+ entry = store.currentAccountId ? store.accounts[store.currentAccountId] : void 0;
108
+ }
109
+ if (entry?.mcp_token) return { token: entry.mcp_token, source: "credentials-file" };
110
+ return { token: null, source: null };
111
+ }
112
+ function readTextOrNull(read, path2) {
113
+ try {
114
+ const raw = read(path2);
115
+ return typeof raw === "string" ? raw : null;
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+ function readLegacyConfigAccountId(read, homeDir) {
121
+ const raw = readTextOrNull(read, join2(homeDir, ".heyspex", "config.yaml"));
122
+ const value = raw?.match(CONFIG_ACCOUNT_ID_RE)?.[1]?.trim() ?? "";
123
+ return value || null;
124
+ }
125
+ function readBearer({ env = process.env, homeDir = homedir(), readFile, accountId } = {}) {
126
+ const read = readFile ?? ((p) => readFileSync(p, "utf8"));
127
+ const credentialsText = readTextOrNull(read, credentialsFilePath(homeDir));
128
+ const named = typeof accountId === "string" && accountId.trim() ? accountId.trim() : null;
129
+ const legacyAccountId = named && parseCredentialsStore(credentialsText).schemaVersion === 1 ? readLegacyConfigAccountId(read, homeDir) : null;
130
+ return resolveBearer({ envToken: env?.[MCP_TOKEN_ENV], credentialsText, accountId: named, legacyAccountId });
131
+ }
132
+ function readCurrentAccountId({ homeDir = homedir(), readFile } = {}) {
133
+ const read = readFile ?? ((p) => readFileSync(p, "utf8"));
134
+ const store = parseCredentialsStore(readTextOrNull(read, credentialsFilePath(homeDir)));
135
+ if (store.schemaVersion === 2 && store.currentAccountId && store.currentAccountId !== LEGACY_ACCOUNT_ID) {
136
+ return store.currentAccountId;
137
+ }
138
+ if (store.schemaVersion === 2 && store.currentAccountId === null) return null;
139
+ return readLegacyConfigAccountId(read, homeDir);
140
+ }
141
+ var MCP_TOKEN_ENV, MCP_TOKEN_LINE_RE, CREDENTIALS_SCHEMA_VERSION, LEGACY_ACCOUNT_ID, ACCOUNT_FIELDS, ACCOUNT_ID_RE, CONFIG_ACCOUNT_ID_RE;
142
+ var init_credentials = __esm({
143
+ "vendor/heyspex-mcp/credentials.mjs"() {
144
+ "use strict";
145
+ MCP_TOKEN_ENV = "HEYSPEX_MCP_TOKEN";
146
+ MCP_TOKEN_LINE_RE = /^mcp_token:[ \t]*["']?([^"'\r\n#]+)/m;
147
+ CREDENTIALS_SCHEMA_VERSION = 2;
148
+ LEGACY_ACCOUNT_ID = "legacy";
149
+ ACCOUNT_FIELDS = ["mcp_token", "console_url", "email", "paired_at"];
150
+ ACCOUNT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.@+-]*$/;
151
+ CONFIG_ACCOUNT_ID_RE = /^account_id:\s*["']?([^"'\r\n#]+)/m;
152
+ }
153
+ });
154
+
155
+ // scripts/lib/dev-mode.mjs
156
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
157
+ import { homedir as homedir6 } from "node:os";
158
+ import { join as join7 } from "node:path";
159
+ function devModeFilePath(homeDir = homedir6()) {
160
+ return join7(homeDir, ".heyspex", DEV_MODE_FILENAME);
161
+ }
162
+ function normalizeMode(raw) {
163
+ const v = String(raw ?? "").trim().toLowerCase();
164
+ if (v === "prod" || v === "production") return { mode: "prod", reliance: "cloud" };
165
+ if (v === "dev") return { mode: "dev", reliance: null };
166
+ if (v === "local") return { mode: "dev", reliance: "local" };
167
+ if (v === "cloud") return { mode: "dev", reliance: "cloud" };
168
+ return null;
169
+ }
170
+ function normalizeReliance(raw) {
171
+ const v = String(raw ?? "").trim().toLowerCase();
172
+ return v === "local" || v === "cloud" ? v : null;
173
+ }
174
+ function deriveSemantics(mode, reliance) {
175
+ const effectiveReliance = mode === "prod" ? "cloud" : reliance ?? "local";
176
+ const local = mode === "dev" && effectiveReliance === "local";
177
+ return {
178
+ reliance: effectiveReliance,
179
+ consoleUrl: local ? LOCAL_CONSOLE_URL : CLOUD_CONSOLE_URL,
180
+ readTier: local ? "disk" : "neon",
181
+ vaultWriteMode: local ? "local" : "cloud"
182
+ };
183
+ }
184
+ function readLocalDevModeRecord({ homeDir = homedir6(), readFile = defaultReadFile } = {}) {
185
+ let raw;
186
+ try {
187
+ raw = readFile(devModeFilePath(homeDir));
188
+ } catch {
189
+ return { record: null, parseError: null };
190
+ }
191
+ try {
192
+ const parsed = JSON.parse(raw);
193
+ const mode = parsed?.mode === "prod" || parsed?.mode === "dev" ? parsed.mode : null;
194
+ const reliance = normalizeReliance(parsed?.reliance);
195
+ if (!mode || mode === "dev" && !reliance) {
196
+ return { record: null, parseError: `invalid dev-mode record: ${JSON.stringify(parsed)}` };
197
+ }
198
+ return {
199
+ record: {
200
+ mode,
201
+ reliance: mode === "prod" ? "cloud" : reliance,
202
+ console_url: typeof parsed.console_url === "string" ? parsed.console_url : null,
203
+ updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : null,
204
+ source: typeof parsed.source === "string" ? parsed.source : null
205
+ },
206
+ parseError: null
207
+ };
208
+ } catch (err) {
209
+ return { record: null, parseError: `unparseable dev-mode.json: ${String(err?.message ?? err)}` };
210
+ }
211
+ }
212
+ function resolveDevMode({
213
+ env = process.env,
214
+ homeDir = homedir6(),
215
+ readFile = defaultReadFile,
216
+ cloudSetting = void 0
217
+ } = {}) {
218
+ const local = readLocalDevModeRecord({ homeDir, readFile });
219
+ const cloudKnown = cloudSetting !== void 0;
220
+ const cloudValue = cloudKnown && cloudSetting ? {
221
+ mode: cloudSetting.mode === "dev" ? "dev" : "prod",
222
+ reliance: normalizeReliance(cloudSetting.reliance) ?? "cloud"
223
+ } : null;
224
+ let mode;
225
+ let reliance;
226
+ let source;
227
+ let relianceSource;
228
+ const envParsed = normalizeMode(env.HEYSPEX_DEV_MODE);
229
+ if (envParsed) {
230
+ mode = envParsed.mode;
231
+ const envReliance = envParsed.reliance ?? normalizeReliance(env.HEYSPEX_DEV_RELIANCE);
232
+ reliance = envReliance ?? "local";
233
+ source = "env";
234
+ relianceSource = envReliance ? "env" : "assumed-local";
235
+ } else if (local.record) {
236
+ mode = local.record.mode;
237
+ reliance = local.record.reliance;
238
+ source = "local-override";
239
+ } else if (cloudValue) {
240
+ mode = cloudValue.mode;
241
+ reliance = cloudValue.reliance;
242
+ source = "cloud-setting";
243
+ } else {
244
+ mode = "prod";
245
+ reliance = "cloud";
246
+ source = "default";
247
+ }
248
+ const semantics = deriveSemantics(mode, reliance);
249
+ const consoleUrl = source === "local-override" && local.record?.console_url ? local.record.console_url.replace(/\/+$/, "") : semantics.consoleUrl;
250
+ const staleAgainstCloud = source === "local-override" && cloudValue !== null && (cloudValue.mode !== mode || mode === "dev" && cloudValue.reliance !== semantics.reliance);
251
+ return {
252
+ mode,
253
+ reliance: semantics.reliance,
254
+ consoleUrl,
255
+ readTier: semantics.readTier,
256
+ vaultWriteMode: semantics.vaultWriteMode,
257
+ sources: {
258
+ mode: source,
259
+ reliance: relianceSource ?? source,
260
+ consoleUrl: source === "local-override" && local.record?.console_url ? "local-override" : "derived",
261
+ readTier: "derived",
262
+ vaultWriteMode: "derived"
263
+ },
264
+ localOverride: {
265
+ present: local.record !== null,
266
+ record: local.record,
267
+ parseError: local.parseError,
268
+ staleAgainstCloud
269
+ },
270
+ cloudSetting: { known: cloudKnown, value: cloudValue }
271
+ };
272
+ }
273
+ var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile;
274
+ var init_dev_mode = __esm({
275
+ "scripts/lib/dev-mode.mjs"() {
276
+ "use strict";
277
+ init_credentials();
278
+ CLOUD_CONSOLE_URL = "https://app.heyspex.com";
279
+ LOCAL_CONSOLE_URL = "http://localhost:3000";
280
+ DEV_MODE_FILENAME = "dev-mode.json";
281
+ defaultReadFile = (p) => readFileSync6(p, "utf8");
282
+ }
283
+ });
3
284
 
4
285
  // spex-console/bin/hs-claim.ts
5
- import { readFileSync as readFileSync4 } from "node:fs";
6
- import process2 from "node:process";
286
+ import { readFileSync as readFileSync8 } from "node:fs";
287
+ import process3 from "node:process";
7
288
 
8
289
  // spex-console/src/lib/work-stages.ts
9
290
  var WORK_STAGES = [
@@ -46,12 +327,356 @@ function resolveAgentIdentity(argAgent, argSession) {
46
327
  }
47
328
 
48
329
  // spex-console/src/lib/cli/control-shared.ts
49
- import { readFileSync } from "node:fs";
50
- import { homedir } from "node:os";
51
- import { join } from "node:path";
330
+ import path from "node:path";
331
+
332
+ // scripts/lib/hs-command.mjs
333
+ import { existsSync } from "node:fs";
334
+ import { delimiter, dirname, join } from "node:path";
335
+ import { fileURLToPath } from "node:url";
336
+ var HS_BIN_NAME = "hs";
337
+ var HS_DISPATCHER_PATH = (() => {
338
+ try {
339
+ const here = dirname(fileURLToPath(import.meta.url));
340
+ return join(dirname(dirname(here)), "bin", "hs.mjs");
341
+ } catch {
342
+ return null;
343
+ }
344
+ })();
345
+ function shellQuote(p) {
346
+ return /[\\\s]/.test(p) ? `"${p}"` : p;
347
+ }
348
+ var HS_DISPATCHER_PREFIX = HS_DISPATCHER_PATH ? `node ${shellQuote(HS_DISPATCHER_PATH)}` : HS_BIN_NAME;
349
+ function candidateNames(name, env) {
350
+ if (process.platform !== "win32") return [name];
351
+ const pathext = (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim().toLowerCase()).filter(Boolean);
352
+ const extensions = [.../* @__PURE__ */ new Set([...pathext, ".ps1"])];
353
+ return [name, ...extensions.map((ext) => name + ext)];
354
+ }
355
+ function findOnPath(name, { env = process.env, exists = existsSync } = {}) {
356
+ try {
357
+ const raw = env.PATH ?? env.Path ?? env.path ?? "";
358
+ for (const dir of String(raw).split(delimiter)) {
359
+ const trimmed = dir.trim().replace(/^"(.*)"$/, "$1");
360
+ if (!trimmed) continue;
361
+ for (const candidate of candidateNames(name, env)) {
362
+ const full = join(trimmed, candidate);
363
+ try {
364
+ if (exists(full)) return full;
365
+ } catch {
366
+ }
367
+ }
368
+ }
369
+ } catch {
370
+ }
371
+ return null;
372
+ }
373
+ var cached = null;
374
+ var HS_ON_PATH_OVERRIDE_ENV = "HEYSPEX_HS_ON_PATH";
375
+ function onPathOverride(env) {
376
+ const raw = String(env?.[HS_ON_PATH_OVERRIDE_ENV] ?? "");
377
+ if (raw === "1") return true;
378
+ if (raw === "0") return false;
379
+ return void 0;
380
+ }
381
+ function describeHsCommand({ env, exists, useCache = true } = {}) {
382
+ const injected = env !== void 0 || exists !== void 0;
383
+ const effectiveEnv = env ?? process.env;
384
+ const override = onPathOverride(effectiveEnv);
385
+ if (override !== void 0) {
386
+ return {
387
+ onPath: override,
388
+ prefix: override ? HS_BIN_NAME : HS_DISPATCHER_PREFIX,
389
+ resolvedPath: null,
390
+ dispatcherPath: HS_DISPATCHER_PATH
391
+ };
392
+ }
393
+ if (useCache && !injected && cached) return cached;
394
+ const resolvedPath = findOnPath(HS_BIN_NAME, {
395
+ env: env ?? process.env,
396
+ exists: exists ?? existsSync
397
+ });
398
+ const answer = {
399
+ onPath: resolvedPath !== null,
400
+ prefix: resolvedPath !== null ? HS_BIN_NAME : HS_DISPATCHER_PREFIX,
401
+ resolvedPath,
402
+ dispatcherPath: HS_DISPATCHER_PATH
403
+ };
404
+ if (useCache && !injected) cached = answer;
405
+ return answer;
406
+ }
407
+ function hsCommand(args = "", options = {}) {
408
+ let prefix = HS_DISPATCHER_PREFIX;
409
+ try {
410
+ prefix = describeHsCommand(options).prefix;
411
+ } catch {
412
+ }
413
+ const tail = String(args ?? "").trim();
414
+ return tail ? `${prefix} ${tail}` : prefix;
415
+ }
416
+
417
+ // vendor/heyspex-mcp/console-fetch.mjs
418
+ init_credentials();
419
+ import { readFileSync as readFileSync4 } from "node:fs";
420
+ import { homedir as homedir4 } from "node:os";
421
+ import { join as join5 } from "node:path";
422
+
423
+ // vendor/heyspex-mcp/session-owner.mjs
424
+ init_credentials();
425
+ import { readFileSync as readFileSync2 } from "node:fs";
426
+ import { homedir as homedir2 } from "node:os";
427
+ import { join as join3 } from "node:path";
428
+ function resolveSessionOwnerUncached({ env = process.env, homeDir = homedir2(), readFile } = {}) {
429
+ const fromEnv = typeof env?.HS_ACCOUNT_ID === "string" ? env.HS_ACCOUNT_ID.trim() : "";
430
+ if (fromEnv) return fromEnv;
431
+ return readCurrentAccountId({ homeDir, readFile });
432
+ }
433
+
434
+ // vendor/heyspex-mcp/vault-less-endpoints.mjs
435
+ var VAULT_LESS_ENDPOINTS = Object.freeze([
436
+ "* /api/mcp/session",
437
+ "* /api/mcp/pairing-codes",
438
+ "POST /api/vaults",
439
+ "GET /api/vaults",
440
+ "POST /api/workspaces",
441
+ "GET /api/workspaces",
442
+ "* /api/admin/dev-mode"
443
+ ]);
444
+ function pathnameOf(path2) {
445
+ const raw = String(path2 ?? "").split("?")[0].split("#")[0];
446
+ const noTrailing = raw.length > 1 ? raw.replace(/\/+$/, "") : raw;
447
+ return noTrailing || "/";
448
+ }
449
+ function isVaultLessEndpoint(method, path2) {
450
+ const m = String(method || "GET").toUpperCase();
451
+ const pathname = pathnameOf(path2);
452
+ return VAULT_LESS_ENDPOINTS.some((row) => {
453
+ const [rowMethod, rowPath] = row.split(" ");
454
+ return rowPath === pathname && (rowMethod === "*" || rowMethod === m);
455
+ });
456
+ }
457
+
458
+ // vendor/heyspex-mcp/vault-resolve.mjs
459
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
460
+ import { homedir as homedir3, tmpdir } from "node:os";
461
+ import { dirname as dirname3, join as join4, resolve as resolve2 } from "node:path";
462
+
463
+ // vendor/heyspex-mcp/console-fetch.mjs
464
+ var VAULT_NOT_REGISTERED = "VAULT_NOT_REGISTERED";
465
+ var VaultNotRegisteredError = class extends Error {
466
+ /**
467
+ * @param {string | null | undefined} repoRootAbs the root the resolver settled on
468
+ * @param {string | null | undefined} source the resolver tier that produced it
469
+ * @param {{ reason?: 'unregistered' | 'legacy-id', legacyId?: string | null }} [detail]
470
+ */
471
+ constructor(repoRootAbs, source, { reason = "unregistered", legacyId = null } = {}) {
472
+ const root = typeof repoRootAbs === "string" && repoRootAbs.trim() ? repoRootAbs.trim() : null;
473
+ const where = root ? ` (${root})` : "";
474
+ const why = reason === "legacy-id" ? `This repo${where} is registered on this machine under a legacy id (\`${legacyId}\`), not the vault id the console knows, and its heyspex.yaml carries no \`vault_id\` to stand in for it, ` : `This repo${where} is not connected to a HeySpex vault on this machine (\`hs connect\` has not registered it), `;
475
+ super(
476
+ why + `so the console call was refused before it was sent. Run \`${hsCommand("connect")}\`${root ? ` in ${root}` : ""} to ${reason === "legacy-id" ? "re-register" : "connect"} it, then try again. Nothing was sent.`
477
+ );
478
+ this.name = "VaultNotRegisteredError";
479
+ this.code = VAULT_NOT_REGISTERED;
480
+ this.reason = reason;
481
+ this.repoRootAbs = root;
482
+ this.source = typeof source === "string" ? source : null;
483
+ this.legacyId = legacyId;
484
+ }
485
+ };
486
+ var CLOUD_VAULT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
487
+ function isCloudVaultId(value) {
488
+ return typeof value === "string" && CLOUD_VAULT_ID_RE.test(value.trim());
489
+ }
490
+ var MARKER_VAULT_ID_RE = /^vault_id:[ \t]*["']?([^"'\r\n#]+)/m;
491
+ function readMarkerVaultId(repoRootAbs, { readFile } = {}) {
492
+ if (typeof repoRootAbs !== "string" || !repoRootAbs.trim()) return null;
493
+ const read = readFile ?? ((p) => readFileSync4(p, "utf8"));
494
+ try {
495
+ const raw = read(join5(repoRootAbs, "heyspex.yaml"));
496
+ const value = String(raw ?? "").match(MARKER_VAULT_ID_RE)?.[1]?.trim() ?? "";
497
+ return value || null;
498
+ } catch {
499
+ return null;
500
+ }
501
+ }
502
+ function cloudVaultIdFor(resolved, { readMarkerVaultId: readMarker = readMarkerVaultId } = {}) {
503
+ const rowId = resolved.vaultId.trim();
504
+ if (isCloudVaultId(rowId)) return rowId;
505
+ const marker = readMarker(resolved.repoRootAbs);
506
+ if (isCloudVaultId(marker)) return marker.trim();
507
+ throw new VaultNotRegisteredError(resolved.repoRootAbs, resolved.source, { reason: "legacy-id", legacyId: rowId });
508
+ }
509
+ var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
510
+
511
+ // spex-console/src/lib/cli/control-shared.ts
512
+ init_credentials();
513
+
514
+ // spex-console/src/lib/vault-registry/registry-file.ts
515
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
516
+ import { homedir as homedir5 } from "node:os";
517
+ import { dirname as dirname4, join as join6, resolve as resolve3 } from "node:path";
518
+
519
+ // spex-console/src/lib/vault-registry/types.ts
520
+ var LEGACY_REGISTRY_ACCOUNT_ID = "legacy";
521
+ var VAULT_REGISTRY_SCHEMA_VERSION = 2;
522
+ function emptyVaultRegistry() {
523
+ return {
524
+ schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
525
+ current_account_id: null,
526
+ accounts: {},
527
+ workspaces: [],
528
+ vaults: []
529
+ };
530
+ }
531
+
532
+ // spex-console/src/lib/vault-registry/registry-file.ts
533
+ function vaultRegistryDir(opts = {}) {
534
+ return opts.registryDir ?? join6(homedir5(), ".heyspex");
535
+ }
536
+ function vaultRegistryFilePath(opts = {}) {
537
+ return join6(vaultRegistryDir(opts), "registry.json");
538
+ }
539
+ function isNonEmptyString(v) {
540
+ return typeof v === "string" && v.trim().length > 0;
541
+ }
542
+ function accountOf(o) {
543
+ return isNonEmptyString(o.account_id) ? o.account_id : LEGACY_REGISTRY_ACCOUNT_ID;
544
+ }
545
+ function normalizeWorkspace(raw) {
546
+ if (!raw || typeof raw !== "object") return null;
547
+ const o = raw;
548
+ if (!isNonEmptyString(o.id)) return null;
549
+ if (!isNonEmptyString(o.display_name)) return null;
550
+ if (!isNonEmptyString(o.created_at)) return null;
551
+ const record = {
552
+ id: o.id,
553
+ display_name: o.display_name,
554
+ created_at: o.created_at,
555
+ archived: o.archived === true,
556
+ account_id: accountOf(o)
557
+ };
558
+ if (isNonEmptyString(o.github_org)) record.github_org = o.github_org;
559
+ return record;
560
+ }
561
+ function normalizeVault(raw) {
562
+ if (!raw || typeof raw !== "object") return null;
563
+ const o = raw;
564
+ if (!isNonEmptyString(o.id)) return null;
565
+ if (!isNonEmptyString(o.workspace_id)) return null;
566
+ if (!isNonEmptyString(o.display_name)) return null;
567
+ if (!isNonEmptyString(o.repo_root_abs)) return null;
568
+ if (typeof o.schema_version !== "number") return null;
569
+ if (!isNonEmptyString(o.registered_at)) return null;
570
+ const record = {
571
+ id: o.id,
572
+ workspace_id: o.workspace_id,
573
+ display_name: o.display_name,
574
+ repo_root_abs: o.repo_root_abs,
575
+ schema_version: o.schema_version,
576
+ registered_at: o.registered_at,
577
+ archived: o.archived === true,
578
+ account_id: accountOf(o)
579
+ };
580
+ if (isNonEmptyString(o.github_remote)) record.github_remote = o.github_remote;
581
+ return record;
582
+ }
583
+ function normalizeStoredPointers(raw) {
584
+ const o = raw && typeof raw === "object" ? raw : {};
585
+ const out = {};
586
+ for (const field of ["active_vault_id", "active_workspace_id"]) {
587
+ const v = o[field];
588
+ if (v === null || isNonEmptyString(v)) out[field] = v;
589
+ }
590
+ return out;
591
+ }
592
+ function normalizePointers(raw) {
593
+ const o = raw && typeof raw === "object" ? raw : {};
594
+ return {
595
+ active_vault_id: isNonEmptyString(o.active_vault_id) ? o.active_vault_id : null,
596
+ active_workspace_id: isNonEmptyString(o.active_workspace_id) ? o.active_workspace_id : null
597
+ };
598
+ }
599
+ function registryFileSchemaOf(json) {
600
+ if (!json || typeof json !== "object" || Array.isArray(json)) return null;
601
+ return json.schema_version === 2 ? 2 : 1;
602
+ }
603
+ function parseVaultRegistryFile(json) {
604
+ const schema = registryFileSchemaOf(json);
605
+ if (schema === null) return emptyVaultRegistry();
606
+ const o = json;
607
+ const workspacesRaw = Array.isArray(o.workspaces) ? o.workspaces : [];
608
+ const workspaces = workspacesRaw.map(normalizeWorkspace).filter((w) => w !== null);
609
+ const vaultsRaw = Array.isArray(o.vaults) ? o.vaults : [];
610
+ const vaults = vaultsRaw.map(normalizeVault).filter((v) => v !== null);
611
+ if (schema === 1) {
612
+ const legacy = normalizePointers(o);
613
+ const hasLegacyState = legacy.active_vault_id !== null || legacy.active_workspace_id !== null || workspaces.length > 0 || vaults.length > 0;
614
+ return {
615
+ schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
616
+ current_account_id: hasLegacyState ? LEGACY_REGISTRY_ACCOUNT_ID : null,
617
+ accounts: hasLegacyState ? { [LEGACY_REGISTRY_ACCOUNT_ID]: legacy } : {},
618
+ workspaces,
619
+ vaults
620
+ };
621
+ }
622
+ const accounts = {};
623
+ if (o.accounts && typeof o.accounts === "object" && !Array.isArray(o.accounts)) {
624
+ for (const [id, pointers] of Object.entries(o.accounts)) {
625
+ if (isNonEmptyString(id)) accounts[id] = normalizeStoredPointers(pointers);
626
+ }
627
+ }
628
+ return {
629
+ schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
630
+ current_account_id: isNonEmptyString(o.current_account_id) ? o.current_account_id : null,
631
+ accounts,
632
+ workspaces,
633
+ vaults
634
+ };
635
+ }
636
+ function readRegistryJson(opts) {
637
+ const filePath = vaultRegistryFilePath(opts);
638
+ if (!existsSync3(filePath)) return { kind: "missing" };
639
+ try {
640
+ const raw = readFileSync5(filePath, "utf8");
641
+ const json = JSON.parse(raw);
642
+ if (registryFileSchemaOf(json) === null) return { kind: "unreadable" };
643
+ return { kind: "ok", raw, json };
644
+ } catch {
645
+ return { kind: "unreadable" };
646
+ }
647
+ }
648
+ function inspectVaultRegistry(opts = {}) {
649
+ const read = readRegistryJson(opts);
650
+ if (read.kind !== "ok") {
651
+ return {
652
+ registry: emptyVaultRegistry(),
653
+ file_schema_version: null,
654
+ registry_unreadable: read.kind === "unreadable"
655
+ };
656
+ }
657
+ return {
658
+ registry: parseVaultRegistryFile(read.json),
659
+ file_schema_version: registryFileSchemaOf(read.json),
660
+ registry_unreadable: false
661
+ };
662
+ }
663
+ function loadVaultRegistry(opts = {}) {
664
+ return inspectVaultRegistry(opts).registry;
665
+ }
666
+
667
+ // spex-console/src/lib/cli/control-shared.ts
52
668
  var EXIT = {
53
669
  /** Success. */
54
670
  OK: 0,
671
+ /**
672
+ * A read-only CHECK ran correctly and its answer is "no" — `hs connect
673
+ * --check` on an unconnected repo. Distinct from {@link EXIT.USAGE} so a
674
+ * script can tell "you typed the command wrong" from "the command worked,
675
+ * the repo is not connected". Both were `1` until 2026-09-18, which made
676
+ * `--check` unusable in a conditional: every caller had to parse stdout to
677
+ * find out which of the two had happened.
678
+ */
679
+ CHECK_FAILED: 2,
55
680
  /** Usage error — unknown subcommand/flag, or a missing required argument. */
56
681
  USAGE: 1,
57
682
  /** Claim race lost — the spec is already claimed by another agent (409). */
@@ -73,31 +698,94 @@ var EXIT = {
73
698
  * from `USAGE`/`INVALID_ARG` — this is an authorization refusal, not a
74
699
  * malformed command.
75
700
  */
76
- FORBIDDEN: 15
701
+ FORBIDDEN: 15,
702
+ /**
703
+ * A CLOUD write refused because no account id could be resolved (no
704
+ * `HS_ACCOUNT_ID` env var, no `account_id:` in `~/.heyspex/config.yaml`) --
705
+ * `resolveCloudAccountIdOrRefuse` in `vault-workspace-shared.ts`
706
+ * ([[HS-YU1S-UAGY-TASK-cloud-writes-refuse-without-a]]). Distinct from
707
+ * `FORBIDDEN` -- this is "not signed in at all", not "signed in but not
708
+ * authorized for this override".
709
+ */
710
+ SIGN_IN_REQUIRED: 16,
711
+ /**
712
+ * Signed in, but the account a repo would be attached to was not confirmed:
713
+ * a non-interactive install/connect with no matching `--account` / `--yes`,
714
+ * an `account_id` that disagrees with the bearer's user, or the human
715
+ * aborting the identity prompt (`install-identity.ts`,
716
+ * [[HS-HAQI-KODY-DIAG-hs-install-and-hs-connect-bind-a-new]]).
717
+ */
718
+ IDENTITY_UNCONFIRMED: 17,
719
+ /**
720
+ * The repo this verb runs in is not registered on this machine (no
721
+ * `~/.heyspex/registry.json` row for its root, or a legacy slug row with no
722
+ * cloud `vault_id` in its heyspex.yaml), so the call was refused LOCALLY —
723
+ * nothing was sent. Names `hs connect`
724
+ * ([[HS-MVG4-WW9R-TASK-mcp-and-hs-name-account-vault-on-every]]).
725
+ */
726
+ VAULT_NOT_REGISTERED: 18,
727
+ /**
728
+ * The local half of a setup verb (`hs connect`, `hs vault *`, `hs workspace
729
+ * *`) was written but the CLOUD half did not land -- the console refused
730
+ * it or was never reached. The verb's receipt names the failed half and the
731
+ * exact command to re-run
732
+ * ([[HS-STCY-WW9R-TASK-add-a-shared-receipt-that-reads-back]], invariant 6).
733
+ * Distinct from {@link EXIT.WRITE_FAILED} so a script can tell "re-run to
734
+ * sync the cloud" from a generic server failure.
735
+ */
736
+ CLOUD_WRITE_FAILED: 19,
737
+ /**
738
+ * The repo's committed `heyspex.yaml` records `account: <A>` but the machine
739
+ * is signed in as another account, so `hs connect` refused and wrote
740
+ * nothing (marker, registry, cloud). Repair: `hs login --switch-account <A>`
741
+ * ([[HS-VWUD-WW9R-TASK-record-the-connecting-account-in]], invariant 4).
742
+ */
743
+ ACCOUNT_MISMATCH: 20,
744
+ /**
745
+ * Both sides of a setup verb's receipt were read back and still DISAGREE
746
+ * after a short bounded re-read of the cloud side (a briefly stale read is
747
+ * absorbed first). The write may well have landed; what the verb cannot
748
+ * claim is that this machine and the console now say the same thing. The
749
+ * receipt prints the diffs and the command to re-run
750
+ * ([[HS-FKSR-WW9R-TASK-make-one-hs-connect-run-create-a-cloud]], invariant 6).
751
+ * Distinct from {@link EXIT.CLOUD_WRITE_FAILED} (a write that did not land).
752
+ */
753
+ RECEIPT_DISAGREE: 21
77
754
  };
78
- function readPairedToken() {
79
- try {
80
- const raw = readFileSync(join(homedir(), ".heyspex", "credentials.yaml"), "utf-8");
81
- const match = raw.match(/\b(hsmcp_[A-Za-z0-9_-]+|hscmp_[A-Za-z0-9_-]+)\b/);
82
- return match?.[1];
83
- } catch {
84
- return void 0;
85
- }
755
+ var VAULT_ID_HEADER = "x-heyspex-vault-id";
756
+ var ACCOUNT_ID_HEADER = "x-heyspex-account-id";
757
+ function readPairedBearer(opts = {}) {
758
+ return readBearer(opts);
86
759
  }
87
- function bearerHeaderFor(consoleUrl) {
760
+ function readPairedToken(opts = {}) {
761
+ return readPairedBearer(opts).token ?? void 0;
762
+ }
763
+ function bearerHeaderFor(consoleUrl, opts = {}) {
88
764
  try {
89
765
  const host = new URL(consoleUrl).hostname;
90
766
  if (host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === "::1") {
91
767
  return {};
92
768
  }
93
- const token = readPairedToken();
769
+ const token = readPairedToken(opts);
94
770
  return token ? { authorization: `Bearer ${token}` } : {};
95
771
  } catch {
96
772
  return {};
97
773
  }
98
774
  }
775
+ function scopeHeadersFor(scope, method, path2) {
776
+ if (scope === "none") {
777
+ if (!isVaultLessEndpoint(method, path2)) {
778
+ throw new Error(
779
+ `controlRequest: scope "none" is only allowed for the vault-less endpoints (${VAULT_LESS_ENDPOINTS.join(", ")}); ${method.toUpperCase()} ${path2} is vault-scoped and must name { accountId, vaultId }.`
780
+ );
781
+ }
782
+ return {};
783
+ }
784
+ return { [VAULT_ID_HEADER]: scope.vaultId, [ACCOUNT_ID_HEADER]: scope.accountId };
785
+ }
99
786
  async function controlRequest(deps, req) {
100
787
  const url = `${deps.consoleUrl}${req.path}`;
788
+ const scopeHeaders = scopeHeadersFor(deps.scope, req.method, req.path);
101
789
  let res;
102
790
  try {
103
791
  res = await deps.fetchImpl(url, {
@@ -108,7 +796,10 @@ async function controlRequest(deps, req) {
108
796
  // authenticates every control call; loopback never asked, so the header
109
797
  // is attached only when the target is non-loopback and a paired token
110
798
  // exists. Same `hsmcp_` bearer the MCP server presents.
111
- ...bearerHeaderFor(deps.consoleUrl)
799
+ ...bearerHeaderFor(deps.consoleUrl),
800
+ // Invariant 2: name the vault and the account on every call, or the
801
+ // console serves whatever its leftover pointer says.
802
+ ...scopeHeaders
112
803
  },
113
804
  ...req.body !== void 0 ? { body: JSON.stringify(req.body) } : {}
114
805
  });
@@ -178,6 +869,72 @@ function parseTtl(token) {
178
869
  }
179
870
  return { value: n };
180
871
  }
872
+ function normalizeRoot(p) {
873
+ return path.resolve(p).replace(/[\\/]+/g, "/").replace(/\/+$/, "").toLowerCase();
874
+ }
875
+ function resolveControlScope(input) {
876
+ const target = normalizeRoot(input.vaultRoot);
877
+ const row = input.vaults.find((v) => normalizeRoot(v.repo_root_abs) === target) ?? null;
878
+ if (!row) {
879
+ return {
880
+ ok: false,
881
+ error: {
882
+ code: EXIT.VAULT_NOT_REGISTERED,
883
+ message: `this repo (${input.vaultRoot}) is not connected to a HeySpex vault on this machine (\`hs connect\` has not registered it), so the call was refused before it was sent. Run \`${hsCommand("connect")}\` in ${input.vaultRoot}, then try again.`,
884
+ payload: {
885
+ ok: false,
886
+ error: "vault_not_registered",
887
+ reason: "unregistered",
888
+ vault_root: input.vaultRoot
889
+ },
890
+ isError: true
891
+ }
892
+ };
893
+ }
894
+ let vaultId;
895
+ try {
896
+ vaultId = cloudVaultIdFor(
897
+ { vaultId: row.id, repoRootAbs: row.repo_root_abs, source: "registry" },
898
+ input.readMarkerVaultId ? { readMarkerVaultId: input.readMarkerVaultId } : {}
899
+ );
900
+ } catch (err) {
901
+ if (!(err instanceof VaultNotRegisteredError)) throw err;
902
+ const refused = err;
903
+ return {
904
+ ok: false,
905
+ error: {
906
+ code: EXIT.VAULT_NOT_REGISTERED,
907
+ message: refused.message,
908
+ payload: {
909
+ ok: false,
910
+ error: "vault_not_registered",
911
+ reason: refused.reason ?? "legacy-id",
912
+ vault_root: input.vaultRoot,
913
+ legacy_id: refused.legacyId ?? row.id
914
+ },
915
+ isError: true
916
+ }
917
+ };
918
+ }
919
+ const accountId = input.accountId?.trim() ?? "";
920
+ if (!accountId) {
921
+ return {
922
+ ok: false,
923
+ error: {
924
+ code: EXIT.SIGN_IN_REQUIRED,
925
+ message: `no HeySpex account is configured on this machine (\`hs login\` has not been run), so the call was refused before it was sent. Run \`${hsCommand("login")}\` on this machine, then try again.`,
926
+ payload: { ok: false, error: "sign_in_required", vault_root: input.vaultRoot },
927
+ isError: true
928
+ }
929
+ };
930
+ }
931
+ return { ok: true, scope: { accountId, vaultId } };
932
+ }
933
+ function resolveControlScopeFromMachine(vaultRoot, opts = {}) {
934
+ const registry = loadVaultRegistry(opts.registry);
935
+ const accountId = resolveSessionOwnerUncached({ env: opts.env, homeDir: opts.homeDir });
936
+ return resolveControlScope({ vaultRoot, accountId, vaults: registry.vaults });
937
+ }
181
938
 
182
939
  // spex-console/src/lib/cli/claim.ts
183
940
  function parseClaimArgs(argv) {
@@ -441,41 +1198,120 @@ var HELP = [
441
1198
  " 12 console-unreachable \xB7 13 server-error \xB7 14 neon-required"
442
1199
  ].join("\n");
443
1200
 
444
- // spex-console/src/lib/cli/defer.ts
445
- import { readFileSync as readFileSync3 } from "node:fs";
446
- import os from "node:os";
447
- import path from "node:path";
448
-
449
1201
  // scripts/lib/console-url-resolver.mjs
450
- import { readFileSync as readFileSync2 } from "node:fs";
451
- import { homedir as homedir2 } from "node:os";
452
- import { join as join2 } from "node:path";
453
- var DEFAULT_CONSOLE_URL = "https://app.heyspex.com";
454
-
455
- // spex-console/src/lib/cli/defer.ts
456
- function resolveSpexConsoleUrl(vaultRoot, readDiscovery2, opts = {}) {
457
- const env = opts.env ?? process.env;
458
- const raw = (env.HEYSPEX_CONSOLE_URL ?? env.HEYSPEX_COCKPIT_URL)?.trim();
459
- if (raw && raw.length > 0) return raw.replace(/\/+$/, "");
1202
+ init_dev_mode();
1203
+ import { readFileSync as readFileSync7 } from "node:fs";
1204
+ import { homedir as homedir7 } from "node:os";
1205
+ import { join as join8 } from "node:path";
1206
+ var DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
1207
+ function trimTrailingSlash(url) {
1208
+ return url.replace(/\/+$/, "");
1209
+ }
1210
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
1211
+ function isLoopbackConsoleUrl(url) {
1212
+ if (typeof url !== "string" || !url) return false;
1213
+ try {
1214
+ return LOOPBACK_HOSTS.has(new URL(url).hostname.toLowerCase());
1215
+ } catch {
1216
+ return false;
1217
+ }
1218
+ }
1219
+ var staleLoopbackCredentialsWarned = false;
1220
+ function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
1221
+ if (staleLoopbackCredentialsWarned) return;
1222
+ staleLoopbackCredentialsWarned = true;
1223
+ try {
1224
+ log(
1225
+ `[heyspex] ignoring console_url ${credentialsUrl} from ~/.heyspex/credentials.yaml: the resolved reliance is cloud, and a loopback console cannot serve it \u2014 using ${derivedUrl} (set HEYSPEX_CONSOLE_URL to override explicitly, or run \`hs dev mode:local\` to go back to a local console).
1226
+ `
1227
+ );
1228
+ } catch {
1229
+ }
1230
+ }
1231
+ function readCredentialsConsoleUrl({ homeDir = homedir7(), readFile = defaultReadFile2 } = {}) {
460
1232
  try {
461
- const homeDir = opts.homeDir ?? os.homedir();
462
- const readFile = opts.readFile ?? ((p) => readFileSync3(p, "utf-8"));
463
- const credentials = readFile(path.join(homeDir, ".heyspex", "credentials.yaml"));
464
- const match = credentials.match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
1233
+ const raw = readFile(join8(homeDir, ".heyspex", "credentials.yaml"));
1234
+ const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
465
1235
  const value = match?.[1]?.trim();
466
- if (value && value.length > 0) return value.replace(/\/+$/, "");
1236
+ return value && value.length > 0 ? trimTrailingSlash(value) : null;
467
1237
  } catch {
1238
+ return null;
468
1239
  }
469
- for (const name of ["console-url", "cockpit-url"]) {
470
- try {
471
- const discovery = readDiscovery2(`${vaultRoot}/.heyspex/${name}`);
472
- if (discovery && discovery.trim().length > 0) {
473
- return discovery.trim().replace(/\/+$/, "");
474
- }
475
- } catch {
1240
+ }
1241
+ function defaultReadFile2(p) {
1242
+ return readFileSync7(p, "utf8");
1243
+ }
1244
+ function readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery: readDiscovery2 = defaultReadFile2 } = {}) {
1245
+ if (!repoRoot) return null;
1246
+ try {
1247
+ const discovery = String(readDiscovery2(join8(repoRoot, ".heyspex", "console-url")) ?? "").trim();
1248
+ if (discovery.length > 0) return trimTrailingSlash(discovery);
1249
+ } catch {
1250
+ }
1251
+ return null;
1252
+ }
1253
+ var ConsoleUrlUnresolvedError = class extends Error {
1254
+ constructor() {
1255
+ super(
1256
+ "resolveConsoleUrl: local reliance with no HEYSPEX_CONSOLE_URL env, no console_url: in ~/.heyspex/credentials.yaml, and no .heyspex/console-url discovery file \u2014 refusing to silently fall back to the localhost:3000 default in strict mode."
1257
+ );
1258
+ this.name = "ConsoleUrlUnresolvedError";
1259
+ }
1260
+ };
1261
+ function resolveModeEnvelope({ env, homeDir }) {
1262
+ try {
1263
+ const resolved = resolveDevMode({ env, homeDir });
1264
+ return {
1265
+ reliance: resolved.reliance === "cloud" ? "cloud" : "local",
1266
+ consoleUrl: resolved.consoleUrl || DEFAULT_CONSOLE_URL
1267
+ };
1268
+ } catch {
1269
+ return { reliance: "local", consoleUrl: DEFAULT_CONSOLE_URL };
1270
+ }
1271
+ }
1272
+ function resolveConsoleUrl({
1273
+ repoRoot,
1274
+ env = process.env,
1275
+ homeDir = homedir7(),
1276
+ strict = false,
1277
+ devMode,
1278
+ log = (line) => process.stderr.write(line),
1279
+ readFile = defaultReadFile2,
1280
+ readDiscovery: readDiscovery2 = defaultReadFile2
1281
+ } = {}) {
1282
+ const envUrl = env.HEYSPEX_CONSOLE_URL?.trim();
1283
+ if (envUrl) return trimTrailingSlash(envUrl);
1284
+ const mode = devMode ?? resolveModeEnvelope({ env, homeDir });
1285
+ const cloud = mode.reliance === "cloud";
1286
+ const derivedUrl = trimTrailingSlash(
1287
+ cloud && isLoopbackConsoleUrl(mode.consoleUrl) ? CLOUD_CONSOLE_URL : mode.consoleUrl || DEFAULT_CONSOLE_URL
1288
+ );
1289
+ const credentialsUrl = readCredentialsConsoleUrl({ homeDir, readFile });
1290
+ if (credentialsUrl) {
1291
+ if (cloud && isLoopbackConsoleUrl(credentialsUrl)) {
1292
+ warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log });
1293
+ } else {
1294
+ return credentialsUrl;
476
1295
  }
477
1296
  }
478
- return DEFAULT_CONSOLE_URL;
1297
+ if (!cloud) {
1298
+ const discoveryUrl = readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery: readDiscovery2 });
1299
+ if (discoveryUrl) return discoveryUrl;
1300
+ if (strict) throw new ConsoleUrlUnresolvedError();
1301
+ }
1302
+ return derivedUrl;
1303
+ }
1304
+
1305
+ // spex-console/src/lib/cli/defer.ts
1306
+ function resolveSpexConsoleUrl(vaultRoot, readDiscovery2, opts = {}) {
1307
+ return resolveConsoleUrl({
1308
+ repoRoot: vaultRoot,
1309
+ env: opts.env,
1310
+ homeDir: opts.homeDir,
1311
+ readFile: opts.readFile,
1312
+ readDiscovery: readDiscovery2,
1313
+ log: opts.log
1314
+ });
479
1315
  }
480
1316
  var HELP2 = [
481
1317
  "Usage:",
@@ -499,43 +1335,80 @@ var HELP2 = [
499
1335
  "Exit codes: 0 ok \xB7 1 usage \xB7 10 not-found \xB7 11 bad-arg \xB7 12 console-unreachable \xB7 13 write-failed"
500
1336
  ].join("\n");
501
1337
 
1338
+ // spex-console/bin/run-entry.ts
1339
+ import process2 from "node:process";
1340
+ var BinExit = class extends Error {
1341
+ code;
1342
+ constructor(code) {
1343
+ super(`exit ${code}`);
1344
+ this.name = "BinExit";
1345
+ this.code = code;
1346
+ }
1347
+ };
1348
+ function runEntry(name, main2, options = {}) {
1349
+ const { fallbackCode = 1, unhandledLabel = "unhandled" } = options;
1350
+ void (async () => {
1351
+ try {
1352
+ await main2();
1353
+ } catch (err) {
1354
+ if (err instanceof BinExit) {
1355
+ process2.exitCode = err.code;
1356
+ return;
1357
+ }
1358
+ const message = err instanceof Error ? err.message : String(err);
1359
+ process2.stderr.write(`${name}: ${unhandledLabel}: ${message}
1360
+ `);
1361
+ process2.exitCode = fallbackCode;
1362
+ }
1363
+ })();
1364
+ }
1365
+
502
1366
  // spex-console/bin/hs-claim.ts
503
1367
  function readDiscovery(p) {
504
1368
  try {
505
- return readFileSync4(p, "utf-8");
1369
+ return readFileSync8(p, "utf-8");
506
1370
  } catch {
507
1371
  return null;
508
1372
  }
509
1373
  }
510
1374
  async function main() {
511
- const vaultRoot = process2.env.HS_VAULT_ROOT;
1375
+ const vaultRoot = process3.env.HS_VAULT_ROOT;
512
1376
  if (!vaultRoot) {
513
- process2.stderr.write(
1377
+ process3.stderr.write(
514
1378
  "hs claim: HS_VAULT_ROOT is not set (the dispatcher must resolve and pass it).\n"
515
1379
  );
516
- process2.exit(EXIT.USAGE);
1380
+ process3.exitCode = EXIT.USAGE;
1381
+ return;
517
1382
  }
518
- const parsed = parseClaimArgs(process2.argv.slice(3));
1383
+ const parsed = parseClaimArgs(process3.argv.slice(3));
519
1384
  if (parsed.help) {
520
- process2.stdout.write(`${HELP}
1385
+ process3.stdout.write(`${HELP}
521
1386
  `);
522
- process2.exit(EXIT.OK);
1387
+ process3.exitCode = EXIT.OK;
1388
+ return;
523
1389
  }
524
1390
  if (parsed.error) {
525
- process2.stderr.write(`hs claim: ${parsed.error.message}
1391
+ process3.stderr.write(`hs claim: ${parsed.error.message}
526
1392
  `);
527
- process2.exit(parsed.error.code);
1393
+ process3.exitCode = parsed.error.code;
1394
+ return;
528
1395
  }
529
1396
  const consoleUrl = resolveSpexConsoleUrl(vaultRoot, readDiscovery);
530
- const result = await runClaim(parsed.args, { fetchImpl: fetch, consoleUrl });
531
- const sink = result.isError ? process2.stderr : process2.stdout;
1397
+ const scoped = resolveControlScopeFromMachine(vaultRoot);
1398
+ if (!scoped.ok) {
1399
+ process3.stderr.write(`hs claim: ${scoped.error.message}
1400
+ `);
1401
+ process3.exitCode = scoped.error.code;
1402
+ return;
1403
+ }
1404
+ const result = await runClaim(parsed.args, {
1405
+ fetchImpl: fetch,
1406
+ consoleUrl,
1407
+ scope: scoped.scope
1408
+ });
1409
+ const sink = result.isError ? process3.stderr : process3.stdout;
532
1410
  sink.write(`${parsed.args.json ? JSON.stringify(result.payload) : result.message}
533
1411
  `);
534
- process2.exit(result.code);
1412
+ process3.exitCode = result.code;
535
1413
  }
536
- main().catch((err) => {
537
- const message = err instanceof Error ? err.message : String(err);
538
- process2.stderr.write(`hs claim: unhandled: ${message}
539
- `);
540
- process2.exit(EXIT.WRITE_FAILED);
541
- });
1414
+ runEntry("hs claim", main, { fallbackCode: EXIT.WRITE_FAILED });