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
package/bin/hs.mjs CHANGED
@@ -12,16 +12,240 @@
12
12
  * node_modules) executes the .ts entrypoints.
13
13
  */
14
14
  import { spawn } from "node:child_process";
15
- import { existsSync, readFileSync, statSync } from "node:fs";
15
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
16
16
  import path from "node:path";
17
17
  import process from "node:process";
18
18
  import { fileURLToPath, pathToFileURL } from "node:url";
19
19
 
20
- import { runtimeFilesCheckRow } from "../scripts/lib/standalone-doctor.mjs";
20
+ import {
21
+ foldConnectionReport,
22
+ runBundledConnectCheck,
23
+ runtimeFilesCheckRow,
24
+ templateCheckRow,
25
+ vaultConfigCheckRow,
26
+ } from "../scripts/lib/standalone-doctor.mjs";
27
+ import {
28
+ isVaultRoot,
29
+ readSatellitePin,
30
+ resolveActiveVault,
31
+ } from "../vendor/heyspex-mcp/vault-resolve.mjs";
21
32
 
22
33
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
34
  const PKG_ROOT = path.resolve(__dirname, "..");
24
35
 
36
+ /**
37
+ * Every script path `hs` spawns directly by filesystem path (as opposed to
38
+ * `dispatchSpexConsoleEntry`'s own three-case tsx/bundle/missing resolution,
39
+ * which already carries its own existence handling). Each entry is
40
+ * PKG_ROOT-relative, posix-separated.
41
+ *
42
+ * Why this exists ([[HS-561M]]): `scripts/verify-mcp-package.mjs`'s
43
+ * `checkClosure` proves the STATIC IMPORT closure of vendor/heyspex-mcp/cli.mjs
44
+ * is fully packed — it says nothing about the ~15 verbs below that reach a
45
+ * script by `spawn()`ing a path string, which is invisible to any import-graph
46
+ * walk. Every dispatch site below resolves its script path THROUGH this
47
+ * manifest (`path.join(PKG_ROOT, SPAWN_TARGETS[key].path)`) instead of
48
+ * building the path inline, so `verify-mcp-package.mjs`'s `spawnTargets` check
49
+ * (which imports this const) can assert every non-`devOnly` entry is actually
50
+ * in the packed tarball file list, naming whichever one is missing.
51
+ *
52
+ * `devOnly: true` entries are scripts this package deliberately does NOT ship
53
+ * (see each `reason`) — the pack gate skips them, and the dispatch site for
54
+ * each already guards the missing case at runtime with a clear message
55
+ * (`doctor` falls back to `runStandaloneDoctor`; `skills` prints a
56
+ * published-install notice — see their dispatch sites below).
57
+ */
58
+ export const SPAWN_TARGETS = {
59
+ "setup:claude": { path: "scripts/install-claude-code-hooks.mjs" },
60
+ "setup:cursor": { path: "scripts/install-cursor-hooks.mjs" },
61
+ "skills:sync": {
62
+ path: "scripts/sync-skills.mjs",
63
+ devOnly: true,
64
+ reason:
65
+ "sync-skills.mjs propagates THIS platform repo's OWN .commandcode/skills " +
66
+ "source content (Command Code SSOT, resolved from the script's own file " +
67
+ "location, not the caller's vault) — that content has no meaning outside " +
68
+ "a clone of the HeySpex platform repo and is not shipped.",
69
+ },
70
+ "heyspex-mcp-cli": { path: "vendor/heyspex-mcp/cli.mjs" }, // `hs login` + `hs mcp *`
71
+ doctor: {
72
+ path: "scripts/heyspex-doctor.mjs",
73
+ devOnly: true,
74
+ reason:
75
+ "the full doctor audits dozens of source-checkout-only surfaces (git " +
76
+ "hooks, Cursor wiring, console secrets, ...) a published install never " +
77
+ "has by design — the dispatch site below already falls back to " +
78
+ "runStandaloneDoctor() when this file is absent.",
79
+ },
80
+ "server:status": { path: "scripts/dev-server-status.mjs" },
81
+ "server:stop": { path: "scripts/dev-server-stop.mjs" },
82
+ wip: { path: "scripts/wip-ledger-health.mjs" },
83
+ worktree: { path: "scripts/hs-worktree.mjs" },
84
+ "source:status": { path: "scripts/lib/hs-source-preflight.mjs" },
85
+ "source:preflight": { path: "scripts/lib/hs-source-preflight.mjs" },
86
+ "source:commit": { path: "scripts/lib/hs-source-commit.mjs" },
87
+ "source:push": { path: "scripts/lib/hs-source-push.mjs" },
88
+ "source:log": { path: "scripts/lib/hs-source-log.mjs" },
89
+ // `hs source identity` — the ONE sanctioned git-identity writer (human-only
90
+ // set/clear, read-only show). Incident 2026-09-19, vault lesson
91
+ // `when-git-says-author-identity-unknown`; docs/admin/workflow/git-identity.md.
92
+ "source:identity": { path: "scripts/lib/hs-source-identity.mjs" },
93
+ view: { path: "scripts/hs-view.mjs" },
94
+ capture: { path: "scripts/hs-capture-cli.mjs" },
95
+ companion: { path: "scripts/hs-companion.mjs" },
96
+ next: { path: "scripts/hs-next.mjs" },
97
+ mode: { path: "scripts/hs-mode.mjs" },
98
+ gateway: { path: "scripts/hs-gateway.mjs" },
99
+ settings: { path: "scripts/hs-settings.mjs" },
100
+ classifier: { path: "scripts/hs-classifier.mjs" },
101
+ auto: { path: "scripts/hs-auto.mjs" },
102
+ "plugin:launch-mcp": { path: "plugins/heyspex/bin/launch-mcp.mjs" },
103
+ };
104
+
105
+ /**
106
+ * Neon control-plane verbs ([[HS-EPIC-CLMW-hs-cli-mcp-wrappers-for-control-plane]]):
107
+ * each namespace maps to a spex-console TS entry, dispatched via
108
+ * `dispatchSpexConsoleEntry` in `main()` below. Hoisted to module scope (was
109
+ * previously declared inline inside `main()`) and exported alongside
110
+ * `SPAWN_TARGETS` so `BUILTIN_VERBS` below — and anything else that needs the
111
+ * full verb manifest, e.g. `scripts/verify-verb-sweep.mjs` — can derive these
112
+ * eight verb names from the dispatcher's own table instead of hand-copying it.
113
+ */
114
+ export const CONTROL_ENTRYPOINTS = {
115
+ pin: "hs-pin.ts",
116
+ claim: "hs-claim.ts",
117
+ handoff: "hs-handoff.ts",
118
+ events: "hs-events.ts",
119
+ "export-events": "hs-export-events.ts",
120
+ prereq: "hs-prereq.ts",
121
+ queue: "hs-queue.ts",
122
+ // `hs model` - one read-only list of every seat that has a model behind it
123
+ // (launch seats from settings.json, SpexFleet seats from routing-table.json).
124
+ model: "hs-model.ts",
125
+ // `hs trash` - the bin every spec delete soft-deletes into. Nothing looked
126
+ // at it before, so deleted specs accumulated on disk forever.
127
+ trash: "hs-trash.ts",
128
+ // `hs admin test-user create` — mint one throwaway test customer against a
129
+ // stated environment ([[HS-4Z1N-SMXX-EPIC-hs-admin-test-user-create-one-command]]).
130
+ // Wraps POST /api/admin/users/create, so it belongs with the other verbs
131
+ // that are thin shells over a console endpoint.
132
+ admin: "hs-admin.ts",
133
+ // `hs seat` — read/add/update/disable a SpexFleet seat, matching
134
+ // /api/seat-registry's operations 1:1 ([[HS-W2H1-NYPR-TASK-create-the-api-endpoint-that-lets-a]]
135
+ // of epic [[HS-RJVM-NYPR-EPIC-seat-registry-every-role-spexfleet-runs]]).
136
+ // Reads/writes dev-factory/seats.json directly through `@/lib/seat-registry`
137
+ // — the same core the API route calls — so it works with the console
138
+ // stopped, same shape as `hs model` / `hs registry`.
139
+ seat: "hs-seat.ts",
140
+ // `hs voice` — add/build/show/set/test/refresh/export a coded voice profile,
141
+ // matching /api/voice's operations 1:1
142
+ // ([[HS-PQPJ-DOJY-TASK-build-the-hs-voice-cli-and-matching]] of epic
143
+ // [[HS-8PG1-DOJY-EPIC-voice-as-a-deterministic-system-a-coded]]). Reads/
144
+ // writes .heyspex/voice-profiles directly through @/lib/voice-cli — the
145
+ // same core the API route calls — so it works with the console stopped,
146
+ // same shape as `hs model` / `hs seat`.
147
+ voice: "hs-voice.ts",
148
+ };
149
+
150
+ /**
151
+ * Every top-level `hs <namespace>` verb `main()` recognizes below, listed in
152
+ * the same order as their dispatch branches — the single manifest
153
+ * [[HS-G65Z-TASK-every-hs-verb-sweeps-clean-against-a]]'s
154
+ * `scripts/verify-verb-sweep.mjs` imports to enumerate what to sweep, instead
155
+ * of hand-maintaining a second, driftable copy of this list in the sweep
156
+ * script. Deleting a verb's dispatch branch above and its entry here (both in
157
+ * this file) is enough to drop it from the sweep with no sweep-script edit.
158
+ * Does NOT distinguish spawn-backed vs. spex-console-TS-backed vs. bundled —
159
+ * `SPAWN_TARGETS` above already carries that detail for the verbs it covers;
160
+ * this list is deliberately just "what does `hs` accept as argv[0]".
161
+ */
162
+ export const BUILTIN_VERBS = [
163
+ "init",
164
+ "install",
165
+ "join",
166
+ "invite",
167
+ "setup",
168
+ "skills",
169
+ "vault",
170
+ "workspace",
171
+ "login",
172
+ "logout",
173
+ "whoami",
174
+ "connect",
175
+ "doctor",
176
+ "index",
177
+ "pull",
178
+ "milestone",
179
+ "factory",
180
+ "code-watcher",
181
+ "server",
182
+ "wip",
183
+ "worktree",
184
+ "source",
185
+ "view",
186
+ "dev",
187
+ "defer",
188
+ "undefer",
189
+ "spawn",
190
+ "capture",
191
+ "companion",
192
+ "registry",
193
+ "env",
194
+ "next",
195
+ "mode",
196
+ "gateway",
197
+ "settings",
198
+ "classifier",
199
+ "auto",
200
+ "mcp",
201
+ "test",
202
+ "harness",
203
+ "hooks",
204
+ "bench",
205
+ "check",
206
+ "revalidate",
207
+ "agent",
208
+ "chat",
209
+ "trust",
210
+ "doctrine",
211
+ ...Object.keys(CONTROL_ENTRYPOINTS),
212
+ ];
213
+
214
+ /**
215
+ * Does this `hs next ...` invocation need the console? Mirror of
216
+ * `isNextConsoleCommand` in `spex-console/src/lib/cli/next.ts` - that module
217
+ * owns the rule; this copy exists because the dispatcher must choose a half
218
+ * BEFORE spawning either one, and cannot import TypeScript. Keep the two in
219
+ * step: any launch/settings flag added there belongs here too.
220
+ */
221
+ export function isNextConsoleArgv(args) {
222
+ if (
223
+ args[0] === "model" ||
224
+ args[0] === "permission-mode" ||
225
+ args[0] === "claim" ||
226
+ args[0] === "front"
227
+ )
228
+ return true;
229
+ return args.some(
230
+ (a) =>
231
+ a === "--launch" ||
232
+ a === "--force" ||
233
+ a === "--claim" ||
234
+ a === "--no-claim" ||
235
+ a === "--model" ||
236
+ a.startsWith("--model=") ||
237
+ a === "--permission-mode" ||
238
+ a.startsWith("--permission-mode="),
239
+ );
240
+ }
241
+
242
+ /** Resolve a SPAWN_TARGETS entry to an absolute path under PKG_ROOT. */
243
+ function spawnTargetPath(key) {
244
+ const entry = SPAWN_TARGETS[key];
245
+ if (!entry) throw new Error(`hs: unknown SPAWN_TARGETS key "${key}"`);
246
+ return path.join(PKG_ROOT, ...entry.path.split("/"));
247
+ }
248
+
25
249
  // Captured before anything in this process could ever chdir (it doesn't
