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,11 +1,360 @@
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 as resolve2 } 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, path3) {
113
+ try {
114
+ const raw = read(path3);
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-check.ts
5
286
  import { spawn, spawnSync } from "node:child_process";
6
- import { existsSync, readFileSync as readFileSync4 } from "node:fs";
287
+ import { existsSync as existsSync4, readFileSync as readFileSync8 } from "node:fs";
7
288
  import path2 from "node:path";
8
- import process2 from "node:process";
289
+ import process3 from "node:process";
290
+
291
+ // scripts/lib/shared-path-helpers.mjs
292
+ import { isAbsolute, resolve, sep } from "node:path";
293
+ function resolveCodeRoot({
294
+ explicit,
295
+ packageRoot,
296
+ vaultRoot,
297
+ env = process.env
298
+ } = {}) {
299
+ const fromExplicit = typeof explicit === "string" ? explicit.trim() : "";
300
+ if (fromExplicit) return fromExplicit;
301
+ const fromEnv = typeof env?.HS_PKG_ROOT === "string" ? env.HS_PKG_ROOT.trim() : "";
302
+ if (fromEnv) return fromEnv;
303
+ const fromPackage = typeof packageRoot === "string" ? packageRoot.trim() : "";
304
+ if (fromPackage) return fromPackage;
305
+ const fromVault = typeof vaultRoot === "string" ? vaultRoot.trim() : "";
306
+ return fromVault;
307
+ }
308
+
309
+ // spexharness/src/verifier-seat.mjs
310
+ function grantsClose({ manifestFound, parseError = null, criteria = [] }) {
311
+ if (!manifestFound) {
312
+ return {
313
+ closeGranted: false,
314
+ reason: "no frozen manifest \u2014 NOT verified",
315
+ verifiedCount: 0,
316
+ failedCount: 0,
317
+ totalCount: 0
318
+ };
319
+ }
320
+ if (parseError) {
321
+ return {
322
+ closeGranted: false,
323
+ reason: `frozen manifest is unparseable (${parseError}) \u2014 NOT verified`,
324
+ verifiedCount: 0,
325
+ failedCount: 0,
326
+ totalCount: 0
327
+ };
328
+ }
329
+ const list = Array.isArray(criteria) ? criteria : [];
330
+ const failed = list.filter((c) => !c.passed);
331
+ const totalCount = list.length;
332
+ if (failed.length > 0) {
333
+ return {
334
+ closeGranted: false,
335
+ reason: `${failed.length} of ${totalCount} machine criteria failed`,
336
+ verifiedCount: totalCount - failed.length,
337
+ failedCount: failed.length,
338
+ totalCount
339
+ };
340
+ }
341
+ if (totalCount === 0) {
342
+ return {
343
+ closeGranted: false,
344
+ reason: "no machine-verifiable criteria \u2014 nothing was verified",
345
+ verifiedCount: 0,
346
+ failedCount: 0,
347
+ totalCount: 0
348
+ };
349
+ }
350
+ return {
351
+ closeGranted: true,
352
+ reason: `frozen manifest GREEN \u2014 ${totalCount} machine criteria verified`,
353
+ verifiedCount: totalCount,
354
+ failedCount: 0,
355
+ totalCount
356
+ };
357
+ }
9
358
 
10
359
  // spex-console/src/lib/cli/checks-manifest.ts
11
360
  var EXIT = {
@@ -31,11 +380,13 @@ function parseCheckRunArgs(argv) {
31
380
  let target;
32
381
  let json = false;
33
382
  let report = false;
383
+ let unattended = false;
34
384
  for (let i = 1; i < argv.length; i++) {
35
385
  const arg = argv[i];
36
386
  if (arg === "--help" || arg === "-h") return { help: true };
37
387
  else if (arg === "--json") json = true;
38
388
  else if (arg === "--report") report = true;
389
+ else if (arg === "--unattended") unattended = true;
39
390
  else if (arg === "--manifest" || arg.startsWith("--manifest=")) {
40
391
  const value = arg.includes("=") ? arg.slice("--manifest=".length) : argv[++i];
41
392
  if (value === void 0) {
@@ -53,7 +404,7 @@ function parseCheckRunArgs(argv) {
53
404
  if (!target) {
54
405
  return { error: { code: EXIT.USAGE, message: "run requires a spec id or manifest path" } };
55
406
  }
56
- return { args: { verb: "run", target, json, report } };
407
+ return { args: { verb: "run", target, json, report, unattended } };
57
408
  }
58
409
  function buildVerdict(report) {
59
410
  return {
@@ -73,12 +424,17 @@ var KIND_ALIASES = {
73
424
  function joinUnderRoot(root, rel) {
74
425
  if (rel === "" || rel === ".") return root;
75
426
  if (/^(?:[a-zA-Z]:[\\/]|[\\/])/.test(rel)) return rel;
76
- const sep = root.includes("\\") && !root.includes("/") ? "\\" : "/";
77
- return `${root.replace(/[\\/]+$/, "")}${sep}${rel.replace(/^[\\/]+/, "")}`;
427
+ const sep2 = root.includes("\\") && !root.includes("/") ? "\\" : "/";
428
+ return `${root.replace(/[\\/]+$/, "")}${sep2}${rel.replace(/^[\\/]+/, "")}`;
78
429
  }
79
430
  function resolveCheckCwds(check, roots) {
80
431
  if (typeof check.cwd === "string" && check.cwd.trim() !== "") {
81
- return [joinUnderRoot(roots.repoRoot, check.cwd.trim())];
432
+ const raw = check.cwd.trim();
433
+ const vaultMatch = /^vault:(.*)$/.exec(raw);
434
+ if (vaultMatch) {
435
+ return [joinUnderRoot(roots.vaultRoot ?? roots.repoRoot, vaultMatch[1].trim())];
436
+ }
437
+ return [joinUnderRoot(roots.repoRoot, raw)];
82
438
  }
83
439
  return roots.repoRoot === roots.consoleRoot ? [roots.repoRoot] : [roots.repoRoot, roots.consoleRoot];
84
440
  }
@@ -193,14 +549,17 @@ function parseChecksManifest(text) {
193
549
  }
194
550
  };
195
551
  }
196
- var ZERO_MATCH_RE = /Passed:\s*0\b|No test is available|No test files found|No test matches the given testcase filter/i;
197
- function detectZeroMatch(rawOutput) {
552
+ var ZERO_MATCH_RE = /Passed:\s*0\b|No test is available|No test files found|No test matches the given testcase filter|^\s*(?:ℹ|#)\s*(?:tests|pass)\s+0\s*$/i;
553
+ var NODE_TEST_FILE_PASS_RE = /^\s*(?:✔|ok\s+\d+\s+-)\s+\S*\.(?:test|spec)\.[cm]?[jt]sx?\s*(?:\(|$)/;
554
+ var NODE_TEST_FILTER_RE = /--test-(?:name|skip)-pattern/;
555
+ function detectZeroMatch(rawOutput, run) {
198
556
  if (!rawOutput) return null;
199
- const line = rawOutput.split(/\r?\n/).find((l) => ZERO_MATCH_RE.test(l));
557
+ const filtered = run !== void 0 && NODE_TEST_FILTER_RE.test(run);
558
+ const line = rawOutput.split(/\r?\n/).find((l) => ZERO_MATCH_RE.test(l) || filtered && NODE_TEST_FILE_PASS_RE.test(l));
200
559
  return line ? line.trim() : null;
201
560
  }
202
561
  function evaluateCheck(check, outcome) {
203
- if (detectZeroMatch(outcome.rawOutput) !== null) return false;
562
+ if (detectZeroMatch(outcome.rawOutput, check.run) !== null) return false;
204
563
  if (check.expect === "no-net-new") {
205
564
  return (outcome.errorCount ?? 0) <= check.baseline;
206
565
  }
@@ -364,6 +723,9 @@ var HELP = [
364
723
  " --report PATCH the verdict to the spec's active claim (the PCFL gate).",
365
724
  " Requires a spec-id target (identifies the claim). The verify",
366
725
  " exit code is unchanged; reporting is a side effect.",
726
+ " --unattended Declare that no human is present at this flip (pipeline",
727
+ " caller). Rides --report: the server's no-manifest",
728
+ " self-report fallback then fails closed instead of shipping.",
367
729
  " --json Emit a machine-readable JSON result (the gate's input shape).",
368
730
  " --help, -h Show this help.",
369
731
  "",
@@ -372,12 +734,356 @@ var HELP = [
372
734
  ].join("\n");
373
735
 
374
736
  // spex-console/src/lib/cli/control-shared.ts
375
- import { readFileSync } from "node:fs";
376
- import { homedir } from "node:os";
377
- import { join } from "node:path";
737
+ import path from "node:path";
738
+
739
+ // scripts/lib/hs-command.mjs
740
+ import { existsSync } from "node:fs";
741
+ import { delimiter, dirname, join } from "node:path";
742
+ import { fileURLToPath } from "node:url";
743
+ var HS_BIN_NAME = "hs";
744
+ var HS_DISPATCHER_PATH = (() => {
745
+ try {
746
+ const here = dirname(fileURLToPath(import.meta.url));
747
+ return join(dirname(dirname(here)), "bin", "hs.mjs");
748
+ } catch {
749
+ return null;
750
+ }
751
+ })();
752
+ function shellQuote(p) {
753
+ return /[\\\s]/.test(p) ? `"${p}"` : p;
754
+ }
755
+ var HS_DISPATCHER_PREFIX = HS_DISPATCHER_PATH ? `node ${shellQuote(HS_DISPATCHER_PATH)}` : HS_BIN_NAME;
756
+ function candidateNames(name, env) {
757
+ if (process.platform !== "win32") return [name];
758
+ const pathext = (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim().toLowerCase()).filter(Boolean);
759
+ const extensions = [.../* @__PURE__ */ new Set([...pathext, ".ps1"])];
760
+ return [name, ...extensions.map((ext) => name + ext)];
761
+ }
762
+ function findOnPath(name, { env = process.env, exists = existsSync } = {}) {
763
+ try {
764
+ const raw = env.PATH ?? env.Path ?? env.path ?? "";
765
+ for (const dir of String(raw).split(delimiter)) {
766
+ const trimmed = dir.trim().replace(/^"(.*)"$/, "$1");
767
+ if (!trimmed) continue;
768
+ for (const candidate of candidateNames(name, env)) {
769
+ const full = join(trimmed, candidate);
770
+ try {
771
+ if (exists(full)) return full;
772
+ } catch {
773
+ }
774
+ }
775
+ }
776
+ } catch {
777
+ }
778
+ return null;
779
+ }
780
+ var cached = null;
781
+ var HS_ON_PATH_OVERRIDE_ENV = "HEYSPEX_HS_ON_PATH";
782
+ function onPathOverride(env) {
783
+ const raw = String(env?.[HS_ON_PATH_OVERRIDE_ENV] ?? "");
784
+ if (raw === "1") return true;
785
+ if (raw === "0") return false;
786
+ return void 0;
787
+ }
788
+ function describeHsCommand({ env, exists, useCache = true } = {}) {
789
+ const injected = env !== void 0 || exists !== void 0;
790
+ const effectiveEnv = env ?? process.env;
791
+ const override = onPathOverride(effectiveEnv);
792
+ if (override !== void 0) {
793
+ return {
794
+ onPath: override,
795
+ prefix: override ? HS_BIN_NAME : HS_DISPATCHER_PREFIX,
796
+ resolvedPath: null,
797
+ dispatcherPath: HS_DISPATCHER_PATH
798
+ };
799
+ }
800
+ if (useCache && !injected && cached) return cached;
801
+ const resolvedPath = findOnPath(HS_BIN_NAME, {
802
+ env: env ?? process.env,
803
+ exists: exists ?? existsSync
804
+ });
805
+ const answer = {
806
+ onPath: resolvedPath !== null,
807
+ prefix: resolvedPath !== null ? HS_BIN_NAME : HS_DISPATCHER_PREFIX,
808
+ resolvedPath,
809
+ dispatcherPath: HS_DISPATCHER_PATH
810
+ };
811
+ if (useCache && !injected) cached = answer;
812
+ return answer;
813
+ }
814
+ function hsCommand(args = "", options = {}) {
815
+ let prefix = HS_DISPATCHER_PREFIX;
816
+ try {
817
+ prefix = describeHsCommand(options).prefix;
818
+ } catch {
819
+ }
820
+ const tail = String(args ?? "").trim();
821
+ return tail ? `${prefix} ${tail}` : prefix;
822
+ }
823
+
824
+ // vendor/heyspex-mcp/console-fetch.mjs
825
+ init_credentials();
826
+ import { readFileSync as readFileSync4 } from "node:fs";
827
+ import { homedir as homedir4 } from "node:os";
828
+ import { join as join5 } from "node:path";
829
+
830
+ // vendor/heyspex-mcp/session-owner.mjs
831
+ init_credentials();
832
+ import { readFileSync as readFileSync2 } from "node:fs";
833
+ import { homedir as homedir2 } from "node:os";
834
+ import { join as join3 } from "node:path";
835
+ function resolveSessionOwnerUncached({ env = process.env, homeDir = homedir2(), readFile } = {}) {
836
+ const fromEnv = typeof env?.HS_ACCOUNT_ID === "string" ? env.HS_ACCOUNT_ID.trim() : "";
837
+ if (fromEnv) return fromEnv;
838
+ return readCurrentAccountId({ homeDir, readFile });
839
+ }
840
+
841
+ // vendor/heyspex-mcp/vault-less-endpoints.mjs
842
+ var VAULT_LESS_ENDPOINTS = Object.freeze([
843
+ "* /api/mcp/session",
844
+ "* /api/mcp/pairing-codes",
845
+ "POST /api/vaults",
846
+ "GET /api/vaults",
847
+ "POST /api/workspaces",
848
+ "GET /api/workspaces",
849
+ "* /api/admin/dev-mode"
850
+ ]);
851
+ function pathnameOf(path3) {
852
+ const raw = String(path3 ?? "").split("?")[0].split("#")[0];
853
+ const noTrailing = raw.length > 1 ? raw.replace(/\/+$/, "") : raw;
854
+ return noTrailing || "/";
855
+ }
856
+ function isVaultLessEndpoint(method, path3) {
857
+ const m = String(method || "GET").toUpperCase();
858
+ const pathname = pathnameOf(path3);
859
+ return VAULT_LESS_ENDPOINTS.some((row) => {
860
+ const [rowMethod, rowPath] = row.split(" ");
861
+ return rowPath === pathname && (rowMethod === "*" || rowMethod === m);
862
+ });
863
+ }
864
+
865
+ // vendor/heyspex-mcp/vault-resolve.mjs
866
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
867
+ import { homedir as homedir3, tmpdir } from "node:os";
868
+ import { dirname as dirname3, join as join4, resolve as resolve3 } from "node:path";
869
+
870
+ // vendor/heyspex-mcp/console-fetch.mjs
871
+ var VAULT_NOT_REGISTERED = "VAULT_NOT_REGISTERED";
872
+ var VaultNotRegisteredError = class extends Error {
873
+ /**
874
+ * @param {string | null | undefined} repoRootAbs the root the resolver settled on
875
+ * @param {string | null | undefined} source the resolver tier that produced it
876
+ * @param {{ reason?: 'unregistered' | 'legacy-id', legacyId?: string | null }} [detail]
877
+ */
878
+ constructor(repoRootAbs, source, { reason = "unregistered", legacyId = null } = {}) {
879
+ const root = typeof repoRootAbs === "string" && repoRootAbs.trim() ? repoRootAbs.trim() : null;
880
+ const where = root ? ` (${root})` : "";
881
+ 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), `;
882
+ super(
883
+ 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.`
884
+ );
885
+ this.name = "VaultNotRegisteredError";
886
+ this.code = VAULT_NOT_REGISTERED;
887
+ this.reason = reason;
888
+ this.repoRootAbs = root;
889
+ this.source = typeof source === "string" ? source : null;
890
+ this.legacyId = legacyId;
891
+ }
892
+ };
893
+ 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;
894
+ function isCloudVaultId(value) {
895
+ return typeof value === "string" && CLOUD_VAULT_ID_RE.test(value.trim());
896
+ }
897
+ var MARKER_VAULT_ID_RE = /^vault_id:[ \t]*["']?([^"'\r\n#]+)/m;
898
+ function readMarkerVaultId(repoRootAbs, { readFile } = {}) {
899
+ if (typeof repoRootAbs !== "string" || !repoRootAbs.trim()) return null;
900
+ const read = readFile ?? ((p) => readFileSync4(p, "utf8"));
901
+ try {
902
+ const raw = read(join5(repoRootAbs, "heyspex.yaml"));
903
+ const value = String(raw ?? "").match(MARKER_VAULT_ID_RE)?.[1]?.trim() ?? "";
904
+ return value || null;
905
+ } catch {
906
+ return null;
907
+ }
908
+ }
909
+ function cloudVaultIdFor(resolved, { readMarkerVaultId: readMarker = readMarkerVaultId } = {}) {
910
+ const rowId = resolved.vaultId.trim();
911
+ if (isCloudVaultId(rowId)) return rowId;
912
+ const marker = readMarker(resolved.repoRootAbs);
913
+ if (isCloudVaultId(marker)) return marker.trim();
914
+ throw new VaultNotRegisteredError(resolved.repoRootAbs, resolved.source, { reason: "legacy-id", legacyId: rowId });
915
+ }
916
+ var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
917
+
918
+ // spex-console/src/lib/cli/control-shared.ts
919
+ init_credentials();
920
+
921
+ // spex-console/src/lib/vault-registry/registry-file.ts
922
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
923
+ import { homedir as homedir5 } from "node:os";
924
+ import { dirname as dirname4, join as join6, resolve as resolve4 } from "node:path";
925
+
926
+ // spex-console/src/lib/vault-registry/types.ts
927
+ var LEGACY_REGISTRY_ACCOUNT_ID = "legacy";
928
+ var VAULT_REGISTRY_SCHEMA_VERSION = 2;
929
+ function emptyVaultRegistry() {
930
+ return {
931
+ schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
932
+ current_account_id: null,
933
+ accounts: {},
934
+ workspaces: [],
935
+ vaults: []
936
+ };
937
+ }
938
+
939
+ // spex-console/src/lib/vault-registry/registry-file.ts
940
+ function vaultRegistryDir(opts = {}) {
941
+ return opts.registryDir ?? join6(homedir5(), ".heyspex");
942
+ }
943
+ function vaultRegistryFilePath(opts = {}) {
944
+ return join6(vaultRegistryDir(opts), "registry.json");
945
+ }
946
+ function isNonEmptyString(v) {
947
+ return typeof v === "string" && v.trim().length > 0;
948
+ }
949
+ function accountOf(o) {
950
+ return isNonEmptyString(o.account_id) ? o.account_id : LEGACY_REGISTRY_ACCOUNT_ID;
951
+ }
952
+ function normalizeWorkspace(raw) {
953
+ if (!raw || typeof raw !== "object") return null;
954
+ const o = raw;
955
+ if (!isNonEmptyString(o.id)) return null;
956
+ if (!isNonEmptyString(o.display_name)) return null;
957
+ if (!isNonEmptyString(o.created_at)) return null;
958
+ const record = {
959
+ id: o.id,
960
+ display_name: o.display_name,
961
+ created_at: o.created_at,
962
+ archived: o.archived === true,
963
+ account_id: accountOf(o)
964
+ };
965
+ if (isNonEmptyString(o.github_org)) record.github_org = o.github_org;
966
+ return record;
967
+ }
968
+ function normalizeVault(raw) {
969
+ if (!raw || typeof raw !== "object") return null;
970
+ const o = raw;
971
+ if (!isNonEmptyString(o.id)) return null;
972
+ if (!isNonEmptyString(o.workspace_id)) return null;
973
+ if (!isNonEmptyString(o.display_name)) return null;
974
+ if (!isNonEmptyString(o.repo_root_abs)) return null;
975
+ if (typeof o.schema_version !== "number") return null;
976
+ if (!isNonEmptyString(o.registered_at)) return null;
977
+ const record = {
978
+ id: o.id,
979
+ workspace_id: o.workspace_id,
980
+ display_name: o.display_name,
981
+ repo_root_abs: o.repo_root_abs,
982
+ schema_version: o.schema_version,
983
+ registered_at: o.registered_at,
984
+ archived: o.archived === true,
985
+ account_id: accountOf(o)
986
+ };
987
+ if (isNonEmptyString(o.github_remote)) record.github_remote = o.github_remote;
988
+ return record;
989
+ }
990
+ function normalizeStoredPointers(raw) {
991
+ const o = raw && typeof raw === "object" ? raw : {};
992
+ const out = {};
993
+ for (const field of ["active_vault_id", "active_workspace_id"]) {
994
+ const v = o[field];
995
+ if (v === null || isNonEmptyString(v)) out[field] = v;
996
+ }
997
+ return out;
998
+ }
999
+ function normalizePointers(raw) {
1000
+ const o = raw && typeof raw === "object" ? raw : {};
1001
+ return {
1002
+ active_vault_id: isNonEmptyString(o.active_vault_id) ? o.active_vault_id : null,
1003
+ active_workspace_id: isNonEmptyString(o.active_workspace_id) ? o.active_workspace_id : null
1004
+ };
1005
+ }
1006
+ function registryFileSchemaOf(json) {
1007
+ if (!json || typeof json !== "object" || Array.isArray(json)) return null;
1008
+ return json.schema_version === 2 ? 2 : 1;
1009
+ }
1010
+ function parseVaultRegistryFile(json) {
1011
+ const schema = registryFileSchemaOf(json);
1012
+ if (schema === null) return emptyVaultRegistry();
1013
+ const o = json;
1014
+ const workspacesRaw = Array.isArray(o.workspaces) ? o.workspaces : [];
1015
+ const workspaces = workspacesRaw.map(normalizeWorkspace).filter((w) => w !== null);
1016
+ const vaultsRaw = Array.isArray(o.vaults) ? o.vaults : [];
1017
+ const vaults = vaultsRaw.map(normalizeVault).filter((v) => v !== null);
1018
+ if (schema === 1) {
1019
+ const legacy = normalizePointers(o);
1020
+ const hasLegacyState = legacy.active_vault_id !== null || legacy.active_workspace_id !== null || workspaces.length > 0 || vaults.length > 0;
1021
+ return {
1022
+ schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
1023
+ current_account_id: hasLegacyState ? LEGACY_REGISTRY_ACCOUNT_ID : null,
1024
+ accounts: hasLegacyState ? { [LEGACY_REGISTRY_ACCOUNT_ID]: legacy } : {},
1025
+ workspaces,
1026
+ vaults
1027
+ };
1028
+ }
1029
+ const accounts = {};
1030
+ if (o.accounts && typeof o.accounts === "object" && !Array.isArray(o.accounts)) {
1031
+ for (const [id, pointers] of Object.entries(o.accounts)) {
1032
+ if (isNonEmptyString(id)) accounts[id] = normalizeStoredPointers(pointers);
1033
+ }
1034
+ }
1035
+ return {
1036
+ schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
1037
+ current_account_id: isNonEmptyString(o.current_account_id) ? o.current_account_id : null,
1038
+ accounts,
1039
+ workspaces,
1040
+ vaults
1041
+ };
1042
+ }
1043
+ function readRegistryJson(opts) {
1044
+ const filePath = vaultRegistryFilePath(opts);
1045
+ if (!existsSync3(filePath)) return { kind: "missing" };
1046
+ try {
1047
+ const raw = readFileSync5(filePath, "utf8");
1048
+ const json = JSON.parse(raw);
1049
+ if (registryFileSchemaOf(json) === null) return { kind: "unreadable" };
1050
+ return { kind: "ok", raw, json };
1051
+ } catch {
1052
+ return { kind: "unreadable" };
1053
+ }
1054
+ }
1055
+ function inspectVaultRegistry(opts = {}) {
1056
+ const read = readRegistryJson(opts);
1057
+ if (read.kind !== "ok") {
1058
+ return {
1059
+ registry: emptyVaultRegistry(),
1060
+ file_schema_version: null,
1061
+ registry_unreadable: read.kind === "unreadable"
1062
+ };
1063
+ }
1064
+ return {
1065
+ registry: parseVaultRegistryFile(read.json),
1066
+ file_schema_version: registryFileSchemaOf(read.json),
1067
+ registry_unreadable: false
1068
+ };
1069
+ }
1070
+ function loadVaultRegistry(opts = {}) {
1071
+ return inspectVaultRegistry(opts).registry;
1072
+ }
1073
+
1074
+ // spex-console/src/lib/cli/control-shared.ts
378
1075
  var EXIT2 = {
379
1076
  /** Success. */
380
1077
  OK: 0,
1078
+ /**
1079
+ * A read-only CHECK ran correctly and its answer is "no" — `hs connect
1080
+ * --check` on an unconnected repo. Distinct from {@link EXIT.USAGE} so a
1081
+ * script can tell "you typed the command wrong" from "the command worked,
1082
+ * the repo is not connected". Both were `1` until 2026-09-18, which made
1083
+ * `--check` unusable in a conditional: every caller had to parse stdout to
1084
+ * find out which of the two had happened.
1085
+ */
1086
+ CHECK_FAILED: 2,
381
1087
  /** Usage error — unknown subcommand/flag, or a missing required argument. */
382
1088
  USAGE: 1,
383
1089
  /** Claim race lost — the spec is already claimed by another agent (409). */
@@ -399,31 +1105,94 @@ var EXIT2 = {
399
1105
  * from `USAGE`/`INVALID_ARG` — this is an authorization refusal, not a
400
1106
  * malformed command.
401
1107
  */
402
- FORBIDDEN: 15
1108
+ FORBIDDEN: 15,
1109
+ /**
1110
+ * A CLOUD write refused because no account id could be resolved (no
1111
+ * `HS_ACCOUNT_ID` env var, no `account_id:` in `~/.heyspex/config.yaml`) --
1112
+ * `resolveCloudAccountIdOrRefuse` in `vault-workspace-shared.ts`
1113
+ * ([[HS-YU1S-UAGY-TASK-cloud-writes-refuse-without-a]]). Distinct from
1114
+ * `FORBIDDEN` -- this is "not signed in at all", not "signed in but not
1115
+ * authorized for this override".
1116
+ */
1117
+ SIGN_IN_REQUIRED: 16,
1118
+ /**
1119
+ * Signed in, but the account a repo would be attached to was not confirmed:
1120
+ * a non-interactive install/connect with no matching `--account` / `--yes`,
1121
+ * an `account_id` that disagrees with the bearer's user, or the human
1122
+ * aborting the identity prompt (`install-identity.ts`,
1123
+ * [[HS-HAQI-KODY-DIAG-hs-install-and-hs-connect-bind-a-new]]).
1124
+ */
1125
+ IDENTITY_UNCONFIRMED: 17,
1126
+ /**
1127
+ * The repo this verb runs in is not registered on this machine (no
1128
+ * `~/.heyspex/registry.json` row for its root, or a legacy slug row with no
1129
+ * cloud `vault_id` in its heyspex.yaml), so the call was refused LOCALLY —
1130
+ * nothing was sent. Names `hs connect`
1131
+ * ([[HS-MVG4-WW9R-TASK-mcp-and-hs-name-account-vault-on-every]]).
1132
+ */
1133
+ VAULT_NOT_REGISTERED: 18,
1134
+ /**
1135
+ * The local half of a setup verb (`hs connect`, `hs vault *`, `hs workspace
1136
+ * *`) was written but the CLOUD half did not land -- the console refused
1137
+ * it or was never reached. The verb's receipt names the failed half and the
1138
+ * exact command to re-run
1139
+ * ([[HS-STCY-WW9R-TASK-add-a-shared-receipt-that-reads-back]], invariant 6).
1140
+ * Distinct from {@link EXIT.WRITE_FAILED} so a script can tell "re-run to
1141
+ * sync the cloud" from a generic server failure.
1142
+ */
1143
+ CLOUD_WRITE_FAILED: 19,
1144
+ /**
1145
+ * The repo's committed `heyspex.yaml` records `account: <A>` but the machine
1146
+ * is signed in as another account, so `hs connect` refused and wrote
1147
+ * nothing (marker, registry, cloud). Repair: `hs login --switch-account <A>`
1148
+ * ([[HS-VWUD-WW9R-TASK-record-the-connecting-account-in]], invariant 4).
1149
+ */
1150
+ ACCOUNT_MISMATCH: 20,
1151
+ /**
1152
+ * Both sides of a setup verb's receipt were read back and still DISAGREE
1153
+ * after a short bounded re-read of the cloud side (a briefly stale read is
1154
+ * absorbed first). The write may well have landed; what the verb cannot
1155
+ * claim is that this machine and the console now say the same thing. The
1156
+ * receipt prints the diffs and the command to re-run
1157
+ * ([[HS-FKSR-WW9R-TASK-make-one-hs-connect-run-create-a-cloud]], invariant 6).
1158
+ * Distinct from {@link EXIT.CLOUD_WRITE_FAILED} (a write that did not land).
1159
+ */
1160
+ RECEIPT_DISAGREE: 21
403
1161
  };
404
- function readPairedToken() {
405
- try {
406
- const raw = readFileSync(join(homedir(), ".heyspex", "credentials.yaml"), "utf-8");
407
- const match = raw.match(/\b(hsmcp_[A-Za-z0-9_-]+|hscmp_[A-Za-z0-9_-]+)\b/);
408
- return match?.[1];
409
- } catch {
410
- return void 0;
411
- }
1162
+ var VAULT_ID_HEADER = "x-heyspex-vault-id";
1163
+ var ACCOUNT_ID_HEADER = "x-heyspex-account-id";
1164
+ function readPairedBearer(opts = {}) {
1165
+ return readBearer(opts);
412
1166
  }
413
- function bearerHeaderFor(consoleUrl) {
1167
+ function readPairedToken(opts = {}) {
1168
+ return readPairedBearer(opts).token ?? void 0;
1169
+ }
1170
+ function bearerHeaderFor(consoleUrl, opts = {}) {
414
1171
  try {
415
1172
  const host = new URL(consoleUrl).hostname;
416
1173
  if (host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === "::1") {
417
1174
  return {};
418
1175
  }
419
- const token = readPairedToken();
1176
+ const token = readPairedToken(opts);
420
1177
  return token ? { authorization: `Bearer ${token}` } : {};
421
1178
  } catch {
422
1179
  return {};
423
1180
  }
424
1181
  }
1182
+ function scopeHeadersFor(scope, method, path3) {
1183
+ if (scope === "none") {
1184
+ if (!isVaultLessEndpoint(method, path3)) {
1185
+ throw new Error(
1186
+ `controlRequest: scope "none" is only allowed for the vault-less endpoints (${VAULT_LESS_ENDPOINTS.join(", ")}); ${method.toUpperCase()} ${path3} is vault-scoped and must name { accountId, vaultId }.`
1187
+ );
1188
+ }
1189
+ return {};
1190
+ }
1191
+ return { [VAULT_ID_HEADER]: scope.vaultId, [ACCOUNT_ID_HEADER]: scope.accountId };
1192
+ }
425
1193
  async function controlRequest(deps, req) {
426
1194
  const url = `${deps.consoleUrl}${req.path}`;
1195
+ const scopeHeaders = scopeHeadersFor(deps.scope, req.method, req.path);
427
1196
  let res;
428
1197
  try {
429
1198
  res = await deps.fetchImpl(url, {
@@ -434,7 +1203,10 @@ async function controlRequest(deps, req) {
434
1203
  // authenticates every control call; loopback never asked, so the header
435
1204
  // is attached only when the target is non-loopback and a paired token
436
1205
  // exists. Same `hsmcp_` bearer the MCP server presents.
437
- ...bearerHeaderFor(deps.consoleUrl)
1206
+ ...bearerHeaderFor(deps.consoleUrl),
1207
+ // Invariant 2: name the vault and the account on every call, or the
1208
+ // console serves whatever its leftover pointer says.
1209
+ ...scopeHeaders
438
1210
  },
439
1211
  ...req.body !== void 0 ? { body: JSON.stringify(req.body) } : {}
440
1212
  });
@@ -479,42 +1251,187 @@ async function controlRequest(deps, req) {
479
1251
  }
480
1252
  return { ok: true, status: res.status, body: parsed };
481
1253
  }
482
-
483
- // spex-console/src/lib/cli/defer.ts
484
- import { readFileSync as readFileSync3 } from "node:fs";
485
- import os from "node:os";
486
- import path from "node:path";
1254
+ function normalizeRoot(p) {
1255
+ return path.resolve(p).replace(/[\\/]+/g, "/").replace(/\/+$/, "").toLowerCase();
1256
+ }
1257
+ function resolveControlScope(input) {
1258
+ const target = normalizeRoot(input.vaultRoot);
1259
+ const row = input.vaults.find((v) => normalizeRoot(v.repo_root_abs) === target) ?? null;
1260
+ if (!row) {
1261
+ return {
1262
+ ok: false,
1263
+ error: {
1264
+ code: EXIT2.VAULT_NOT_REGISTERED,
1265
+ 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.`,
1266
+ payload: {
1267
+ ok: false,
1268
+ error: "vault_not_registered",
1269
+ reason: "unregistered",
1270
+ vault_root: input.vaultRoot
1271
+ },
1272
+ isError: true
1273
+ }
1274
+ };
1275
+ }
1276
+ let vaultId;
1277
+ try {
1278
+ vaultId = cloudVaultIdFor(
1279
+ { vaultId: row.id, repoRootAbs: row.repo_root_abs, source: "registry" },
1280
+ input.readMarkerVaultId ? { readMarkerVaultId: input.readMarkerVaultId } : {}
1281
+ );
1282
+ } catch (err) {
1283
+ if (!(err instanceof VaultNotRegisteredError)) throw err;
1284
+ const refused = err;
1285
+ return {
1286
+ ok: false,
1287
+ error: {
1288
+ code: EXIT2.VAULT_NOT_REGISTERED,
1289
+ message: refused.message,
1290
+ payload: {
1291
+ ok: false,
1292
+ error: "vault_not_registered",
1293
+ reason: refused.reason ?? "legacy-id",
1294
+ vault_root: input.vaultRoot,
1295
+ legacy_id: refused.legacyId ?? row.id
1296
+ },
1297
+ isError: true
1298
+ }
1299
+ };
1300
+ }
1301
+ const accountId = input.accountId?.trim() ?? "";
1302
+ if (!accountId) {
1303
+ return {
1304
+ ok: false,
1305
+ error: {
1306
+ code: EXIT2.SIGN_IN_REQUIRED,
1307
+ 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.`,
1308
+ payload: { ok: false, error: "sign_in_required", vault_root: input.vaultRoot },
1309
+ isError: true
1310
+ }
1311
+ };
1312
+ }
1313
+ return { ok: true, scope: { accountId, vaultId } };
1314
+ }
1315
+ function resolveControlScopeFromMachine(vaultRoot, opts = {}) {
1316
+ const registry = loadVaultRegistry(opts.registry);
1317
+ const accountId = resolveSessionOwnerUncached({ env: opts.env, homeDir: opts.homeDir });
1318
+ return resolveControlScope({ vaultRoot, accountId, vaults: registry.vaults });
1319
+ }
487
1320
 
488
1321
  // scripts/lib/console-url-resolver.mjs
489
- import { readFileSync as readFileSync2 } from "node:fs";
490
- import { homedir as homedir2 } from "node:os";
491
- import { join as join2 } from "node:path";
492
- var DEFAULT_CONSOLE_URL = "https://app.heyspex.com";
493
-
494
- // spex-console/src/lib/cli/defer.ts
495
- function resolveSpexConsoleUrl(vaultRoot, readDiscovery, opts = {}) {
496
- const env = opts.env ?? process.env;
497
- const raw = (env.HEYSPEX_CONSOLE_URL ?? env.HEYSPEX_COCKPIT_URL)?.trim();
498
- if (raw && raw.length > 0) return raw.replace(/\/+$/, "");
1322
+ init_dev_mode();
1323
+ import { readFileSync as readFileSync7 } from "node:fs";
1324
+ import { homedir as homedir7 } from "node:os";
1325
+ import { join as join8 } from "node:path";
1326
+ var DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
1327
+ function trimTrailingSlash(url) {
1328
+ return url.replace(/\/+$/, "");
1329
+ }
1330
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
1331
+ function isLoopbackConsoleUrl(url) {
1332
+ if (typeof url !== "string" || !url) return false;
1333
+ try {
1334
+ return LOOPBACK_HOSTS.has(new URL(url).hostname.toLowerCase());
1335
+ } catch {
1336
+ return false;
1337
+ }
1338
+ }
1339
+ var staleLoopbackCredentialsWarned = false;
1340
+ function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
1341
+ if (staleLoopbackCredentialsWarned) return;
1342
+ staleLoopbackCredentialsWarned = true;
1343
+ try {
1344
+ log(
1345
+ `[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).
1346
+ `
1347
+ );
1348
+ } catch {
1349
+ }
1350
+ }
1351
+ function readCredentialsConsoleUrl({ homeDir = homedir7(), readFile = defaultReadFile2 } = {}) {
499
1352
  try {
500
- const homeDir = opts.homeDir ?? os.homedir();
501
- const readFile = opts.readFile ?? ((p) => readFileSync3(p, "utf-8"));
502
- const credentials = readFile(path.join(homeDir, ".heyspex", "credentials.yaml"));
503
- const match = credentials.match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
1353
+ const raw = readFile(join8(homeDir, ".heyspex", "credentials.yaml"));
1354
+ const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
504
1355
  const value = match?.[1]?.trim();
505
- if (value && value.length > 0) return value.replace(/\/+$/, "");
1356
+ return value && value.length > 0 ? trimTrailingSlash(value) : null;
506
1357
  } catch {
1358
+ return null;
507
1359
  }
508
- for (const name of ["console-url", "cockpit-url"]) {
509
- try {
510
- const discovery = readDiscovery(`${vaultRoot}/.heyspex/${name}`);
511
- if (discovery && discovery.trim().length > 0) {
512
- return discovery.trim().replace(/\/+$/, "");
513
- }
514
- } catch {
1360
+ }
1361
+ function defaultReadFile2(p) {
1362
+ return readFileSync7(p, "utf8");
1363
+ }
1364
+ function readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery = defaultReadFile2 } = {}) {
1365
+ if (!repoRoot) return null;
1366
+ try {
1367
+ const discovery = String(readDiscovery(join8(repoRoot, ".heyspex", "console-url")) ?? "").trim();
1368
+ if (discovery.length > 0) return trimTrailingSlash(discovery);
1369
+ } catch {
1370
+ }
1371
+ return null;
1372
+ }
1373
+ var ConsoleUrlUnresolvedError = class extends Error {
1374
+ constructor() {
1375
+ super(
1376
+ "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."
1377
+ );
1378
+ this.name = "ConsoleUrlUnresolvedError";
1379
+ }
1380
+ };
1381
+ function resolveModeEnvelope({ env, homeDir }) {
1382
+ try {
1383
+ const resolved = resolveDevMode({ env, homeDir });
1384
+ return {
1385
+ reliance: resolved.reliance === "cloud" ? "cloud" : "local",
1386
+ consoleUrl: resolved.consoleUrl || DEFAULT_CONSOLE_URL
1387
+ };
1388
+ } catch {
1389
+ return { reliance: "local", consoleUrl: DEFAULT_CONSOLE_URL };
1390
+ }
1391
+ }
1392
+ function resolveConsoleUrl({
1393
+ repoRoot,
1394
+ env = process.env,
1395
+ homeDir = homedir7(),
1396
+ strict = false,
1397
+ devMode,
1398
+ log = (line) => process.stderr.write(line),
1399
+ readFile = defaultReadFile2,
1400
+ readDiscovery = defaultReadFile2
1401
+ } = {}) {
1402
+ const envUrl = env.HEYSPEX_CONSOLE_URL?.trim();
1403
+ if (envUrl) return trimTrailingSlash(envUrl);
1404
+ const mode = devMode ?? resolveModeEnvelope({ env, homeDir });
1405
+ const cloud = mode.reliance === "cloud";
1406
+ const derivedUrl = trimTrailingSlash(
1407
+ cloud && isLoopbackConsoleUrl(mode.consoleUrl) ? CLOUD_CONSOLE_URL : mode.consoleUrl || DEFAULT_CONSOLE_URL
1408
+ );
1409
+ const credentialsUrl = readCredentialsConsoleUrl({ homeDir, readFile });
1410
+ if (credentialsUrl) {
1411
+ if (cloud && isLoopbackConsoleUrl(credentialsUrl)) {
1412
+ warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log });
1413
+ } else {
1414
+ return credentialsUrl;
515
1415
  }
516
1416
  }
517
- return DEFAULT_CONSOLE_URL;
1417
+ if (!cloud) {
1418
+ const discoveryUrl = readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery });
1419
+ if (discoveryUrl) return discoveryUrl;
1420
+ if (strict) throw new ConsoleUrlUnresolvedError();
1421
+ }
1422
+ return derivedUrl;
1423
+ }
1424
+
1425
+ // spex-console/src/lib/cli/defer.ts
1426
+ function resolveSpexConsoleUrl(vaultRoot, readDiscovery, opts = {}) {
1427
+ return resolveConsoleUrl({
1428
+ repoRoot: vaultRoot,
1429
+ env: opts.env,
1430
+ homeDir: opts.homeDir,
1431
+ readFile: opts.readFile,
1432
+ readDiscovery,
1433
+ log: opts.log
1434
+ });
518
1435
  }
519
1436
  var HELP2 = [
520
1437
  "Usage:",
@@ -538,16 +1455,44 @@ var HELP2 = [
538
1455
  "Exit codes: 0 ok \xB7 1 usage \xB7 10 not-found \xB7 11 bad-arg \xB7 12 console-unreachable \xB7 13 write-failed"
539
1456
  ].join("\n");
540
1457
 
1458
+ // spex-console/bin/run-entry.ts
1459
+ import process2 from "node:process";
1460
+ var BinExit = class extends Error {
1461
+ code;
1462
+ constructor(code) {
1463
+ super(`exit ${code}`);
1464
+ this.name = "BinExit";
1465
+ this.code = code;
1466
+ }
1467
+ };
1468
+ function runEntry(name, main2, options = {}) {
1469
+ const { fallbackCode = 1, unhandledLabel = "unhandled" } = options;
1470
+ void (async () => {
1471
+ try {
1472
+ await main2();
1473
+ } catch (err) {
1474
+ if (err instanceof BinExit) {
1475
+ process2.exitCode = err.code;
1476
+ return;
1477
+ }
1478
+ const message = err instanceof Error ? err.message : String(err);
1479
+ process2.stderr.write(`${name}: ${unhandledLabel}: ${message}
1480
+ `);
1481
+ process2.exitCode = fallbackCode;
1482
+ }
1483
+ })();
1484
+ }
1485
+
541
1486
  // spex-console/bin/hs-check.ts
542
1487
  function manifestForSpecId(vaultRoot, specId) {
543
1488
  return path2.join(vaultRoot, "specs", "checks", `${specId}.checks.json`);
544
1489
  }
545
1490
  function resolveManifestPath(target, vaultRoot, invokedCwd) {
546
1491
  const literal = path2.resolve(invokedCwd, target);
547
- if (existsSync(literal)) return literal;
1492
+ if (existsSync4(literal)) return literal;
548
1493
  if (target.endsWith(".json")) return null;
549
1494
  const byId = manifestForSpecId(vaultRoot, target);
550
- return existsSync(byId) ? byId : null;
1495
+ return existsSync4(byId) ? byId : null;
551
1496
  }
552
1497
  function countTscErrors(output) {
553
1498
  const matches = output.match(/error TS\d+/g);
@@ -557,31 +1502,31 @@ var cachedPosixShell;
557
1502
  function resolveWindowsPosixShell() {
558
1503
  if (cachedPosixShell !== void 0) return cachedPosixShell;
559
1504
  const candidates = [
560
- process2.env.HEYSPEX_CHECK_SHELL,
1505
+ process3.env.HEYSPEX_CHECK_SHELL,
561
1506
  "C:\\Program Files\\Git\\bin\\bash.exe"
562
1507
  ].filter((c) => typeof c === "string" && c.length > 0);
563
1508
  for (const candidate of candidates) {
564
- if (existsSync(candidate)) return cachedPosixShell = candidate;
1509
+ if (existsSync4(candidate)) return cachedPosixShell = candidate;
565
1510
  }
566
1511
  const where = spawnSync("where", ["bash"], { encoding: "utf8", windowsHide: true });
567
1512
  if (where.status === 0 && where.stdout) {
568
1513
  const hit = where.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).find((line) => !/\\system32\\/i.test(line));
569
- if (hit && existsSync(hit)) return cachedPosixShell = hit;
1514
+ if (hit && existsSync4(hit)) return cachedPosixShell = hit;
570
1515
  }
571
1516
  return cachedPosixShell = null;
572
1517
  }
573
1518
  var MISSING_BINARY_RE = /command not found|is not recognized as an internal or external command/i;
574
1519
  var warnedCmdFallback = false;
575
1520
  function runCheckInDirectory(check, cwd) {
576
- return new Promise((resolve) => {
1521
+ return new Promise((resolve5) => {
577
1522
  const started = Date.now();
578
1523
  const needsErrorCount = check.expect === "no-net-new";
579
1524
  const needsStdout = check.expect === "empty-output";
580
1525
  const stdio = ["ignore", "pipe", "pipe"];
581
- const posixShell = process2.platform === "win32" ? resolveWindowsPosixShell() : null;
582
- if (process2.platform === "win32" && !posixShell && !warnedCmdFallback) {
1526
+ const posixShell = process3.platform === "win32" ? resolveWindowsPosixShell() : null;
1527
+ if (process3.platform === "win32" && !posixShell && !warnedCmdFallback) {
583
1528
  warnedCmdFallback = true;
584
- process2.stderr.write(
1529
+ process3.stderr.write(
585
1530
  "hs check: WARNING \u2014 no POSIX shell found (git-bash missing; set HEYSPEX_CHECK_SHELL to a bash.exe).\n Falling back to cmd.exe: POSIX-authored checks (test/grep/find/globs) will MISREPORT \u2014 false-fails, and inverted checks can false-PASS (HS-FCW8).\n"
586
1531
  );
587
1532
  }
@@ -596,7 +1541,7 @@ function runCheckInDirectory(check, cwd) {
596
1541
  captured += chunk.toString();
597
1542
  });
598
1543
  child.on("error", (err) => {
599
- resolve({ exitCode: -1, durationMs: Date.now() - started, message: err.message });
1544
+ resolve5({ exitCode: -1, durationMs: Date.now() - started, message: err.message });
600
1545
  });
601
1546
  child.on("close", (code) => {
602
1547
  const durationMs = Date.now() - started;
@@ -615,15 +1560,16 @@ function runCheckInDirectory(check, cwd) {
615
1560
  const trimmed = captured.trim();
616
1561
  outcome.message = trimmed.length > 0 ? trimmed.split("\n").slice(-3).join(" ") : `command exited ${exitCode}`;
617
1562
  }
618
- resolve(outcome);
1563
+ resolve5(outcome);
619
1564
  });
620
1565
  });
621
1566
  }
622
- function makeRunCheck(vaultRoot) {
1567
+ function makeRunCheck(vaultRoot, codeRoot) {
623
1568
  return async (check) => {
624
1569
  const cwds = resolveCheckCwds(check, {
625
- repoRoot: vaultRoot,
626
- consoleRoot: path2.join(vaultRoot, "spex-console")
1570
+ repoRoot: codeRoot,
1571
+ consoleRoot: path2.join(codeRoot, "spex-console"),
1572
+ vaultRoot
627
1573
  });
628
1574
  let lastFailure = null;
629
1575
  for (const cwd of cwds) {
@@ -643,105 +1589,141 @@ function makeRunCheck(vaultRoot) {
643
1589
  };
644
1590
  }
645
1591
  async function main() {
646
- const vaultRoot = process2.env.HS_VAULT_ROOT;
1592
+ const vaultRoot = process3.env.HS_VAULT_ROOT;
647
1593
  if (!vaultRoot) {
648
- process2.stderr.write(
1594
+ process3.stderr.write(
649
1595
  "hs check: HS_VAULT_ROOT is not set (the dispatcher must resolve and pass it).\n"
650
1596
  );
651
- process2.exit(EXIT.USAGE);
1597
+ process3.exitCode = EXIT.USAGE;
1598
+ return;
1599
+ }
1600
+ if (process3.argv[3] === "judge") {
1601
+ const pkgRoot = resolveCodeRoot({ vaultRoot });
1602
+ const judgeScript = path2.join(pkgRoot, "dev-factory", "judge.mjs");
1603
+ if (!existsSync4(judgeScript)) {
1604
+ process3.stderr.write(
1605
+ `hs check judge: script not found at ${judgeScript} (source checkout only \u2014 not available in a published install).
1606
+ `
1607
+ );
1608
+ process3.exitCode = EXIT.NOT_FOUND;
1609
+ return;
1610
+ }
1611
+ const forwardArgs = process3.argv.slice(4);
1612
+ const child = spawnSync(process3.execPath, [judgeScript, ...forwardArgs], {
1613
+ cwd: pkgRoot,
1614
+ stdio: "inherit",
1615
+ windowsHide: true
1616
+ });
1617
+ process3.exitCode = child.status ?? 1;
1618
+ return;
652
1619
  }
653
- const parsed = parseCheckRunArgs(process2.argv.slice(3));
1620
+ const parsed = parseCheckRunArgs(process3.argv.slice(3));
654
1621
  if (parsed.help) {
655
- process2.stdout.write(`${HELP}
1622
+ process3.stdout.write(`${HELP}
656
1623
  `);
657
- process2.exit(EXIT.OK);
1624
+ process3.exitCode = EXIT.OK;
1625
+ return;
658
1626
  }
659
1627
  if (parsed.error) {
660
- process2.stderr.write(`hs check: ${parsed.error.message}
1628
+ process3.stderr.write(`hs check: ${parsed.error.message}
661
1629
  `);
662
- process2.exit(parsed.error.code);
1630
+ process3.exitCode = parsed.error.code;
1631
+ return;
663
1632
  }
664
1633
  const args = parsed.args;
665
- const invokedCwd = process2.env.HS_INVOKED_CWD?.trim() || process2.cwd();
1634
+ const invokedCwd = process3.env.HS_INVOKED_CWD?.trim() || process3.cwd();
666
1635
  const manifestPath = resolveManifestPath(args.target, vaultRoot, invokedCwd);
667
1636
  if (!manifestPath) {
668
- process2.stderr.write(
1637
+ process3.stderr.write(
669
1638
  `hs check: no manifest for "${args.target}".
670
1639
  Pass a manifest path, or create ${path2.join("specs", "checks", `${args.target}.checks.json`)} under the vault root.
671
1640
  `
672
1641
  );
673
- process2.exit(EXIT.NOT_FOUND);
1642
+ process3.exitCode = EXIT.NOT_FOUND;
1643
+ return;
674
1644
  }
675
- const { manifest, error } = parseChecksManifest(readFileSync4(manifestPath, "utf-8"));
1645
+ const { manifest, error } = parseChecksManifest(readFileSync8(manifestPath, "utf-8"));
676
1646
  if (error) {
677
- process2.stderr.write(`hs check: ${manifestPath}: ${error}
1647
+ process3.stderr.write(`hs check: ${manifestPath}: ${error}
678
1648
  `);
679
- process2.exit(EXIT.INVALID_MANIFEST);
1649
+ process3.exitCode = EXIT.INVALID_MANIFEST;
1650
+ return;
680
1651
  }
681
- const report = await runChecksManifest(manifest, { runCheck: makeRunCheck(vaultRoot) });
682
- const sink = report.passed ? process2.stdout : process2.stderr;
1652
+ const codeRoot = resolveCodeRoot({ vaultRoot });
1653
+ const report = await runChecksManifest(manifest, {
1654
+ runCheck: makeRunCheck(vaultRoot, codeRoot)
1655
+ });
1656
+ const verifierSeatDecision = grantsClose({
1657
+ manifestFound: true,
1658
+ parseError: null,
1659
+ criteria: report.criteria.filter((c) => c.method !== "human").map((c) => ({ index: c.ac, passed: c.verdict === "pass" }))
1660
+ });
1661
+ const sink = report.passed ? process3.stdout : process3.stderr;
683
1662
  if (args.json) {
684
- sink.write(`${JSON.stringify(reportToJson(report))}
685
- `);
1663
+ sink.write(
1664
+ `${JSON.stringify({ ...reportToJson(report), verifierSeat: verifierSeatDecision })}
1665
+ `
1666
+ );
686
1667
  } else {
687
1668
  sink.write(`${formatReport(report)}
688
1669
  `);
689
1670
  }
690
1671
  if (args.report) {
691
- await reportVerdict(args.target, report, vaultRoot);
1672
+ await reportVerdict(args.target, report, vaultRoot, args.unattended);
692
1673
  }
693
- process2.exit(reportExitCode(report));
1674
+ process3.exitCode = reportExitCode(report);
694
1675
  }
695
- async function reportVerdict(target, report, vaultRoot) {
696
- if (existsSync(target) || target.endsWith(".json")) {
697
- process2.stderr.write(
1676
+ async function reportVerdict(target, report, vaultRoot, unattended = false) {
1677
+ if (existsSync4(target) || target.endsWith(".json")) {
1678
+ process3.stderr.write(
698
1679
  "hs check: --report needs a spec-id target (to identify the claim), not a manifest path.\n"
699
1680
  );
700
1681
  return;
701
1682
  }
702
1683
  const consoleUrl = resolveSpexConsoleUrl(vaultRoot, (p) => {
703
1684
  try {
704
- return readFileSync4(p, "utf-8");
1685
+ return readFileSync8(p, "utf-8");
705
1686
  } catch {
706
1687
  return null;
707
1688
  }
708
1689
  });
709
1690
  const verdict = buildVerdict(report);
1691
+ const scoped = resolveControlScopeFromMachine(vaultRoot);
1692
+ if (!scoped.ok) {
1693
+ process3.stderr.write(`hs check: verdict not reported \u2014 ${scoped.error.message}
1694
+ `);
1695
+ return;
1696
+ }
710
1697
  const res = await controlRequest(
711
- { fetchImpl: fetch, consoleUrl },
1698
+ { fetchImpl: fetch, consoleUrl, scope: scoped.scope },
712
1699
  {
713
1700
  method: "PATCH",
714
1701
  path: `/api/specs/${encodeURIComponent(target)}/claim/status`,
715
- body: { status: "working", verification: verdict }
1702
+ body: { status: "working", verification: verdict, unattended }
716
1703
  }
717
1704
  );
718
1705
  if (!res.ok) {
719
- process2.stderr.write(`hs check: verdict not reported \u2014 ${res.error.message}
1706
+ process3.stderr.write(`hs check: verdict not reported \u2014 ${res.error.message}
720
1707
  `);
721
1708
  return;
722
1709
  }
723
1710
  if (res.status === 404) {
724
- process2.stderr.write(
1711
+ process3.stderr.write(
725
1712
  `hs check: no active claim for ${target} \u2014 verdict not reported (claim it first).
726
1713
  `
727
1714
  );
728
1715
  return;
729
1716
  }
730
1717
  if (res.status !== 200) {
731
- process2.stderr.write(`hs check: verdict not reported \u2014 console returned ${res.status}.
1718
+ process3.stderr.write(`hs check: verdict not reported \u2014 console returned ${res.status}.
732
1719
  `);
733
1720
  return;
734
1721
  }
735
1722
  const flipped = res.body?.autoShip;
736
1723
  const note = flipped && flipped.flipped !== false ? ` \u2192 gate flipped to ${flipped.effective_status}` : "";
737
- process2.stderr.write(
1724
+ process3.stderr.write(
738
1725
  `hs check: reported verdict (manifestPassed=${verdict.manifestPassed})${note}
739
1726
  `
740
1727
  );
741
1728
  }
742
- main().catch((err) => {
743
- const message = err instanceof Error ? err.message : String(err);
744
- process2.stderr.write(`hs check: unhandled: ${message}
745
- `);
746
- process2.exit(EXIT.RUNNER_MISSING);
747
- });
1729
+ runEntry("hs check", main, { fallbackCode: EXIT.RUNNER_MISSING });