heyspex 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (352) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/README.md +21 -0
  3. package/bin/hs.mjs +1089 -104
  4. package/bin/spexcode.mjs +262 -0
  5. package/dev-factory/engine/worktree.mjs +740 -0
  6. package/dev-factory/lib/code-repo-root.mjs +67 -0
  7. package/dev-factory/lib/context-budget.mjs +84 -0
  8. package/dev-factory/lib/epic-detect.mjs +190 -0
  9. package/dev-factory/lib/fleet-queue-file.mjs +846 -0
  10. package/dev-factory/lib/fleet-queue.mjs +426 -0
  11. package/dev-factory/lib/fleet-state.mjs +14 -18
  12. package/dev-factory/lib/model-catalog.mjs +338 -0
  13. package/dev-factory/lib/seat-context.mjs +199 -0
  14. package/dev-factory/lib/spec-io.mjs +93 -0
  15. package/dev-factory/lib/sufficiency-score.mjs +8 -2
  16. package/dev-factory/lib/worktree-allocator.mjs +744 -0
  17. package/dev-factory/model-catalog.json +222 -0
  18. package/dist/cli/hs-check.mjs +1127 -103
  19. package/dist/cli/hs-claim.mjs +981 -63
  20. package/dist/cli/hs-connect.mjs +10059 -1729
  21. package/dist/cli/hs-defer.mjs +5507 -3072
  22. package/dist/cli/hs-dev.mjs +576 -0
  23. package/dist/cli/hs-env.mjs +416 -302
  24. package/dist/cli/hs-index-rebuild.mjs +7334 -0
  25. package/dist/cli/hs-index-reconcile.mjs +8138 -0
  26. package/dist/cli/hs-init.mjs +2840 -149
  27. package/dist/cli/hs-install.mjs +7214 -594
  28. package/dist/cli/hs-join.mjs +10640 -2421
  29. package/dist/cli/hs-vault-pull.mjs +8157 -6370
  30. package/dist/cli/hs-vault-push.mjs +6433 -0
  31. package/dist/cli/hs-vault.mjs +4029 -1555
  32. package/dist/cli/hs-workspace.mjs +2702 -538
  33. package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
  34. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
  35. package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
  36. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
  37. package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
  38. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
  39. package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
  40. package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
  41. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
  42. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
  43. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
  44. package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
  45. package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
  46. package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -0
  47. package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
  48. package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
  49. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
  50. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
  51. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
  52. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
  53. package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
  54. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
  55. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
  56. package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -0
  57. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
  58. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
  59. package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
  60. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
  61. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
  62. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
  63. package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
  64. package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
  65. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +33 -0
  66. package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
  67. package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
  68. package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
  69. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
  70. package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
  71. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
  72. package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
  73. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
  74. package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
  75. package/dist/cli-templates/harness-commands/cursor/hs-view.md +32 -0
  76. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
  77. package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
  78. package/docs/copilot-plugin-install.md +162 -0
  79. package/docs/driving-heyspex-from-copilot.md +165 -0
  80. package/docs/mcp-client-config.md +259 -6
  81. package/package.json +249 -124
  82. package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
  83. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
  84. package/plugins/heyspex/.mcp.json +12 -0
  85. package/plugins/heyspex/README.md +309 -0
  86. package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
  87. package/plugins/heyspex/bin/run-hook.mjs +80 -0
  88. package/plugins/heyspex/hooks/cursor-hooks.json +308 -0
  89. package/plugins/heyspex/hooks/hooks.json +418 -0
  90. package/plugins/heyspex/mcp.json +13 -0
  91. package/scripts/check-mcp-freshness.mjs +17 -9
  92. package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
  93. package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
  94. package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
  95. package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -0
  96. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +356 -0
  97. package/scripts/claude-hooks/_run.mjs +413 -0
  98. package/scripts/claude-hooks/_transcript-text.mjs +60 -0
  99. package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
  100. package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
  101. package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
  102. package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
  103. package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
  104. package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
  105. package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
  106. package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
  107. package/scripts/claude-hooks/claim-liveness-shared.mjs +577 -0
  108. package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
  109. package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
  110. package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
  111. package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
  112. package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
  113. package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
  114. package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
  115. package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
  116. package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
  117. package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
  118. package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
  119. package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
  120. package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
  121. package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
  122. package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
  123. package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
  124. package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
  125. package/scripts/claude-hooks/inject-communication-style.mjs +86 -0
  126. package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
  127. package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
  128. package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
  129. package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
  130. package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -0
  131. package/scripts/claude-hooks/log-session-activity.mjs +341 -0
  132. package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
  133. package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
  134. package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
  135. package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
  136. package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
  137. package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
  138. package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
  139. package/scripts/claude-hooks/research-method-guard.mjs +398 -0
  140. package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
  141. package/scripts/claude-hooks/session-end.mjs +451 -0
  142. package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
  143. package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
  144. package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
  145. package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
  146. package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
  147. package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
  148. package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
  149. package/scripts/claude-hooks/wip-checkpoint.mjs +302 -0
  150. package/scripts/cursor-hooks/manifest.mjs +281 -0
  151. package/scripts/dev-server-status.mjs +34 -13
  152. package/scripts/dev-server-stop.mjs +313 -0
  153. package/scripts/heyspex-mode.mjs +288 -0
  154. package/scripts/hs-auto-resolve.mjs +240 -0
  155. package/scripts/hs-auto-source.mjs +398 -0
  156. package/scripts/hs-auto.mjs +134 -0
  157. package/scripts/hs-capture-cli.mjs +326 -0
  158. package/scripts/hs-classifier.mjs +136 -0
  159. package/scripts/hs-companion.mjs +88 -0
  160. package/scripts/hs-gateway.mjs +115 -0
  161. package/scripts/hs-mode.mjs +125 -0
  162. package/scripts/hs-next.mjs +441 -0
  163. package/scripts/hs-settings.mjs +338 -0
  164. package/scripts/hs-view.mjs +496 -0
  165. package/scripts/hs-worktree.mjs +201 -0
  166. package/scripts/install-claude-code-hooks.mjs +272 -0
  167. package/scripts/install-cursor-hooks.mjs +471 -0
  168. package/scripts/lib/agent-env-markers.mjs +38 -0
  169. package/scripts/lib/console-url-resolver.mjs +199 -61
  170. package/scripts/lib/customer-kit.mjs +1203 -0
  171. package/scripts/lib/dev-mode.mjs +249 -32
  172. package/scripts/lib/directive-obligations.mjs +349 -0
  173. package/scripts/lib/heyspex-home.mjs +72 -0
  174. package/scripts/lib/hook-dispositions.mjs +210 -0
  175. package/scripts/lib/hook-vault-header.mjs +30 -0
  176. package/scripts/lib/hs-command.mjs +277 -0
  177. package/scripts/lib/hs-companion-settings.mjs +472 -0
  178. package/scripts/lib/hs-source-branch.mjs +196 -0
  179. package/scripts/lib/hs-source-commit.mjs +59 -5
  180. package/scripts/lib/hs-source-identity.mjs +526 -0
  181. package/scripts/lib/hs-source-log.mjs +13 -22
  182. package/scripts/lib/hs-source-preflight.mjs +18 -4
  183. package/scripts/lib/hs-source-push.mjs +386 -307
  184. package/scripts/lib/hs-worktree-args.mjs +115 -0
  185. package/scripts/lib/shared-path-helpers.mjs +134 -0
  186. package/scripts/lib/standalone-doctor.mjs +540 -3
  187. package/scripts/lib/subagent-git-lock.mjs +109 -0
  188. package/scripts/lib/template-pin.mjs +34 -0
  189. package/scripts/lib/window-placement.mjs +109 -0
  190. package/scripts/provenance-lib.mjs +91 -0
  191. package/scripts/stamp-provenance-on-edit.mjs +222 -0
  192. package/scripts/wip-ledger-health.mjs +653 -0
  193. package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
  194. package/spexharness/src/adapters/audit.mjs +197 -0
  195. package/spexharness/src/adapters/claude-code.mjs +449 -0
  196. package/spexharness/src/adapters/copilot.mjs +236 -0
  197. package/spexharness/src/adapters/detect-harness.mjs +34 -0
  198. package/spexharness/src/chat/tui/ansi.mjs +448 -0
  199. package/spexharness/src/chat/tui/palette.mjs +22 -0
  200. package/spexharness/src/chat/tui/select.mjs +208 -0
  201. package/spexharness/src/config-root.mjs +54 -0
  202. package/spexharness/src/context-budget.mjs +141 -0
  203. package/spexharness/src/dev-server-control.mjs +389 -0
  204. package/spexharness/src/doctrine/behaviour.mjs +371 -0
  205. package/spexharness/src/doctrine/corpus.mjs +733 -0
  206. package/spexharness/src/doctrine/inject.mjs +338 -0
  207. package/spexharness/src/doctrine/merge.mjs +184 -0
  208. package/spexharness/src/doctrine/sources.mjs +252 -0
  209. package/spexharness/src/doctrine/synthesize.mjs +401 -0
  210. package/spexharness/src/doctrine/vault-source.mjs +574 -0
  211. package/spexharness/src/doctrine/writer.mjs +176 -0
  212. package/spexharness/src/events.mjs +84 -0
  213. package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
  214. package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
  215. package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
  216. package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
  217. package/spexharness/src/gates/deep-research-cost-gate.mjs +275 -0
  218. package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
  219. package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
  220. package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
  221. package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
  222. package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
  223. package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
  224. package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
  225. package/spexharness/src/gates/git-identity-guard.mjs +61 -0
  226. package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
  227. package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
  228. package/spexharness/src/gates/git-stash-gate.mjs +159 -0
  229. package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
  230. package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
  231. package/spexharness/src/gates/inject-vault-context.mjs +239 -0
  232. package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
  233. package/spexharness/src/gates/plan-review-gate.mjs +233 -0
  234. package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
  235. package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
  236. package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
  237. package/spexharness/src/gates/settings-write-gate.mjs +104 -0
  238. package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
  239. package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
  240. package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
  241. package/spexharness/src/git-reflog-audit.mjs +294 -0
  242. package/spexharness/src/intent-classifier.mjs +196 -0
  243. package/spexharness/src/intent-directive.mjs +626 -0
  244. package/spexharness/src/profile/learn.mjs +551 -0
  245. package/spexharness/src/profile/proposals.mjs +357 -0
  246. package/spexharness/src/profile/vault-inclusion.mjs +386 -0
  247. package/spexharness/src/redact.mjs +151 -0
  248. package/spexharness/src/rule-table.mjs +849 -0
  249. package/spexharness/src/seat-context.mjs +122 -0
  250. package/spexharness/src/session-bootstrap.mjs +512 -0
  251. package/spexharness/src/session-record.mjs +815 -0
  252. package/spexharness/src/token-inventory.mjs +207 -0
  253. package/vendor/heyspex-mcp/account-switch.mjs +96 -0
  254. package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
  255. package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
  256. package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
  257. package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
  258. package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
  259. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +77 -8
  260. package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
  261. package/vendor/heyspex-mcp/capture-console-client.mjs +117 -3
  262. package/vendor/heyspex-mcp/capture-focus.mjs +1061 -31
  263. package/vendor/heyspex-mcp/capture-outbox.mjs +15 -5
  264. package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
  265. package/vendor/heyspex-mcp/capture-tools.mjs +1264 -0
  266. package/vendor/heyspex-mcp/claim-tools.mjs +1147 -0
  267. package/vendor/heyspex-mcp/cli-verbs.mjs +2074 -0
  268. package/vendor/heyspex-mcp/cli.mjs +207 -15524
  269. package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
  270. package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
  271. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +602 -0
  272. package/vendor/heyspex-mcp/config-seed.yaml +15 -0
  273. package/vendor/heyspex-mcp/config-settings.mjs +290 -7
  274. package/vendor/heyspex-mcp/console-fetch.mjs +822 -17
  275. package/vendor/heyspex-mcp/console-transport.mjs +172 -0
  276. package/vendor/heyspex-mcp/credentials.mjs +585 -0
  277. package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
  278. package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
  279. package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
  280. package/vendor/heyspex-mcp/design-tools.mjs +4 -1
  281. package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
  282. package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
  283. package/vendor/heyspex-mcp/dev-tools.mjs +131 -0
  284. package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
  285. package/vendor/heyspex-mcp/events-tools.mjs +207 -0
  286. package/vendor/heyspex-mcp/filing-tools.mjs +723 -0
  287. package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
  288. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +71 -15
  289. package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
  290. package/vendor/heyspex-mcp/folder-trust-store.mjs +60 -0
  291. package/vendor/heyspex-mcp/http-transport.mjs +302 -0
  292. package/vendor/heyspex-mcp/list-tools.mjs +257 -0
  293. package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
  294. package/vendor/heyspex-mcp/mcp-config.mjs +107 -0
  295. package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
  296. package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
  297. package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
  298. package/vendor/heyspex-mcp/occupied-keys.mjs +142 -0
  299. package/vendor/heyspex-mcp/on-deck-brief.mjs +3377 -0
  300. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
  301. package/vendor/heyspex-mcp/package-root.mjs +32 -0
  302. package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
  303. package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
  304. package/vendor/heyspex-mcp/plan-push.mjs +908 -22
  305. package/vendor/heyspex-mcp/plan-review.mjs +124 -30
  306. package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
  307. package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -0
  308. package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -970
  309. package/vendor/heyspex-mcp/promote-capture.mjs +388 -69
  310. package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
  311. package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
  312. package/vendor/heyspex-mcp/promote-research.mjs +666 -53
  313. package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
  314. package/vendor/heyspex-mcp/prompts.mjs +1460 -0
  315. package/vendor/heyspex-mcp/read-path.mjs +191 -15
  316. package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
  317. package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
  318. package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
  319. package/vendor/heyspex-mcp/register-spec.mjs +266 -54
  320. package/vendor/heyspex-mcp/request-context.mjs +83 -0
  321. package/vendor/heyspex-mcp/research-focus.mjs +207 -4
  322. package/vendor/heyspex-mcp/research-handlers.mjs +625 -0
  323. package/vendor/heyspex-mcp/research-tools.mjs +4 -4
  324. package/vendor/heyspex-mcp/response-budget.mjs +348 -0
  325. package/vendor/heyspex-mcp/review-focus.mjs +25 -1
  326. package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
  327. package/vendor/heyspex-mcp/search-specs.mjs +280 -0
  328. package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
  329. package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
  330. package/vendor/heyspex-mcp/session-owner.mjs +109 -0
  331. package/vendor/heyspex-mcp/session-tools.mjs +162 -0
  332. package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
  333. package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
  334. package/vendor/heyspex-mcp/spec-edit-tools.mjs +453 -0
  335. package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
  336. package/vendor/heyspex-mcp/split-research.mjs +304 -56
  337. package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
  338. package/vendor/heyspex-mcp/submit-for-review.mjs +150 -7
  339. package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
  340. package/vendor/heyspex-mcp/summary-write-tools.mjs +228 -0
  341. package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
  342. package/vendor/heyspex-mcp/test-helpers.mjs +123 -1
  343. package/vendor/heyspex-mcp/tool-catalog.mjs +2819 -0
  344. package/vendor/heyspex-mcp/tool-dispatch.mjs +657 -0
  345. package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
  346. package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
  347. package/vendor/heyspex-mcp/vault-io.mjs +720 -0
  348. package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
  349. package/vendor/heyspex-mcp/vault-resolve.mjs +554 -68
  350. package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
  351. package/vendor/heyspex-mcp/whoami.mjs +92 -0
  352. package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