26
250
  // today, but every dispatched entry below is spawned with its own `cwd`
27
251
  // override — see dispatchSpexConsoleEntry). This is the ONE true record of
@@ -42,22 +266,167 @@ const INVOKED_CWD = process.cwd();
42
266
  process.env.HS_INVOKED_CWD = INVOKED_CWD;
43
267
 
44
268
  /**
45
- * Walk up from `start` until we find a directory containing `specs/`.
46
- * That directory is the vault root.
269
+ * Walk up from `start` until we find a vault root — a directory containing
270
+ * a `heyspex.yaml` marker file OR a `specs/` dir. Delegates the "is this a
271
+ * vault root" test to the shared `isVaultRoot` helper (same one the MCP
272
+ * resolver's cwd walk uses) so the two walks can never disagree about what
273
+ * counts as a vault ([[HS-GKAI]]).
47
274
  */
48
- function findVaultRoot(start) {
275
+ export function findVaultRoot(start, stopAt) {
49
276
  let dir = path.resolve(start);
277
+ const boundary = stopAt ? path.resolve(stopAt) : null;
50
278
  while (true) {
51
- const candidate = path.join(dir, "specs");
52
- if (existsSync(candidate) && statSync(candidate).isDirectory()) {
279
+ // A SATELLITE repo (a code repo attached to an existing vault -- see
280
+ // spex-console/src/lib/cli/satellite.ts) carries no marker of its own and
281
+ // names its vault in its MCP manifests instead. Checked FIRST at each
282
+ // level so an explicit attachment beats a vault that merely happens to
283
+ // sit further up the tree, and so `hs` in a satellite follows THAT vault
284
+ // rather than whatever the machine-global active pointer last named.
285
+ const pinned = readSatellitePin(dir);
286
+ if (pinned) {
287
+ return pinned;
288
+ }
289
+ if (isVaultRoot(dir)) {
53
290
  return dir;
54
291
  }
292
+ // `stopAt` is a TEST-ONLY walk boundary (mirrors vault-resolve.mjs's
293
+ // `cwdWalkRoot`) — checked, then the walk stops, never climbing past it.
294
+ // Unset in production, so the walk always reaches the real filesystem
295
+ // root; tests fence it inside a disposable fixture so an unrelated
296
+ // ancestor higher up the real tree (e.g. a stray `specs/` dir left over
297
+ // in a temp folder) cannot produce a false match ([[HS-0I1G]]).
298
+ if (boundary && dir === boundary) return null;
55
299
  const parent = path.dirname(dir);
56
300
  if (parent === dir) return null;
57
301
  dir = parent;
58
302
  }
59
303
  }
60
304
 
305
+ /**
306
+ * Registry fallback for when the specs/ walk-up finds nothing — since the
307
+ * repo split (2026-08-31) the vault lives outside the code repo, so `hs`
308
+ * run from the code repo would otherwise exit "no vault detected".
309
+ * Delegates to the shared resolver (same one the MCP server trusts) but
310
+ * only accepts a result that names a real vault: the resolver's last-resort
311
+ * cwd fallback is rejected so the "no vault detected" error below still
312
+ * fires when there is genuinely no vault anywhere.
313
+ *
314
+ * The test is deliberately "anything but `cwd-heuristic`", so every real
315
+ * tier passes it: `explicit`, `account-pointer`, `env-vault-root`,
316
+ * `env-vault-dir`, and `satellite-pin` (an on-disk manifest attachment found
317
+ * by the resolver's tier-3 walk -- that tier reported the untrue
318
+ * `env-vault-root` until 2026-09-18; both labels are accepted here, so the
319
+ * rename changed nothing). `cwd-marker` and `marker-registry-mismatch` (a
320
+ * heyspex.yaml `vault_id` named the vault, [[HS-VVNQ]]) pass too.
321
+ */
322
+ export function findRegistryVaultRoot(options = {}) {
323
+ let resolved;
324
+ try {
325
+ resolved = resolveActiveVault(options);
326
+ } catch {
327
+ return null;
328
+ }
329
+ if (!resolved || resolved.source === "cwd-heuristic") return null;
330
+ if (!existsSync(resolved.specsDirAbs)) return null;
331
+ return resolved.repoRootAbs;
332
+ }
333
+
334
+ /**
335
+ * The vault-resolution ladder `main()` runs at the top of every invocation,
336
+ * factored out so it is unit-testable without spawning a child process
337
+ * ([[HS-0I1G-B0IT-TASK-track-and-announce-the-registry-active]]).
338
+ *
339
+ * Reports `usedRegistryFallback: true` only when NONE of `HS_VAULT_ROOT`, an
340
+ * explicit `--target`-equivalent, or a marker/satellite pin found walking up
341
+ * from `cwd` answered, and `findRegistryVaultRoot()` (the machine's
342
+ * last-active vault, `~/.heyspex/registry.json`) is what resolved the root.
343
+ * `hs install` uses that flag to warn (always) and — when `--register-vault`
344
+ * is given without `--dry-run` — refuse, so a customer onboarding an empty
345
+ * directory doesn't silently write into whatever vault this machine last had
346
+ * active in some other terminal/console tab.
347
+ *
348
+ * `stopAt` is a TEST-ONLY walk-boundary fence, threaded through to both
349
+ * `findVaultRoot`'s own walk and `resolveActiveVault`'s inner cwd walk (via
350
+ * its `cwdWalkRoot` option) — production callers (`main()`) omit it.
351
+ *
352
+ * @param {string} cwd
353
+ * @param {NodeJS.ProcessEnv} env
354
+ * @param {string} [stopAt]
355
+ * @returns {{ vaultRoot: string|null, usedRegistryFallback: boolean }}
356
+ */
357
+ export function resolveVaultRootForMain(cwd, env, stopAt) {
358
+ if (env.HS_VAULT_ROOT) {
359
+ return { vaultRoot: env.HS_VAULT_ROOT, usedRegistryFallback: false };
360
+ }
361
+ const walkedRoot = findVaultRoot(cwd, stopAt);
362
+ if (walkedRoot) {
363
+ return { vaultRoot: walkedRoot, usedRegistryFallback: false };
364
+ }
365
+ // Same ONE order as both resolvers ([[HS-VVNQ]], shared fixture
366
+ // spex-console/src/lib/vault-resolve/precedence-fixtures.json):
367
+ // `HS_VAULT_ROOT` (this CLI's explicit tier) → the walk above (satellite
368
+ // pin, then marker) → `HEYSPEX_VAULT_ROOT`/`HEYSPEX_VAULT_DIR` env → the
369
+ // registry pointer, the last two via the shared resolver. `cwd` is passed
370
+ // explicitly so the resolver's own walk starts where ours did — left to
371
+ // its default it would start from `CLAUDE_PROJECT_DIR`, and a marker found
372
+ // THERE (a different folder) would be mislabelled a registry fallback.
373
+ const registryRoot = findRegistryVaultRoot(
374
+ stopAt ? { cwd, cwdWalkRoot: stopAt } : { cwd },
375
+ );
376
+ return {
377
+ vaultRoot: registryRoot,
378
+ usedRegistryFallback: Boolean(registryRoot),
379
+ };
380
+ }
381
+
382
+ /** True when `argv` carries `--attach-vault` (bare or `=value` form). */
383
+ export function hasAttachVaultFlag(argv) {
384
+ return argv.some(
385
+ (a) => a === "--attach-vault" || a.startsWith("--attach-vault="),
386
+ );
387
+ }
388
+
389
+ /**
390
+ * True when `argv` carries an explicit `--target` (bare or `=value` form) —
391
+ * mirrors `install.ts`'s own `parseInstallArgs` flag test. `hs-install.ts`'s
392
+ * `resolveInstallTargetDir` already honors `--target` over `HS_VAULT_ROOT`
393
+ * unconditionally, so a caller who named their target explicitly is never
394
+ * actually affected by the registry fallback `bin/hs.mjs` computed for
395
+ * `HS_VAULT_ROOT` — warning or refusing anyway would be a false alarm about a
396
+ * value the install never consults.
397
+ */
398
+ export function hasExplicitTargetFlag(argv) {
399
+ return argv.some((a) => a === "--target" || a.startsWith("--target="));
400
+ }
401
+
402
+ /**
403
+ * The warning `hs install` prints (to stderr, in both `--dry-run` and real
404
+ * output) when the target it is about to use came ONLY from the registry
405
+ * active-vault fallback — i.e. cwd sits under no vault at all and this
406
+ * machine's last-active vault pointer answered instead.
407
+ */
408
+ export function registryFallbackWarningLine(cwd, vaultRoot) {
409
+ return (
410
+ `hs install: no vault found in or above ${cwd} — falling back to this ` +
411
+ `machine's last-active vault (${vaultRoot}, from ~/.heyspex/registry.json). ` +
412
+ "Pass --target <dir> to install into this directory instead.\n"
413
+ );
414
+ }
415
+
416
+ /**
417
+ * The refusal `hs install --register-vault` (without `--dry-run`) prints and
418
+ * exits non-zero on instead of registering, when the fallback above is what
419
+ * answered — registering here would file the machine's last-active vault
420
+ * under a fresh pointer, not the one the caller meant.
421
+ */
422
+ export function registryFallbackRefusalLine() {
423
+ return (
424
+ "hs install: refusing to run --register-vault from an empty directory that " +
425
+ "resolved only through the registry active-vault fallback. Pass --target <dir> " +
426
+ "naming the vault to register, or --dry-run to preview first.\n"
427
+ );
428
+ }
429
+
61
430
  /**
62
431
  * Extract the global `hs --vault <id> <subcommand>` one-shot override
63
432
  * ([[HS-Q1HZ-TASK-hs-vault-cli-list-register-switch-active]]). `--vault`
@@ -93,8 +462,14 @@ function usage() {
93
462
  "",
94
463
  "Usage:",
95
464
  " hs login [--console-url <url>] [--label <t>] [--json] Sign in via your browser (no code to type)",
465
+ " hs login --switch-account [<id|email>] | --create-account Switch to a stored account, or choose/create one in the browser",
466
+ " hs login --migrate [--dry-run] Re-key a single-account credentials file under its confirmed account",
467
+ " hs whoami [--account <id|email>] [--json] Which account this machine is signed in as (email, id, source, console)",
468
+ " hs logout [--account <id|email>] [--json] Sign one account out of this machine (default: the current one)",
96
469
  " hs init [--console-url <url>] [--force] Write the heyspex.yaml vault marker",
97
470
  " hs install [--target <dir>] [--platform <dir>] Bootstrap vault + Cursor/Claude MCP",
471
+ " hs install --attach-vault <id|path> Wire THIS code repo to an existing vault",
472
+ " (satellite: no specs/, no heyspex.yaml here)",
98
473
  " hs invite [<name>] Mint a short pairing code (other computer: hs join CODE)",
99
474
  " hs join <code> Join with a pairing code (LAN auto-find; no IP to type)",
100
475
  " hs join [<repo>] [--dir d] [--code c] [--at url] [--profile p] [--skip steps] [--json] [--yes] Fresh-machine onboarding (clone → toolchain → deps → doctor → agent layer → secrets → verify)",
@@ -108,20 +483,33 @@ function usage() {
108
483
  " hs dev [mode:local|mode:cloud|mode:prod] Dev/prod mode + local/cloud reliance (status when bare)",
109
484
  " hs setup claude|cursor [--check|--uninstall] Install agent lifecycle hooks",
110
485
  " hs skills sync|check Propagate skills to Cursor/Codex/Claude (cwd-safe)",
486
+ " hs skills add <module-path> --name <n> --trigger </slash> [--export <n>] [--description <t>] Register a skill (extension seam)",
487
+ " hs skills remove <name> Remove a customer-registered skill",
488
+ " hs skills list [--json] List registered skills (built-ins first, locked)",
111
489
  " hs index rebuild [--verbose] [--dry-run] Full idempotent rebuild of the index",
490
+ " hs index reconcile [--apply] [--cap-docs keep-out|index] Per-spec newest-wins between disk and cloud (reports unless --apply)",
112
491
  " hs pull [--apply] [--only <id>] [--overwrite] Bring cloud-written specs down to the disk vault (reports unless --apply)",
492
+ " hs source status|preflight|commit|push Local git: tree status, path-scoped checks, commit, safe push",
493
+ " hs source identity [show|set|clear] Git author identity: show (read-only) / set, clear (HUMAN-ONLY)",
113
494
  " hs milestone show <id> [--json] Show a milestone (flags needs-breakdown when epicless)",
114
495
  " hs factory status|fleet|results [--json] Dev-Factory read-only status (fleet, routing, calibrations, results)",
115
496
  " hs factory run status [--json] Live assisted/autonomous dispatcher run (pids, queue, stop hints)",
116
497
  " hs code-watcher scan [--json] Capture build-correctness failures (tsc) → Resolution",
117
498
  " hs server status [--json] Ground-truth dev-server status (running? which port[s]?)",
499
+ " hs wip status [--json] WIP ledger health report (lines vs 600-line budget, age tiers, archive)",
500
+ " hs wip restore <id> [--json] [--force] Restore an archived WIP ledger entry back onto the live ledger",
501
+ " hs worktree list|alloc|release|reap [--json] [--dry-run] Session/build worktree registry (HS-03DO)",
118
502
  " hs view <surface> [spec-id] [--width N --height N --browser edge|chrome] Open one surface in a chromeless app-mode window",
119
503
  " hs server stop [--json] Stop the spex-console dev server (any terminal)",
504
+ " hs source status|preflight|commit|push SpexSource local git operations",
505
+ " hs source identity [show|set|clear] Git author identity (set/clear are human-only; see hs source --help)",
120
506
  " hs defer <id> [--until <when>] [--json] Time-shift a spec (defer_until)",
121
507
  " hs undefer <id> [--json] Clear a spec's defer_until",
122
508
  " hs spawn [<id>] [--dry-run] [--json] Launch a fresh /hs-next agent in a new terminal",
123
- " hs capture \"<text>\"|--clipboard|--file <p> [--add] Capture → console drafts (--add chains a group)",
509
+ ' hs capture "<text>"|--clipboard|--file <p> [--add] Capture → console drafts (--add chains a group)',
124
510
  " hs companion start|stop|status|build Desktop companion tray app",
511
+ " hs companion settings [list|get <k>|set <k> <v>] [--json] Every Companion setting (console-backed)",
512
+ " hs companion mute|unmute|volume|orb|wake|activation|hotkey [<v>] Setting shortcuts (omit <v> to read)",
125
513
  " hs registry list [--json] List launch registry entries",
126
514
  " hs env init|set|list|run|export|rotate Encrypted env vault (ADR-0020)",
127
515
  " hs env profiles | key export|import Named profiles + second-machine key transfer",
@@ -129,23 +517,52 @@ function usage() {
129
517
  " hs settings list|get|set Read/change workflow settings (.heyspex/config.yaml)",
130
518
  " hs classifier stats [--json] [--recent N] Intent Classifier decision stats (read-only)",
131
519
  " hs next [spec_id] [--summary] [--json] Resolve on-deck brief (MCP fallback)",
520
+ " hs next <id> --launch [--dry-run] [--claim] [--backend <anthropic|ollama|deepseek>] Launch a session on that spec",
521
+ " hs next model|permission-mode [<kind>] [<v>] Per-kind launch defaults",
522
+ " hs next backend [<kind>] [<v>] Per-kind launch backend default (anthropic|ollama|deepseek)",
523
+ " hs next claim [on|off] Auto-claim vs ask-first default",
524
+ " hs next front [handheld|assist|auto] Default front: one task with you, or a dispatcher run",
525
+ " hs next assist|auto <milestone...> --launch Arm a dispatcher run on one or more milestones",
526
+ " hs model [<seat>] [--launch|--fleet] Every seat and the model set for it",
527
+ " hs seat list|add|update|disable [--json] Read/add/update/disable a seat in the registry (dev-factory/seats.json)",
528
+ " hs voice add|build|show|set|test|refresh|export Build and manage a coded voice profile",
529
+ " hs trash [list|restore|empty] The bin spec deletes go into",
132
530
  " hs auto [filename] [--json] Resolve /hs-auto dispatch plan",
133
531
  " hs mcp call <tool> [--arg key=value ...] [--args '<json>'] [--args-file <path>] [--json] One-shot MCP tool",
134
532
  " hs mcp pair <code> [--console-url <url>] Redeem a console pairing code (cross-machine fallback for hs login)",
533
+ " hs mcp install --remote [--write] [--json] Write/print .mcp.json for the cloud (remote) MCP endpoint",
135
534
  " hs test run <id|manifest> [--format ...] Run open-format API tests (Hurl/.http) per AC",
136
535
  " hs test gate [dir] [--report <j>] [--threshold n] Mutation-testing gate — reject suites that kill no mutants",
137
536
  " hs test conform <spec> --url <u> | --report <x> OpenAPI conformance / docs↔impl drift (Schemathesis)",
537
+ " hs chat [--model <name>] [--script <file>] [--seat <seat>] HeySpex's own chat front (any model); !<cmd> or /shell for CLI passthrough",
538
+ " hs trust list List spexcode's stored folder-trust decisions",
539
+ " hs trust revoke [path] Clear one folder's trust decision (default: cwd) so the next launch asks again",
540
+ " hs doctrine extract [--since <iso>] [--project <name>] [--dry-run] Stream every session/taste source into .heyspex/doctrine/observations.jsonl",
541
+ " hs doctrine learn [--full] [--max-batches <n>] [--dry-run] Extract, then synthesize into spexharness/doctrine/SPEXCODE-DOCTRINE.md/json",
542
+ " hs doctrine show Print the current SPEXCODE-DOCTRINE.md",
543
+ " hs harness gates [--json] List every SpexHarness gate: id, event, severity, last fixture result",
544
+ " hs hooks add <module-path> --event <event> [--id <id>] [--priority <n>] Register a hook (extension seam)",
545
+ " hs hooks remove <id> Remove a customer-registered hook",
546
+ " hs hooks list [--json] List registered hooks (built-ins first, locked)",
138
547
  " hs check run <id|manifest> [--json] Verify acceptance criteria vs the frozen checks manifest (per-AC pass/fail)",
548
+ " hs check judge <id> [--session <id>] [--dry-run] Decorrelated judge seat: verdicts on method:human criteria, posts to the verification API",
139
549
  " hs revalidate [--json] [--apply] Re-run the frozen checks of already-stamped work; report what has decayed",
140
550
  " hs agent import <collection> <role> Import a community agent into a type:agent vault spec",
141
551
  " hs agent list [collection] List vendored agent collections / roles",
142
552
  "",
553
+ " node dev-factory/spexcrawl/spexcrawl-cli.mjs search|scrape|crawl SpexCrawl — web",
554
+ " search/scrape/crawl CLI (standalone script,",
555
+ " not an `hs` verb; also reachable from the",
556
+ " Companion)",
557
+ "",
143
558
  "Control plane (Neon — requires a reachable console):",
144
559
  " hs pin get|set <id>|clear Read/set/clear the on-deck pin",
145
560
  " hs claim acquire|release <id> | list Atomic spec claims",
146
561
  " hs claim progress <id> --stage <verb> Report live work-stage",
147
- " hs handoff record|load [--lane <k>] Resume-context coordination",
562
+ " hs handoff record|load [--lane <k>] Resume-context coordination (never launches)",
563
+ " hs handoff <CODE> [--no-launch] Resolve + LAUNCH the lane's resume by default",
148
564
  " hs events show <id> [--kind --since] Per-spec audit log",
565
+ " hs admin test-user create --env <e> Mint one throwaway test customer",
149
566
  " hs export-events [--since <date>] Idempotent audit export → specs/events/",
150
567
  " hs prereq <blocked-id> [<action> <prereq>] Cross-milestone prerequisite resolution",
151
568
  " hs queue submit|next|claim|complete|fail|list Fleet-queue dispatch (Neon, direct — no console hop)",
@@ -155,7 +572,13 @@ function usage() {
155
572
  " hs --version Show CLI version",
156
573
  "",
157
574
  "Vault root is detected by walking up from the current directory until a",
158
- "specs/ folder is found. Override with HS_VAULT_ROOT.",
575
+ "heyspex.yaml marker or a specs/ folder is found, then falling back to the",
576
+ "registry's active vault (~/.heyspex/registry.json). Override with HS_VAULT_ROOT.",
577
+ "When hs install resolves ONLY through that registry fallback (a fully empty",
578
+ "cwd — no heyspex.yaml/specs/ found walking up, no HS_VAULT_ROOT, no --target),",
579
+ "it warns, naming the fallback vault, in both --dry-run and real output; and",
580
+ "`hs install --register-vault` without --dry-run in that case refuses instead of",
581
+ "registering, asking for --target. --attach-vault (satellite mode) is exempt.",
159
582
  "",
160
583
  ].join("\n"),
161
584
  );
@@ -169,9 +592,13 @@ function usage() {
169
592
  * Three cases, in order ([[HS-JOQX-0JNK-TASK-published-package-runs-hs-init-and-hs]]):
170
593
  * 1. Monorepo checkout: `.ts` source AND `tsx` both present — run via tsx,
171
594
  * exactly as before this task.
172
- * 2. Published install of a BUNDLED verb (currently only `hs-init.ts` /
173
- * `hs-install.ts` — see `scripts/build-cli-bundle.mjs`'s doc comment
174
- * for why only these two): neither the source nor tsx exists, but
595
+ * 2. Published install of a BUNDLED verb — currently `hs-init.ts`,
596
+ * `hs-install.ts`, `hs-claim.ts`, `hs-check.ts`, `hs-env.ts`,
597
+ * `hs-connect.ts`, `hs-join.ts`, `hs-vault.ts`, `hs-workspace.ts`,
598
+ * `hs-defer.ts`, `hs-vault-pull.ts`, `hs-dev.ts`, `hs-index-rebuild.ts`,
599
+ * `hs-index-reconcile.ts` — see `scripts/build-cli-bundle.mjs`'s
600
+ * `ENTRIES` array and doc comment for the authoritative list and why
601
+ * each qualifies: neither the source nor tsx exists, but
175
602
  * `dist/cli/<name>.mjs` does — run the plain-JS bundle directly with
176
603
  * `node`, no tsx/TypeScript involved.
177
604
  * 3. Neither exists: the package install is incomplete (a verb this
@@ -256,7 +683,7 @@ function dispatchSpexConsoleEntry(entryFile, args, vaultRoot, extraEnv = {}) {
256
683
  * it), so there is nothing to check about `hs` other than reporting its
257
684
  * version.
258
685
  */
259
- function runStandaloneDoctor(args) {
686
+ function runStandaloneDoctor(args, vaultRoot) {
260
687
  const jsonOut = args.includes("--json");
261
688
  let version = "0.0.0";
262
689
  try {
@@ -292,6 +719,46 @@ function runStandaloneDoctor(args) {
292
719
  if (runtimeFilesRow.status !== "ok") report.ok = false;
293
720
  report.checks.push(runtimeFilesRow);
294
721
 
722
+ // `vault-config` ([[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]]
723
+ // fix 5): the vault's `.heyspex/config.yaml` (ADR-0009: vault root, git-
724
+ // tracked) must exist — without it every settings reader runs on defaults.
725
+ // `vaultRoot` is main()'s resolution (HS_VAULT_ROOT → walk-up → registry);
726
+ // null means no vault at all, which the row reports as its own failure.
727
+ const vaultConfigRow = vaultConfigCheckRow(vaultRoot ?? null);
728
+ if (vaultConfigRow.status !== "ok") report.ok = false;
729
+ report.checks.push(vaultConfigRow);
730
+
731
+ // `vault-template` (HS-KH0Q): shipped core docs + template_version pin present.
732
+ const templateRow = templateCheckRow(vaultRoot ?? null);
733
+ if (templateRow.status === "fail") report.ok = false;
734
+ report.checks.push(templateRow);
735
+
736
+ // `connection` ([[HS-W73V-WW9R-TASK-make-both-doctor-shapes-fail-on-cloud]],
737
+ // invariants 8 and 10): the BUNDLED `hs connect --check --json` from the
738
+ // invoked cwd, folded by the SAME function the full doctor uses
739
+ // (`foldConnectionReport`), so a customer machine gets the local-vs-cloud
740
+ // rows too. A disagreeing row fails the run with its repair command; an
741
+ // unreadable cloud side (offline, signed out) is a warn and leaves `ok`
742
+ // alone. The check reads the console with GET only. Its subject is the repo
743
+ // the caller stands in (the walk-up from the invoked cwd, else the cwd
744
+ // itself), NOT `vaultRoot`: that can be an inherited HS_VAULT_ROOT or the
745
+ // registry fallback, i.e. ANOTHER repo (seen when `hs connect`'s acceptance
746
+ // pass ran this doctor in a fresh repo and it audited the previous one).
747
+ const connectSubject = findVaultRoot(INVOKED_CWD) ?? INVOKED_CWD;
748
+ const connection = foldConnectionReport(
749
+ runBundledConnectCheck(PKG_ROOT, {
750
+ cwd: INVOKED_CWD,
751
+ env: {
752
+ ...process.env,
753
+ HS_INVOKED_CWD: INVOKED_CWD,
754
+ HS_VAULT_ROOT: connectSubject,
755
+ },
756
+ }),
757
+ );
758
+ if (!connection.ok) report.ok = false;
759
+ report.checks.push(...connection.rows);
760
+ report.connection = connection.json;
761
+
295
762
  const hsLinkLib = path.join(PKG_ROOT, "scripts", "lib", "hs-link.mjs");
296
763
  if (existsSync(hsLinkLib)) {
297
764
  // Deliberately synchronous top-level work below via a nested async IIFE
@@ -340,8 +807,15 @@ function printStandaloneDoctorReport(report, jsonOut) {
340
807
  "HeySpex setup check — published-package install (not a source clone)\n\n",
341
808
  );
342
809
  for (const check of report.checks) {
343
- const label = check.status === "ok" ? " ok " : " FAIL ";
344
- process.stdout.write(`[ ${label} ] ${check.detail}\n`);
810
+ const label =
811
+ check.status === "ok"
812
+ ? " ok "
813
+ : check.status === "warn"
814
+ ? " warn "
815
+ : " FAIL ";
816
+ process.stdout.write(
817
+ `[ ${label} ] ${check.label ? `${check.label}: ` : ""}${check.detail}\n`,
818
+ );
345
819
  if (check.human) process.stdout.write(` ${check.human}\n`);
346
820
  }
347
821
  process.stdout.write(
@@ -384,20 +858,65 @@ function main(argv) {
384
858
  const command = argv[1];
385
859
  const rest = argv.slice(2);
386
860
 
387
- const vaultRoot = process.env.HS_VAULT_ROOT || findVaultRoot(process.cwd());
861
+ const { vaultRoot, usedRegistryFallback } = resolveVaultRootForMain(
862
+ process.cwd(),
863
+ process.env,
864
+ );
388
865
 
389
- // `hs init` writes the top-level heyspex.yaml vault marker
390
- // ([[HS-EPIC-AEHR-audit-export-heyspex-yaml-and-heyspex-retirement]] §2). It
391
- // may run on a fresh directory that has no specs/ yet, so it bypasses the
392
- // hard vault gate below and falls back to the current directory.
866
+ // `hs init` writes the top-level heyspex.yaml vault marker AND scaffolds
867
+ // specs/ ([[HS-EPIC-AEHR-audit-export-heyspex-yaml-and-heyspex-retirement]]
868
+ // §2). It always acts on the caller's invocation directory (`INVOKED_CWD`,
869
+ // captured before any dispatch below runs) — NEVER on `vaultRoot`'s
870
+ // walk-up/registry result. `hs init` may legitimately run *inside* a stray
871
+ // ancestor's `specs/` folder (a half-set-up nested checkout, a doc repo
872
+ // that happens to have a `specs/` dir, etc.); using `vaultRoot` here would
873
+ // silently write the marker into that unrelated ancestor instead of where
874
+ // the user actually typed `hs init` (HS-HISX: first fresh-machine walkthrough
875
+ // break — the vault marker landed in the wrong folder).
876
+ // HS_PKG_ROOT is forwarded so the entrypoint can resolve the vendored
877
+ // `.heyspex/config.yaml` seed template (`vendor/heyspex-mcp/config-seed.yaml`)
878
+ // from the PACKAGE root, never from the caller's cwd (HS-4C76 fix 4).
393
879
  if (namespace === "init") {
394
- dispatchSpexConsoleEntry("hs-init.ts", argv, vaultRoot || process.cwd());
880
+ dispatchSpexConsoleEntry("hs-init.ts", argv, INVOKED_CWD, {
881
+ HS_PKG_ROOT: PKG_ROOT,
882
+ });
395
883
  return;
396
884
  }
397
885
 
398
886
  // `hs install` bootstraps a customer/project vault + agent MCP wiring. May run
399
887
  // on a fresh directory with no specs/ yet (like init).
888
+ //
889
+ // [[HS-0I1G-B0IT-TASK-track-and-announce-the-registry-active]]: when the
890
+ // resolution above bottomed out at the registry active-vault fallback
891
+ // (nothing set HS_VAULT_ROOT, and no marker/satellite pin was found walking
892
+ // up from cwd), say so — silently bootstrapping into whatever vault this
893
+ // machine last had active in some OTHER terminal/console tab is exactly the
894
+ // wrong-vault failure this task closes. Two flags are exempt because
895
+ // `hs-install.ts` never actually consults the vaultRoot this fallback
896
+ // computed for them, so warning/refusing about it would be a false alarm:
897
+ // `--attach-vault` (satellite mode) — its target resolution
898
+ // (resolveInstallTargetDir's satellite branch) ignores HS_VAULT_ROOT
899
+ // entirely and reads HS_INVOKED_CWD instead (and `--register-vault` is
900
+ // already mutually exclusive with it via assertSatelliteFlagCombination, so
901
+ // the refusal below can never apply either way) — and an explicit
902
+ // `--target`, which resolveInstallTargetDir honors over HS_VAULT_ROOT
903
+ // unconditionally (see hasExplicitTargetFlag's doc comment).
400
904
  if (namespace === "install") {
905
+ if (
906
+ usedRegistryFallback &&
907
+ !hasAttachVaultFlag(argv) &&
908
+ !hasExplicitTargetFlag(argv)
909
+ ) {
910
+ process.stderr.write(
911
+ registryFallbackWarningLine(process.cwd(), vaultRoot),
912
+ );
913
+ const isDryRun = argv.includes("--dry-run");
914
+ const isRegisterVault = argv.includes("--register-vault");
915
+ if (isRegisterVault && !isDryRun) {
916
+ process.stderr.write(registryFallbackRefusalLine());
917
+ process.exit(1);
918
+ }
919
+ }
401
920
  dispatchSpexConsoleEntry(
402
921
  "hs-install.ts",
403
922
  argv,
@@ -426,24 +945,28 @@ function main(argv) {
426
945
  if (namespace === "invite") {
427
946
  if (!vaultRoot) {
428
947
  process.stderr.write(
429
- "hs: no vault detected — could not find a specs/ folder " +
430
- "walking up from the current directory. " +
948
+ "hs: no vault detected — could not find a heyspex.yaml marker or a " +
949
+ "specs/ folder walking up from the current directory. " +
431
950
  "Set HS_VAULT_ROOT or run from inside a vault.\n",
432
951
  );
433
952
  process.exit(1);
434
953
  }
435
- dispatchSpexConsoleEntry("hs-env.ts", ["env", "invite", ...argv.slice(1)], vaultRoot);
954
+ dispatchSpexConsoleEntry(
955
+ "hs-env.ts",
956
+ ["env", "invite", ...argv.slice(1)],
957
+ vaultRoot,
958
+ );
436
959
  return;
437
960
  }
438
961
 
439
962
  // `hs setup claude|cursor` — install agent lifecycle hooks into the vault root.
440
963
  if (namespace === "setup") {
441
964
  const agent = command?.toLowerCase();
442
- const SCRIPTS = {
443
- claude: "install-claude-code-hooks.mjs",
444
- cursor: "install-cursor-hooks.mjs",
965
+ const SPAWN_KEYS = {
966
+ claude: "setup:claude",
967
+ cursor: "setup:cursor",
445
968
  };
446
- if (!agent || !Object.hasOwn(SCRIPTS, agent)) {
969
+ if (!agent || !Object.hasOwn(SPAWN_KEYS, agent)) {
447
970
  process.stderr.write(
448
971
  `hs setup: expected \`claude\` or \`cursor\` (got \`${command ?? ""}\`)\n` +
449
972
  " hs setup claude [--check|--uninstall] Claude Code lifecycle hooks\n" +
@@ -454,7 +977,7 @@ function main(argv) {
454
977
  const setupFlags = rest.filter(
455
978
  (a) => a === "--check" || a === "--uninstall",
456
979
  );
457
- const script = path.join(PKG_ROOT, "scripts", SCRIPTS[agent]);
980
+ const script = spawnTargetPath(SPAWN_KEYS[agent]);
458
981
  const target = vaultRoot || process.cwd();
459
982
  const child = spawn(process.execPath, [script, ...setupFlags], {
460
983
  cwd: target,
@@ -464,7 +987,7 @@ function main(argv) {
464
987
  child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
465
988
  child.on("error", (err) => {
466
989
  process.stderr.write(
467
- `hs setup: failed to run ${SCRIPTS[agent]}: ${err.message}\n`,
990
+ `hs setup: failed to run ${SPAWN_TARGETS[SPAWN_KEYS[agent]].path}: ${err.message}\n`,
468
991
  );
469
992
  process.exit(1);
470
993
  });
@@ -472,17 +995,44 @@ function main(argv) {
472
995
  }
473
996
 
474
997
  // `hs skills sync|check` — propagate .commandcode/skills → Cursor/Codex/Claude shims.
998
+ // `hs skills add|remove|list` — a DIFFERENT verb: the extension seam's skill
999
+ // registry ([[HS-8N3K-NYPR-TASK-add-cli-verbs-so-a-developer-can]],
1000
+ // [[HS-8EDJ-NYPR-EPIC-extension-seam-for-other-developers]]), routed to its
1001
+ // own entrypoint below so it never touches the sync/check spawn path.
475
1002
  if (namespace === "skills") {
476
1003
  const action = command?.toLowerCase();
1004
+ if (action === "add" || action === "remove" || action === "list") {
1005
+ dispatchSpexConsoleEntry(
1006
+ "hs-skills.ts",
1007
+ argv,
1008
+ vaultRoot || process.cwd(),
1009
+ );
1010
+ return;
1011
+ }
477
1012
  if (action !== "sync" && action !== "check") {
478
1013
  process.stderr.write(
479
- `hs skills: expected \`sync\` or \`check\` (got \`${command ?? ""}\`)\n` +
480
- " hs skills sync Copy skills + slash shims to all agent harnesses\n" +
481
- " hs skills check Exit 1 if any harness copy is stale\n",
1014
+ `hs skills: expected \`sync\`, \`check\`, \`add\`, \`remove\`, or \`list\` (got \`${command ?? ""}\`)\n` +
1015
+ " hs skills sync Copy skills + slash shims to all agent harnesses\n" +
1016
+ " hs skills check Exit 1 if any harness copy is stale\n" +
1017
+ " hs skills add <module-path> --name <n> --trigger </slash> Register a skill (extension seam)\n" +
1018
+ " hs skills remove <name> Remove a customer-registered skill\n" +
1019
+ " hs skills list [--json] List registered skills\n",
482
1020
  );
483
1021
  process.exit(2);
484
1022
  }
485
- const script = path.join(PKG_ROOT, "scripts", "sync-skills.mjs");
1023
+ const script = spawnTargetPath("skills:sync");
1024
+ if (!existsSync(script)) {
1025
+ // `skills:sync` is a deliberately dev-only SPAWN_TARGETS entry — see its
1026
+ // `reason` above. A published `npm install heyspex` never has
1027
+ // `.commandcode/skills` (the platform's own SSOT content) on disk, so
1028
+ // report that plainly instead of a bare MODULE_NOT_FOUND.
1029
+ process.stderr.write(
1030
+ "hs skills: not available in a published `heyspex` install — this verb " +
1031
+ "propagates the HeySpex platform repo's OWN `.commandcode/skills` " +
1032
+ "source content, which only exists inside a clone of that repo.\n",
1033
+ );
1034
+ process.exit(1);
1035
+ }
486
1036
  const target = vaultRoot || PKG_ROOT;
487
1037
  const childArgs = action === "check" ? ["--check"] : [];
488
1038
  const child = spawn(process.execPath, [script, ...childArgs], {
@@ -532,18 +1082,22 @@ function main(argv) {
532
1082
  // auth verb, so it bypasses the hard vault gate below like init/install —
533
1083
  // signing in is exactly what you do BEFORE you have a vault here.
534
1084
  if (namespace === "login") {
535
- const cliPath = path.join(PKG_ROOT, "vendor", "heyspex-mcp", "cli.mjs");
1085
+ const cliPath = spawnTargetPath("heyspex-mcp-cli");
536
1086
  const loginRoot = vaultRoot || process.cwd();
537
- const child = spawn(process.execPath, [cliPath, "login", ...argv.slice(1)], {
538
- cwd: PKG_ROOT,
539
- stdio: "inherit",
540
- env: {
541
- ...process.env,
542
- HEYSPEX_VAULT_ROOT: loginRoot,
543
- HS_VAULT_ROOT: loginRoot,
544
- HEYSPEX_DISABLE_STDIO_BOOTSTRAP: "1",
1087
+ const child = spawn(
1088
+ process.execPath,
1089
+ [cliPath, "login", ...argv.slice(1)],
1090
+ {
1091
+ cwd: PKG_ROOT,
1092
+ stdio: "inherit",
1093
+ env: {
1094
+ ...process.env,
1095
+ HEYSPEX_VAULT_ROOT: loginRoot,
1096
+ HS_VAULT_ROOT: loginRoot,
1097
+ HEYSPEX_DISABLE_STDIO_BOOTSTRAP: "1",
1098
+ },
545
1099
  },
546
- });
1100
+ );
547
1101
  child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
548
1102
  child.on("error", (err) => {
549
1103
  process.stderr.write(`hs: failed to run login: ${err.message}\n`);
@@ -552,15 +1106,53 @@ function main(argv) {
552
1106
  return;
553
1107
  }
554
1108
 
1109
+ // `hs whoami` — which account this machine's credential belongs to (email,
1110
+ // account id, console), asked of GET /api/mcp/session. The account a repo
1111
+ // would be attached to by `hs connect` / `hs install`
1112
+ // ([[HS-HAQI-KODY-DIAG-hs-install-and-hs-connect-bind-a-new]]). Auth verb:
1113
+ // bypasses the vault gate like `hs login`.
1114
+ // `hs logout [--account <id|email>]` — remove one stored account's token
1115
+ // ([[HS-UA9S-WW9R-TASK-store-credentials-per-account-login]]). Auth verb:
1116
+ // bypasses the vault gate like `hs login` / `hs whoami`.
1117
+ if (namespace === "whoami" || namespace === "logout") {
1118
+ const cliPath = spawnTargetPath("heyspex-mcp-cli");
1119
+ const whoamiRoot = vaultRoot || process.cwd();
1120
+ const child = spawn(
1121
+ process.execPath,
1122
+ [cliPath, namespace, ...argv.slice(1)],
1123
+ {
1124
+ cwd: PKG_ROOT,
1125
+ stdio: "inherit",
1126
+ env: {
1127
+ ...process.env,
1128
+ HEYSPEX_VAULT_ROOT: whoamiRoot,
1129
+ HS_VAULT_ROOT: whoamiRoot,
1130
+ HEYSPEX_DISABLE_STDIO_BOOTSTRAP: "1",
1131
+ },
1132
+ },
1133
+ );
1134
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
1135
+ child.on("error", (err) => {
1136
+ process.stderr.write(`hs: failed to run ${namespace}: ${err.message}\n`);
1137
+ process.exit(1);
1138
+ });
1139
+ return;
1140
+ }
1141
+
555
1142
  // `hs connect --check [--json]` — the read-only connect-checks report
556
1143
  // ([[HS-I2OO-EKLJ-TASK-shared-connect-checks-module-with-a]] on
557
1144
  // [[HS-QYH8-EKLJ-EPIC-hs-connect-one-command-repo-connect-verb]]). May run
558
1145
  // in an unconnected repo (no specs/ yet — that is the point), so it
559
1146
  // bypasses the hard vault gate below like init/install/vault/workspace.
560
1147
  if (namespace === "connect") {
561
- dispatchSpexConsoleEntry("hs-connect.ts", argv, vaultRoot || process.cwd(), {
562
- HS_INVOKED_CWD: INVOKED_CWD,
563
- });
1148
+ dispatchSpexConsoleEntry(
1149
+ "hs-connect.ts",
1150
+ argv,
1151
+ vaultRoot || process.cwd(),
1152
+ {
1153
+ HS_INVOKED_CWD: INVOKED_CWD,
1154
+ },
1155
+ );
564
1156
  return;
565
1157
  }
566
1158
 
@@ -572,7 +1164,7 @@ function main(argv) {
572
1164
  // the very first thing a stranger runs after `npm install heyspex` in an
573
1165
  // empty project is exactly this, from a directory with no `specs/` yet.
574
1166
  if (namespace === "doctor") {
575
- const script = path.join(PKG_ROOT, "scripts", "heyspex-doctor.mjs");
1167
+ const script = spawnTargetPath("doctor");
576
1168
  if (!existsSync(script)) {
577
1169
  // Published-package install (per HS-ZJSP-0JNK): the full doctor audits
578
1170
  // dozens of source-checkout-only surfaces (git hooks, Cursor wiring,
@@ -580,7 +1172,7 @@ function main(argv) {
580
1172
  // it is deliberately not in the package's `files`. Run the small
581
1173
  // subset that DOES apply to a published install instead of crashing
582
1174
  // with MODULE_NOT_FOUND.
583
- runStandaloneDoctor(argv.slice(1));
1175
+ runStandaloneDoctor(argv.slice(1), vaultRoot);
584
1176
  return;
585
1177
  }
586
1178
  const child = spawn(process.execPath, [script, ...argv.slice(1)], {
@@ -598,15 +1190,28 @@ function main(argv) {
598
1190
 
599
1191
  if (!vaultRoot) {
600
1192
  process.stderr.write(
601
- "hs: no vault detected — could not find a specs/ folder " +
602
- "walking up from the current directory. " +
1193
+ "hs: no vault detected — could not find a heyspex.yaml marker or a " +
1194
+ "specs/ folder walking up from the current directory. " +
603
1195
  "Set HS_VAULT_ROOT or run from inside a vault.\n",
604
1196
  );
605
1197
  process.exit(1);
606
1198
  }
607
1199
 
608
1200
  if (namespace === "index" && command === "rebuild") {
609
- dispatchSpexConsoleEntry("hs-index-rebuild.ts", rest, vaultRoot);
1201
+ dispatchSpexConsoleEntry("hs-index-rebuild.ts", rest, vaultRoot, {
1202
+ ...(vaultIdOverride ? { HS_VAULT_ID_OVERRIDE: vaultIdOverride } : {}),
1203
+ });
1204
+ return;
1205
+ }
1206
+
1207
+ // `hs index reconcile` — per-spec newest-wins between the disk vault and the
1208
+ // cloud index ([[HS-MS75]]). Read-only unless `--apply`; status moves go
1209
+ // through the status route (refusals are listed), content through the
1210
+ // phased reindex route with preserve_status; never deletes, never finalizes.
1211
+ if (namespace === "index" && command === "reconcile") {
1212
+ dispatchSpexConsoleEntry("hs-index-reconcile.ts", rest, vaultRoot, {
1213
+ ...(vaultIdOverride ? { HS_VAULT_ID_OVERRIDE: vaultIdOverride } : {}),
1214
+ });
610
1215
  return;
611
1216
  }
612
1217
 
@@ -617,7 +1222,9 @@ function main(argv) {
617
1222
  // pushes, never deletes, and only REPORTS unless `--apply` is given.
618
1223
  // Forwards the full argv; the entrypoint drops the leading `pull` token.
619
1224
  if (namespace === "pull") {
620
- dispatchSpexConsoleEntry("hs-vault-pull.ts", argv, vaultRoot);
1225
+ dispatchSpexConsoleEntry("hs-vault-pull.ts", argv, vaultRoot, {
1226
+ ...(vaultIdOverride ? { HS_VAULT_ID_OVERRIDE: vaultIdOverride } : {}),
1227
+ });
621
1228
  return;
622
1229
  }
623
1230
 
@@ -629,6 +1236,14 @@ function main(argv) {
629
1236
  return;
630
1237
  }
631
1238
 
1239
+ // NOTE: `hs source` is handled further down by the plain-Node
1240
+ // scripts/lib/hs-source-*.mjs cores ([[HS-Q0K8-CWPX-EPIC-spexsource-local-git-preflight-commit]]),
1241
+ // which ship inside the published package. An earlier TypeScript build of the
1242
+ // same verb ([[HS-KN93-V4RB-EPIC-local-git-preflight-commit-and-push-for]],
1243
+ // spex-console/bin/hs-source.ts + src/lib/source-local/) used to be dispatched
1244
+ // from this spot; it was superseded when the two branches merged and must not
1245
+ // be re-wired here, or the package-shipping handler below becomes dead code.
1246
+
632
1247
  if (namespace === "factory" && command === "run") {
633
1248
  dispatchSpexConsoleEntry("hs-factory-run.ts", argv, vaultRoot);
634
1249
  return;
@@ -659,9 +1274,8 @@ function main(argv) {
659
1274
  // server-impacting task; stop from any terminal (not only the one that
660
1275
  // started it). See [[dev-server-lifecycle-and-db-safety]].
661
1276
  if (namespace === "server" && (command === "status" || command === "stop")) {
662
- const scriptName =
663
- command === "stop" ? "dev-server-stop.mjs" : "dev-server-status.mjs";
664
- const script = path.join(PKG_ROOT, "scripts", scriptName);
1277
+ const spawnKey = command === "stop" ? "server:stop" : "server:status";
1278
+ const script = spawnTargetPath(spawnKey);
665
1279
  const child = spawn(process.execPath, [script, ...rest], {
666
1280
  cwd: PKG_ROOT,
667
1281
  stdio: "inherit",
@@ -670,7 +1284,55 @@ function main(argv) {
670
1284
  child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
671
1285
  child.on("error", (err) => {
672
1286
  process.stderr.write(
673
- `hs: failed to run ${scriptName}: ${err.message}\n`,
1287
+ `hs: failed to run ${SPAWN_TARGETS[spawnKey].path}: ${err.message}\n`,
1288
+ );
1289
+ process.exit(1);
1290
+ });
1291
+ return;
1292
+ }
1293
+
1294
+ // `hs wip status` / `hs wip restore <id>` — health report + restore for the
1295
+ // WIP hand-off ledger (`.heyspex/wip-state.json`) and its archive
1296
+ // (`.heyspex/wip-archive/*.jsonl`). Pure Node core under scripts/ (same
1297
+ // pattern as `hs server status/stop` above). See
1298
+ // [[HS-B8OK-PFMN]] / capture [[HS-817Y-83XQ]] ("the WIP ledger maintains
1299
+ // itself"). Forwards `command` plus the rest of argv; the script itself
1300
+ // decides status vs restore off that first token.
1301
+ if (namespace === "wip" && (command === "status" || command === "restore")) {
1302
+ const script = spawnTargetPath("wip");
1303
+ const child = spawn(process.execPath, [script, command, ...rest], {
1304
+ cwd: PKG_ROOT,
1305
+ stdio: "inherit",
1306
+ env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
1307
+ });
1308
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
1309
+ child.on("error", (err) => {
1310
+ process.stderr.write(
1311
+ `hs: failed to run wip-ledger-health.mjs: ${err.message}\n`,
1312
+ );
1313
+ process.exit(1);
1314
+ });
1315
+ return;
1316
+ }
1317
+
1318
+ // `hs worktree list|alloc|release|reap|rebind` — session/build worktree
1319
+ // registry (HS-03DO / HS-V9XH). Pure Node core under scripts/, itself a
1320
+ // thin shell over dev-factory/lib/worktree-allocator.mjs (called in-process,
1321
+ // never shelled out to). Same dispatch shape as `hs server status/stop` and
1322
+ // `hs wip status/restore` above. Forwards the full sub-argv; the entrypoint
1323
+ // drops the leading `worktree` token itself.
1324
+ if (namespace === "worktree") {
1325
+ const script = spawnTargetPath("worktree");
1326
+ const worktreeArgs = command ? [command, ...rest] : rest;
1327
+ const child = spawn(process.execPath, [script, ...worktreeArgs], {
1328
+ cwd: PKG_ROOT,
1329
+ stdio: "inherit",
1330
+ env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
1331
+ });
1332
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
1333
+ child.on("error", (err) => {
1334
+ process.stderr.write(
1335
+ `hs: failed to run hs-worktree.mjs: ${err.message}\n`,
674
1336
  );
675
1337
  process.exit(1);
676
1338
  });
@@ -697,25 +1359,36 @@ function main(argv) {
697
1359
  " hs source commit Stage + commit per the preflight plan\n" +
698
1360
  " hs source push Push the committed branch\n" +
699
1361
  " hs source log <spec-id> Commit-history lookup for a spec (console settlement,\n" +
700
- " falling back to local git log)\n",
1362
+ " falling back to local git log)\n" +
1363
+ " hs source identity [show] [--json]\n" +
1364
+ " Effective git user.name/user.email, where each comes from,\n" +
1365
+ " and whether a repo-local override is authorized (read-only)\n" +
1366
+ " hs source identity set --email <email> [--name <name>] [--global] [--human]\n" +
1367
+ " HUMAN-ONLY: write the identity and record the authorization\n" +
1368
+ " in .heyspex/git-identity.json (agents are refused, exit 3)\n" +
1369
+ " hs source identity clear [--human]\n" +
1370
+ " HUMAN-ONLY: remove the repo-local [user] override + authorization\n" +
1371
+ " Type set/clear yourself — terminal: hs source identity set --email you@example.com\n" +
1372
+ " inside Claude Code: !hs source identity set --email you@example.com --human\n",
701
1373
  );
702
1374
  process.exit(0);
703
1375
  }
704
- const SOURCE_ENTRYPOINTS = {
705
- status: "hs-source-preflight.mjs",
706
- preflight: "hs-source-preflight.mjs",
707
- commit: "hs-source-commit.mjs",
708
- push: "hs-source-push.mjs",
709
- log: "hs-source-log.mjs",
1376
+ const SOURCE_SPAWN_KEYS = {
1377
+ status: "source:status",
1378
+ preflight: "source:preflight",
1379
+ commit: "source:commit",
1380
+ push: "source:push",
1381
+ log: "source:log",
1382
+ identity: "source:identity",
710
1383
  };
711
- const scriptName = SOURCE_ENTRYPOINTS[command];
712
- if (!scriptName) {
1384
+ const spawnKey = SOURCE_SPAWN_KEYS[command];
1385
+ if (!spawnKey) {
713
1386
  process.stderr.write(
714
- `hs: unknown \`hs source\` verb \`${command ?? ""}\` — expected one of: ${Object.keys(SOURCE_ENTRYPOINTS).join(", ")}\n`,
1387
+ `hs: unknown \`hs source\` verb \`${command ?? ""}\` — expected one of: ${Object.keys(SOURCE_SPAWN_KEYS).join(", ")}\n`,
715
1388
  );
716
1389
  process.exit(2);
717
1390
  }
718
- const script = path.join(PKG_ROOT, "scripts", "lib", scriptName);
1391
+ const script = spawnTargetPath(spawnKey);
719
1392
  const child = spawn(process.execPath, [script, command, ...rest], {
720
1393
  cwd: PKG_ROOT,
721
1394
  stdio: "inherit",
@@ -723,7 +1396,9 @@ function main(argv) {
723
1396
  });
724
1397
  child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
725
1398
  child.on("error", (err) => {
726
- process.stderr.write(`hs: failed to run ${scriptName}: ${err.message}\n`);
1399
+ process.stderr.write(
1400
+ `hs: failed to run ${SPAWN_TARGETS[spawnKey].path}: ${err.message}\n`,
1401
+ );
727
1402
  process.exit(1);
728
1403
  });
729
1404
  return;
@@ -734,7 +1409,7 @@ function main(argv) {
734
1409
  // [[HS-RU2R-I87U-RES-solo-surface-cli-launcher-single]]). Probes ground-truth
735
1410
  // server status first; a down console prints a clear message, never a blank window.
736
1411
  if (namespace === "view") {
737
- const script = path.join(PKG_ROOT, "scripts", "hs-view.mjs");
1412
+ const script = spawnTargetPath("view");
738
1413
  const child = spawn(process.execPath, [script, ...argv.slice(1)], {
739
1414
  cwd: PKG_ROOT,
740
1415
  stdio: "inherit",
@@ -779,7 +1454,7 @@ function main(argv) {
779
1454
  // group (the multi-capture "add another?" gesture). Pure Node, posts the
780
1455
  // same heyspex.capture/0.1 envelope the companion sends to /api/drafts.
781
1456
  if (namespace === "capture") {
782
- const script = path.join(PKG_ROOT, "scripts", "hs-capture-cli.mjs");
1457
+ const script = spawnTargetPath("capture");
783
1458
  const captureArgs = command ? [command, ...rest] : rest;
784
1459
  const child = spawn(process.execPath, [script, ...captureArgs], {
785
1460
  cwd: PKG_ROOT,
@@ -795,12 +1470,17 @@ function main(argv) {
795
1470
  }
796
1471
 
797
1472
  // `hs companion start|stop|status|build` — companion tray host (companion/src/HeySpex.Companion.Host).
1473
+ // `hs companion settings ...` and the shortcuts (mute|volume|orb|wake|...) — every
1474
+ // Companion setting via the console's /api/companion/settings ([[HS-UDH1]]); the
1475
+ // script resolves the console through the shared resolver, which needs the
1476
+ // vault root for the `.heyspex/console-url` discovery file (same as `capture`).
798
1477
  if (namespace === "companion") {
799
- const script = path.join(PKG_ROOT, "scripts", "hs-companion.mjs");
1478
+ const script = spawnTargetPath("companion");
800
1479
  const companionArgs = command ? [command, ...rest] : ["start", ...rest];
801
1480
  const child = spawn(process.execPath, [script, ...companionArgs], {
802
1481
  cwd: PKG_ROOT,
803
1482
  stdio: "inherit",
1483
+ env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
804
1484
  });
805
1485
  child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
806
1486
  child.on("error", (err) => {
@@ -825,12 +1505,28 @@ function main(argv) {
825
1505
 
826
1506
  // `hs next [spec_id] [--summary] [--json]` — resolve the on-deck brief without
827
1507
  // MCP ([[HS-TASK-ISK7]] / [[tool-agnostic-mcp-first-cli-second]]).
1508
+ // `hs next` has TWO halves and this branch picks one.
1509
+ //
1510
+ // - `hs next [spec_id] [--summary] [--json]` resolves the on-deck brief
1511
+ // with no console and no MCP (`scripts/hs-next.mjs`).
1512
+ // - `hs next <id> --launch`, and the `model` / `permission-mode` / `claim`
1513
+ // subcommands, need the console (POST /api/agent/start, PUT
1514
+ // /api/settings) and run through `spex-console/bin/hs-next-launch.ts` -
1515
+ // the same shape `hs handoff` uses.
828
1516
  if (namespace === "next") {
829
- const script = path.join(PKG_ROOT, "scripts", "hs-next.mjs");
1517
+ if (isNextConsoleArgv(argv.slice(1))) {
1518
+ dispatchSpexConsoleEntry("hs-next-launch.ts", argv, vaultRoot);
1519
+ return;
1520
+ }
1521
+ const script = spawnTargetPath("next");
830
1522
  const child = spawn(process.execPath, [script, ...argv.slice(1)], {
831
1523
  cwd: PKG_ROOT,
832
1524
  stdio: "inherit",
833
- env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
1525
+ // hs-next.mjs loads vendor/heyspex-mcp/cli.mjs, which is CODE, not vault
1526
+ // content — since the repo split, HS_VAULT_ROOT can point at a checkout
1527
+ // (e.g. heyspex-specs) with no vendor/ at all
1528
+ // ([[HS-1LCC-4XKS-DIAG-frozen-checks-run-against-the-vault]]).
1529
+ env: { ...process.env, HS_VAULT_ROOT: vaultRoot, HS_PKG_ROOT: PKG_ROOT },
834
1530
  });
835
1531
  child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
836
1532
  child.on("error", (err) => {
@@ -845,7 +1541,7 @@ function main(argv) {
845
1541
  // Turning it off makes this repo behave like a plain codebase in Claude Code.
846
1542
  // Core: scripts/heyspex-mode.mjs · docs/claude-code-setup.md.
847
1543
  if (namespace === "mode") {
848
- const script = path.join(PKG_ROOT, "scripts", "hs-mode.mjs");
1544
+ const script = spawnTargetPath("mode");
849
1545
  const child = spawn(process.execPath, [script, ...argv.slice(1)], {
850
1546
  cwd: PKG_ROOT,
851
1547
  stdio: "inherit",
@@ -864,7 +1560,7 @@ function main(argv) {
864
1560
  // Registration subcommands print a blocked-on-HS-DCCP notice until the
865
1561
  // connector's server-registration table exists.
866
1562
  if (namespace === "gateway") {
867
- const script = path.join(PKG_ROOT, "scripts", "hs-gateway.mjs");
1563
+ const script = spawnTargetPath("gateway");
868
1564
  const child = spawn(process.execPath, [script, ...argv.slice(1)], {
869
1565
  cwd: PKG_ROOT,
870
1566
  stdio: "inherit",
@@ -882,7 +1578,7 @@ function main(argv) {
882
1578
  // .heyspex/config.yaml (settings capability; core: vendor/heyspex-mcp/
883
1579
  // config-settings.mjs; write-permission rules: agent-settings-protocol).
884
1580
  if (namespace === "settings") {
885
- const script = path.join(PKG_ROOT, "scripts", "hs-settings.mjs");
1581
+ const script = spawnTargetPath("settings");
886
1582
  const child = spawn(process.execPath, [script, ...argv], {
887
1583
  cwd: PKG_ROOT,
888
1584
  stdio: "inherit",
@@ -899,7 +1595,7 @@ function main(argv) {
899
1595
  // `hs classifier stats` — read-only view of HeySpex Intent Classifier decisions
900
1596
  // (.heyspex/intent-classifier-history.jsonl). Pure local read.
901
1597
  if (namespace === "classifier") {
902
- const script = path.join(PKG_ROOT, "scripts", "hs-classifier.mjs");
1598
+ const script = spawnTargetPath("classifier");
903
1599
  const child = spawn(process.execPath, [script, ...argv], {
904
1600
  cwd: PKG_ROOT,
905
1601
  stdio: "inherit",
@@ -915,7 +1611,7 @@ function main(argv) {
915
1611
 
916
1612
  // `hs auto [filename] [--json]` — resolve /hs-auto COO dispatch plan.
917
1613
  if (namespace === "auto") {
918
- const script = path.join(PKG_ROOT, "scripts", "hs-auto.mjs");
1614
+ const script = spawnTargetPath("auto");
919
1615
  const child = spawn(process.execPath, [script, ...argv.slice(1)], {
920
1616
  cwd: PKG_ROOT,
921
1617
  stdio: "inherit",
@@ -936,16 +1632,22 @@ function main(argv) {
936
1632
  // code for a persistent `hsmcp_` bearer, stored in ~/.heyspex/credentials.yaml
937
1633
  // ([[HS-GD45-MCPC-TASK-mcp-cloud-auth-shared-console-resolver]]).
938
1634
  if (namespace === "mcp") {
939
- if (command !== "call" && command !== "pair" && command !== "login") {
1635
+ if (
1636
+ command !== "call" &&
1637
+ command !== "pair" &&
1638
+ command !== "login" &&
1639
+ command !== "install"
1640
+ ) {
940
1641
  process.stderr.write(
941
- `hs: unknown mcp subcommand \`${command ?? ""}\` (expected: call | pair | login)\n` +
1642
+ `hs: unknown mcp subcommand \`${command ?? ""}\` (expected: call | pair | login | install)\n` +
942
1643
  " hs mcp call <tool> [--arg key=value ...] [--args '<json>'] [--args-file <path>] [--json]\n" +
943
1644
  " hs mcp pair <code> [--console-url <url>] [--label <text>] [--json]\n" +
944
- " hs mcp login [--console-url <url>] [--label <text>] [--json] (alias of `hs login`)\n",
1645
+ " hs mcp login [--console-url <url>] [--label <text>] [--json] (alias of `hs login`)\n" +
1646
+ " hs mcp install --remote [--console-url <url>] [--write] [--json] Wire a client to the cloud endpoint\n",
945
1647
  );
946
1648
  process.exit(2);
947
1649
  }
948
- const cliPath = path.join(PKG_ROOT, "vendor", "heyspex-mcp", "cli.mjs");
1650
+ const cliPath = spawnTargetPath("heyspex-mcp-cli");
949
1651
  const child = spawn(process.execPath, [cliPath, command, ...rest], {
950
1652
  cwd: PKG_ROOT,
951
1653
  stdio: "inherit",
@@ -959,7 +1661,9 @@ function main(argv) {
959
1661
  });
960
1662
  child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
961
1663
  child.on("error", (err) => {
962
- process.stderr.write(`hs: failed to run mcp ${command}: ${err.message}\n`);
1664
+ process.stderr.write(
1665
+ `hs: failed to run mcp ${command}: ${err.message}\n`,
1666
+ );
963
1667
  process.exit(1);
964
1668
  });
965
1669
  return;
@@ -974,13 +1678,54 @@ function main(argv) {
974
1678
  return;
975
1679
  }
976
1680
 
1681
+ // `hs harness gates [--json]` — list every registered SpexHarness gate (id,
1682
+ // event, severity, last fixture result), reading the live gate registry +
1683
+ // rule table straight from spexharness/ — no vault/console dependency
1684
+ // ([[HS-XKYY-NYPR-TASK-hs-harness-gates-and-a-console]]).
1685
+ if (namespace === "harness") {
1686
+ dispatchSpexConsoleEntry("hs-harness.ts", argv, vaultRoot || process.cwd());
1687
+ return;
1688
+ }
1689
+
1690
+ // `hs hooks add|remove|list` — the CLI face of the extension seam
1691
+ // ([[HS-8N3K-NYPR-TASK-add-cli-verbs-so-a-developer-can]],
1692
+ // [[HS-8EDJ-NYPR-EPIC-extension-seam-for-other-developers]]). Reads/writes
1693
+ // `.heyspex/harness.json` straight through `spexharness/src/extension/
1694
+ // cli-face.mjs` — the SAME core the API face
1695
+ // (spex-console/src/app/api/harness/extension/route.ts) calls — no
1696
+ // vault/console dependency, same shell shape as `harness gates` above.
1697
+ if (namespace === "hooks") {
1698
+ dispatchSpexConsoleEntry("hs-hooks.ts", argv, vaultRoot || process.cwd());
1699
+ return;
1700
+ }
1701
+
1702
+ // `hs bench <phase1|phase2> --model <name> --harness <on|off>` — run the
1703
+ // harness-on-versus-harness-off benchmark's task set once and write a dated
1704
+ // report under dev-factory/reports/calibrations/bench/
1705
+ // ([[HS-DQ0S-NYPR-TASK-add-a-bench-command-that-runs-the-same]], epic
1706
+ // [[HS-IZCF-NYPR-EPIC-harness-on-versus-harness-off-benchmark]]). Thin CLI
1707
+ // verb only — the real orchestration lives in spexharness/src/bench/*.mjs;
1708
+ // this dispatch branch just reaches the thin shell like `harness` above.
1709
+ if (namespace === "bench") {
1710
+ dispatchSpexConsoleEntry("hs-bench.ts", argv, vaultRoot || process.cwd());
1711
+ return;
1712
+ }
1713
+
977
1714
  // `hs check run <id|manifest>` — verify a spec's acceptance criteria against its
978
1715
  // frozen checks manifest, per-criterion pass/fail against the real tree
979
1716
  // ([[HS-EPIC-DCEP-acceptance-criteria-checks-manifest-dete]], the deterministic
980
1717
  // kernel of [[HS-MIL-Y5DN-verification-engine-shared-deterministic]]). Forwards
981
1718
  // the full argv; the entrypoint drops the leading `check` token.
1719
+ //
1720
+ // HS_PKG_ROOT is forwarded so the entrypoint can resolve a check's `cwd`
1721
+ // against the CODE root rather than `vaultRoot` — since the repo split, the
1722
+ // vault and the code can be two different checkouts, and a check almost
1723
+ // always runs code, not vault content
1724
+ // ([[HS-1LCC-4XKS-DIAG-frozen-checks-run-against-the-vault]]).
982
1725
  if (namespace === "check") {
983
- dispatchSpexConsoleEntry("hs-check.ts", argv, vaultRoot);
1726
+ dispatchSpexConsoleEntry("hs-check.ts", argv, vaultRoot, {
1727
+ HS_PKG_ROOT: PKG_ROOT,
1728
+ });
984
1729
  return;
985
1730
  }
986
1731
 
@@ -988,8 +1733,15 @@ function main(argv) {
988
1733
  // `review_pending`/`in_review` and report what has decayed since the stamp
989
1734
  // ([[HS-R5LF-36QL-EPIC-stamped-work-is-re-verified-on-a]]). Read-only with
990
1735
  // respect to the repo; the Resolution plan is a dry run unless `--apply`.
1736
+ //
1737
+ // HS_PKG_ROOT is forwarded for the same reason `check` forwards it: the sweep
1738
+ // re-runs those same frozen manifests, so a check's `cwd` has to resolve
1739
+ // against the CODE root, not the vault
1740
+ // ([[HS-1LCC-4XKS-DIAG-frozen-checks-run-against-the-vault]]).
991
1741
  if (namespace === "revalidate") {
992
- dispatchSpexConsoleEntry("hs-revalidate.ts", argv, vaultRoot);
1742
+ dispatchSpexConsoleEntry("hs-revalidate.ts", argv, vaultRoot, {
1743
+ HS_PKG_ROOT: PKG_ROOT,
1744
+ });
993
1745
  return;
994
1746
  }
995
1747
 
@@ -1003,18 +1755,49 @@ function main(argv) {
1003
1755
  return;
1004
1756
  }
1005
1757
 
1758
+ // `hs chat [--model <name>] [--script <file>] [--seat <seat>]` — HeySpex's own chat
1759
+ // front (step 0 of [[HS-2KYS-NYPR-EPIC-phase-1-end-to-end-on-deepseek-and-on-a]], the
1760
+ // prerequisite that finishes [[HS-F7N3-NYPR-EPIC-the-powershell-front-hs-chat]]). Thin
1761
+ // CLI verb only — every real behavior lives in spexharness/src/chat/*.mjs; this
1762
+ // dispatch branch just reaches the thin shell like every other verb here. Forwards the
1763
+ // full argv; the entrypoint drops the leading `chat` token.
1764
+ if (namespace === "chat") {
1765
+ dispatchSpexConsoleEntry("hs-chat.ts", argv, vaultRoot || process.cwd());
1766
+ return;
1767
+ }
1768
+
1769
+ // `hs trust list` / `hs trust revoke [path]` — standalone management of the machine-local
1770
+ // folder-trust store `hs chat`'s first-run prompt writes to
1771
+ // ([[HS-25AJ-8CMW-TASK-folder-trust-revoke-through-the]], deferred from
1772
+ // [[HS-ZBEB-8CMW-TASK-folder-trust-for-spexcode-first-run]], which shipped only `hs chat
1773
+ // --revoke-trust`). Thin CLI verb — same shell shape as `chat`/`hooks`/`bench` above; the real
1774
+ // logic lives in `spex-console/src/lib/cli/trust-cli.ts` over `folder-trust.ts`'s pure
1775
+ // functions. Forwards the full argv; the entrypoint drops the leading `trust` token.
1776
+ if (namespace === "trust") {
1777
+ dispatchSpexConsoleEntry("hs-trust.ts", argv, vaultRoot || process.cwd());
1778
+ return;
1779
+ }
1780
+
1781
+ // `hs doctrine extract|learn|show` — SpexCode Doctrine corpus reader + synthesis
1782
+ // ([[HS-TWTC-8CMW-TASK-dr1-doctrine-corpus-reader-stream-every]],
1783
+ // [[HS-YMPY-8CMW-TASK-dr2-doctrine-synthesis-a-routed]] of epic
1784
+ // [[HS-KNHZ-8CMW-EPIC-spexcode-doctrine-v0-the-foundational]]). Thin CLI verb —
1785
+ // same shell shape as `chat`/`trust`/`harness` above; the real logic lives in
1786
+ // `spexharness/src/doctrine/*.mjs`. Forwards the full argv; the entrypoint drops
1787
+ // the leading `doctrine` token.
1788
+ if (namespace === "doctrine") {
1789
+ dispatchSpexConsoleEntry(
1790
+ "hs-doctrine.ts",
1791
+ argv,
1792
+ vaultRoot || process.cwd(),
1793
+ );
1794
+ return;
1795
+ }
1796
+
1006
1797
  // Neon control-plane verbs ([[HS-EPIC-CLMW-hs-cli-mcp-wrappers-for-control-plane]]).
1007
1798
  // Each forwards the full argv (namespace/verb + args); the entrypoint drops
1008
- // the leading namespace token and parses the rest.
1009
- const CONTROL_ENTRYPOINTS = {
1010
- pin: "hs-pin.ts",
1011
- claim: "hs-claim.ts",
1012
- handoff: "hs-handoff.ts",
1013
- events: "hs-events.ts",
1014
- "export-events": "hs-export-events.ts",
1015
- prereq: "hs-prereq.ts",
1016
- queue: "hs-queue.ts",
1017
- };
1799
+ // the leading namespace token and parses the rest. (Table itself now lives
1800
+ // at module scope as CONTROL_ENTRYPOINTS — see its definition above main().)
1018
1801
  if (Object.hasOwn(CONTROL_ENTRYPOINTS, namespace)) {
1019
1802
  dispatchSpexConsoleEntry(CONTROL_ENTRYPOINTS[namespace], argv, vaultRoot);
1020
1803
  return;
@@ -1025,4 +1808,54 @@ function main(argv) {
1025
1808
  process.exit(2);
1026
1809
  }
1027
1810
 
1028
- main(process.argv.slice(2));
1811
+ // Only run the CLI when this file is executed directly (`node bin/hs.mjs` /
1812
+ // the `hs`/`heyspex` bin shims), not when another script imports it — e.g.
1813
+ // `scripts/verify-mcp-package.mjs`'s `spawnTargets` check and
1814
+ // `scripts/lib/spawn-target-manifest.test.mjs` import `SPAWN_TARGETS` above
1815
+ // without wanting `main()` to run the dispatcher against THEIR argv.
1816
+ //
1817
+ // A naive `import.meta.url === pathToFileURL(process.argv[1]).href` (the
1818
+ // pattern `scripts/verify-mcp-package.mjs`'s own `isMain` guard uses — safe
1819
+ // there because that script is never reached through an npm bin shim) is
1820
+ // NOT enough here: an `npm link`ed / globally-installed `hs` runs
1821
+ // `node "<npm-prefix>/node_modules/heyspex/bin/hs.mjs"`, and when that path
1822
+ // is a symlink (npm link's install shape), Node's default module resolution
1823
+ // realpaths it for `import.meta.url` but leaves `process.argv[1]` as the
1824
+ // ORIGINAL (symlinked) path the shim passed — so the two never matched,
1825
+ // `isMain` came out false, and the globally-installed `hs` silently printed
1826
+ // nothing and exited 0 for every verb ([[HS-561M]] regression). Fix:
1827
+ // realpath BOTH sides before comparing (so the symlink resolves the same way
1828
+ // on each side), case-insensitively on Windows (drive-letter casing can
1829
+ // differ between how a shim built the path and how realpath reports it).
1830
+ function isSameFile(a, b) {
1831
+ const normalize = (p) => {
1832
+ let real;
1833
+ try {
1834
+ real = realpathSync.native(p);
1835
+ } catch {
1836
+ real = path.resolve(p);
1837
+ }
1838
+ return process.platform === "win32" ? real.toLowerCase() : real;
1839
+ };
1840
+ return normalize(a) === normalize(b);
1841
+ }
1842
+
1843
+ const isMain = (() => {
1844
+ if (!process.argv[1]) return false;
1845
+ try {
1846
+ if (isSameFile(process.argv[1], fileURLToPath(import.meta.url)))
1847
+ return true;
1848
+ } catch {
1849
+ // fall through to the basename fallback below
1850
+ }
1851
+ // Belt-and-suspenders fallback for install shapes realpath can't reconcile
1852
+ // (a shim invoking through an intermediate wrapper, a path realpath can't
1853
+ // stat, etc.): recognize this file by its own known bin names rather than
1854
+ // failing closed into silent no-output.
1855
+ const base = path.basename(process.argv[1]).toLowerCase();
1856
+ return base === "hs.mjs" || base === "hs" || base === "heyspex";
1857
+ })();
1858
+
1859
+ if (isMain) {
1860
+ main(process.argv.slice(2));
1861
+ }