@@ -1,11 +1,377 @@
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
+ // scripts/lib/heyspex-home.mjs
9
+ import { homedir } from "node:os";
10
+ function heyspexHome(env = process.env) {
11
+ const override = env?.HEYSPEX_HOME;
12
+ if (typeof override === "string" && override.trim().length > 0) {
13
+ return override.trim();
14
+ }
15
+ return homedir();
16
+ }
17
+ var init_heyspex_home = __esm({
18
+ "scripts/lib/heyspex-home.mjs"() {
19
+ "use strict";
20
+ }
21
+ });
22
+
23
+ // vendor/heyspex-mcp/credentials.mjs
24
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
25
+ import os, { homedir as homedir2 } from "node:os";
26
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
27
+ function credentialsFilePath(homeDir = heyspexHome()) {
28
+ return join2(homeDir, ".heyspex", "credentials.yaml");
29
+ }
30
+ function parseMcpToken(raw) {
31
+ if (typeof raw !== "string" || raw.length === 0) return null;
32
+ const token = raw.match(MCP_TOKEN_LINE_RE)?.[1]?.trim();
33
+ return token ? token : null;
34
+ }
35
+ function isValidAccountId(id) {
36
+ return typeof id === "string" && ACCOUNT_ID_RE.test(id);
37
+ }
38
+ function topLevelScalar(raw, key) {
39
+ const re = new RegExp(`^${key}:[ \\t]*["']?([^"'\\r\\n#]+)`, "m");
40
+ const value = raw.match(re)?.[1]?.trim();
41
+ return value ? value : null;
42
+ }
43
+ function decodeScalar(text) {
44
+ const t = text.trim();
45
+ if (t === "" || t === "null" || t === "~") return null;
46
+ if (t.startsWith('"')) {
47
+ try {
48
+ const v = JSON.parse(t);
49
+ return typeof v === "string" && v ? v : null;
50
+ } catch {
51
+ return t.replace(/^"|"$/g, "") || null;
52
+ }
53
+ }
54
+ if (t.startsWith("'")) return t.replace(/^'|'$/g, "") || null;
55
+ const bare = t.replace(/\s+#.*$/, "").trim();
56
+ return bare || null;
57
+ }
58
+ function emptyAccount() {
59
+ return { mcp_token: null, console_url: null, email: null, paired_at: null };
60
+ }
61
+ function findAccountsBlock(lines) {
62
+ const start = lines.findIndex((l) => /^accounts:[ \t]*(#.*)?$/.test(l));
63
+ if (start < 0) return null;
64
+ let end = start + 1;
65
+ while (end < lines.length && (lines[end].trim() === "" || /^[ \t]/.test(lines[end]))) end += 1;
66
+ while (end > start + 1 && lines[end - 1].trim() === "") end -= 1;
67
+ return { start, end };
68
+ }
69
+ function parseAccountsBlock(lines) {
70
+ const accounts = {};
71
+ const block = findAccountsBlock(lines);
72
+ if (!block) return accounts;
73
+ let current = null;
74
+ for (const line of lines.slice(block.start + 1, block.end)) {
75
+ const header = line.match(/^ {2}([^\s:#][^:]*):[ \t]*(#.*)?$/);
76
+ if (header) {
77
+ const id = decodeScalar(header[1]);
78
+ current = isValidAccountId(id) ? id : null;
79
+ if (current) accounts[current] = emptyAccount();
80
+ continue;
81
+ }
82
+ const field = line.match(/^ {4}([a-z_]+):(.*)$/);
83
+ if (field && current && ACCOUNT_FIELDS.includes(field[1])) {
84
+ accounts[current][field[1]] = decodeScalar(field[2]);
85
+ }
86
+ }
87
+ return accounts;
88
+ }
89
+ function parseCredentialsStore(raw) {
90
+ const text = typeof raw === "string" ? raw : "";
91
+ if (topLevelScalar(text, "schema_version") !== String(CREDENTIALS_SCHEMA_VERSION)) {
92
+ const token = parseMcpToken(text);
93
+ if (!token) return { schemaVersion: 1, currentAccountId: null, accounts: {} };
94
+ return {
95
+ schemaVersion: 1,
96
+ currentAccountId: LEGACY_ACCOUNT_ID,
97
+ accounts: {
98
+ [LEGACY_ACCOUNT_ID]: { ...emptyAccount(), mcp_token: token, console_url: topLevelScalar(text, "console_url") }
99
+ }
100
+ };
101
+ }
102
+ const accounts = parseAccountsBlock(text.split(/\r?\n/));
103
+ const current = topLevelScalar(text, "current_account_id");
104
+ return {
105
+ schemaVersion: 2,
106
+ currentAccountId: current && accounts[current] ? current : null,
107
+ accounts
108
+ };
109
+ }
110
+ function resolveBearer({ envToken, credentialsText, accountId, legacyAccountId } = {}) {
111
+ const fromEnv = typeof envToken === "string" ? envToken.trim() : "";
112
+ if (fromEnv) return { token: fromEnv, source: "env" };
113
+ const store = parseCredentialsStore(credentialsText);
114
+ let entry;
115
+ if (typeof accountId === "string" && accountId.trim()) {
116
+ const wanted = accountId.trim();
117
+ entry = store.accounts[wanted];
118
+ if (!entry && store.schemaVersion === 1 && legacyAccountId && wanted === legacyAccountId) {
119
+ entry = store.accounts[LEGACY_ACCOUNT_ID];
120
+ }
121
+ } else {
122
+ entry = store.currentAccountId ? store.accounts[store.currentAccountId] : void 0;
123
+ }
124
+ if (entry?.mcp_token) return { token: entry.mcp_token, source: "credentials-file" };
125
+ return { token: null, source: null };
126
+ }
127
+ function readTextOrNull(read, path3) {
128
+ try {
129
+ const raw = read(path3);
130
+ return typeof raw === "string" ? raw : null;
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+ function readLegacyConfigAccountId(read, homeDir) {
136
+ const raw = readTextOrNull(read, join2(homeDir, ".heyspex", "config.yaml"));
137
+ const value = raw?.match(CONFIG_ACCOUNT_ID_RE)?.[1]?.trim() ?? "";
138
+ return value || null;
139
+ }
140
+ function readBearer({ env = process.env, homeDir = heyspexHome(), readFile, accountId } = {}) {
141
+ const read = readFile ?? ((p) => readFileSync(p, "utf8"));
142
+ const credentialsText = readTextOrNull(read, credentialsFilePath(homeDir));
143
+ const named = typeof accountId === "string" && accountId.trim() ? accountId.trim() : null;
144
+ const legacyAccountId = named && parseCredentialsStore(credentialsText).schemaVersion === 1 ? readLegacyConfigAccountId(read, homeDir) : null;
145
+ return resolveBearer({ envToken: env?.[MCP_TOKEN_ENV], credentialsText, accountId: named, legacyAccountId });
146
+ }
147
+ function readCurrentAccountId({ homeDir = heyspexHome(), readFile } = {}) {
148
+ const read = readFile ?? ((p) => readFileSync(p, "utf8"));
149
+ const store = parseCredentialsStore(readTextOrNull(read, credentialsFilePath(homeDir)));
150
+ if (store.schemaVersion === 2 && store.currentAccountId && store.currentAccountId !== LEGACY_ACCOUNT_ID) {
151
+ return store.currentAccountId;
152
+ }
153
+ if (store.schemaVersion === 2 && store.currentAccountId === null) return null;
154
+ return readLegacyConfigAccountId(read, homeDir);
155
+ }
156
+ var MCP_TOKEN_ENV, MCP_TOKEN_LINE_RE, CREDENTIALS_SCHEMA_VERSION, LEGACY_ACCOUNT_ID, ACCOUNT_FIELDS, ACCOUNT_ID_RE, CONFIG_ACCOUNT_ID_RE;
157
+ var init_credentials = __esm({
158
+ "vendor/heyspex-mcp/credentials.mjs"() {
159
+ "use strict";
160
+ init_heyspex_home();
161
+ MCP_TOKEN_ENV = "HEYSPEX_MCP_TOKEN";
162
+ MCP_TOKEN_LINE_RE = /^mcp_token:[ \t]*["']?([^"'\r\n#]+)/m;
163
+ CREDENTIALS_SCHEMA_VERSION = 2;
164
+ LEGACY_ACCOUNT_ID = "legacy";
165
+ ACCOUNT_FIELDS = ["mcp_token", "console_url", "email", "paired_at"];
166
+ ACCOUNT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.@+-]*$/;
167
+ CONFIG_ACCOUNT_ID_RE = /^account_id:\s*["']?([^"'\r\n#]+)/m;
168
+ }
169
+ });
170
+
171
+ // scripts/lib/dev-mode.mjs
172
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
173
+ import { homedir as homedir7 } from "node:os";
174
+ import { join as join8 } from "node:path";
175
+ function devModeFilePath(homeDir = heyspexHome()) {
176
+ return join8(homeDir, ".heyspex", DEV_MODE_FILENAME);
177
+ }
178
+ function normalizeMode(raw) {
179
+ const v = String(raw ?? "").trim().toLowerCase();
180
+ if (v === "prod" || v === "production") return { mode: "prod", reliance: "cloud" };
181
+ if (v === "dev") return { mode: "dev", reliance: null };
182
+ if (v === "local") return { mode: "dev", reliance: "local" };
183
+ if (v === "cloud") return { mode: "dev", reliance: "cloud" };
184
+ return null;
185
+ }
186
+ function normalizeReliance(raw) {
187
+ const v = String(raw ?? "").trim().toLowerCase();
188
+ return v === "local" || v === "cloud" ? v : null;
189
+ }
190
+ function deriveSemantics(mode, reliance) {
191
+ const effectiveReliance = mode === "prod" ? "cloud" : reliance ?? "local";
192
+ const local = mode === "dev" && effectiveReliance === "local";
193
+ return {
194
+ reliance: effectiveReliance,
195
+ consoleUrl: local ? LOCAL_CONSOLE_URL : CLOUD_CONSOLE_URL,
196
+ readTier: local ? "disk" : "neon",
197
+ vaultWriteMode: local ? "local" : "cloud"
198
+ };
199
+ }
200
+ function readLocalDevModeRecord({ homeDir = heyspexHome(), readFile = defaultReadFile } = {}) {
201
+ let raw;
202
+ try {
203
+ raw = readFile(devModeFilePath(homeDir));
204
+ } catch {
205
+ return { record: null, parseError: null };
206
+ }
207
+ try {
208
+ const parsed = JSON.parse(raw);
209
+ const mode = parsed?.mode === "prod" || parsed?.mode === "dev" ? parsed.mode : null;
210
+ const reliance = normalizeReliance(parsed?.reliance);
211
+ if (!mode || mode === "dev" && !reliance) {
212
+ return { record: null, parseError: `invalid dev-mode record: ${JSON.stringify(parsed)}` };
213
+ }
214
+ return {
215
+ record: {
216
+ mode,
217
+ reliance: mode === "prod" ? "cloud" : reliance,
218
+ console_url: typeof parsed.console_url === "string" ? parsed.console_url : null,
219
+ updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : null,
220
+ source: typeof parsed.source === "string" ? parsed.source : null
221
+ },
222
+ parseError: null
223
+ };
224
+ } catch (err) {
225
+ return { record: null, parseError: `unparseable dev-mode.json: ${String(err?.message ?? err)}` };
226
+ }
227
+ }
228
+ function resolveDevMode({
229
+ env = process.env,
230
+ homeDir = heyspexHome(),
231
+ readFile = defaultReadFile,
232
+ cloudSetting = void 0
233
+ } = {}) {
234
+ const local = readLocalDevModeRecord({ homeDir, readFile });
235
+ const cloudKnown = cloudSetting !== void 0;
236
+ const cloudValue = cloudKnown && cloudSetting ? {
237
+ mode: cloudSetting.mode === "dev" ? "dev" : "prod",
238
+ reliance: normalizeReliance(cloudSetting.reliance) ?? "cloud"
239
+ } : null;
240
+ let mode;
241
+ let reliance;
242
+ let source;
243
+ let relianceSource;
244
+ const envParsed = normalizeMode(env.HEYSPEX_DEV_MODE);
245
+ if (envParsed) {
246
+ mode = envParsed.mode;
247
+ const envReliance = envParsed.reliance ?? normalizeReliance(env.HEYSPEX_DEV_RELIANCE);
248
+ reliance = envReliance ?? "local";
249
+ source = "env";
250
+ relianceSource = envReliance ? "env" : "assumed-local";
251
+ } else if (local.record) {
252
+ mode = local.record.mode;
253
+ reliance = local.record.reliance;
254
+ source = "local-override";
255
+ } else if (cloudValue) {
256
+ mode = cloudValue.mode;
257
+ reliance = cloudValue.reliance;
258
+ source = "cloud-setting";
259
+ } else {
260
+ mode = "prod";
261
+ reliance = "cloud";
262
+ source = "default";
263
+ }
264
+ const semantics = deriveSemantics(mode, reliance);
265
+ const consoleUrl = source === "local-override" && local.record?.console_url ? local.record.console_url.replace(/\/+$/, "") : semantics.consoleUrl;
266
+ const staleAgainstCloud = source === "local-override" && cloudValue !== null && (cloudValue.mode !== mode || mode === "dev" && cloudValue.reliance !== semantics.reliance);
267
+ return {
268
+ mode,
269
+ reliance: semantics.reliance,
270
+ consoleUrl,
271
+ readTier: semantics.readTier,
272
+ vaultWriteMode: semantics.vaultWriteMode,
273
+ sources: {
274
+ mode: source,
275
+ reliance: relianceSource ?? source,
276
+ consoleUrl: source === "local-override" && local.record?.console_url ? "local-override" : "derived",
277
+ readTier: "derived",
278
+ vaultWriteMode: "derived"
279
+ },
280
+ localOverride: {
281
+ present: local.record !== null,
282
+ record: local.record,
283
+ parseError: local.parseError,
284
+ staleAgainstCloud
285
+ },
286
+ cloudSetting: { known: cloudKnown, value: cloudValue }
287
+ };
288
+ }
289
+ var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile;
290
+ var init_dev_mode = __esm({
291
+ "scripts/lib/dev-mode.mjs"() {
292
+ "use strict";
293
+ init_heyspex_home();
294
+ init_credentials();
295
+ CLOUD_CONSOLE_URL = "https://app.heyspex.com";
296
+ LOCAL_CONSOLE_URL = "http://localhost:3000";
297
+ DEV_MODE_FILENAME = "dev-mode.json";
298
+ defaultReadFile = (p) => readFileSync7(p, "utf8");
299
+ }
300
+ });
3
301
 
4
302
  // spex-console/bin/hs-check.ts
5
303
  import { spawn, spawnSync } from "node:child_process";
6
- import { existsSync, readFileSync as readFileSync4 } from "node:fs";
304
+ import { existsSync as existsSync5, readFileSync as readFileSync9 } from "node:fs";
7
305
  import path2 from "node:path";
8
- import process2 from "node:process";
306
+ import process3 from "node:process";
307
+
308
+ // scripts/lib/shared-path-helpers.mjs
309
+ import { isAbsolute, resolve, sep } from "node:path";
310
+ function resolveCodeRoot({
311
+ explicit,
312
+ packageRoot,
313
+ vaultRoot,
314
+ env = process.env
315
+ } = {}) {
316
+ const fromExplicit = typeof explicit === "string" ? explicit.trim() : "";
317
+ if (fromExplicit) return fromExplicit;
318
+ const fromEnv = typeof env?.HS_PKG_ROOT === "string" ? env.HS_PKG_ROOT.trim() : "";
319
+ if (fromEnv) return fromEnv;
320
+ const fromPackage = typeof packageRoot === "string" ? packageRoot.trim() : "";
321
+ if (fromPackage) return fromPackage;
322
+ const fromVault = typeof vaultRoot === "string" ? vaultRoot.trim() : "";
323
+ return fromVault;
324
+ }
325
+
326
+ // spexharness/src/verifier-seat.mjs
327
+ function grantsClose({ manifestFound, parseError = null, criteria = [] }) {
328
+ if (!manifestFound) {
329
+ return {
330
+ closeGranted: false,
331
+ reason: "no frozen manifest \u2014 NOT verified",
332
+ verifiedCount: 0,
333
+ failedCount: 0,
334
+ totalCount: 0
335
+ };
336
+ }
337
+ if (parseError) {
338
+ return {
339
+ closeGranted: false,
340
+ reason: `frozen manifest is unparseable (${parseError}) \u2014 NOT verified`,
341
+ verifiedCount: 0,
342
+ failedCount: 0,
343
+ totalCount: 0
344
+ };
345
+ }
346
+ const list = Array.isArray(criteria) ? criteria : [];
347
+ const failed = list.filter((c) => !c.passed);
348
+ const totalCount = list.length;
349
+ if (failed.length > 0) {
350
+ return {
351
+ closeGranted: false,
352
+ reason: `${failed.length} of ${totalCount} machine criteria failed`,
353
+ verifiedCount: totalCount - failed.length,
354
+ failedCount: failed.length,
355
+ totalCount
356
+ };
357
+ }
358
+ if (totalCount === 0) {
359
+ return {
360
+ closeGranted: false,
361
+ reason: "no machine-verifiable criteria \u2014 nothing was verified",
362
+ verifiedCount: 0,
363
+ failedCount: 0,
364
+ totalCount: 0
365
+ };
366
+ }
367
+ return {
368
+ closeGranted: true,
369
+ reason: `frozen manifest GREEN \u2014 ${totalCount} machine criteria verified`,
370
+ verifiedCount: totalCount,
371
+ failedCount: 0,
372
+ totalCount
373
+ };
374
+ }
9
375
 
10
376
  // spex-console/src/lib/cli/checks-manifest.ts
11
377
  var EXIT = {
@@ -31,11 +397,13 @@ function parseCheckRunArgs(argv) {
31
397
  let target;
32
398
  let json = false;
33
399
  let report = false;
400
+ let unattended = false;
34
401
  for (let i = 1; i < argv.length; i++) {
35
402
  const arg = argv[i];
36
403
  if (arg === "--help" || arg === "-h") return { help: true };
37
404
  else if (arg === "--json") json = true;
38
405
  else if (arg === "--report") report = true;
406
+ else if (arg === "--unattended") unattended = true;
39
407
  else if (arg === "--manifest" || arg.startsWith("--manifest=")) {
40
408
  const value = arg.includes("=") ? arg.slice("--manifest=".length) : argv[++i];
41
409
  if (value === void 0) {
@@ -53,7 +421,7 @@ function parseCheckRunArgs(argv) {
53
421
  if (!target) {
54
422
  return { error: { code: EXIT.USAGE, message: "run requires a spec id or manifest path" } };
55
423
  }
56
- return { args: { verb: "run", target, json, report } };
424
+ return { args: { verb: "run", target, json, report, unattended } };
57
425
  }
58
426
  function buildVerdict(report) {
59
427
  return {
@@ -73,12 +441,17 @@ var KIND_ALIASES = {
73
441
  function joinUnderRoot(root, rel) {
74
442
  if (rel === "" || rel === ".") return root;
75
443
  if (/^(?:[a-zA-Z]:[\\/]|[\\/])/.test(rel)) return rel;
76
- const sep = root.includes("\\") && !root.includes("/") ? "\\" : "/";
77
- return `${root.replace(/[\\/]+$/, "")}${sep}${rel.replace(/^[\\/]+/, "")}`;
444
+ const sep2 = root.includes("\\") && !root.includes("/") ? "\\" : "/";
445
+ return `${root.replace(/[\\/]+$/, "")}${sep2}${rel.replace(/^[\\/]+/, "")}`;
78
446
  }
79
447
  function resolveCheckCwds(check, roots) {
80
448
  if (typeof check.cwd === "string" && check.cwd.trim() !== "") {
81
- return [joinUnderRoot(roots.repoRoot, check.cwd.trim())];
449
+ const raw = check.cwd.trim();
450
+ const vaultMatch = /^vault:(.*)$/.exec(raw);
451
+ if (vaultMatch) {
452
+ return [joinUnderRoot(roots.vaultRoot ?? roots.repoRoot, vaultMatch[1].trim())];
453
+ }
454
+ return [joinUnderRoot(roots.repoRoot, raw)];
82
455
  }
83
456
  return roots.repoRoot === roots.consoleRoot ? [roots.repoRoot] : [roots.repoRoot, roots.consoleRoot];
84
457
  }
@@ -193,14 +566,17 @@ function parseChecksManifest(text) {
193
566
  }
194
567
  };
195
568
  }
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) {
569
+ 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;
570
+ var NODE_TEST_FILE_PASS_RE = /^\s*(?:✔|ok\s+\d+\s+-)\s+\S*\.(?:test|spec)\.[cm]?[jt]sx?\s*(?:\(|$)/;
571
+ var NODE_TEST_FILTER_RE = /--test-(?:name|skip)-pattern/;
572
+ function detectZeroMatch(rawOutput, run) {
198
573
  if (!rawOutput) return null;
199
- const line = rawOutput.split(/\r?\n/).find((l) => ZERO_MATCH_RE.test(l));
574
+ const filtered = run !== void 0 && NODE_TEST_FILTER_RE.test(run);
575
+ const line = rawOutput.split(/\r?\n/).find((l) => ZERO_MATCH_RE.test(l) || filtered && NODE_TEST_FILE_PASS_RE.test(l));
200
576
  return line ? line.trim() : null;
201
577
  }
202
578
  function evaluateCheck(check, outcome) {
203
- if (detectZeroMatch(outcome.rawOutput) !== null) return false;
579
+ if (detectZeroMatch(outcome.rawOutput, check.run) !== null) return false;
204
580
  if (check.expect === "no-net-new") {
205
581
  return (outcome.errorCount ?? 0) <= check.baseline;
206
582
  }
@@ -364,6 +740,9 @@ var HELP = [
364
740
  " --report PATCH the verdict to the spec's active claim (the PCFL gate).",
365
741
  " Requires a spec-id target (identifies the claim). The verify",
366
742
  " exit code is unchanged; reporting is a side effect.",
743
+ " --unattended Declare that no human is present at this flip (pipeline",
744
+ " caller). Rides --report: the server's no-manifest",
745
+ " self-report fallback then fails closed instead of shipping.",
367
746
  " --json Emit a machine-readable JSON result (the gate's input shape).",
368
747
  " --help, -h Show this help.",
369
748
  "",
@@ -372,12 +751,380 @@ var HELP = [
372
751
  ].join("\n");
373
752
 
374
753
  // 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";
754
+ import path from "node:path";
755
+
756
+ // scripts/lib/hs-command.mjs
757
+ import { existsSync } from "node:fs";
758
+ import { delimiter, dirname, join } from "node:path";
759
+ import { fileURLToPath } from "node:url";
760
+ var HS_BIN_NAME = "hs";
761
+ var HS_DISPATCHER_PATH = (() => {
762
+ try {
763
+ const here = dirname(fileURLToPath(import.meta.url));
764
+ return join(dirname(dirname(here)), "bin", "hs.mjs");
765
+ } catch {
766
+ return null;
767
+ }
768
+ })();
769
+ function shellQuote(p) {
770
+ return /[\\\s]/.test(p) ? `"${p}"` : p;
771
+ }
772
+ var HS_DISPATCHER_PREFIX = HS_DISPATCHER_PATH ? `node ${shellQuote(HS_DISPATCHER_PATH)}` : HS_BIN_NAME;
773
+ function candidateNames(name, env) {
774
+ if (process.platform !== "win32") return [name];
775
+ const pathext = (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim().toLowerCase()).filter(Boolean);
776
+ const extensions = [.../* @__PURE__ */ new Set([...pathext, ".ps1"])];
777
+ return [name, ...extensions.map((ext) => name + ext)];
778
+ }
779
+ function findOnPath(name, { env = process.env, exists = existsSync } = {}) {
780
+ try {
781
+ const raw = env.PATH ?? env.Path ?? env.path ?? "";
782
+ for (const dir of String(raw).split(delimiter)) {
783
+ const trimmed = dir.trim().replace(/^"(.*)"$/, "$1");
784
+ if (!trimmed) continue;
785
+ for (const candidate of candidateNames(name, env)) {
786
+ const full = join(trimmed, candidate);
787
+ try {
788
+ if (exists(full)) return full;
789
+ } catch {
790
+ }
791
+ }
792
+ }
793
+ } catch {
794
+ }
795
+ return null;
796
+ }
797
+ var cached = null;
798
+ var HS_ON_PATH_OVERRIDE_ENV = "HEYSPEX_HS_ON_PATH";
799
+ function onPathOverride(env) {
800
+ const raw = String(env?.[HS_ON_PATH_OVERRIDE_ENV] ?? "");
801
+ if (raw === "1") return true;
802
+ if (raw === "0") return false;
803
+ return void 0;
804
+ }
805
+ function describeHsCommand({ env, exists, useCache = true } = {}) {
806
+ const injected = env !== void 0 || exists !== void 0;
807
+ const effectiveEnv = env ?? process.env;
808
+ const override = onPathOverride(effectiveEnv);
809
+ if (override !== void 0) {
810
+ return {
811
+ onPath: override,
812
+ prefix: override ? HS_BIN_NAME : HS_DISPATCHER_PREFIX,
813
+ resolvedPath: null,
814
+ dispatcherPath: HS_DISPATCHER_PATH
815
+ };
816
+ }
817
+ if (useCache && !injected && cached) return cached;
818
+ const resolvedPath = findOnPath(HS_BIN_NAME, {
819
+ env: env ?? process.env,
820
+ exists: exists ?? existsSync
821
+ });
822
+ const answer = {
823
+ onPath: resolvedPath !== null,
824
+ prefix: resolvedPath !== null ? HS_BIN_NAME : HS_DISPATCHER_PREFIX,
825
+ resolvedPath,
826
+ dispatcherPath: HS_DISPATCHER_PATH
827
+ };
828
+ if (useCache && !injected) cached = answer;
829
+ return answer;
830
+ }
831
+ function hsCommand(args = "", options = {}) {
832
+ let prefix = HS_DISPATCHER_PREFIX;
833
+ try {
834
+ prefix = describeHsCommand(options).prefix;
835
+ } catch {
836
+ }
837
+ const tail = String(args ?? "").trim();
838
+ return tail ? `${prefix} ${tail}` : prefix;
839
+ }
840
+
841
+ // vendor/heyspex-mcp/console-fetch.mjs
842
+ init_heyspex_home();
843
+ init_credentials();
844
+ import { readFileSync as readFileSync5 } from "node:fs";
845
+ import { homedir as homedir5 } from "node:os";
846
+ import { join as join6 } from "node:path";
847
+
848
+ // vendor/heyspex-mcp/session-owner.mjs
849
+ init_heyspex_home();
850
+ init_credentials();
851
+ import { readFileSync as readFileSync2 } from "node:fs";
852
+ import { homedir as homedir3 } from "node:os";
853
+ import { join as join3 } from "node:path";
854
+ function resolveSessionOwnerUncached({ env = process.env, homeDir = heyspexHome(), readFile } = {}) {
855
+ const fromEnv = typeof env?.HS_ACCOUNT_ID === "string" ? env.HS_ACCOUNT_ID.trim() : "";
856
+ if (fromEnv) return fromEnv;
857
+ return readCurrentAccountId({ homeDir, readFile });
858
+ }
859
+
860
+ // vendor/heyspex-mcp/vault-less-endpoints.mjs
861
+ var VAULT_LESS_ENDPOINTS = Object.freeze([
862
+ "* /api/mcp/session",
863
+ "* /api/mcp/pairing-codes",
864
+ "POST /api/vaults",
865
+ "GET /api/vaults",
866
+ "POST /api/workspaces",
867
+ "GET /api/workspaces",
868
+ "* /api/admin/dev-mode"
869
+ ]);
870
+ function pathnameOf(path3) {
871
+ const raw = String(path3 ?? "").split("?")[0].split("#")[0];
872
+ const noTrailing = raw.length > 1 ? raw.replace(/\/+$/, "") : raw;
873
+ return noTrailing || "/";
874
+ }
875
+ function isVaultLessEndpoint(method, path3) {
876
+ const m = String(method || "GET").toUpperCase();
877
+ const pathname = pathnameOf(path3);
878
+ return VAULT_LESS_ENDPOINTS.some((row) => {
879
+ const [rowMethod, rowPath] = row.split(" ");
880
+ return rowPath === pathname && (rowMethod === "*" || rowMethod === m);
881
+ });
882
+ }
883
+
884
+ // vendor/heyspex-mcp/write-outbox.mjs
885
+ import {
886
+ readdirSync,
887
+ readFileSync as readFileSync3,
888
+ writeFileSync as writeFileSync2,
889
+ mkdirSync as mkdirSync2,
890
+ existsSync as existsSync2,
891
+ renameSync as renameSync2,
892
+ rmSync
893
+ } from "node:fs";
894
+ import { basename, join as join4, isAbsolute as isAbsolute2 } from "node:path";
895
+
896
+ // vendor/heyspex-mcp/vault-resolve.mjs
897
+ init_heyspex_home();
898
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
899
+ import { homedir as homedir4, tmpdir } from "node:os";
900
+ import { dirname as dirname3, join as join5, resolve as resolve3 } from "node:path";
901
+
902
+ // vendor/heyspex-mcp/console-fetch.mjs
903
+ var VAULT_NOT_REGISTERED = "VAULT_NOT_REGISTERED";
904
+ var VaultNotRegisteredError = class extends Error {
905
+ /**
906
+ * @param {string | null | undefined} repoRootAbs the root the resolver settled on
907
+ * @param {string | null | undefined} source the resolver tier that produced it
908
+ * @param {{ reason?: 'unregistered' | 'legacy-id', legacyId?: string | null }} [detail]
909
+ */
910
+ constructor(repoRootAbs, source, { reason = "unregistered", legacyId = null } = {}) {
911
+ const root = typeof repoRootAbs === "string" && repoRootAbs.trim() ? repoRootAbs.trim() : null;
912
+ const where = root ? ` (${root})` : "";
913
+ 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), `;
914
+ super(
915
+ 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.`
916
+ );
917
+ this.name = "VaultNotRegisteredError";
918
+ this.code = VAULT_NOT_REGISTERED;
919
+ this.reason = reason;
920
+ this.repoRootAbs = root;
921
+ this.source = typeof source === "string" ? source : null;
922
+ this.legacyId = legacyId;
923
+ }
924
+ };
925
+ 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;
926
+ function isCloudVaultId(value) {
927
+ return typeof value === "string" && CLOUD_VAULT_ID_RE.test(value.trim());
928
+ }
929
+ var MARKER_VAULT_ID_RE = /^vault_id:[ \t]*["']?([^"'\r\n#]+)/m;
930
+ function readMarkerVaultId(repoRootAbs, { readFile } = {}) {
931
+ if (typeof repoRootAbs !== "string" || !repoRootAbs.trim()) return null;
932
+ const read = readFile ?? ((p) => readFileSync5(p, "utf8"));
933
+ try {
934
+ const raw = read(join6(repoRootAbs, "heyspex.yaml"));
935
+ const value = String(raw ?? "").match(MARKER_VAULT_ID_RE)?.[1]?.trim() ?? "";
936
+ return value || null;
937
+ } catch {
938
+ return null;
939
+ }
940
+ }
941
+ function cloudVaultIdFor(resolved, { readMarkerVaultId: readMarker = readMarkerVaultId } = {}) {
942
+ const rowId = resolved.vaultId.trim();
943
+ if (isCloudVaultId(rowId)) return rowId;
944
+ const marker = readMarker(resolved.repoRootAbs);
945
+ if (isCloudVaultId(marker)) return marker.trim();
946
+ throw new VaultNotRegisteredError(resolved.repoRootAbs, resolved.source, { reason: "legacy-id", legacyId: rowId });
947
+ }
948
+ var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
949
+
950
+ // spex-console/src/lib/cli/control-shared.ts
951
+ init_credentials();
952
+
953
+ // spex-console/src/lib/vault-registry/registry-file.ts
954
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync6, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "node:fs";
955
+ import { dirname as dirname4, join as join7, resolve as resolve4 } from "node:path";
956
+
957
+ // spex-console/src/lib/heyspex-home.ts
958
+ import { homedir as homedir6 } from "node:os";
959
+ function heyspexHome2(env = process.env) {
960
+ const override = env?.HEYSPEX_HOME;
961
+ if (typeof override === "string" && override.trim().length > 0) {
962
+ return override.trim();
963
+ }
964
+ return homedir6();
965
+ }
966
+
967
+ // spex-console/src/lib/vault-registry/types.ts
968
+ var LEGACY_REGISTRY_ACCOUNT_ID = "legacy";
969
+ var VAULT_REGISTRY_SCHEMA_VERSION = 2;
970
+ function emptyVaultRegistry() {
971
+ return {
972
+ schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
973
+ current_account_id: null,
974
+ accounts: {},
975
+ workspaces: [],
976
+ vaults: []
977
+ };
978
+ }
979
+
980
+ // spex-console/src/lib/vault-registry/registry-file.ts
981
+ function vaultRegistryDir(opts = {}) {
982
+ return opts.registryDir ?? join7(heyspexHome2(), ".heyspex");
983
+ }
984
+ function vaultRegistryFilePath(opts = {}) {
985
+ return join7(vaultRegistryDir(opts), "registry.json");
986
+ }
987
+ function isNonEmptyString(v) {
988
+ return typeof v === "string" && v.trim().length > 0;
989
+ }
990
+ function accountOf(o) {
991
+ return isNonEmptyString(o.account_id) ? o.account_id : LEGACY_REGISTRY_ACCOUNT_ID;
992
+ }
993
+ function normalizeWorkspace(raw) {
994
+ if (!raw || typeof raw !== "object") return null;
995
+ const o = raw;
996
+ if (!isNonEmptyString(o.id)) return null;
997
+ if (!isNonEmptyString(o.display_name)) return null;
998
+ if (!isNonEmptyString(o.created_at)) return null;
999
+ const record = {
1000
+ id: o.id,
1001
+ display_name: o.display_name,
1002
+ created_at: o.created_at,
1003
+ archived: o.archived === true,
1004
+ account_id: accountOf(o)
1005
+ };
1006
+ if (isNonEmptyString(o.github_org)) record.github_org = o.github_org;
1007
+ return record;
1008
+ }
1009
+ function normalizeVault(raw) {
1010
+ if (!raw || typeof raw !== "object") return null;
1011
+ const o = raw;
1012
+ if (!isNonEmptyString(o.id)) return null;
1013
+ if (!isNonEmptyString(o.workspace_id)) return null;
1014
+ if (!isNonEmptyString(o.display_name)) return null;
1015
+ if (!isNonEmptyString(o.repo_root_abs)) return null;
1016
+ if (typeof o.schema_version !== "number") return null;
1017
+ if (!isNonEmptyString(o.registered_at)) return null;
1018
+ const record = {
1019
+ id: o.id,
1020
+ workspace_id: o.workspace_id,
1021
+ display_name: o.display_name,
1022
+ repo_root_abs: o.repo_root_abs,
1023
+ schema_version: o.schema_version,
1024
+ registered_at: o.registered_at,
1025
+ archived: o.archived === true,
1026
+ account_id: accountOf(o)
1027
+ };
1028
+ if (isNonEmptyString(o.github_remote)) record.github_remote = o.github_remote;
1029
+ return record;
1030
+ }
1031
+ function normalizeStoredPointers(raw) {
1032
+ const o = raw && typeof raw === "object" ? raw : {};
1033
+ const out = {};
1034
+ for (const field of ["active_vault_id", "active_workspace_id"]) {
1035
+ const v = o[field];
1036
+ if (v === null || isNonEmptyString(v)) out[field] = v;
1037
+ }
1038
+ return out;
1039
+ }
1040
+ function normalizePointers(raw) {
1041
+ const o = raw && typeof raw === "object" ? raw : {};
1042
+ return {
1043
+ active_vault_id: isNonEmptyString(o.active_vault_id) ? o.active_vault_id : null,
1044
+ active_workspace_id: isNonEmptyString(o.active_workspace_id) ? o.active_workspace_id : null
1045
+ };
1046
+ }
1047
+ function registryFileSchemaOf(json) {
1048
+ if (!json || typeof json !== "object" || Array.isArray(json)) return null;
1049
+ return json.schema_version === 2 ? 2 : 1;
1050
+ }
1051
+ function parseVaultRegistryFile(json) {
1052
+ const schema = registryFileSchemaOf(json);
1053
+ if (schema === null) return emptyVaultRegistry();
1054
+ const o = json;
1055
+ const workspacesRaw = Array.isArray(o.workspaces) ? o.workspaces : [];
1056
+ const workspaces = workspacesRaw.map(normalizeWorkspace).filter((w) => w !== null);
1057
+ const vaultsRaw = Array.isArray(o.vaults) ? o.vaults : [];
1058
+ const vaults = vaultsRaw.map(normalizeVault).filter((v) => v !== null);
1059
+ if (schema === 1) {
1060
+ const legacy = normalizePointers(o);
1061
+ const hasLegacyState = legacy.active_vault_id !== null || legacy.active_workspace_id !== null || workspaces.length > 0 || vaults.length > 0;
1062
+ return {
1063
+ schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
1064
+ current_account_id: hasLegacyState ? LEGACY_REGISTRY_ACCOUNT_ID : null,
1065
+ accounts: hasLegacyState ? { [LEGACY_REGISTRY_ACCOUNT_ID]: legacy } : {},
1066
+ workspaces,
1067
+ vaults
1068
+ };
1069
+ }
1070
+ const accounts = {};
1071
+ if (o.accounts && typeof o.accounts === "object" && !Array.isArray(o.accounts)) {
1072
+ for (const [id, pointers] of Object.entries(o.accounts)) {
1073
+ if (isNonEmptyString(id)) accounts[id] = normalizeStoredPointers(pointers);
1074
+ }
1075
+ }
1076
+ return {
1077
+ schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
1078
+ current_account_id: isNonEmptyString(o.current_account_id) ? o.current_account_id : null,
1079
+ accounts,
1080
+ workspaces,
1081
+ vaults
1082
+ };
1083
+ }
1084
+ function readRegistryJson(opts) {
1085
+ const filePath = vaultRegistryFilePath(opts);
1086
+ if (!existsSync4(filePath)) return { kind: "missing" };
1087
+ try {
1088
+ const raw = readFileSync6(filePath, "utf8");
1089
+ const json = JSON.parse(raw);
1090
+ if (registryFileSchemaOf(json) === null) return { kind: "unreadable" };
1091
+ return { kind: "ok", raw, json };
1092
+ } catch {
1093
+ return { kind: "unreadable" };
1094
+ }
1095
+ }
1096
+ function inspectVaultRegistry(opts = {}) {
1097
+ const read = readRegistryJson(opts);
1098
+ if (read.kind !== "ok") {
1099
+ return {
1100
+ registry: emptyVaultRegistry(),
1101
+ file_schema_version: null,
1102
+ registry_unreadable: read.kind === "unreadable"
1103
+ };
1104
+ }
1105
+ return {
1106
+ registry: parseVaultRegistryFile(read.json),
1107
+ file_schema_version: registryFileSchemaOf(read.json),
1108
+ registry_unreadable: false
1109
+ };
1110
+ }
1111
+ function loadVaultRegistry(opts = {}) {
1112
+ return inspectVaultRegistry(opts).registry;
1113
+ }
1114
+
1115
+ // spex-console/src/lib/cli/control-shared.ts
378
1116
  var EXIT2 = {
379
1117
  /** Success. */
380
1118
  OK: 0,
1119
+ /**
1120
+ * A read-only CHECK ran correctly and its answer is "no" — `hs connect
1121
+ * --check` on an unconnected repo. Distinct from {@link EXIT.USAGE} so a
1122
+ * script can tell "you typed the command wrong" from "the command worked,
1123
+ * the repo is not connected". Both were `1` until 2026-09-18, which made
1124
+ * `--check` unusable in a conditional: every caller had to parse stdout to
1125
+ * find out which of the two had happened.
1126
+ */
1127
+ CHECK_FAILED: 2,
381
1128
  /** Usage error — unknown subcommand/flag, or a missing required argument. */
382
1129
  USAGE: 1,
383
1130
  /** Claim race lost — the spec is already claimed by another agent (409). */
@@ -399,31 +1146,94 @@ var EXIT2 = {
399
1146
  * from `USAGE`/`INVALID_ARG` — this is an authorization refusal, not a
400
1147
  * malformed command.
401
1148
  */
402
- FORBIDDEN: 15
1149
+ FORBIDDEN: 15,
1150
+ /**
1151
+ * A CLOUD write refused because no account id could be resolved (no
1152
+ * `HS_ACCOUNT_ID` env var, no `account_id:` in `~/.heyspex/config.yaml`) --
1153
+ * `resolveCloudAccountIdOrRefuse` in `vault-workspace-shared.ts`
1154
+ * ([[HS-YU1S-UAGY-TASK-cloud-writes-refuse-without-a]]). Distinct from
1155
+ * `FORBIDDEN` -- this is "not signed in at all", not "signed in but not
1156
+ * authorized for this override".
1157
+ */
1158
+ SIGN_IN_REQUIRED: 16,
1159
+ /**
1160
+ * Signed in, but the account a repo would be attached to was not confirmed:
1161
+ * a non-interactive install/connect with no matching `--account` / `--yes`,
1162
+ * an `account_id` that disagrees with the bearer's user, or the human
1163
+ * aborting the identity prompt (`install-identity.ts`,
1164
+ * [[HS-HAQI-KODY-DIAG-hs-install-and-hs-connect-bind-a-new]]).
1165
+ */
1166
+ IDENTITY_UNCONFIRMED: 17,
1167
+ /**
1168
+ * The repo this verb runs in is not registered on this machine (no
1169
+ * `~/.heyspex/registry.json` row for its root, or a legacy slug row with no
1170
+ * cloud `vault_id` in its heyspex.yaml), so the call was refused LOCALLY —
1171
+ * nothing was sent. Names `hs connect`
1172
+ * ([[HS-MVG4-WW9R-TASK-mcp-and-hs-name-account-vault-on-every]]).
1173
+ */
1174
+ VAULT_NOT_REGISTERED: 18,
1175
+ /**
1176
+ * The local half of a setup verb (`hs connect`, `hs vault *`, `hs workspace
1177
+ * *`) was written but the CLOUD half did not land -- the console refused
1178
+ * it or was never reached. The verb's receipt names the failed half and the
1179
+ * exact command to re-run
1180
+ * ([[HS-STCY-WW9R-TASK-add-a-shared-receipt-that-reads-back]], invariant 6).
1181
+ * Distinct from {@link EXIT.WRITE_FAILED} so a script can tell "re-run to
1182
+ * sync the cloud" from a generic server failure.
1183
+ */
1184
+ CLOUD_WRITE_FAILED: 19,
1185
+ /**
1186
+ * The repo's committed `heyspex.yaml` records `account: <A>` but the machine
1187
+ * is signed in as another account, so `hs connect` refused and wrote
1188
+ * nothing (marker, registry, cloud). Repair: `hs login --switch-account <A>`
1189
+ * ([[HS-VWUD-WW9R-TASK-record-the-connecting-account-in]], invariant 4).
1190
+ */
1191
+ ACCOUNT_MISMATCH: 20,
1192
+ /**
1193
+ * Both sides of a setup verb's receipt were read back and still DISAGREE
1194
+ * after a short bounded re-read of the cloud side (a briefly stale read is
1195
+ * absorbed first). The write may well have landed; what the verb cannot
1196
+ * claim is that this machine and the console now say the same thing. The
1197
+ * receipt prints the diffs and the command to re-run
1198
+ * ([[HS-FKSR-WW9R-TASK-make-one-hs-connect-run-create-a-cloud]], invariant 6).
1199
+ * Distinct from {@link EXIT.CLOUD_WRITE_FAILED} (a write that did not land).
1200
+ */
1201
+ RECEIPT_DISAGREE: 21
403
1202
  };
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
- }
1203
+ var VAULT_ID_HEADER = "x-heyspex-vault-id";
1204
+ var ACCOUNT_ID_HEADER = "x-heyspex-account-id";
1205
+ function readPairedBearer(opts = {}) {
1206
+ return readBearer(opts);
1207
+ }
1208
+ function readPairedToken(opts = {}) {
1209
+ return readPairedBearer(opts).token ?? void 0;
412
1210
  }
413
- function bearerHeaderFor(consoleUrl) {
1211
+ function bearerHeaderFor(consoleUrl, opts = {}) {
414
1212
  try {
415
1213
  const host = new URL(consoleUrl).hostname;
416
1214
  if (host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === "::1") {
417
1215
  return {};
418
1216
  }
419
- const token = readPairedToken();
1217
+ const token = readPairedToken(opts);
420
1218
  return token ? { authorization: `Bearer ${token}` } : {};
421
1219
  } catch {
422
1220
  return {};
423
1221
  }
424
1222
  }
1223
+ function scopeHeadersFor(scope, method, path3) {
1224
+ if (scope === "none") {
1225
+ if (!isVaultLessEndpoint(method, path3)) {
1226
+ throw new Error(
1227
+ `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 }.`
1228
+ );
1229
+ }
1230
+ return {};
1231
+ }
1232
+ return { [VAULT_ID_HEADER]: scope.vaultId, [ACCOUNT_ID_HEADER]: scope.accountId };
1233
+ }
425
1234
  async function controlRequest(deps, req) {
426
1235
  const url = `${deps.consoleUrl}${req.path}`;
1236
+ const scopeHeaders = scopeHeadersFor(deps.scope, req.method, req.path);
427
1237
  let res;
428
1238
  try {
429
1239
  res = await deps.fetchImpl(url, {
@@ -434,7 +1244,10 @@ async function controlRequest(deps, req) {
434
1244
  // authenticates every control call; loopback never asked, so the header
435
1245
  // is attached only when the target is non-loopback and a paired token
436
1246
  // exists. Same `hsmcp_` bearer the MCP server presents.
437
- ...bearerHeaderFor(deps.consoleUrl)
1247
+ ...bearerHeaderFor(deps.consoleUrl),
1248
+ // Invariant 2: name the vault and the account on every call, or the
1249
+ // console serves whatever its leftover pointer says.
1250
+ ...scopeHeaders
438
1251
  },
439
1252
  ...req.body !== void 0 ? { body: JSON.stringify(req.body) } : {}
440
1253
  });
@@ -479,42 +1292,188 @@ async function controlRequest(deps, req) {
479
1292
  }
480
1293
  return { ok: true, status: res.status, body: parsed };
481
1294
  }
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";
1295
+ function normalizeRoot(p) {
1296
+ return path.resolve(p).replace(/[\\/]+/g, "/").replace(/\/+$/, "").toLowerCase();
1297
+ }
1298
+ function resolveControlScope(input) {
1299
+ const target = normalizeRoot(input.vaultRoot);
1300
+ const row = input.vaults.find((v) => normalizeRoot(v.repo_root_abs) === target) ?? null;
1301
+ if (!row) {
1302
+ return {
1303
+ ok: false,
1304
+ error: {
1305
+ code: EXIT2.VAULT_NOT_REGISTERED,
1306
+ 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.`,
1307
+ payload: {
1308
+ ok: false,
1309
+ error: "vault_not_registered",
1310
+ reason: "unregistered",
1311
+ vault_root: input.vaultRoot
1312
+ },
1313
+ isError: true
1314
+ }
1315
+ };
1316
+ }
1317
+ let vaultId;
1318
+ try {
1319
+ vaultId = cloudVaultIdFor(
1320
+ { vaultId: row.id, repoRootAbs: row.repo_root_abs, source: "registry" },
1321
+ input.readMarkerVaultId ? { readMarkerVaultId: input.readMarkerVaultId } : {}
1322
+ );
1323
+ } catch (err) {
1324
+ if (!(err instanceof VaultNotRegisteredError)) throw err;
1325
+ const refused = err;
1326
+ return {
1327
+ ok: false,
1328
+ error: {
1329
+ code: EXIT2.VAULT_NOT_REGISTERED,
1330
+ message: refused.message,
1331
+ payload: {
1332
+ ok: false,
1333
+ error: "vault_not_registered",
1334
+ reason: refused.reason ?? "legacy-id",
1335
+ vault_root: input.vaultRoot,
1336
+ legacy_id: refused.legacyId ?? row.id
1337
+ },
1338
+ isError: true
1339
+ }
1340
+ };
1341
+ }
1342
+ const accountId = input.accountId?.trim() ?? "";
1343
+ if (!accountId) {
1344
+ return {
1345
+ ok: false,
1346
+ error: {
1347
+ code: EXIT2.SIGN_IN_REQUIRED,
1348
+ 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.`,
1349
+ payload: { ok: false, error: "sign_in_required", vault_root: input.vaultRoot },
1350
+ isError: true
1351
+ }
1352
+ };
1353
+ }
1354
+ return { ok: true, scope: { accountId, vaultId } };
1355
+ }
1356
+ function resolveControlScopeFromMachine(vaultRoot, opts = {}) {
1357
+ const registry = loadVaultRegistry(opts.registry);
1358
+ const accountId = resolveSessionOwnerUncached({ env: opts.env, homeDir: opts.homeDir });
1359
+ return resolveControlScope({ vaultRoot, accountId, vaults: registry.vaults });
1360
+ }
487
1361
 
488
1362
  // 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(/\/+$/, "");
1363
+ init_heyspex_home();
1364
+ init_dev_mode();
1365
+ import { readFileSync as readFileSync8 } from "node:fs";
1366
+ import { homedir as homedir8 } from "node:os";
1367
+ import { join as join9 } from "node:path";
1368
+ var DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
1369
+ function trimTrailingSlash(url) {
1370
+ return url.replace(/\/+$/, "");
1371
+ }
1372
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
1373
+ function isLoopbackConsoleUrl(url) {
1374
+ if (typeof url !== "string" || !url) return false;
1375
+ try {
1376
+ return LOOPBACK_HOSTS.has(new URL(url).hostname.toLowerCase());
1377
+ } catch {
1378
+ return false;
1379
+ }
1380
+ }
1381
+ var staleLoopbackCredentialsWarned = false;
1382
+ function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
1383
+ if (staleLoopbackCredentialsWarned) return;
1384
+ staleLoopbackCredentialsWarned = true;
499
1385
  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);
1386
+ log(
1387
+ `[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).
1388
+ `
1389
+ );
1390
+ } catch {
1391
+ }
1392
+ }
1393
+ function readCredentialsConsoleUrl({ homeDir = heyspexHome(), readFile = defaultReadFile2 } = {}) {
1394
+ try {
1395
+ const raw = readFile(join9(homeDir, ".heyspex", "credentials.yaml"));
1396
+ const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
504
1397
  const value = match?.[1]?.trim();
505
- if (value && value.length > 0) return value.replace(/\/+$/, "");
1398
+ return value && value.length > 0 ? trimTrailingSlash(value) : null;
506
1399
  } catch {
1400
+ return null;
507
1401
  }
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 {
1402
+ }
1403
+ function defaultReadFile2(p) {
1404
+ return readFileSync8(p, "utf8");
1405
+ }
1406
+ function readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery = defaultReadFile2 } = {}) {
1407
+ if (!repoRoot) return null;
1408
+ try {
1409
+ const discovery = String(readDiscovery(join9(repoRoot, ".heyspex", "console-url")) ?? "").trim();
1410
+ if (discovery.length > 0) return trimTrailingSlash(discovery);
1411
+ } catch {
1412
+ }
1413
+ return null;
1414
+ }
1415
+ var ConsoleUrlUnresolvedError = class extends Error {
1416
+ constructor() {
1417
+ super(
1418
+ "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."
1419
+ );
1420
+ this.name = "ConsoleUrlUnresolvedError";
1421
+ }
1422
+ };
1423
+ function resolveModeEnvelope({ env, homeDir }) {
1424
+ try {
1425
+ const resolved = resolveDevMode({ env, homeDir });
1426
+ return {
1427
+ reliance: resolved.reliance === "cloud" ? "cloud" : "local",
1428
+ consoleUrl: resolved.consoleUrl || DEFAULT_CONSOLE_URL
1429
+ };
1430
+ } catch {
1431
+ return { reliance: "local", consoleUrl: DEFAULT_CONSOLE_URL };
1432
+ }
1433
+ }
1434
+ function resolveConsoleUrl({
1435
+ repoRoot,
1436
+ env = process.env,
1437
+ homeDir = heyspexHome(),
1438
+ strict = false,
1439
+ devMode,
1440
+ log = (line) => process.stderr.write(line),
1441
+ readFile = defaultReadFile2,
1442
+ readDiscovery = defaultReadFile2
1443
+ } = {}) {
1444
+ const envUrl = env.HEYSPEX_CONSOLE_URL?.trim();
1445
+ if (envUrl) return trimTrailingSlash(envUrl);
1446
+ const mode = devMode ?? resolveModeEnvelope({ env, homeDir });
1447
+ const cloud = mode.reliance === "cloud";
1448
+ const derivedUrl = trimTrailingSlash(
1449
+ cloud && isLoopbackConsoleUrl(mode.consoleUrl) ? CLOUD_CONSOLE_URL : mode.consoleUrl || DEFAULT_CONSOLE_URL
1450
+ );
1451
+ const credentialsUrl = readCredentialsConsoleUrl({ homeDir, readFile });
1452
+ if (credentialsUrl) {
1453
+ if (cloud && isLoopbackConsoleUrl(credentialsUrl)) {
1454
+ warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log });
1455
+ } else {
1456
+ return credentialsUrl;
515
1457
  }
516
1458
  }
517
- return DEFAULT_CONSOLE_URL;
1459
+ if (!cloud) {
1460
+ const discoveryUrl = readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery });
1461
+ if (discoveryUrl) return discoveryUrl;
1462
+ if (strict) throw new ConsoleUrlUnresolvedError();
1463
+ }
1464
+ return derivedUrl;
1465
+ }
1466
+
1467
+ // spex-console/src/lib/cli/defer.ts
1468
+ function resolveSpexConsoleUrl(vaultRoot, readDiscovery, opts = {}) {
1469
+ return resolveConsoleUrl({
1470
+ repoRoot: vaultRoot,
1471
+ env: opts.env,
1472
+ homeDir: opts.homeDir,
1473
+ readFile: opts.readFile,
1474
+ readDiscovery,
1475
+ log: opts.log
1476
+ });
518
1477
  }
519
1478
  var HELP2 = [
520
1479
  "Usage:",
@@ -538,16 +1497,44 @@ var HELP2 = [
538
1497
  "Exit codes: 0 ok \xB7 1 usage \xB7 10 not-found \xB7 11 bad-arg \xB7 12 console-unreachable \xB7 13 write-failed"
539
1498
  ].join("\n");
540
1499
 
1500
+ // spex-console/bin/run-entry.ts
1501
+ import process2 from "node:process";
1502
+ var BinExit = class extends Error {
1503
+ code;
1504
+ constructor(code) {
1505
+ super(`exit ${code}`);
1506
+ this.name = "BinExit";
1507
+ this.code = code;
1508
+ }
1509
+ };
1510
+ function runEntry(name, main2, options = {}) {
1511
+ const { fallbackCode = 1, unhandledLabel = "unhandled" } = options;
1512
+ void (async () => {
1513
+ try {
1514
+ await main2();
1515
+ } catch (err) {
1516
+ if (err instanceof BinExit) {
1517
+ process2.exitCode = err.code;
1518
+ return;
1519
+ }
1520
+ const message = err instanceof Error ? err.message : String(err);
1521
+ process2.stderr.write(`${name}: ${unhandledLabel}: ${message}
1522
+ `);
1523
+ process2.exitCode = fallbackCode;
1524
+ }
1525
+ })();
1526
+ }
1527
+
541
1528
  // spex-console/bin/hs-check.ts
542
1529
  function manifestForSpecId(vaultRoot, specId) {
543
1530
  return path2.join(vaultRoot, "specs", "checks", `${specId}.checks.json`);
544
1531
  }
545
1532
  function resolveManifestPath(target, vaultRoot, invokedCwd) {
546
1533
  const literal = path2.resolve(invokedCwd, target);
547
- if (existsSync(literal)) return literal;
1534
+ if (existsSync5(literal)) return literal;
548
1535
  if (target.endsWith(".json")) return null;
549
1536
  const byId = manifestForSpecId(vaultRoot, target);
550
- return existsSync(byId) ? byId : null;
1537
+ return existsSync5(byId) ? byId : null;
551
1538
  }
552
1539
  function countTscErrors(output) {
553
1540
  const matches = output.match(/error TS\d+/g);
@@ -557,31 +1544,31 @@ var cachedPosixShell;
557
1544
  function resolveWindowsPosixShell() {
558
1545
  if (cachedPosixShell !== void 0) return cachedPosixShell;
559
1546
  const candidates = [
560
- process2.env.HEYSPEX_CHECK_SHELL,
1547
+ process3.env.HEYSPEX_CHECK_SHELL,
561
1548
  "C:\\Program Files\\Git\\bin\\bash.exe"
562
1549
  ].filter((c) => typeof c === "string" && c.length > 0);
563
1550
  for (const candidate of candidates) {
564
- if (existsSync(candidate)) return cachedPosixShell = candidate;
1551
+ if (existsSync5(candidate)) return cachedPosixShell = candidate;
565
1552
  }
566
1553
  const where = spawnSync("where", ["bash"], { encoding: "utf8", windowsHide: true });
567
1554
  if (where.status === 0 && where.stdout) {
568
1555
  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;
1556
+ if (hit && existsSync5(hit)) return cachedPosixShell = hit;
570
1557
  }
571
1558
  return cachedPosixShell = null;
572
1559
  }
573
1560
  var MISSING_BINARY_RE = /command not found|is not recognized as an internal or external command/i;
574
1561
  var warnedCmdFallback = false;
575
1562
  function runCheckInDirectory(check, cwd) {
576
- return new Promise((resolve) => {
1563
+ return new Promise((resolve5) => {
577
1564
  const started = Date.now();
578
1565
  const needsErrorCount = check.expect === "no-net-new";
579
1566
  const needsStdout = check.expect === "empty-output";
580
1567
  const stdio = ["ignore", "pipe", "pipe"];
581
- const posixShell = process2.platform === "win32" ? resolveWindowsPosixShell() : null;
582
- if (process2.platform === "win32" && !posixShell && !warnedCmdFallback) {
1568
+ const posixShell = process3.platform === "win32" ? resolveWindowsPosixShell() : null;
1569
+ if (process3.platform === "win32" && !posixShell && !warnedCmdFallback) {
583
1570
  warnedCmdFallback = true;
584
- process2.stderr.write(
1571
+ process3.stderr.write(
585
1572
  "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
1573
  );
587
1574
  }
@@ -596,7 +1583,7 @@ function runCheckInDirectory(check, cwd) {
596
1583
  captured += chunk.toString();
597
1584
  });
598
1585
  child.on("error", (err) => {
599
- resolve({ exitCode: -1, durationMs: Date.now() - started, message: err.message });
1586
+ resolve5({ exitCode: -1, durationMs: Date.now() - started, message: err.message });
600
1587
  });
601
1588
  child.on("close", (code) => {
602
1589
  const durationMs = Date.now() - started;
@@ -615,15 +1602,16 @@ function runCheckInDirectory(check, cwd) {
615
1602
  const trimmed = captured.trim();
616
1603
  outcome.message = trimmed.length > 0 ? trimmed.split("\n").slice(-3).join(" ") : `command exited ${exitCode}`;
617
1604
  }
618
- resolve(outcome);
1605
+ resolve5(outcome);
619
1606
  });
620
1607
  });
621
1608
  }
622
- function makeRunCheck(vaultRoot) {
1609
+ function makeRunCheck(vaultRoot, codeRoot) {
623
1610
  return async (check) => {
624
1611
  const cwds = resolveCheckCwds(check, {
625
- repoRoot: vaultRoot,
626
- consoleRoot: path2.join(vaultRoot, "spex-console")
1612
+ repoRoot: codeRoot,
1613
+ consoleRoot: path2.join(codeRoot, "spex-console"),
1614
+ vaultRoot
627
1615
  });
628
1616
  let lastFailure = null;
629
1617
  for (const cwd of cwds) {
@@ -643,105 +1631,141 @@ function makeRunCheck(vaultRoot) {
643
1631
  };
644
1632
  }
645
1633
  async function main() {
646
- const vaultRoot = process2.env.HS_VAULT_ROOT;
1634
+ const vaultRoot = process3.env.HS_VAULT_ROOT;
647
1635
  if (!vaultRoot) {
648
- process2.stderr.write(
1636
+ process3.stderr.write(
649
1637
  "hs check: HS_VAULT_ROOT is not set (the dispatcher must resolve and pass it).\n"
650
1638
  );
651
- process2.exit(EXIT.USAGE);
1639
+ process3.exitCode = EXIT.USAGE;
1640
+ return;
652
1641
  }
653
- const parsed = parseCheckRunArgs(process2.argv.slice(3));
1642
+ if (process3.argv[3] === "judge") {
1643
+ const pkgRoot = resolveCodeRoot({ vaultRoot });
1644
+ const judgeScript = path2.join(pkgRoot, "dev-factory", "judge.mjs");
1645
+ if (!existsSync5(judgeScript)) {
1646
+ process3.stderr.write(
1647
+ `hs check judge: script not found at ${judgeScript} (source checkout only \u2014 not available in a published install).
1648
+ `
1649
+ );
1650
+ process3.exitCode = EXIT.NOT_FOUND;
1651
+ return;
1652
+ }
1653
+ const forwardArgs = process3.argv.slice(4);
1654
+ const child = spawnSync(process3.execPath, [judgeScript, ...forwardArgs], {
1655
+ cwd: pkgRoot,
1656
+ stdio: "inherit",
1657
+ windowsHide: true
1658
+ });
1659
+ process3.exitCode = child.status ?? 1;
1660
+ return;
1661
+ }
1662
+ const parsed = parseCheckRunArgs(process3.argv.slice(3));
654
1663
  if (parsed.help) {
655
- process2.stdout.write(`${HELP}
1664
+ process3.stdout.write(`${HELP}
656
1665
  `);
657
- process2.exit(EXIT.OK);
1666
+ process3.exitCode = EXIT.OK;
1667
+ return;
658
1668
  }
659
1669
  if (parsed.error) {
660
- process2.stderr.write(`hs check: ${parsed.error.message}
1670
+ process3.stderr.write(`hs check: ${parsed.error.message}
661
1671
  `);
662
- process2.exit(parsed.error.code);
1672
+ process3.exitCode = parsed.error.code;
1673
+ return;
663
1674
  }
664
1675
  const args = parsed.args;
665
- const invokedCwd = process2.env.HS_INVOKED_CWD?.trim() || process2.cwd();
1676
+ const invokedCwd = process3.env.HS_INVOKED_CWD?.trim() || process3.cwd();
666
1677
  const manifestPath = resolveManifestPath(args.target, vaultRoot, invokedCwd);
667
1678
  if (!manifestPath) {
668
- process2.stderr.write(
1679
+ process3.stderr.write(
669
1680
  `hs check: no manifest for "${args.target}".
670
1681
  Pass a manifest path, or create ${path2.join("specs", "checks", `${args.target}.checks.json`)} under the vault root.
671
1682
  `
672
1683
  );
673
- process2.exit(EXIT.NOT_FOUND);
1684
+ process3.exitCode = EXIT.NOT_FOUND;
1685
+ return;
674
1686
  }
675
- const { manifest, error } = parseChecksManifest(readFileSync4(manifestPath, "utf-8"));
1687
+ const { manifest, error } = parseChecksManifest(readFileSync9(manifestPath, "utf-8"));
676
1688
  if (error) {
677
- process2.stderr.write(`hs check: ${manifestPath}: ${error}
1689
+ process3.stderr.write(`hs check: ${manifestPath}: ${error}
678
1690
  `);
679
- process2.exit(EXIT.INVALID_MANIFEST);
1691
+ process3.exitCode = EXIT.INVALID_MANIFEST;
1692
+ return;
680
1693
  }
681
- const report = await runChecksManifest(manifest, { runCheck: makeRunCheck(vaultRoot) });
682
- const sink = report.passed ? process2.stdout : process2.stderr;
1694
+ const codeRoot = resolveCodeRoot({ vaultRoot });
1695
+ const report = await runChecksManifest(manifest, {
1696
+ runCheck: makeRunCheck(vaultRoot, codeRoot)
1697
+ });
1698
+ const verifierSeatDecision = grantsClose({
1699
+ manifestFound: true,
1700
+ parseError: null,
1701
+ criteria: report.criteria.filter((c) => c.method !== "human").map((c) => ({ index: c.ac, passed: c.verdict === "pass" }))
1702
+ });
1703
+ const sink = report.passed ? process3.stdout : process3.stderr;
683
1704
  if (args.json) {
684
- sink.write(`${JSON.stringify(reportToJson(report))}
685
- `);
1705
+ sink.write(
1706
+ `${JSON.stringify({ ...reportToJson(report), verifierSeat: verifierSeatDecision })}
1707
+ `
1708
+ );
686
1709
  } else {
687
1710
  sink.write(`${formatReport(report)}
688
1711
  `);
689
1712
  }
690
1713
  if (args.report) {
691
- await reportVerdict(args.target, report, vaultRoot);
1714
+ await reportVerdict(args.target, report, vaultRoot, args.unattended);
692
1715
  }
693
- process2.exit(reportExitCode(report));
1716
+ process3.exitCode = reportExitCode(report);
694
1717
  }
695
- async function reportVerdict(target, report, vaultRoot) {
696
- if (existsSync(target) || target.endsWith(".json")) {
697
- process2.stderr.write(
1718
+ async function reportVerdict(target, report, vaultRoot, unattended = false) {
1719
+ if (existsSync5(target) || target.endsWith(".json")) {
1720
+ process3.stderr.write(
698
1721
  "hs check: --report needs a spec-id target (to identify the claim), not a manifest path.\n"
699
1722
  );
700
1723
  return;
701
1724
  }
702
1725
  const consoleUrl = resolveSpexConsoleUrl(vaultRoot, (p) => {
703
1726
  try {
704
- return readFileSync4(p, "utf-8");
1727
+ return readFileSync9(p, "utf-8");
705
1728
  } catch {
706
1729
  return null;
707
1730
  }
708
1731
  });
709
1732
  const verdict = buildVerdict(report);
1733
+ const scoped = resolveControlScopeFromMachine(vaultRoot);
1734
+ if (!scoped.ok) {
1735
+ process3.stderr.write(`hs check: verdict not reported \u2014 ${scoped.error.message}
1736
+ `);
1737
+ return;
1738
+ }
710
1739
  const res = await controlRequest(
711
- { fetchImpl: fetch, consoleUrl },
1740
+ { fetchImpl: fetch, consoleUrl, scope: scoped.scope },
712
1741
  {
713
1742
  method: "PATCH",
714
1743
  path: `/api/specs/${encodeURIComponent(target)}/claim/status`,
715
- body: { status: "working", verification: verdict }
1744
+ body: { status: "working", verification: verdict, unattended }
716
1745
  }
717
1746
  );
718
1747
  if (!res.ok) {
719
- process2.stderr.write(`hs check: verdict not reported \u2014 ${res.error.message}
1748
+ process3.stderr.write(`hs check: verdict not reported \u2014 ${res.error.message}
720
1749
  `);
721
1750
  return;
722
1751
  }
723
1752
  if (res.status === 404) {
724
- process2.stderr.write(
1753
+ process3.stderr.write(
725
1754
  `hs check: no active claim for ${target} \u2014 verdict not reported (claim it first).
726
1755
  `
727
1756
  );
728
1757
  return;
729
1758
  }
730
1759
  if (res.status !== 200) {
731
- process2.stderr.write(`hs check: verdict not reported \u2014 console returned ${res.status}.
1760
+ process3.stderr.write(`hs check: verdict not reported \u2014 console returned ${res.status}.
732
1761
  `);
733
1762
  return;
734
1763
  }
735
1764
  const flipped = res.body?.autoShip;
736
1765
  const note = flipped && flipped.flipped !== false ? ` \u2192 gate flipped to ${flipped.effective_status}` : "";
737
- process2.stderr.write(
1766
+ process3.stderr.write(
738
1767
  `hs check: reported verdict (manifestPassed=${verdict.manifestPassed})${note}
739
1768
  `
740
1769
  );
741
1770
  }
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
- });
1771
+ runEntry("hs check", main, { fallbackCode: EXIT.RUNNER_MISSING });