heyspex 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (352) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/README.md +21 -0
  3. package/bin/hs.mjs +1089 -104
  4. package/bin/spexcode.mjs +262 -0
  5. package/dev-factory/engine/worktree.mjs +740 -0
  6. package/dev-factory/lib/code-repo-root.mjs +67 -0
  7. package/dev-factory/lib/context-budget.mjs +84 -0
  8. package/dev-factory/lib/epic-detect.mjs +190 -0
  9. package/dev-factory/lib/fleet-queue-file.mjs +846 -0
  10. package/dev-factory/lib/fleet-queue.mjs +426 -0
  11. package/dev-factory/lib/fleet-state.mjs +14 -18
  12. package/dev-factory/lib/model-catalog.mjs +338 -0
  13. package/dev-factory/lib/seat-context.mjs +199 -0
  14. package/dev-factory/lib/spec-io.mjs +93 -0
  15. package/dev-factory/lib/sufficiency-score.mjs +8 -2
  16. package/dev-factory/lib/worktree-allocator.mjs +744 -0
  17. package/dev-factory/model-catalog.json +222 -0
  18. package/dist/cli/hs-check.mjs +1127 -103
  19. package/dist/cli/hs-claim.mjs +981 -63
  20. package/dist/cli/hs-connect.mjs +10059 -1729
  21. package/dist/cli/hs-defer.mjs +5507 -3072
  22. package/dist/cli/hs-dev.mjs +576 -0
  23. package/dist/cli/hs-env.mjs +416 -302
  24. package/dist/cli/hs-index-rebuild.mjs +7334 -0
  25. package/dist/cli/hs-index-reconcile.mjs +8138 -0
  26. package/dist/cli/hs-init.mjs +2840 -149
  27. package/dist/cli/hs-install.mjs +7214 -594
  28. package/dist/cli/hs-join.mjs +10640 -2421
  29. package/dist/cli/hs-vault-pull.mjs +8157 -6370
  30. package/dist/cli/hs-vault-push.mjs +6433 -0
  31. package/dist/cli/hs-vault.mjs +4029 -1555
  32. package/dist/cli/hs-workspace.mjs +2702 -538
  33. package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
  34. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
  35. package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
  36. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
  37. package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
  38. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
  39. package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
  40. package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
  41. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
  42. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
  43. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
  44. package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
  45. package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
  46. package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -0
  47. package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
  48. package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
  49. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
  50. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
  51. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
  52. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
  53. package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
  54. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
  55. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
  56. package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -0
  57. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
  58. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
  59. package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
  60. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
  61. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
  62. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
  63. package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
  64. package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
  65. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +33 -0
  66. package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
  67. package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
  68. package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
  69. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
  70. package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
  71. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
  72. package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
  73. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
  74. package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
  75. package/dist/cli-templates/harness-commands/cursor/hs-view.md +32 -0
  76. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
  77. package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
  78. package/docs/copilot-plugin-install.md +162 -0
  79. package/docs/driving-heyspex-from-copilot.md +165 -0
  80. package/docs/mcp-client-config.md +259 -6
  81. package/package.json +249 -124
  82. package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
  83. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
  84. package/plugins/heyspex/.mcp.json +12 -0
  85. package/plugins/heyspex/README.md +309 -0
  86. package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
  87. package/plugins/heyspex/bin/run-hook.mjs +80 -0
  88. package/plugins/heyspex/hooks/cursor-hooks.json +308 -0
  89. package/plugins/heyspex/hooks/hooks.json +418 -0
  90. package/plugins/heyspex/mcp.json +13 -0
  91. package/scripts/check-mcp-freshness.mjs +17 -9
  92. package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
  93. package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
  94. package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
  95. package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -0
  96. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +356 -0
  97. package/scripts/claude-hooks/_run.mjs +413 -0
  98. package/scripts/claude-hooks/_transcript-text.mjs +60 -0
  99. package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
  100. package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
  101. package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
  102. package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
  103. package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
  104. package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
  105. package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
  106. package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
  107. package/scripts/claude-hooks/claim-liveness-shared.mjs +577 -0
  108. package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
  109. package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
  110. package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
  111. package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
  112. package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
  113. package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
  114. package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
  115. package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
  116. package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
  117. package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
  118. package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
  119. package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
  120. package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
  121. package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
  122. package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
  123. package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
  124. package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
  125. package/scripts/claude-hooks/inject-communication-style.mjs +86 -0
  126. package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
  127. package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
  128. package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
  129. package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
  130. package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -0
  131. package/scripts/claude-hooks/log-session-activity.mjs +341 -0
  132. package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
  133. package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
  134. package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
  135. package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
  136. package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
  137. package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
  138. package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
  139. package/scripts/claude-hooks/research-method-guard.mjs +398 -0
  140. package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
  141. package/scripts/claude-hooks/session-end.mjs +451 -0
  142. package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
  143. package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
  144. package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
  145. package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
  146. package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
  147. package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
  148. package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
  149. package/scripts/claude-hooks/wip-checkpoint.mjs +302 -0
  150. package/scripts/cursor-hooks/manifest.mjs +281 -0
  151. package/scripts/dev-server-status.mjs +34 -13
  152. package/scripts/dev-server-stop.mjs +313 -0
  153. package/scripts/heyspex-mode.mjs +288 -0
  154. package/scripts/hs-auto-resolve.mjs +240 -0
  155. package/scripts/hs-auto-source.mjs +398 -0
  156. package/scripts/hs-auto.mjs +134 -0
  157. package/scripts/hs-capture-cli.mjs +326 -0
  158. package/scripts/hs-classifier.mjs +136 -0
  159. package/scripts/hs-companion.mjs +88 -0
  160. package/scripts/hs-gateway.mjs +115 -0
  161. package/scripts/hs-mode.mjs +125 -0
  162. package/scripts/hs-next.mjs +441 -0
  163. package/scripts/hs-settings.mjs +338 -0
  164. package/scripts/hs-view.mjs +496 -0
  165. package/scripts/hs-worktree.mjs +201 -0
  166. package/scripts/install-claude-code-hooks.mjs +272 -0
  167. package/scripts/install-cursor-hooks.mjs +471 -0
  168. package/scripts/lib/agent-env-markers.mjs +38 -0
  169. package/scripts/lib/console-url-resolver.mjs +199 -61
  170. package/scripts/lib/customer-kit.mjs +1203 -0
  171. package/scripts/lib/dev-mode.mjs +249 -32
  172. package/scripts/lib/directive-obligations.mjs +349 -0
  173. package/scripts/lib/heyspex-home.mjs +72 -0
  174. package/scripts/lib/hook-dispositions.mjs +210 -0
  175. package/scripts/lib/hook-vault-header.mjs +30 -0
  176. package/scripts/lib/hs-command.mjs +277 -0
  177. package/scripts/lib/hs-companion-settings.mjs +472 -0
  178. package/scripts/lib/hs-source-branch.mjs +196 -0
  179. package/scripts/lib/hs-source-commit.mjs +59 -5
  180. package/scripts/lib/hs-source-identity.mjs +526 -0
  181. package/scripts/lib/hs-source-log.mjs +13 -22
  182. package/scripts/lib/hs-source-preflight.mjs +18 -4
  183. package/scripts/lib/hs-source-push.mjs +386 -307
  184. package/scripts/lib/hs-worktree-args.mjs +115 -0
  185. package/scripts/lib/shared-path-helpers.mjs +134 -0
  186. package/scripts/lib/standalone-doctor.mjs +540 -3
  187. package/scripts/lib/subagent-git-lock.mjs +109 -0
  188. package/scripts/lib/template-pin.mjs +34 -0
  189. package/scripts/lib/window-placement.mjs +109 -0
  190. package/scripts/provenance-lib.mjs +91 -0
  191. package/scripts/stamp-provenance-on-edit.mjs +222 -0
  192. package/scripts/wip-ledger-health.mjs +653 -0
  193. package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
  194. package/spexharness/src/adapters/audit.mjs +197 -0
  195. package/spexharness/src/adapters/claude-code.mjs +449 -0
  196. package/spexharness/src/adapters/copilot.mjs +236 -0
  197. package/spexharness/src/adapters/detect-harness.mjs +34 -0
  198. package/spexharness/src/chat/tui/ansi.mjs +448 -0
  199. package/spexharness/src/chat/tui/palette.mjs +22 -0
  200. package/spexharness/src/chat/tui/select.mjs +208 -0
  201. package/spexharness/src/config-root.mjs +54 -0
  202. package/spexharness/src/context-budget.mjs +141 -0
  203. package/spexharness/src/dev-server-control.mjs +389 -0
  204. package/spexharness/src/doctrine/behaviour.mjs +371 -0
  205. package/spexharness/src/doctrine/corpus.mjs +733 -0
  206. package/spexharness/src/doctrine/inject.mjs +338 -0
  207. package/spexharness/src/doctrine/merge.mjs +184 -0
  208. package/spexharness/src/doctrine/sources.mjs +252 -0
  209. package/spexharness/src/doctrine/synthesize.mjs +401 -0
  210. package/spexharness/src/doctrine/vault-source.mjs +574 -0
  211. package/spexharness/src/doctrine/writer.mjs +176 -0
  212. package/spexharness/src/events.mjs +84 -0
  213. package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
  214. package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
  215. package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
  216. package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
  217. package/spexharness/src/gates/deep-research-cost-gate.mjs +275 -0
  218. package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
  219. package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
  220. package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
  221. package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
  222. package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
  223. package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
  224. package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
  225. package/spexharness/src/gates/git-identity-guard.mjs +61 -0
  226. package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
  227. package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
  228. package/spexharness/src/gates/git-stash-gate.mjs +159 -0
  229. package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
  230. package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
  231. package/spexharness/src/gates/inject-vault-context.mjs +239 -0
  232. package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
  233. package/spexharness/src/gates/plan-review-gate.mjs +233 -0
  234. package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
  235. package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
  236. package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
  237. package/spexharness/src/gates/settings-write-gate.mjs +104 -0
  238. package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
  239. package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
  240. package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
  241. package/spexharness/src/git-reflog-audit.mjs +294 -0
  242. package/spexharness/src/intent-classifier.mjs +196 -0
  243. package/spexharness/src/intent-directive.mjs +626 -0
  244. package/spexharness/src/profile/learn.mjs +551 -0
  245. package/spexharness/src/profile/proposals.mjs +357 -0
  246. package/spexharness/src/profile/vault-inclusion.mjs +386 -0
  247. package/spexharness/src/redact.mjs +151 -0
  248. package/spexharness/src/rule-table.mjs +849 -0
  249. package/spexharness/src/seat-context.mjs +122 -0
  250. package/spexharness/src/session-bootstrap.mjs +512 -0
  251. package/spexharness/src/session-record.mjs +815 -0
  252. package/spexharness/src/token-inventory.mjs +207 -0
  253. package/vendor/heyspex-mcp/account-switch.mjs +96 -0
  254. package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
  255. package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
  256. package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
  257. package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
  258. package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
  259. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +77 -8
  260. package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
  261. package/vendor/heyspex-mcp/capture-console-client.mjs +117 -3
  262. package/vendor/heyspex-mcp/capture-focus.mjs +1061 -31
  263. package/vendor/heyspex-mcp/capture-outbox.mjs +15 -5
  264. package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
  265. package/vendor/heyspex-mcp/capture-tools.mjs +1264 -0
  266. package/vendor/heyspex-mcp/claim-tools.mjs +1147 -0
  267. package/vendor/heyspex-mcp/cli-verbs.mjs +2074 -0
  268. package/vendor/heyspex-mcp/cli.mjs +207 -15524
  269. package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
  270. package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
  271. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +602 -0
  272. package/vendor/heyspex-mcp/config-seed.yaml +15 -0
  273. package/vendor/heyspex-mcp/config-settings.mjs +290 -7
  274. package/vendor/heyspex-mcp/console-fetch.mjs +822 -17
  275. package/vendor/heyspex-mcp/console-transport.mjs +172 -0
  276. package/vendor/heyspex-mcp/credentials.mjs +585 -0
  277. package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
  278. package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
  279. package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
  280. package/vendor/heyspex-mcp/design-tools.mjs +4 -1
  281. package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
  282. package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
  283. package/vendor/heyspex-mcp/dev-tools.mjs +131 -0
  284. package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
  285. package/vendor/heyspex-mcp/events-tools.mjs +207 -0
  286. package/vendor/heyspex-mcp/filing-tools.mjs +723 -0
  287. package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
  288. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +71 -15
  289. package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
  290. package/vendor/heyspex-mcp/folder-trust-store.mjs +60 -0
  291. package/vendor/heyspex-mcp/http-transport.mjs +302 -0
  292. package/vendor/heyspex-mcp/list-tools.mjs +257 -0
  293. package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
  294. package/vendor/heyspex-mcp/mcp-config.mjs +107 -0
  295. package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
  296. package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
  297. package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
  298. package/vendor/heyspex-mcp/occupied-keys.mjs +142 -0
  299. package/vendor/heyspex-mcp/on-deck-brief.mjs +3377 -0
  300. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
  301. package/vendor/heyspex-mcp/package-root.mjs +32 -0
  302. package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
  303. package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
  304. package/vendor/heyspex-mcp/plan-push.mjs +908 -22
  305. package/vendor/heyspex-mcp/plan-review.mjs +124 -30
  306. package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
  307. package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -0
  308. package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -970
  309. package/vendor/heyspex-mcp/promote-capture.mjs +388 -69
  310. package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
  311. package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
  312. package/vendor/heyspex-mcp/promote-research.mjs +666 -53
  313. package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
  314. package/vendor/heyspex-mcp/prompts.mjs +1460 -0
  315. package/vendor/heyspex-mcp/read-path.mjs +191 -15
  316. package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
  317. package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
  318. package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
  319. package/vendor/heyspex-mcp/register-spec.mjs +266 -54
  320. package/vendor/heyspex-mcp/request-context.mjs +83 -0
  321. package/vendor/heyspex-mcp/research-focus.mjs +207 -4
  322. package/vendor/heyspex-mcp/research-handlers.mjs +625 -0
  323. package/vendor/heyspex-mcp/research-tools.mjs +4 -4
  324. package/vendor/heyspex-mcp/response-budget.mjs +348 -0
  325. package/vendor/heyspex-mcp/review-focus.mjs +25 -1
  326. package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
  327. package/vendor/heyspex-mcp/search-specs.mjs +280 -0
  328. package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
  329. package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
  330. package/vendor/heyspex-mcp/session-owner.mjs +109 -0
  331. package/vendor/heyspex-mcp/session-tools.mjs +162 -0
  332. package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
  333. package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
  334. package/vendor/heyspex-mcp/spec-edit-tools.mjs +453 -0
  335. package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
  336. package/vendor/heyspex-mcp/split-research.mjs +304 -56
  337. package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
  338. package/vendor/heyspex-mcp/submit-for-review.mjs +150 -7
  339. package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
  340. package/vendor/heyspex-mcp/summary-write-tools.mjs +228 -0
  341. package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
  342. package/vendor/heyspex-mcp/test-helpers.mjs +123 -1
  343. package/vendor/heyspex-mcp/tool-catalog.mjs +2819 -0
  344. package/vendor/heyspex-mcp/tool-dispatch.mjs +657 -0
  345. package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
  346. package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
  347. package/vendor/heyspex-mcp/vault-io.mjs +720 -0
  348. package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
  349. package/vendor/heyspex-mcp/vault-resolve.mjs +554 -68
  350. package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
  351. package/vendor/heyspex-mcp/whoami.mjs +92 -0
  352. package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
package/bin/hs.mjs CHANGED
@@ -12,16 +12,245 @@
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
+ findBrokenSatellitePin,
29
+ isVaultRoot,
30
+ readMarkerSatellite,
31
+ readSatellitePin,
32
+ resolveActiveVault,
33
+ } from "../vendor/heyspex-mcp/vault-resolve.mjs";
21
34
 
22
35
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
36
  const PKG_ROOT = path.resolve(__dirname, "..");
24
37
 
38
+ /**
39
+ * Every script path `hs` spawns directly by filesystem path (as opposed to
40
+ * `dispatchSpexConsoleEntry`'s own three-case tsx/bundle/missing resolution,
41
+ * which already carries its own existence handling). Each entry is
42
+ * PKG_ROOT-relative, posix-separated.
43
+ *
44
+ * Why this exists ([[HS-561M]]): `scripts/verify-mcp-package.mjs`'s
45
+ * `checkClosure` proves the STATIC IMPORT closure of vendor/heyspex-mcp/cli.mjs
46
+ * is fully packed — it says nothing about the ~15 verbs below that reach a
47
+ * script by `spawn()`ing a path string, which is invisible to any import-graph
48
+ * walk. Every dispatch site below resolves its script path THROUGH this
49
+ * manifest (`path.join(PKG_ROOT, SPAWN_TARGETS[key].path)`) instead of
50
+ * building the path inline, so `verify-mcp-package.mjs`'s `spawnTargets` check
51
+ * (which imports this const) can assert every non-`devOnly` entry is actually
52
+ * in the packed tarball file list, naming whichever one is missing.
53
+ *
54
+ * `devOnly: true` entries are scripts this package deliberately does NOT ship
55
+ * (see each `reason`) — the pack gate skips them, and the dispatch site for
56
+ * each already guards the missing case at runtime with a clear message
57
+ * (`doctor` falls back to `runStandaloneDoctor`; `skills` prints a
58
+ * published-install notice — see their dispatch sites below).
59
+ */
60
+ export const SPAWN_TARGETS = {
61
+ "setup:claude": { path: "scripts/install-claude-code-hooks.mjs" },
62
+ "setup:cursor": { path: "scripts/install-cursor-hooks.mjs" },
63
+ "skills:sync": {
64
+ path: "scripts/sync-skills.mjs",
65
+ devOnly: true,
66
+ reason:
67
+ "sync-skills.mjs propagates THIS platform repo's OWN .commandcode/skills " +
68
+ "source content (Command Code SSOT, resolved from the script's own file " +
69
+ "location, not the caller's vault) — that content has no meaning outside " +
70
+ "a clone of the HeySpex platform repo and is not shipped.",
71
+ },
72
+ "heyspex-mcp-cli": { path: "vendor/heyspex-mcp/cli.mjs" }, // `hs login` + `hs mcp *`
73
+ doctor: {
74
+ path: "scripts/heyspex-doctor.mjs",
75
+ devOnly: true,
76
+ reason:
77
+ "the full doctor audits dozens of source-checkout-only surfaces (git " +
78
+ "hooks, Cursor wiring, console secrets, ...) a published install never " +
79
+ "has by design — the dispatch site below already falls back to " +
80
+ "runStandaloneDoctor() when this file is absent.",
81
+ },
82
+ "server:status": { path: "scripts/dev-server-status.mjs" },
83
+ "server:stop": { path: "scripts/dev-server-stop.mjs" },
84
+ wip: { path: "scripts/wip-ledger-health.mjs" },
85
+ worktree: { path: "scripts/hs-worktree.mjs" },
86
+ "source:status": { path: "scripts/lib/hs-source-preflight.mjs" },
87
+ "source:preflight": { path: "scripts/lib/hs-source-preflight.mjs" },
88
+ "source:commit": { path: "scripts/lib/hs-source-commit.mjs" },
89
+ "source:push": { path: "scripts/lib/hs-source-push.mjs" },
90
+ "source:branch": { path: "scripts/lib/hs-source-branch.mjs" },
91
+ "source:log": { path: "scripts/lib/hs-source-log.mjs" },
92
+ // `hs source identity` — the ONE sanctioned git-identity writer (human-only
93
+ // set/clear, read-only show). Incident 2026-09-19, vault lesson
94
+ // `when-git-says-author-identity-unknown`; docs/admin/workflow/git-identity.md.
95
+ "source:identity": { path: "scripts/lib/hs-source-identity.mjs" },
96
+ view: { path: "scripts/hs-view.mjs" },
97
+ capture: { path: "scripts/hs-capture-cli.mjs" },
98
+ companion: { path: "scripts/hs-companion.mjs" },
99
+ next: { path: "scripts/hs-next.mjs" },
100
+ mode: { path: "scripts/hs-mode.mjs" },
101
+ gateway: { path: "scripts/hs-gateway.mjs" },
102
+ settings: { path: "scripts/hs-settings.mjs" },
103
+ classifier: { path: "scripts/hs-classifier.mjs" },
104
+ auto: { path: "scripts/hs-auto.mjs" },
105
+ "plugin:launch-mcp": { path: "plugins/heyspex/bin/launch-mcp.mjs" },
106
+ };
107
+
108
+ /**
109
+ * Neon control-plane verbs ([[HS-EPIC-CLMW-hs-cli-mcp-wrappers-for-control-plane]]):
110
+ * each namespace maps to a spex-console TS entry, dispatched via
111
+ * `dispatchSpexConsoleEntry` in `main()` below. Hoisted to module scope (was
112
+ * previously declared inline inside `main()`) and exported alongside
113
+ * `SPAWN_TARGETS` so `BUILTIN_VERBS` below — and anything else that needs the
114
+ * full verb manifest, e.g. `scripts/verify-verb-sweep.mjs` — can derive these
115
+ * eight verb names from the dispatcher's own table instead of hand-copying it.
116
+ */
117
+ export const CONTROL_ENTRYPOINTS = {
118
+ pin: "hs-pin.ts",
119
+ claim: "hs-claim.ts",
120
+ handoff: "hs-handoff.ts",
121
+ events: "hs-events.ts",
122
+ "export-events": "hs-export-events.ts",
123
+ prereq: "hs-prereq.ts",
124
+ queue: "hs-queue.ts",
125
+ // `hs model` - one read-only list of every seat that has a model behind it
126
+ // (launch seats from settings.json, SpexFleet seats from routing-table.json).
127
+ model: "hs-model.ts",
128
+ // `hs trash` - the bin every spec delete soft-deletes into. Nothing looked
129
+ // at it before, so deleted specs accumulated on disk forever.
130
+ trash: "hs-trash.ts",
131
+ // `hs admin test-user create` — mint one throwaway test customer against a
132
+ // stated environment ([[HS-4Z1N-SMXX-EPIC-hs-admin-test-user-create-one-command]]).
133
+ // Wraps POST /api/admin/users/create, so it belongs with the other verbs
134
+ // that are thin shells over a console endpoint.
135
+ admin: "hs-admin.ts",
136
+ // `hs seat` — read/add/update/disable a SpexFleet seat, matching
137
+ // /api/seat-registry's operations 1:1 ([[HS-W2H1-NYPR-TASK-create-the-api-endpoint-that-lets-a]]
138
+ // of epic [[HS-RJVM-NYPR-EPIC-seat-registry-every-role-spexfleet-runs]]).
139
+ // Reads/writes dev-factory/seats.json directly through `@/lib/seat-registry`
140
+ // — the same core the API route calls — so it works with the console
141
+ // stopped, same shape as `hs model` / `hs registry`.
142
+ seat: "hs-seat.ts",
143
+ // `hs voice` — add/build/show/set/test/refresh/export a coded voice profile,
144
+ // matching /api/voice's operations 1:1
145
+ // ([[HS-PQPJ-DOJY-TASK-build-the-hs-voice-cli-and-matching]] of epic
146
+ // [[HS-8PG1-DOJY-EPIC-voice-as-a-deterministic-system-a-coded]]). Reads/
147
+ // writes .heyspex/voice-profiles directly through @/lib/voice-cli — the
148
+ // same core the API route calls — so it works with the console stopped,
149
+ // same shape as `hs model` / `hs seat`.
150
+ voice: "hs-voice.ts",
151
+ };
152
+
153
+ /**
154
+ * Every top-level `hs <namespace>` verb `main()` recognizes below, listed in
155
+ * the same order as their dispatch branches — the single manifest
156
+ * [[HS-G65Z-TASK-every-hs-verb-sweeps-clean-against-a]]'s
157
+ * `scripts/verify-verb-sweep.mjs` imports to enumerate what to sweep, instead
158
+ * of hand-maintaining a second, driftable copy of this list in the sweep
159
+ * script. Deleting a verb's dispatch branch above and its entry here (both in
160
+ * this file) is enough to drop it from the sweep with no sweep-script edit.
161
+ * Does NOT distinguish spawn-backed vs. spex-console-TS-backed vs. bundled —
162
+ * `SPAWN_TARGETS` above already carries that detail for the verbs it covers;
163
+ * this list is deliberately just "what does `hs` accept as argv[0]".
164
+ */
165
+ export const BUILTIN_VERBS = [
166
+ "init",
167
+ "install",
168
+ "join",
169
+ "invite",
170
+ "setup",
171
+ "skills",
172
+ "vault",
173
+ "workspace",
174
+ "login",
175
+ "logout",
176
+ "whoami",
177
+ "connect",
178
+ "doctor",
179
+ "index",
180
+ "pull",
181
+ "push",
182
+ "import",
183
+ "milestone",
184
+ "factory",
185
+ "code-watcher",
186
+ "server",
187
+ "wip",
188
+ "worktree",
189
+ "source",
190
+ "view",
191
+ "dev",
192
+ "defer",
193
+ "undefer",
194
+ "spawn",
195
+ "capture",
196
+ "companion",
197
+ "registry",
198
+ "env",
199
+ "next",
200
+ "mode",
201
+ "gateway",
202
+ "settings",
203
+ "classifier",
204
+ "auto",
205
+ "mcp",
206
+ "test",
207
+ "harness",
208
+ "hooks",
209
+ "bench",
210
+ "check",
211
+ "revalidate",
212
+ "agent",
213
+ "chat",
214
+ "trust",
215
+ "doctrine",
216
+ ...Object.keys(CONTROL_ENTRYPOINTS),
217
+ ];
218
+
219
+ /**
220
+ * Does this `hs next ...` invocation need the console? Mirror of
221
+ * `isNextConsoleCommand` in `spex-console/src/lib/cli/next.ts` - that module
222
+ * owns the rule; this copy exists because the dispatcher must choose a half
223
+ * BEFORE spawning either one, and cannot import TypeScript. Keep the two in
224
+ * step: any launch/settings flag added there belongs here too.
225
+ */
226
+ export function isNextConsoleArgv(args) {
227
+ if (
228
+ args[0] === "model" ||
229
+ args[0] === "permission-mode" ||
230
+ args[0] === "claim" ||
231
+ args[0] === "front"
232
+ )
233
+ return true;
234
+ return args.some(
235
+ (a) =>
236
+ a === "--launch" ||
237
+ a === "--force" ||
238
+ a === "--claim" ||
239
+ a === "--no-claim" ||
240
+ a === "--model" ||
241
+ a.startsWith("--model=") ||
242
+ a === "--permission-mode" ||
243
+ a.startsWith("--permission-mode="),
244
+ );
245
+ }
246
+
247
+ /** Resolve a SPAWN_TARGETS entry to an absolute path under PKG_ROOT. */
248
+ function spawnTargetPath(key) {
249
+ const entry = SPAWN_TARGETS[key];
250
+ if (!entry) throw new Error(`hs: unknown SPAWN_TARGETS key "${key}"`);
251
+ return path.join(PKG_ROOT, ...entry.path.split("/"));
252
+ }
253
+
25
254
  // Captured before anything in this process could ever chdir (it doesn't
26
255
  // today, but every dispatched entry below is spawned with its own `cwd`
27
256
  // override — see dispatchSpexConsoleEntry). This is the ONE true record of
@@ -42,20 +271,244 @@ const INVOKED_CWD = process.cwd();
42
271
  process.env.HS_INVOKED_CWD = INVOKED_CWD;
43
272
 
44
273
  /**
45
- * Walk up from `start` until we find a directory containing `specs/`.
46
- * That directory is the vault root.
274
+ * Walk up from `start` until we find a vault root — a directory containing
275
+ * a `heyspex.yaml` marker file OR a `specs/` dir. Delegates the "is this a
276
+ * vault root" test to the shared `isVaultRoot` helper (same one the MCP
277
+ * resolver's cwd walk uses) so the two walks can never disagree about what
278
+ * counts as a vault ([[HS-GKAI]]).
47
279
  */
48
- function findVaultRoot(start) {
280
+ export function findVaultRoot(start, stopAt) {
49
281
  let dir = path.resolve(start);
282
+ const boundary = stopAt ? path.resolve(stopAt) : null;
50
283
  while (true) {
51
- const candidate = path.join(dir, "specs");
52
- if (existsSync(candidate) && statSync(candidate).isDirectory()) {
284
+ // A SATELLITE repo (a code repo attached to an existing vault -- see
285
+ // spex-console/src/lib/cli/satellite.ts) carries no marker of its own and
286
+ // names its vault in its MCP manifests instead. Checked FIRST at each
287
+ // level so an explicit attachment beats a vault that merely happens to
288
+ // sit further up the tree, and so `hs` in a satellite follows THAT vault
289
+ // rather than whatever the machine-global active pointer last named.
290
+ const pinned = readSatellitePin(dir);
291
+ if (pinned) {
292
+ return pinned;
293
+ }
294
+ // A committed, portable satellite marker (heyspex.yaml with a relative
295
+ // `vault_path`): the attached vault's clone when it is on this machine,
296
+ // else this repo itself (cloud-only; the marker's vault_id binds it).
297
+ const marked = readMarkerSatellite(dir);
298
+ if (marked) {
299
+ return marked.vaultRootAbs ?? dir;
300
+ }
301
+ if (isVaultRoot(dir)) {
53
302
  return dir;
54
303
  }
304
+ // `stopAt` is a TEST-ONLY walk boundary (mirrors vault-resolve.mjs's
305
+ // `cwdWalkRoot`) — checked, then the walk stops, never climbing past it.
306
+ // Unset in production, so the walk always reaches the real filesystem
307
+ // root; tests fence it inside a disposable fixture so an unrelated
308
+ // ancestor higher up the real tree (e.g. a stray `specs/` dir left over
309
+ // in a temp folder) cannot produce a false match ([[HS-0I1G]]).
310
+ if (boundary && dir === boundary) return null;
311
+ const parent = path.dirname(dir);
312
+ if (parent === dir) return null;
313
+ dir = parent;
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Registry fallback for when the specs/ walk-up finds nothing — since the
319
+ * repo split (2026-08-31) the vault lives outside the code repo, so `hs`
320
+ * run from the code repo would otherwise exit "no vault detected".
321
+ * Delegates to the shared resolver (same one the MCP server trusts) but
322
+ * only accepts a result that names a real vault: the resolver's last-resort
323
+ * cwd fallback is rejected so the "no vault detected" error below still
324
+ * fires when there is genuinely no vault anywhere.
325
+ *
326
+ * The test is deliberately "anything but `cwd-heuristic`", so every real
327
+ * tier passes it: `explicit`, `account-pointer`, `env-vault-root`,
328
+ * `env-vault-dir`, and `satellite-pin` (an on-disk manifest attachment found
329
+ * by the resolver's tier-3 walk -- that tier reported the untrue
330
+ * `env-vault-root` until 2026-09-18; both labels are accepted here, so the
331
+ * rename changed nothing). `cwd-marker` and `marker-registry-mismatch` (a
332
+ * heyspex.yaml `vault_id` named the vault, [[HS-VVNQ]]) pass too.
333
+ */
334
+ export function findRegistryVaultRoot(options = {}) {
335
+ let resolved;
336
+ try {
337
+ resolved = resolveActiveVault(options);
338
+ } catch {
339
+ return null;
340
+ }
341
+ if (!resolved || resolved.source === "cwd-heuristic") return null;
342
+ if (!existsSync(resolved.specsDirAbs)) return null;
343
+ return resolved.repoRootAbs;
344
+ }
345
+
346
+ /** Env hatch: follow the account-wide active-vault pointer on purpose (same name the MCP transport uses). */
347
+ export function pointerVaultAllowed(env = process.env) {
348
+ const v = String(env.HEYSPEX_ALLOW_POINTER_VAULT ?? "").trim().toLowerCase();
349
+ return v === "1" || v === "true" || v === "yes";
350
+ }
351
+
352
+ /**
353
+ * Verbs that may legitimately run with only the machine's last-active vault
354
+ * (or none): they onboard, sign in, or manage the registry itself. EVERY other
355
+ * verb acts on a vault, and P1 (owner, 2026-10-16) says that vault is the one
356
+ * bound to the repo the agent runs in -- never the account-wide pointer.
357
+ */
358
+ const POINTER_FALLBACK_NAMESPACES = new Set([
359
+ "init", "install", "join", "setup", "skills", "vault", "workspace",
360
+ "login", "whoami", "logout", "doctor", "registry", "companion", "trust",
361
+ ]);
362
+
363
+ /** True when `hs <namespace> <command>` may run on the pointer-only fallback. */
364
+ export function pointerFallbackAllowedFor(namespace, command) {
365
+ if (namespace === "mcp") return command === "pair" || command === "login" || command === "install";
366
+ return POINTER_FALLBACK_NAMESPACES.has(namespace);
367
+ }
368
+
369
+ /** The refusal printed when a satellite's pin / marker cannot be honoured. */
370
+ export function brokenPinRefusalLine(broken) {
371
+ return (
372
+ `hs: this repo's vault pin cannot be honoured, so nothing was run. ${broken.dir}/${broken.source} ` +
373
+ `names \`${broken.raw}\`, but ${broken.reason}. ` +
374
+ "Fix the pin (a relative path such as ../<vault-repo> or ${CLAUDE_PROJECT_DIR:-.}/../<vault-repo> is portable), " +
375
+ "or commit a heyspex.yaml with vault_id and vault_path; hs will NOT fall back to the account-wide active vault.\n"
376
+ );
377
+ }
378
+
379
+ /** Nearest directory at/above `start` whose heyspex.yaml is a satellite marker (has vault_path), else null. */
380
+ export function markerSatelliteDirFrom(start) {
381
+ let dir = path.resolve(start);
382
+ for (let i = 0; i < 256; i += 1) {
383
+ if (readMarkerSatellite(dir)) return dir;
55
384
  const parent = path.dirname(dir);
56
385
  if (parent === dir) return null;
57
386
  dir = parent;
58
387
  }
388
+ return null;
389
+ }
390
+
391
+ /** The refusal for disk-writing verbs in a satellite whose vault clone is missing. */
392
+ export function unclonedVaultRefusalLine(repo) {
393
+ const m = readMarkerSatellite(repo);
394
+ return (
395
+ `hs: ${repo} is bound to vault ${m?.vaultId ?? "(no vault_id)"} by its heyspex.yaml, but the vault ` +
396
+ `is not cloned at ${m?.vaultPath ?? "its vault_path"} on this machine, so this verb has no local vault to act on. ` +
397
+ "Clone the vault repo to that relative path (agents and cloud-routed MCP writes work without it).\n"
398
+ );
399
+ }
400
+
401
+ /** The refusal printed when only the account-wide pointer answered and the verb acts on a vault. */
402
+ export function pointerOnlyRefusalLine(cwd, vaultRoot) {
403
+ return (
404
+ `hs: ${cwd} is not bound to a HeySpex vault (no heyspex.yaml, no specs/, no vault pin in .mcp.json), ` +
405
+ `so \`hs\` refused to follow the account-wide active vault${vaultRoot ? ` (${vaultRoot})` : ""}. ` +
406
+ "Bind this repo: run `hs connect` in a vault repo, `hs install --attach-vault <id> --target <repo>` for a code repo, " +
407
+ "or commit a heyspex.yaml with vault_id and a relative vault_path. " +
408
+ "To follow the pointer on purpose set HEYSPEX_ALLOW_POINTER_VAULT=1.\n"
409
+ );
410
+ }
411
+
412
+ /**
413
+ * The vault-resolution ladder `main()` runs at the top of every invocation,
414
+ * factored out so it is unit-testable without spawning a child process
415
+ * ([[HS-0I1G-B0IT-TASK-track-and-announce-the-registry-active]]).
416
+ *
417
+ * Reports `usedRegistryFallback: true` only when NONE of `HS_VAULT_ROOT`, an
418
+ * explicit `--target`-equivalent, or a marker/satellite pin found walking up
419
+ * from `cwd` answered, and `findRegistryVaultRoot()` (the machine's
420
+ * last-active vault, `~/.heyspex/registry.json`) is what resolved the root.
421
+ * `hs install` uses that flag to warn (always) and — when `--register-vault`
422
+ * is given without `--dry-run` — refuse, so a customer onboarding an empty
423
+ * directory doesn't silently write into whatever vault this machine last had
424
+ * active in some other terminal/console tab.
425
+ *
426
+ * `stopAt` is a TEST-ONLY walk-boundary fence, threaded through to both
427
+ * `findVaultRoot`'s own walk and `resolveActiveVault`'s inner cwd walk (via
428
+ * its `cwdWalkRoot` option) — production callers (`main()`) omit it.
429
+ *
430
+ * @param {string} cwd
431
+ * @param {NodeJS.ProcessEnv} env
432
+ * @param {string} [stopAt]
433
+ * @returns {{ vaultRoot: string|null, usedRegistryFallback: boolean }}
434
+ */
435
+ export function resolveVaultRootForMain(cwd, env, stopAt) {
436
+ if (env.HS_VAULT_ROOT) {
437
+ return { vaultRoot: env.HS_VAULT_ROOT, usedRegistryFallback: false };
438
+ }
439
+ // A pin that cannot be honoured is a REFUSAL, never a reason to fall
440
+ // through to the account-wide pointer (owner principle P1, 2026-10-16).
441
+ const brokenPin = findBrokenSatellitePin(cwd, stopAt);
442
+ if (brokenPin) {
443
+ return { vaultRoot: null, usedRegistryFallback: false, brokenPin };
444
+ }
445
+ const walkedRoot = findVaultRoot(cwd, stopAt);
446
+ if (walkedRoot) {
447
+ return { vaultRoot: walkedRoot, usedRegistryFallback: false };
448
+ }
449
+ // Same ONE order as both resolvers ([[HS-VVNQ]], shared fixture
450
+ // spex-console/src/lib/vault-resolve/precedence-fixtures.json):
451
+ // `HS_VAULT_ROOT` (this CLI's explicit tier) → the walk above (satellite
452
+ // pin, then marker) → `HEYSPEX_VAULT_ROOT`/`HEYSPEX_VAULT_DIR` env → the
453
+ // registry pointer, the last two via the shared resolver. `cwd` is passed
454
+ // explicitly so the resolver's own walk starts where ours did — left to
455
+ // its default it would start from `CLAUDE_PROJECT_DIR`, and a marker found
456
+ // THERE (a different folder) would be mislabelled a registry fallback.
457
+ const registryRoot = findRegistryVaultRoot(
458
+ stopAt ? { cwd, cwdWalkRoot: stopAt } : { cwd },
459
+ );
460
+ return {
461
+ vaultRoot: registryRoot,
462
+ usedRegistryFallback: Boolean(registryRoot),
463
+ };
464
+ }
465
+
466
+ /** True when `argv` carries `--attach-vault` (bare or `=value` form). */
467
+ export function hasAttachVaultFlag(argv) {
468
+ return argv.some(
469
+ (a) => a === "--attach-vault" || a.startsWith("--attach-vault="),
470
+ );
471
+ }
472
+
473
+ /**
474
+ * True when `argv` carries an explicit `--target` (bare or `=value` form) —
475
+ * mirrors `install.ts`'s own `parseInstallArgs` flag test. `hs-install.ts`'s
476
+ * `resolveInstallTargetDir` already honors `--target` over `HS_VAULT_ROOT`
477
+ * unconditionally, so a caller who named their target explicitly is never
478
+ * actually affected by the registry fallback `bin/hs.mjs` computed for
479
+ * `HS_VAULT_ROOT` — warning or refusing anyway would be a false alarm about a
480
+ * value the install never consults.
481
+ */
482
+ export function hasExplicitTargetFlag(argv) {
483
+ return argv.some((a) => a === "--target" || a.startsWith("--target="));
484
+ }
485
+
486
+ /**
487
+ * The warning `hs install` prints (to stderr, in both `--dry-run` and real
488
+ * output) when the target it is about to use came ONLY from the registry
489
+ * active-vault fallback — i.e. cwd sits under no vault at all and this
490
+ * machine's last-active vault pointer answered instead.
491
+ */
492
+ export function registryFallbackWarningLine(cwd, vaultRoot) {
493
+ return (
494
+ `hs install: no vault found in or above ${cwd} — falling back to this ` +
495
+ `machine's last-active vault (${vaultRoot}, from ~/.heyspex/registry.json). ` +
496
+ "Pass --target <dir> to install into this directory instead.\n"
497
+ );
498
+ }
499
+
500
+ /**
501
+ * The refusal `hs install --register-vault` (without `--dry-run`) prints and
502
+ * exits non-zero on instead of registering, when the fallback above is what
503
+ * answered — registering here would file the machine's last-active vault
504
+ * under a fresh pointer, not the one the caller meant.
505
+ */
506
+ export function registryFallbackRefusalLine() {
507
+ return (
508
+ "hs install: refusing to run --register-vault from an empty directory that " +
509
+ "resolved only through the registry active-vault fallback. Pass --target <dir> " +
510
+ "naming the vault to register, or --dry-run to preview first.\n"
511
+ );
59
512
  }
60
513
 
61
514
  /**
@@ -93,8 +546,14 @@ function usage() {
93
546
  "",
94
547
  "Usage:",
95
548
  " hs login [--console-url <url>] [--label <t>] [--json] Sign in via your browser (no code to type)",
549
+ " hs login --switch-account [<id|email>] | --create-account Switch to a stored account, or choose/create one in the browser",
550
+ " hs login --migrate [--dry-run] Re-key a single-account credentials file under its confirmed account",
551
+ " hs whoami [--account <id|email>] [--json] Which account this machine is signed in as (email, id, source, console)",
552
+ " hs logout [--account <id|email>] [--json] Sign one account out of this machine (default: the current one)",
96
553
  " hs init [--console-url <url>] [--force] Write the heyspex.yaml vault marker",
97
554
  " hs install [--target <dir>] [--platform <dir>] Bootstrap vault + Cursor/Claude MCP",
555
+ " hs install --attach-vault <id|path> Wire THIS code repo to an existing vault",
556
+ " (satellite: no specs/, no heyspex.yaml here)",
98
557
  " hs invite [<name>] Mint a short pairing code (other computer: hs join CODE)",
99
558
  " hs join <code> Join with a pairing code (LAN auto-find; no IP to type)",
100
559
  " 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 +567,35 @@ function usage() {
108
567
  " hs dev [mode:local|mode:cloud|mode:prod] Dev/prod mode + local/cloud reliance (status when bare)",
109
568
  " hs setup claude|cursor [--check|--uninstall] Install agent lifecycle hooks",
110
569
  " hs skills sync|check Propagate skills to Cursor/Codex/Claude (cwd-safe)",
570
+ " hs skills add <module-path> --name <n> --trigger </slash> [--export <n>] [--description <t>] Register a skill (extension seam)",
571
+ " hs skills remove <name> Remove a customer-registered skill",
572
+ " hs skills list [--json] List registered skills (built-ins first, locked)",
111
573
  " hs index rebuild [--verbose] [--dry-run] Full idempotent rebuild of the index",
112
- " hs pull [--apply] [--only <id>] [--overwrite] Bring cloud-written specs down to the disk vault (reports unless --apply)",
574
+ " hs index reconcile [--apply] [--cap-docs keep-out|index] Per-spec newest-wins between disk and cloud (reports unless --apply)",
575
+ " hs pull [--apply] [--only <id>] [--overwrite] Bring cloud-written specs down to the disk vault (reports unless --apply; --watch --push adds the disk -> cloud leg)",
576
+ " hs push [--apply] [--only <id>] [--force] Send local spec edits up to the cloud vault (reports unless --apply)",
577
+ " hs import <source-dir> [--folders a,b] [--plan f] [--dry-run] [--limit N] Import a doc tree as GROUNDED spec-import captures (never raw, never research)",
578
+ " hs source status|preflight|commit|push|branch Local git: tree status, path-scoped checks, commit, safe push, session branch",
579
+ " hs source identity [show|set|clear] Git author identity: show (read-only) / set, clear (HUMAN-ONLY)",
113
580
  " hs milestone show <id> [--json] Show a milestone (flags needs-breakdown when epicless)",
114
581
  " hs factory status|fleet|results [--json] Dev-Factory read-only status (fleet, routing, calibrations, results)",
115
582
  " hs factory run status [--json] Live assisted/autonomous dispatcher run (pids, queue, stop hints)",
116
583
  " hs code-watcher scan [--json] Capture build-correctness failures (tsc) → Resolution",
117
584
  " hs server status [--json] Ground-truth dev-server status (running? which port[s]?)",
585
+ " hs wip status [--json] WIP ledger health report (lines vs 600-line budget, age tiers, archive)",
586
+ " hs wip restore <id> [--json] [--force] Restore an archived WIP ledger entry back onto the live ledger",
587
+ " hs worktree list|alloc|release|reap [--json] [--dry-run] Session/build worktree registry (HS-03DO)",
118
588
  " hs view <surface> [spec-id] [--width N --height N --browser edge|chrome] Open one surface in a chromeless app-mode window",
119
589
  " hs server stop [--json] Stop the spex-console dev server (any terminal)",
590
+ " hs source status|preflight|commit|push SpexSource local git operations",
591
+ " hs source identity [show|set|clear] Git author identity (set/clear are human-only; see hs source --help)",
120
592
  " hs defer <id> [--until <when>] [--json] Time-shift a spec (defer_until)",
121
593
  " hs undefer <id> [--json] Clear a spec's defer_until",
122
594
  " 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)",
595
+ ' hs capture "<text>"|--clipboard|--file <p> [--add] Capture → console drafts (--add chains a group)',
124
596
  " hs companion start|stop|status|build Desktop companion tray app",
597
+ " hs companion settings [list|get <k>|set <k> <v>] [--json] Every Companion setting (console-backed)",
598
+ " hs companion mute|unmute|volume|orb|wake|activation|hotkey [<v>] Setting shortcuts (omit <v> to read)",
125
599
  " hs registry list [--json] List launch registry entries",
126
600
  " hs env init|set|list|run|export|rotate Encrypted env vault (ADR-0020)",
127
601
  " hs env profiles | key export|import Named profiles + second-machine key transfer",
@@ -129,23 +603,52 @@ function usage() {
129
603
  " hs settings list|get|set Read/change workflow settings (.heyspex/config.yaml)",
130
604
  " hs classifier stats [--json] [--recent N] Intent Classifier decision stats (read-only)",
131
605
  " hs next [spec_id] [--summary] [--json] Resolve on-deck brief (MCP fallback)",
606
+ " hs next <id> --launch [--dry-run] [--claim] [--backend <anthropic|ollama|deepseek>] Launch a session on that spec",
607
+ " hs next model|permission-mode [<kind>] [<v>] Per-kind launch defaults",
608
+ " hs next backend [<kind>] [<v>] Per-kind launch backend default (anthropic|ollama|deepseek)",
609
+ " hs next claim [on|off] Auto-claim vs ask-first default",
610
+ " hs next front [handheld|assist|auto] Default front: one task with you, or a dispatcher run",
611
+ " hs next assist|auto <milestone...> --launch Arm a dispatcher run on one or more milestones",
612
+ " hs model [<seat>] [--launch|--fleet] Every seat and the model set for it",
613
+ " hs seat list|add|update|disable [--json] Read/add/update/disable a seat in the registry (dev-factory/seats.json)",
614
+ " hs voice add|build|show|set|test|refresh|export Build and manage a coded voice profile",
615
+ " hs trash [list|restore|empty] The bin spec deletes go into",
132
616
  " hs auto [filename] [--json] Resolve /hs-auto dispatch plan",
133
617
  " hs mcp call <tool> [--arg key=value ...] [--args '<json>'] [--args-file <path>] [--json] One-shot MCP tool",
134
618
  " hs mcp pair <code> [--console-url <url>] Redeem a console pairing code (cross-machine fallback for hs login)",
619
+ " hs mcp install --remote [--write] [--json] Write/print .mcp.json for the cloud (remote) MCP endpoint",
135
620
  " hs test run <id|manifest> [--format ...] Run open-format API tests (Hurl/.http) per AC",
136
621
  " hs test gate [dir] [--report <j>] [--threshold n] Mutation-testing gate — reject suites that kill no mutants",
137
622
  " hs test conform <spec> --url <u> | --report <x> OpenAPI conformance / docs↔impl drift (Schemathesis)",
623
+ " hs chat [--model <name>] [--script <file>] [--seat <seat>] HeySpex's own chat front (any model); !<cmd> or /shell for CLI passthrough",
624
+ " hs trust list List spexcode's stored folder-trust decisions",
625
+ " hs trust revoke [path] Clear one folder's trust decision (default: cwd) so the next launch asks again",
626
+ " hs doctrine extract [--since <iso>] [--project <name>] [--dry-run] Stream every session/taste source into .heyspex/doctrine/observations.jsonl",
627
+ " hs doctrine learn [--full] [--max-batches <n>] [--dry-run] Extract, then synthesize into spexharness/doctrine/SPEXCODE-DOCTRINE.md/json",
628
+ " hs doctrine show Print the current SPEXCODE-DOCTRINE.md",
629
+ " hs harness gates [--json] List every SpexHarness gate: id, event, severity, last fixture result",
630
+ " hs hooks add <module-path> --event <event> [--id <id>] [--priority <n>] Register a hook (extension seam)",
631
+ " hs hooks remove <id> Remove a customer-registered hook",
632
+ " hs hooks list [--json] List registered hooks (built-ins first, locked)",
138
633
  " hs check run <id|manifest> [--json] Verify acceptance criteria vs the frozen checks manifest (per-AC pass/fail)",
634
+ " hs check judge <id> [--session <id>] [--dry-run] Decorrelated judge seat: verdicts on method:human criteria, posts to the verification API",
139
635
  " hs revalidate [--json] [--apply] Re-run the frozen checks of already-stamped work; report what has decayed",
140
636
  " hs agent import <collection> <role> Import a community agent into a type:agent vault spec",
141
637
  " hs agent list [collection] List vendored agent collections / roles",
142
638
  "",
639
+ " node dev-factory/spexcrawl/spexcrawl-cli.mjs search|scrape|crawl SpexCrawl — web",
640
+ " search/scrape/crawl CLI (standalone script,",
641
+ " not an `hs` verb; also reachable from the",
642
+ " Companion)",
643
+ "",
143
644
  "Control plane (Neon — requires a reachable console):",
144
645
  " hs pin get|set <id>|clear Read/set/clear the on-deck pin",
145
646
  " hs claim acquire|release <id> | list Atomic spec claims",
146
647
  " hs claim progress <id> --stage <verb> Report live work-stage",
147
- " hs handoff record|load [--lane <k>] Resume-context coordination",
648
+ " hs handoff record|load [--lane <k>] Resume-context coordination (never launches)",
649
+ " hs handoff <CODE> [--no-launch] Resolve + LAUNCH the lane's resume by default",
148
650
  " hs events show <id> [--kind --since] Per-spec audit log",
651
+ " hs admin test-user create --env <e> Mint one throwaway test customer",
149
652
  " hs export-events [--since <date>] Idempotent audit export → specs/events/",
150
653
  " hs prereq <blocked-id> [<action> <prereq>] Cross-milestone prerequisite resolution",
151
654
  " hs queue submit|next|claim|complete|fail|list Fleet-queue dispatch (Neon, direct — no console hop)",
@@ -155,7 +658,13 @@ function usage() {
155
658
  " hs --version Show CLI version",
156
659
  "",
157
660
  "Vault root is detected by walking up from the current directory until a",
158
- "specs/ folder is found. Override with HS_VAULT_ROOT.",
661
+ "heyspex.yaml marker or a specs/ folder is found, then falling back to the",
662
+ "registry's active vault (~/.heyspex/registry.json). Override with HS_VAULT_ROOT.",
663
+ "When hs install resolves ONLY through that registry fallback (a fully empty",
664
+ "cwd — no heyspex.yaml/specs/ found walking up, no HS_VAULT_ROOT, no --target),",
665
+ "it warns, naming the fallback vault, in both --dry-run and real output; and",
666
+ "`hs install --register-vault` without --dry-run in that case refuses instead of",
667
+ "registering, asking for --target. --attach-vault (satellite mode) is exempt.",
159
668
  "",
160
669
  ].join("\n"),
161
670
  );
@@ -169,9 +678,13 @@ function usage() {
169
678
  * Three cases, in order ([[HS-JOQX-0JNK-TASK-published-package-runs-hs-init-and-hs]]):
170
679
  * 1. Monorepo checkout: `.ts` source AND `tsx` both present — run via tsx,
171
680
  * 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
681
+ * 2. Published install of a BUNDLED verb — currently `hs-init.ts`,
682
+ * `hs-install.ts`, `hs-claim.ts`, `hs-check.ts`, `hs-env.ts`,
683
+ * `hs-connect.ts`, `hs-join.ts`, `hs-vault.ts`, `hs-workspace.ts`,
684
+ * `hs-defer.ts`, `hs-vault-pull.ts`, `hs-dev.ts`, `hs-index-rebuild.ts`,
685
+ * `hs-index-reconcile.ts` — see `scripts/build-cli-bundle.mjs`'s
686
+ * `ENTRIES` array and doc comment for the authoritative list and why
687
+ * each qualifies: neither the source nor tsx exists, but
175
688
  * `dist/cli/<name>.mjs` does — run the plain-JS bundle directly with
176
689
  * `node`, no tsx/TypeScript involved.
177
690
  * 3. Neither exists: the package install is incomplete (a verb this
@@ -256,7 +769,7 @@ function dispatchSpexConsoleEntry(entryFile, args, vaultRoot, extraEnv = {}) {
256
769
  * it), so there is nothing to check about `hs` other than reporting its
257
770
  * version.
258
771
  */
259
- function runStandaloneDoctor(args) {
772
+ function runStandaloneDoctor(args, vaultRoot) {
260
773
  const jsonOut = args.includes("--json");
261
774
  let version = "0.0.0";
262
775
  try {
@@ -292,6 +805,46 @@ function runStandaloneDoctor(args) {
292
805
  if (runtimeFilesRow.status !== "ok") report.ok = false;
293
806
  report.checks.push(runtimeFilesRow);
294
807
 
808
+ // `vault-config` ([[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]]
809
+ // fix 5): the vault's `.heyspex/config.yaml` (ADR-0009: vault root, git-
810
+ // tracked) must exist — without it every settings reader runs on defaults.
811
+ // `vaultRoot` is main()'s resolution (HS_VAULT_ROOT → walk-up → registry);
812
+ // null means no vault at all, which the row reports as its own failure.
813
+ const vaultConfigRow = vaultConfigCheckRow(vaultRoot ?? null);
814
+ if (vaultConfigRow.status !== "ok") report.ok = false;
815
+ report.checks.push(vaultConfigRow);
816
+
817
+ // `vault-template` (HS-KH0Q): shipped core docs + template_version pin present.
818
+ const templateRow = templateCheckRow(vaultRoot ?? null);
819
+ if (templateRow.status === "fail") report.ok = false;
820
+ report.checks.push(templateRow);
821
+
822
+ // `connection` ([[HS-W73V-WW9R-TASK-make-both-doctor-shapes-fail-on-cloud]],
823
+ // invariants 8 and 10): the BUNDLED `hs connect --check --json` from the
824
+ // invoked cwd, folded by the SAME function the full doctor uses
825
+ // (`foldConnectionReport`), so a customer machine gets the local-vs-cloud
826
+ // rows too. A disagreeing row fails the run with its repair command; an
827
+ // unreadable cloud side (offline, signed out) is a warn and leaves `ok`
828
+ // alone. The check reads the console with GET only. Its subject is the repo
829
+ // the caller stands in (the walk-up from the invoked cwd, else the cwd
830
+ // itself), NOT `vaultRoot`: that can be an inherited HS_VAULT_ROOT or the
831
+ // registry fallback, i.e. ANOTHER repo (seen when `hs connect`'s acceptance
832
+ // pass ran this doctor in a fresh repo and it audited the previous one).
833
+ const connectSubject = findVaultRoot(INVOKED_CWD) ?? INVOKED_CWD;
834
+ const connection = foldConnectionReport(
835
+ runBundledConnectCheck(PKG_ROOT, {
836
+ cwd: INVOKED_CWD,
837
+ env: {
838
+ ...process.env,
839
+ HS_INVOKED_CWD: INVOKED_CWD,
840
+ HS_VAULT_ROOT: connectSubject,
841
+ },
842
+ }),
843
+ );
844
+ if (!connection.ok) report.ok = false;
845
+ report.checks.push(...connection.rows);
846
+ report.connection = connection.json;
847
+
295
848
  const hsLinkLib = path.join(PKG_ROOT, "scripts", "lib", "hs-link.mjs");
296
849
  if (existsSync(hsLinkLib)) {
297
850
  // Deliberately synchronous top-level work below via a nested async IIFE
@@ -340,8 +893,15 @@ function printStandaloneDoctorReport(report, jsonOut) {
340
893
  "HeySpex setup check — published-package install (not a source clone)\n\n",
341
894
  );
342
895
  for (const check of report.checks) {
343
- const label = check.status === "ok" ? " ok " : " FAIL ";
344
- process.stdout.write(`[ ${label} ] ${check.detail}\n`);
896
+ const label =
897
+ check.status === "ok"
898
+ ? " ok "
899
+ : check.status === "warn"
900
+ ? " warn "
901
+ : " FAIL ";
902
+ process.stdout.write(
903
+ `[ ${label} ] ${check.label ? `${check.label}: ` : ""}${check.detail}\n`,
904
+ );
345
905
  if (check.human) process.stdout.write(` ${check.human}\n`);
346
906
  }
347
907
  process.stdout.write(
@@ -384,24 +944,88 @@ function main(argv) {
384
944
  const command = argv[1];
385
945
  const rest = argv.slice(2);
386
946
 
387
- const vaultRoot = process.env.HS_VAULT_ROOT || findVaultRoot(process.cwd());
947
+ const resolvedMain = resolveVaultRootForMain(process.cwd(), process.env);
948
+ const { usedRegistryFallback, brokenPin } = resolvedMain;
949
+ // P1: the pointer is not a binding. A verb that acts on a vault runs with
950
+ // NO vault (and is refused at the gate) rather than on whatever vault the
951
+ // console last selected; onboarding/registry verbs keep the old fallback.
952
+ const pointerOnly =
953
+ usedRegistryFallback &&
954
+ !pointerVaultAllowed(process.env) &&
955
+ !pointerFallbackAllowedFor(namespace, command);
956
+ const vaultRoot = pointerOnly ? null : resolvedMain.vaultRoot;
957
+ if (pointerOnly) {
958
+ process.stderr.write(pointerOnlyRefusalLine(process.cwd(), resolvedMain.vaultRoot));
959
+ }
960
+ if (
961
+ brokenPin &&
962
+ namespace !== "doctor" &&
963
+ !pointerFallbackAllowedFor(namespace, command)
964
+ ) {
965
+ process.stderr.write(brokenPinRefusalLine(brokenPin));
966
+ process.exit(2);
967
+ }
388
968
 
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.
969
+ // `hs init` writes the top-level heyspex.yaml vault marker AND scaffolds
970
+ // specs/ ([[HS-EPIC-AEHR-audit-export-heyspex-yaml-and-heyspex-retirement]]
971
+ // §2). It always acts on the caller's invocation directory (`INVOKED_CWD`,
972
+ // captured before any dispatch below runs) — NEVER on `vaultRoot`'s
973
+ // walk-up/registry result. `hs init` may legitimately run *inside* a stray
974
+ // ancestor's `specs/` folder (a half-set-up nested checkout, a doc repo
975
+ // that happens to have a `specs/` dir, etc.); using `vaultRoot` here would
976
+ // silently write the marker into that unrelated ancestor instead of where
977
+ // the user actually typed `hs init` (HS-HISX: first fresh-machine walkthrough
978
+ // break — the vault marker landed in the wrong folder).
979
+ // HS_PKG_ROOT is forwarded so the entrypoint can resolve the vendored
980
+ // `.heyspex/config.yaml` seed template (`vendor/heyspex-mcp/config-seed.yaml`)
981
+ // from the PACKAGE root, never from the caller's cwd (HS-4C76 fix 4).
393
982
  if (namespace === "init") {
394
- dispatchSpexConsoleEntry("hs-init.ts", argv, vaultRoot || process.cwd());
983
+ dispatchSpexConsoleEntry("hs-init.ts", argv, INVOKED_CWD, {
984
+ HS_PKG_ROOT: PKG_ROOT,
985
+ });
395
986
  return;
396
987
  }
397
988
 
398
989
  // `hs install` bootstraps a customer/project vault + agent MCP wiring. May run
399
990
  // on a fresh directory with no specs/ yet (like init).
991
+ //
992
+ // [[HS-0I1G-B0IT-TASK-track-and-announce-the-registry-active]]: when the
993
+ // resolution above bottomed out at the registry active-vault fallback
994
+ // (nothing set HS_VAULT_ROOT, and no marker/satellite pin was found walking
995
+ // up from cwd), say so — silently bootstrapping into whatever vault this
996
+ // machine last had active in some OTHER terminal/console tab is exactly the
997
+ // wrong-vault failure this task closes. Two flags are exempt because
998
+ // `hs-install.ts` never actually consults the vaultRoot this fallback
999
+ // computed for them, so warning/refusing about it would be a false alarm:
1000
+ // `--attach-vault` (satellite mode) — its target resolution
1001
+ // (resolveInstallTargetDir's satellite branch) ignores HS_VAULT_ROOT
1002
+ // entirely and reads HS_INVOKED_CWD instead (and `--register-vault` is
1003
+ // already mutually exclusive with it via assertSatelliteFlagCombination, so
1004
+ // the refusal below can never apply either way) — and an explicit
1005
+ // `--target`, which resolveInstallTargetDir honors over HS_VAULT_ROOT
1006
+ // unconditionally (see hasExplicitTargetFlag's doc comment).
400
1007
  if (namespace === "install") {
1008
+ if (
1009
+ usedRegistryFallback &&
1010
+ !hasAttachVaultFlag(argv) &&
1011
+ !hasExplicitTargetFlag(argv)
1012
+ ) {
1013
+ process.stderr.write(
1014
+ registryFallbackWarningLine(process.cwd(), vaultRoot),
1015
+ );
1016
+ const isDryRun = argv.includes("--dry-run");
1017
+ const isRegisterVault = argv.includes("--register-vault");
1018
+ if (isRegisterVault && !isDryRun) {
1019
+ process.stderr.write(registryFallbackRefusalLine());
1020
+ process.exit(1);
1021
+ }
1022
+ }
401
1023
  dispatchSpexConsoleEntry(
402
1024
  "hs-install.ts",
403
1025
  argv,
404
- vaultRoot || process.cwd(),
1026
+ // In a committed-marker satellite the install target is the CODE repo
1027
+ // (MCP entry, hooks, commands), never the vault clone the marker names.
1028
+ markerSatelliteDirFrom(INVOKED_CWD) ?? (vaultRoot || process.cwd()),
405
1029
  {
406
1030
  HS_PKG_ROOT: PKG_ROOT,
407
1031
  },
@@ -426,24 +1050,28 @@ function main(argv) {
426
1050
  if (namespace === "invite") {
427
1051
  if (!vaultRoot) {
428
1052
  process.stderr.write(
429
- "hs: no vault detected — could not find a specs/ folder " +
430
- "walking up from the current directory. " +
1053
+ "hs: no vault detected — could not find a heyspex.yaml marker or a " +
1054
+ "specs/ folder walking up from the current directory. " +
431
1055
  "Set HS_VAULT_ROOT or run from inside a vault.\n",
432
1056
  );
433
1057
  process.exit(1);
434
1058
  }
435
- dispatchSpexConsoleEntry("hs-env.ts", ["env", "invite", ...argv.slice(1)], vaultRoot);
1059
+ dispatchSpexConsoleEntry(
1060
+ "hs-env.ts",
1061
+ ["env", "invite", ...argv.slice(1)],
1062
+ vaultRoot,
1063
+ );
436
1064
  return;
437
1065
  }
438
1066
 
439
1067
  // `hs setup claude|cursor` — install agent lifecycle hooks into the vault root.
440
1068
  if (namespace === "setup") {
441
1069
  const agent = command?.toLowerCase();
442
- const SCRIPTS = {
443
- claude: "install-claude-code-hooks.mjs",
444
- cursor: "install-cursor-hooks.mjs",
1070
+ const SPAWN_KEYS = {
1071
+ claude: "setup:claude",
1072
+ cursor: "setup:cursor",
445
1073
  };
446
- if (!agent || !Object.hasOwn(SCRIPTS, agent)) {
1074
+ if (!agent || !Object.hasOwn(SPAWN_KEYS, agent)) {
447
1075
  process.stderr.write(
448
1076
  `hs setup: expected \`claude\` or \`cursor\` (got \`${command ?? ""}\`)\n` +
449
1077
  " hs setup claude [--check|--uninstall] Claude Code lifecycle hooks\n" +
@@ -454,7 +1082,7 @@ function main(argv) {
454
1082
  const setupFlags = rest.filter(
455
1083
  (a) => a === "--check" || a === "--uninstall",
456
1084
  );
457
- const script = path.join(PKG_ROOT, "scripts", SCRIPTS[agent]);
1085
+ const script = spawnTargetPath(SPAWN_KEYS[agent]);
458
1086
  const target = vaultRoot || process.cwd();
459
1087
  const child = spawn(process.execPath, [script, ...setupFlags], {
460
1088
  cwd: target,
@@ -464,7 +1092,7 @@ function main(argv) {
464
1092
  child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
465
1093
  child.on("error", (err) => {
466
1094
  process.stderr.write(
467
- `hs setup: failed to run ${SCRIPTS[agent]}: ${err.message}\n`,
1095
+ `hs setup: failed to run ${SPAWN_TARGETS[SPAWN_KEYS[agent]].path}: ${err.message}\n`,
468
1096
  );
469
1097
  process.exit(1);
470
1098
  });
@@ -472,17 +1100,44 @@ function main(argv) {
472
1100
  }
473
1101
 
474
1102
  // `hs skills sync|check` — propagate .commandcode/skills → Cursor/Codex/Claude shims.
1103
+ // `hs skills add|remove|list` — a DIFFERENT verb: the extension seam's skill
1104
+ // registry ([[HS-8N3K-NYPR-TASK-add-cli-verbs-so-a-developer-can]],
1105
+ // [[HS-8EDJ-NYPR-EPIC-extension-seam-for-other-developers]]), routed to its
1106
+ // own entrypoint below so it never touches the sync/check spawn path.
475
1107
  if (namespace === "skills") {
476
1108
  const action = command?.toLowerCase();
1109
+ if (action === "add" || action === "remove" || action === "list") {
1110
+ dispatchSpexConsoleEntry(
1111
+ "hs-skills.ts",
1112
+ argv,
1113
+ vaultRoot || process.cwd(),
1114
+ );
1115
+ return;
1116
+ }
477
1117
  if (action !== "sync" && action !== "check") {
478
1118
  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",
1119
+ `hs skills: expected \`sync\`, \`check\`, \`add\`, \`remove\`, or \`list\` (got \`${command ?? ""}\`)\n` +
1120
+ " hs skills sync Copy skills + slash shims to all agent harnesses\n" +
1121
+ " hs skills check Exit 1 if any harness copy is stale\n" +
1122
+ " hs skills add <module-path> --name <n> --trigger </slash> Register a skill (extension seam)\n" +
1123
+ " hs skills remove <name> Remove a customer-registered skill\n" +
1124
+ " hs skills list [--json] List registered skills\n",
482
1125
  );
483
1126
  process.exit(2);
484
1127
  }
485
- const script = path.join(PKG_ROOT, "scripts", "sync-skills.mjs");
1128
+ const script = spawnTargetPath("skills:sync");
1129
+ if (!existsSync(script)) {
1130
+ // `skills:sync` is a deliberately dev-only SPAWN_TARGETS entry — see its
1131
+ // `reason` above. A published `npm install heyspex` never has
1132
+ // `.commandcode/skills` (the platform's own SSOT content) on disk, so
1133
+ // report that plainly instead of a bare MODULE_NOT_FOUND.
1134
+ process.stderr.write(
1135
+ "hs skills: not available in a published `heyspex` install — this verb " +
1136
+ "propagates the HeySpex platform repo's OWN `.commandcode/skills` " +
1137
+ "source content, which only exists inside a clone of that repo.\n",
1138
+ );
1139
+ process.exit(1);
1140
+ }
486
1141
  const target = vaultRoot || PKG_ROOT;
487
1142
  const childArgs = action === "check" ? ["--check"] : [];
488
1143
  const child = spawn(process.execPath, [script, ...childArgs], {
@@ -532,18 +1187,22 @@ function main(argv) {
532
1187
  // auth verb, so it bypasses the hard vault gate below like init/install —
533
1188
  // signing in is exactly what you do BEFORE you have a vault here.
534
1189
  if (namespace === "login") {
535
- const cliPath = path.join(PKG_ROOT, "vendor", "heyspex-mcp", "cli.mjs");
1190
+ const cliPath = spawnTargetPath("heyspex-mcp-cli");
536
1191
  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",
1192
+ const child = spawn(
1193
+ process.execPath,
1194
+ [cliPath, "login", ...argv.slice(1)],
1195
+ {
1196
+ cwd: PKG_ROOT,
1197
+ stdio: "inherit",
1198
+ env: {
1199
+ ...process.env,
1200
+ HEYSPEX_VAULT_ROOT: loginRoot,
1201
+ HS_VAULT_ROOT: loginRoot,
1202
+ HEYSPEX_DISABLE_STDIO_BOOTSTRAP: "1",
1203
+ },
545
1204
  },
546
- });
1205
+ );
547
1206
  child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
548
1207
  child.on("error", (err) => {
549
1208
  process.stderr.write(`hs: failed to run login: ${err.message}\n`);
@@ -552,15 +1211,53 @@ function main(argv) {
552
1211
  return;
553
1212
  }
554
1213
 
1214
+ // `hs whoami` — which account this machine's credential belongs to (email,
1215
+ // account id, console), asked of GET /api/mcp/session. The account a repo
1216
+ // would be attached to by `hs connect` / `hs install`
1217
+ // ([[HS-HAQI-KODY-DIAG-hs-install-and-hs-connect-bind-a-new]]). Auth verb:
1218
+ // bypasses the vault gate like `hs login`.
1219
+ // `hs logout [--account <id|email>]` — remove one stored account's token
1220
+ // ([[HS-UA9S-WW9R-TASK-store-credentials-per-account-login]]). Auth verb:
1221
+ // bypasses the vault gate like `hs login` / `hs whoami`.
1222
+ if (namespace === "whoami" || namespace === "logout") {
1223
+ const cliPath = spawnTargetPath("heyspex-mcp-cli");
1224
+ const whoamiRoot = vaultRoot || process.cwd();
1225
+ const child = spawn(
1226
+ process.execPath,
1227
+ [cliPath, namespace, ...argv.slice(1)],
1228
+ {
1229
+ cwd: PKG_ROOT,
1230
+ stdio: "inherit",
1231
+ env: {
1232
+ ...process.env,
1233
+ HEYSPEX_VAULT_ROOT: whoamiRoot,
1234
+ HS_VAULT_ROOT: whoamiRoot,
1235
+ HEYSPEX_DISABLE_STDIO_BOOTSTRAP: "1",
1236
+ },
1237
+ },
1238
+ );
1239
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
1240
+ child.on("error", (err) => {
1241
+ process.stderr.write(`hs: failed to run ${namespace}: ${err.message}\n`);
1242
+ process.exit(1);
1243
+ });
1244
+ return;
1245
+ }
1246
+
555
1247
  // `hs connect --check [--json]` — the read-only connect-checks report
556
1248
  // ([[HS-I2OO-EKLJ-TASK-shared-connect-checks-module-with-a]] on
557
1249
  // [[HS-QYH8-EKLJ-EPIC-hs-connect-one-command-repo-connect-verb]]). May run
558
1250
  // in an unconnected repo (no specs/ yet — that is the point), so it
559
1251
  // bypasses the hard vault gate below like init/install/vault/workspace.
560
1252
  if (namespace === "connect") {
561
- dispatchSpexConsoleEntry("hs-connect.ts", argv, vaultRoot || process.cwd(), {
562
- HS_INVOKED_CWD: INVOKED_CWD,
563
- });
1253
+ dispatchSpexConsoleEntry(
1254
+ "hs-connect.ts",
1255
+ argv,
1256
+ vaultRoot || process.cwd(),
1257
+ {
1258
+ HS_INVOKED_CWD: INVOKED_CWD,
1259
+ },
1260
+ );
564
1261
  return;
565
1262
  }
566
1263
 
@@ -572,7 +1269,7 @@ function main(argv) {
572
1269
  // the very first thing a stranger runs after `npm install heyspex` in an
573
1270
  // empty project is exactly this, from a directory with no `specs/` yet.
574
1271
  if (namespace === "doctor") {
575
- const script = path.join(PKG_ROOT, "scripts", "heyspex-doctor.mjs");
1272
+ const script = spawnTargetPath("doctor");
576
1273
  if (!existsSync(script)) {
577
1274
  // Published-package install (per HS-ZJSP-0JNK): the full doctor audits
578
1275
  // dozens of source-checkout-only surfaces (git hooks, Cursor wiring,
@@ -580,7 +1277,7 @@ function main(argv) {
580
1277
  // it is deliberately not in the package's `files`. Run the small
581
1278
  // subset that DOES apply to a published install instead of crashing
582
1279
  // with MODULE_NOT_FOUND.
583
- runStandaloneDoctor(argv.slice(1));
1280
+ runStandaloneDoctor(argv.slice(1), vaultRoot);
584
1281
  return;
585
1282
  }
586
1283
  const child = spawn(process.execPath, [script, ...argv.slice(1)], {
@@ -598,15 +1295,40 @@ function main(argv) {
598
1295
 
599
1296
  if (!vaultRoot) {
600
1297
  process.stderr.write(
601
- "hs: no vault detected — could not find a specs/ folder " +
602
- "walking up from the current directory. " +
1298
+ "hs: no vault detected — could not find a heyspex.yaml marker or a " +
1299
+ "specs/ folder walking up from the current directory. " +
603
1300
  "Set HS_VAULT_ROOT or run from inside a vault.\n",
604
1301
  );
605
1302
  process.exit(1);
606
1303
  }
607
1304
 
1305
+ // A committed satellite marker whose vault clone is NOT on this machine
1306
+ // resolves to the code repo itself (cloud-only binding). Verbs that write the
1307
+ // vault's files to disk must never scaffold specs/ into a code repo.
1308
+ if (
1309
+ ["pull", "push", "index"].includes(namespace) &&
1310
+ readMarkerSatellite(vaultRoot) &&
1311
+ !existsSync(path.join(vaultRoot, "specs"))
1312
+ ) {
1313
+ process.stderr.write(unclonedVaultRefusalLine(vaultRoot));
1314
+ process.exit(2);
1315
+ }
1316
+
608
1317
  if (namespace === "index" && command === "rebuild") {
609
- dispatchSpexConsoleEntry("hs-index-rebuild.ts", rest, vaultRoot);
1318
+ dispatchSpexConsoleEntry("hs-index-rebuild.ts", rest, vaultRoot, {
1319
+ ...(vaultIdOverride ? { HS_VAULT_ID_OVERRIDE: vaultIdOverride } : {}),
1320
+ });
1321
+ return;
1322
+ }
1323
+
1324
+ // `hs index reconcile` — per-spec newest-wins between the disk vault and the
1325
+ // cloud index ([[HS-MS75]]). Read-only unless `--apply`; status moves go
1326
+ // through the status route (refusals are listed), content through the
1327
+ // phased reindex route with preserve_status; never deletes, never finalizes.
1328
+ if (namespace === "index" && command === "reconcile") {
1329
+ dispatchSpexConsoleEntry("hs-index-reconcile.ts", rest, vaultRoot, {
1330
+ ...(vaultIdOverride ? { HS_VAULT_ID_OVERRIDE: vaultIdOverride } : {}),
1331
+ });
610
1332
  return;
611
1333
  }
612
1334
 
@@ -617,7 +1339,34 @@ function main(argv) {
617
1339
  // pushes, never deletes, and only REPORTS unless `--apply` is given.
618
1340
  // Forwards the full argv; the entrypoint drops the leading `pull` token.
619
1341
  if (namespace === "pull") {
620
- dispatchSpexConsoleEntry("hs-vault-pull.ts", argv, vaultRoot);
1342
+ dispatchSpexConsoleEntry("hs-vault-pull.ts", argv, vaultRoot, {
1343
+ ...(vaultIdOverride ? { HS_VAULT_ID_OVERRIDE: vaultIdOverride } : {}),
1344
+ });
1345
+ return;
1346
+ }
1347
+
1348
+ // `hs import <source-dir>` — a doc tree becomes GROUNDED spec-import captures
1349
+ // in the cwd's vault (heyspex.yaml marker, never the account pointer): group-
1350
+ // intent pass, foundational-to-specific order, sequential Ground=Expand with
1351
+ // a running batch ledger, one intent-map doc, source links; never promotes
1352
+ // ([[HS-0GOV-43G9-CAP-the-spec-import-function-should-not-stop]]). Forwards
1353
+ // the full argv; the entrypoint drops the leading `import` token and resolves
1354
+ // a relative <source-dir> against HS_INVOKED_CWD.
1355
+ if (namespace === "import") {
1356
+ dispatchSpexConsoleEntry("hs-import.ts", argv, vaultRoot, {
1357
+ HS_INVOKED_CWD: INVOKED_CWD,
1358
+ ...(vaultIdOverride ? { HS_VAULT_ID_OVERRIDE: vaultIdOverride } : {}),
1359
+ });
1360
+ return;
1361
+ }
1362
+
1363
+ // `hs push` — disk -> cloud (SpexRelay local leg, HS-FCNH). Report-only
1364
+ // unless --apply; never creates specs, never pushes status, refuses over a
1365
+ // newer cloud copy. Forwards the full argv; the entrypoint drops `push`.
1366
+ if (namespace === "push") {
1367
+ dispatchSpexConsoleEntry("hs-vault-push.ts", argv, vaultRoot, {
1368
+ ...(vaultIdOverride ? { HS_VAULT_ID_OVERRIDE: vaultIdOverride } : {}),
1369
+ });
621
1370
  return;
622
1371
  }
623
1372
 
@@ -629,6 +1378,14 @@ function main(argv) {
629
1378
  return;
630
1379
  }
631
1380
 
1381
+ // NOTE: `hs source` is handled further down by the plain-Node
1382
+ // scripts/lib/hs-source-*.mjs cores ([[HS-Q0K8-CWPX-EPIC-spexsource-local-git-preflight-commit]]),
1383
+ // which ship inside the published package. An earlier TypeScript build of the
1384
+ // same verb ([[HS-KN93-V4RB-EPIC-local-git-preflight-commit-and-push-for]],
1385
+ // spex-console/bin/hs-source.ts + src/lib/source-local/) used to be dispatched
1386
+ // from this spot; it was superseded when the two branches merged and must not
1387
+ // be re-wired here, or the package-shipping handler below becomes dead code.
1388
+
632
1389
  if (namespace === "factory" && command === "run") {
633
1390
  dispatchSpexConsoleEntry("hs-factory-run.ts", argv, vaultRoot);
634
1391
  return;
@@ -659,9 +1416,8 @@ function main(argv) {
659
1416
  // server-impacting task; stop from any terminal (not only the one that
660
1417
  // started it). See [[dev-server-lifecycle-and-db-safety]].
661
1418
  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);
1419
+ const spawnKey = command === "stop" ? "server:stop" : "server:status";
1420
+ const script = spawnTargetPath(spawnKey);
665
1421
  const child = spawn(process.execPath, [script, ...rest], {
666
1422
  cwd: PKG_ROOT,
667
1423
  stdio: "inherit",
@@ -670,7 +1426,55 @@ function main(argv) {
670
1426
  child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
671
1427
  child.on("error", (err) => {
672
1428
  process.stderr.write(
673
- `hs: failed to run ${scriptName}: ${err.message}\n`,
1429
+ `hs: failed to run ${SPAWN_TARGETS[spawnKey].path}: ${err.message}\n`,
1430
+ );
1431
+ process.exit(1);
1432
+ });
1433
+ return;
1434
+ }
1435
+
1436
+ // `hs wip status` / `hs wip restore <id>` — health report + restore for the
1437
+ // WIP hand-off ledger (`.heyspex/wip-state.json`) and its archive
1438
+ // (`.heyspex/wip-archive/*.jsonl`). Pure Node core under scripts/ (same
1439
+ // pattern as `hs server status/stop` above). See
1440
+ // [[HS-B8OK-PFMN]] / capture [[HS-817Y-83XQ]] ("the WIP ledger maintains
1441
+ // itself"). Forwards `command` plus the rest of argv; the script itself
1442
+ // decides status vs restore off that first token.
1443
+ if (namespace === "wip" && (command === "status" || command === "restore")) {
1444
+ const script = spawnTargetPath("wip");
1445
+ const child = spawn(process.execPath, [script, command, ...rest], {
1446
+ cwd: PKG_ROOT,
1447
+ stdio: "inherit",
1448
+ env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
1449
+ });
1450
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
1451
+ child.on("error", (err) => {
1452
+ process.stderr.write(
1453
+ `hs: failed to run wip-ledger-health.mjs: ${err.message}\n`,
1454
+ );
1455
+ process.exit(1);
1456
+ });
1457
+ return;
1458
+ }
1459
+
1460
+ // `hs worktree list|alloc|release|reap|rebind` — session/build worktree
1461
+ // registry (HS-03DO / HS-V9XH). Pure Node core under scripts/, itself a
1462
+ // thin shell over dev-factory/lib/worktree-allocator.mjs (called in-process,
1463
+ // never shelled out to). Same dispatch shape as `hs server status/stop` and
1464
+ // `hs wip status/restore` above. Forwards the full sub-argv; the entrypoint
1465
+ // drops the leading `worktree` token itself.
1466
+ if (namespace === "worktree") {
1467
+ const script = spawnTargetPath("worktree");
1468
+ const worktreeArgs = command ? [command, ...rest] : rest;
1469
+ const child = spawn(process.execPath, [script, ...worktreeArgs], {
1470
+ cwd: PKG_ROOT,
1471
+ stdio: "inherit",
1472
+ env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
1473
+ });
1474
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
1475
+ child.on("error", (err) => {
1476
+ process.stderr.write(
1477
+ `hs: failed to run hs-worktree.mjs: ${err.message}\n`,
674
1478
  );
675
1479
  process.exit(1);
676
1480
  });
@@ -695,27 +1499,41 @@ function main(argv) {
695
1499
  " hs source status Read-only tree status + path-scoped stage plan\n" +
696
1500
  " hs source preflight Alias of status\n" +
697
1501
  " hs source commit Stage + commit per the preflight plan\n" +
698
- " hs source push Push the committed branch\n" +
1502
+ " hs source push [branch] Push the committed branch (no branch: the shared session-branch resolver's target)\n" +
1503
+ " hs source branch declare [<branch>] | show\n" +
1504
+ " Declare the branch this session works on (cloud spec saves then land on it) / show the target\n" +
699
1505
  " hs source log <spec-id> Commit-history lookup for a spec (console settlement,\n" +
700
- " falling back to local git log)\n",
1506
+ " falling back to local git log)\n" +
1507
+ " hs source identity [show] [--json]\n" +
1508
+ " Effective git user.name/user.email, where each comes from,\n" +
1509
+ " and whether a repo-local override is authorized (read-only)\n" +
1510
+ " hs source identity set --email <email> [--name <name>] [--global] [--human]\n" +
1511
+ " HUMAN-ONLY: write the identity and record the authorization\n" +
1512
+ " in .heyspex/git-identity.json (agents are refused, exit 3)\n" +
1513
+ " hs source identity clear [--human]\n" +
1514
+ " HUMAN-ONLY: remove the repo-local [user] override + authorization\n" +
1515
+ " Type set/clear yourself — terminal: hs source identity set --email you@example.com\n" +
1516
+ " inside Claude Code: !hs source identity set --email you@example.com --human\n",
701
1517
  );
702
1518
  process.exit(0);
703
1519
  }
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",
1520
+ const SOURCE_SPAWN_KEYS = {
1521
+ status: "source:status",
1522
+ preflight: "source:preflight",
1523
+ commit: "source:commit",
1524
+ push: "source:push",
1525
+ branch: "source:branch",
1526
+ log: "source:log",
1527
+ identity: "source:identity",
710
1528
  };
711
- const scriptName = SOURCE_ENTRYPOINTS[command];
712
- if (!scriptName) {
1529
+ const spawnKey = SOURCE_SPAWN_KEYS[command];
1530
+ if (!spawnKey) {
713
1531
  process.stderr.write(
714
- `hs: unknown \`hs source\` verb \`${command ?? ""}\` — expected one of: ${Object.keys(SOURCE_ENTRYPOINTS).join(", ")}\n`,
1532
+ `hs: unknown \`hs source\` verb \`${command ?? ""}\` — expected one of: ${Object.keys(SOURCE_SPAWN_KEYS).join(", ")}\n`,
715
1533
  );
716
1534
  process.exit(2);
717
1535
  }
718
- const script = path.join(PKG_ROOT, "scripts", "lib", scriptName);
1536
+ const script = spawnTargetPath(spawnKey);
719
1537
  const child = spawn(process.execPath, [script, command, ...rest], {
720
1538
  cwd: PKG_ROOT,
721
1539
  stdio: "inherit",
@@ -723,7 +1541,9 @@ function main(argv) {
723
1541
  });
724
1542
  child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
725
1543
  child.on("error", (err) => {
726
- process.stderr.write(`hs: failed to run ${scriptName}: ${err.message}\n`);
1544
+ process.stderr.write(
1545
+ `hs: failed to run ${SPAWN_TARGETS[spawnKey].path}: ${err.message}\n`,
1546
+ );
727
1547
  process.exit(1);
728
1548
  });
729
1549
  return;
@@ -734,7 +1554,7 @@ function main(argv) {
734
1554
  // [[HS-RU2R-I87U-RES-solo-surface-cli-launcher-single]]). Probes ground-truth
735
1555
  // server status first; a down console prints a clear message, never a blank window.
736
1556
  if (namespace === "view") {
737
- const script = path.join(PKG_ROOT, "scripts", "hs-view.mjs");
1557
+ const script = spawnTargetPath("view");
738
1558
  const child = spawn(process.execPath, [script, ...argv.slice(1)], {
739
1559
  cwd: PKG_ROOT,
740
1560
  stdio: "inherit",
@@ -779,7 +1599,7 @@ function main(argv) {
779
1599
  // group (the multi-capture "add another?" gesture). Pure Node, posts the
780
1600
  // same heyspex.capture/0.1 envelope the companion sends to /api/drafts.
781
1601
  if (namespace === "capture") {
782
- const script = path.join(PKG_ROOT, "scripts", "hs-capture-cli.mjs");
1602
+ const script = spawnTargetPath("capture");
783
1603
  const captureArgs = command ? [command, ...rest] : rest;
784
1604
  const child = spawn(process.execPath, [script, ...captureArgs], {
785
1605
  cwd: PKG_ROOT,
@@ -795,12 +1615,17 @@ function main(argv) {
795
1615
  }
796
1616
 
797
1617
  // `hs companion start|stop|status|build` — companion tray host (companion/src/HeySpex.Companion.Host).
1618
+ // `hs companion settings ...` and the shortcuts (mute|volume|orb|wake|...) — every
1619
+ // Companion setting via the console's /api/companion/settings ([[HS-UDH1]]); the
1620
+ // script resolves the console through the shared resolver, which needs the
1621
+ // vault root for the `.heyspex/console-url` discovery file (same as `capture`).
798
1622
  if (namespace === "companion") {
799
- const script = path.join(PKG_ROOT, "scripts", "hs-companion.mjs");
1623
+ const script = spawnTargetPath("companion");
800
1624
  const companionArgs = command ? [command, ...rest] : ["start", ...rest];
801
1625
  const child = spawn(process.execPath, [script, ...companionArgs], {
802
1626
  cwd: PKG_ROOT,
803
1627
  stdio: "inherit",
1628
+ env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
804
1629
  });
805
1630
  child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
806
1631
  child.on("error", (err) => {
@@ -825,12 +1650,28 @@ function main(argv) {
825
1650
 
826
1651
  // `hs next [spec_id] [--summary] [--json]` — resolve the on-deck brief without
827
1652
  // MCP ([[HS-TASK-ISK7]] / [[tool-agnostic-mcp-first-cli-second]]).
1653
+ // `hs next` has TWO halves and this branch picks one.
1654
+ //
1655
+ // - `hs next [spec_id] [--summary] [--json]` resolves the on-deck brief
1656
+ // with no console and no MCP (`scripts/hs-next.mjs`).
1657
+ // - `hs next <id> --launch`, and the `model` / `permission-mode` / `claim`
1658
+ // subcommands, need the console (POST /api/agent/start, PUT
1659
+ // /api/settings) and run through `spex-console/bin/hs-next-launch.ts` -
1660
+ // the same shape `hs handoff` uses.
828
1661
  if (namespace === "next") {
829
- const script = path.join(PKG_ROOT, "scripts", "hs-next.mjs");
1662
+ if (isNextConsoleArgv(argv.slice(1))) {
1663
+ dispatchSpexConsoleEntry("hs-next-launch.ts", argv, vaultRoot);
1664
+ return;
1665
+ }
1666
+ const script = spawnTargetPath("next");
830
1667
  const child = spawn(process.execPath, [script, ...argv.slice(1)], {
831
1668
  cwd: PKG_ROOT,
832
1669
  stdio: "inherit",
833
- env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
1670
+ // hs-next.mjs loads vendor/heyspex-mcp/cli.mjs, which is CODE, not vault
1671
+ // content — since the repo split, HS_VAULT_ROOT can point at a checkout
1672
+ // (e.g. heyspex-specs) with no vendor/ at all
1673
+ // ([[HS-1LCC-4XKS-DIAG-frozen-checks-run-against-the-vault]]).
1674
+ env: { ...process.env, HS_VAULT_ROOT: vaultRoot, HS_PKG_ROOT: PKG_ROOT },
834
1675
  });
835
1676
  child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
836
1677
  child.on("error", (err) => {
@@ -845,7 +1686,7 @@ function main(argv) {
845
1686
  // Turning it off makes this repo behave like a plain codebase in Claude Code.
846
1687
  // Core: scripts/heyspex-mode.mjs · docs/claude-code-setup.md.
847
1688
  if (namespace === "mode") {
848
- const script = path.join(PKG_ROOT, "scripts", "hs-mode.mjs");
1689
+ const script = spawnTargetPath("mode");
849
1690
  const child = spawn(process.execPath, [script, ...argv.slice(1)], {
850
1691
  cwd: PKG_ROOT,
851
1692
  stdio: "inherit",
@@ -864,7 +1705,7 @@ function main(argv) {
864
1705
  // Registration subcommands print a blocked-on-HS-DCCP notice until the
865
1706
  // connector's server-registration table exists.
866
1707
  if (namespace === "gateway") {
867
- const script = path.join(PKG_ROOT, "scripts", "hs-gateway.mjs");
1708
+ const script = spawnTargetPath("gateway");
868
1709
  const child = spawn(process.execPath, [script, ...argv.slice(1)], {
869
1710
  cwd: PKG_ROOT,
870
1711
  stdio: "inherit",
@@ -882,7 +1723,7 @@ function main(argv) {
882
1723
  // .heyspex/config.yaml (settings capability; core: vendor/heyspex-mcp/
883
1724
  // config-settings.mjs; write-permission rules: agent-settings-protocol).
884
1725
  if (namespace === "settings") {
885
- const script = path.join(PKG_ROOT, "scripts", "hs-settings.mjs");
1726
+ const script = spawnTargetPath("settings");
886
1727
  const child = spawn(process.execPath, [script, ...argv], {
887
1728
  cwd: PKG_ROOT,
888
1729
  stdio: "inherit",
@@ -899,7 +1740,7 @@ function main(argv) {
899
1740
  // `hs classifier stats` — read-only view of HeySpex Intent Classifier decisions
900
1741
  // (.heyspex/intent-classifier-history.jsonl). Pure local read.
901
1742
  if (namespace === "classifier") {
902
- const script = path.join(PKG_ROOT, "scripts", "hs-classifier.mjs");
1743
+ const script = spawnTargetPath("classifier");
903
1744
  const child = spawn(process.execPath, [script, ...argv], {
904
1745
  cwd: PKG_ROOT,
905
1746
  stdio: "inherit",
@@ -915,7 +1756,7 @@ function main(argv) {
915
1756
 
916
1757
  // `hs auto [filename] [--json]` — resolve /hs-auto COO dispatch plan.
917
1758
  if (namespace === "auto") {
918
- const script = path.join(PKG_ROOT, "scripts", "hs-auto.mjs");
1759
+ const script = spawnTargetPath("auto");
919
1760
  const child = spawn(process.execPath, [script, ...argv.slice(1)], {
920
1761
  cwd: PKG_ROOT,
921
1762
  stdio: "inherit",
@@ -936,30 +1777,45 @@ function main(argv) {
936
1777
  // code for a persistent `hsmcp_` bearer, stored in ~/.heyspex/credentials.yaml
937
1778
  // ([[HS-GD45-MCPC-TASK-mcp-cloud-auth-shared-console-resolver]]).
938
1779
  if (namespace === "mcp") {
939
- if (command !== "call" && command !== "pair" && command !== "login") {
1780
+ if (
1781
+ command !== "call" &&
1782
+ command !== "pair" &&
1783
+ command !== "login" &&
1784
+ command !== "install"
1785
+ ) {
940
1786
  process.stderr.write(
941
- `hs: unknown mcp subcommand \`${command ?? ""}\` (expected: call | pair | login)\n` +
1787
+ `hs: unknown mcp subcommand \`${command ?? ""}\` (expected: call | pair | login | install)\n` +
942
1788
  " hs mcp call <tool> [--arg key=value ...] [--args '<json>'] [--args-file <path>] [--json]\n" +
943
1789
  " 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",
1790
+ " hs mcp login [--console-url <url>] [--label <text>] [--json] (alias of `hs login`)\n" +
1791
+ " hs mcp install --remote [--console-url <url>] [--write] [--json] Wire a client to the cloud endpoint\n",
945
1792
  );
946
1793
  process.exit(2);
947
1794
  }
948
- const cliPath = path.join(PKG_ROOT, "vendor", "heyspex-mcp", "cli.mjs");
1795
+ const cliPath = spawnTargetPath("heyspex-mcp-cli");
1796
+ // The MCP engine resolves its vault by walking UP from ITS cwd. Launching
1797
+ // it with cwd = the HeySpex checkout made every `hs mcp call` read the
1798
+ // HeySpex vault no matter where the agent stood (found 2026-10-16 from
1799
+ // mlc-specs / mlc-app). Run it from the repo the caller is in; an explicit
1800
+ // HS_VAULT_ROOT keeps winning (the engine is started inside that vault).
1801
+ const mcpCwd = process.env.HS_VAULT_ROOT && vaultRoot ? vaultRoot : INVOKED_CWD;
949
1802
  const child = spawn(process.execPath, [cliPath, command, ...rest], {
950
- cwd: PKG_ROOT,
1803
+ cwd: mcpCwd,
951
1804
  stdio: "inherit",
952
1805
  env: {
953
1806
  ...process.env,
954
- HEYSPEX_VAULT_ROOT: vaultRoot,
955
- HS_VAULT_ROOT: vaultRoot,
1807
+ CLAUDE_PROJECT_DIR: mcpCwd,
1808
+ HEYSPEX_VAULT_ROOT: vaultRoot ?? "",
1809
+ HS_VAULT_ROOT: vaultRoot ?? "",
956
1810
  // One-shot MCP tool — no stdio loop, no stderr brief-tier noise in PS.
957
1811
  HEYSPEX_DISABLE_STDIO_BOOTSTRAP: "1",
958
1812
  },
959
1813
  });
960
1814
  child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
961
1815
  child.on("error", (err) => {
962
- process.stderr.write(`hs: failed to run mcp ${command}: ${err.message}\n`);
1816
+ process.stderr.write(
1817
+ `hs: failed to run mcp ${command}: ${err.message}\n`,
1818
+ );
963
1819
  process.exit(1);
964
1820
  });
965
1821
  return;
@@ -974,13 +1830,54 @@ function main(argv) {
974
1830
  return;
975
1831
  }
976
1832
 
1833
+ // `hs harness gates [--json]` — list every registered SpexHarness gate (id,
1834
+ // event, severity, last fixture result), reading the live gate registry +
1835
+ // rule table straight from spexharness/ — no vault/console dependency
1836
+ // ([[HS-XKYY-NYPR-TASK-hs-harness-gates-and-a-console]]).
1837
+ if (namespace === "harness") {
1838
+ dispatchSpexConsoleEntry("hs-harness.ts", argv, vaultRoot || process.cwd());
1839
+ return;
1840
+ }
1841
+
1842
+ // `hs hooks add|remove|list` — the CLI face of the extension seam
1843
+ // ([[HS-8N3K-NYPR-TASK-add-cli-verbs-so-a-developer-can]],
1844
+ // [[HS-8EDJ-NYPR-EPIC-extension-seam-for-other-developers]]). Reads/writes
1845
+ // `.heyspex/harness.json` straight through `spexharness/src/extension/
1846
+ // cli-face.mjs` — the SAME core the API face
1847
+ // (spex-console/src/app/api/harness/extension/route.ts) calls — no
1848
+ // vault/console dependency, same shell shape as `harness gates` above.
1849
+ if (namespace === "hooks") {
1850
+ dispatchSpexConsoleEntry("hs-hooks.ts", argv, vaultRoot || process.cwd());
1851
+ return;
1852
+ }
1853
+
1854
+ // `hs bench <phase1|phase2> --model <name> --harness <on|off>` — run the
1855
+ // harness-on-versus-harness-off benchmark's task set once and write a dated
1856
+ // report under dev-factory/reports/calibrations/bench/
1857
+ // ([[HS-DQ0S-NYPR-TASK-add-a-bench-command-that-runs-the-same]], epic
1858
+ // [[HS-IZCF-NYPR-EPIC-harness-on-versus-harness-off-benchmark]]). Thin CLI
1859
+ // verb only — the real orchestration lives in spexharness/src/bench/*.mjs;
1860
+ // this dispatch branch just reaches the thin shell like `harness` above.
1861
+ if (namespace === "bench") {
1862
+ dispatchSpexConsoleEntry("hs-bench.ts", argv, vaultRoot || process.cwd());
1863
+ return;
1864
+ }
1865
+
977
1866
  // `hs check run <id|manifest>` — verify a spec's acceptance criteria against its
978
1867
  // frozen checks manifest, per-criterion pass/fail against the real tree
979
1868
  // ([[HS-EPIC-DCEP-acceptance-criteria-checks-manifest-dete]], the deterministic
980
1869
  // kernel of [[HS-MIL-Y5DN-verification-engine-shared-deterministic]]). Forwards
981
1870
  // the full argv; the entrypoint drops the leading `check` token.
1871
+ //
1872
+ // HS_PKG_ROOT is forwarded so the entrypoint can resolve a check's `cwd`
1873
+ // against the CODE root rather than `vaultRoot` — since the repo split, the
1874
+ // vault and the code can be two different checkouts, and a check almost
1875
+ // always runs code, not vault content
1876
+ // ([[HS-1LCC-4XKS-DIAG-frozen-checks-run-against-the-vault]]).
982
1877
  if (namespace === "check") {
983
- dispatchSpexConsoleEntry("hs-check.ts", argv, vaultRoot);
1878
+ dispatchSpexConsoleEntry("hs-check.ts", argv, vaultRoot, {
1879
+ HS_PKG_ROOT: PKG_ROOT,
1880
+ });
984
1881
  return;
985
1882
  }
986
1883
 
@@ -988,8 +1885,15 @@ function main(argv) {
988
1885
  // `review_pending`/`in_review` and report what has decayed since the stamp
989
1886
  // ([[HS-R5LF-36QL-EPIC-stamped-work-is-re-verified-on-a]]). Read-only with
990
1887
  // respect to the repo; the Resolution plan is a dry run unless `--apply`.
1888
+ //
1889
+ // HS_PKG_ROOT is forwarded for the same reason `check` forwards it: the sweep
1890
+ // re-runs those same frozen manifests, so a check's `cwd` has to resolve
1891
+ // against the CODE root, not the vault
1892
+ // ([[HS-1LCC-4XKS-DIAG-frozen-checks-run-against-the-vault]]).
991
1893
  if (namespace === "revalidate") {
992
- dispatchSpexConsoleEntry("hs-revalidate.ts", argv, vaultRoot);
1894
+ dispatchSpexConsoleEntry("hs-revalidate.ts", argv, vaultRoot, {
1895
+ HS_PKG_ROOT: PKG_ROOT,
1896
+ });
993
1897
  return;
994
1898
  }
995
1899
 
@@ -1003,18 +1907,49 @@ function main(argv) {
1003
1907
  return;
1004
1908
  }
1005
1909
 
1910
+ // `hs chat [--model <name>] [--script <file>] [--seat <seat>]` — HeySpex's own chat
1911
+ // front (step 0 of [[HS-2KYS-NYPR-EPIC-phase-1-end-to-end-on-deepseek-and-on-a]], the
1912
+ // prerequisite that finishes [[HS-F7N3-NYPR-EPIC-the-powershell-front-hs-chat]]). Thin
1913
+ // CLI verb only — every real behavior lives in spexharness/src/chat/*.mjs; this
1914
+ // dispatch branch just reaches the thin shell like every other verb here. Forwards the
1915
+ // full argv; the entrypoint drops the leading `chat` token.
1916
+ if (namespace === "chat") {
1917
+ dispatchSpexConsoleEntry("hs-chat.ts", argv, vaultRoot || process.cwd());
1918
+ return;
1919
+ }
1920
+
1921
+ // `hs trust list` / `hs trust revoke [path]` — standalone management of the machine-local
1922
+ // folder-trust store `hs chat`'s first-run prompt writes to
1923
+ // ([[HS-25AJ-8CMW-TASK-folder-trust-revoke-through-the]], deferred from
1924
+ // [[HS-ZBEB-8CMW-TASK-folder-trust-for-spexcode-first-run]], which shipped only `hs chat
1925
+ // --revoke-trust`). Thin CLI verb — same shell shape as `chat`/`hooks`/`bench` above; the real
1926
+ // logic lives in `spex-console/src/lib/cli/trust-cli.ts` over `folder-trust.ts`'s pure
1927
+ // functions. Forwards the full argv; the entrypoint drops the leading `trust` token.
1928
+ if (namespace === "trust") {
1929
+ dispatchSpexConsoleEntry("hs-trust.ts", argv, vaultRoot || process.cwd());
1930
+ return;
1931
+ }
1932
+
1933
+ // `hs doctrine extract|learn|show` — SpexCode Doctrine corpus reader + synthesis
1934
+ // ([[HS-TWTC-8CMW-TASK-dr1-doctrine-corpus-reader-stream-every]],
1935
+ // [[HS-YMPY-8CMW-TASK-dr2-doctrine-synthesis-a-routed]] of epic
1936
+ // [[HS-KNHZ-8CMW-EPIC-spexcode-doctrine-v0-the-foundational]]). Thin CLI verb —
1937
+ // same shell shape as `chat`/`trust`/`harness` above; the real logic lives in
1938
+ // `spexharness/src/doctrine/*.mjs`. Forwards the full argv; the entrypoint drops
1939
+ // the leading `doctrine` token.
1940
+ if (namespace === "doctrine") {
1941
+ dispatchSpexConsoleEntry(
1942
+ "hs-doctrine.ts",
1943
+ argv,
1944
+ vaultRoot || process.cwd(),
1945
+ );
1946
+ return;
1947
+ }
1948
+
1006
1949
  // Neon control-plane verbs ([[HS-EPIC-CLMW-hs-cli-mcp-wrappers-for-control-plane]]).
1007
1950
  // 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
- };
1951
+ // the leading namespace token and parses the rest. (Table itself now lives
1952
+ // at module scope as CONTROL_ENTRYPOINTS — see its definition above main().)
1018
1953
  if (Object.hasOwn(CONTROL_ENTRYPOINTS, namespace)) {
1019
1954
  dispatchSpexConsoleEntry(CONTROL_ENTRYPOINTS[namespace], argv, vaultRoot);
1020
1955
  return;
@@ -1025,4 +1960,54 @@ function main(argv) {
1025
1960
  process.exit(2);
1026
1961
  }
1027
1962
 
1028
- main(process.argv.slice(2));
1963
+ // Only run the CLI when this file is executed directly (`node bin/hs.mjs` /
1964
+ // the `hs`/`heyspex` bin shims), not when another script imports it — e.g.
1965
+ // `scripts/verify-mcp-package.mjs`'s `spawnTargets` check and
1966
+ // `scripts/lib/spawn-target-manifest.test.mjs` import `SPAWN_TARGETS` above
1967
+ // without wanting `main()` to run the dispatcher against THEIR argv.
1968
+ //
1969
+ // A naive `import.meta.url === pathToFileURL(process.argv[1]).href` (the
1970
+ // pattern `scripts/verify-mcp-package.mjs`'s own `isMain` guard uses — safe
1971
+ // there because that script is never reached through an npm bin shim) is
1972
+ // NOT enough here: an `npm link`ed / globally-installed `hs` runs
1973
+ // `node "<npm-prefix>/node_modules/heyspex/bin/hs.mjs"`, and when that path
1974
+ // is a symlink (npm link's install shape), Node's default module resolution
1975
+ // realpaths it for `import.meta.url` but leaves `process.argv[1]` as the
1976
+ // ORIGINAL (symlinked) path the shim passed — so the two never matched,
1977
+ // `isMain` came out false, and the globally-installed `hs` silently printed
1978
+ // nothing and exited 0 for every verb ([[HS-561M]] regression). Fix:
1979
+ // realpath BOTH sides before comparing (so the symlink resolves the same way
1980
+ // on each side), case-insensitively on Windows (drive-letter casing can
1981
+ // differ between how a shim built the path and how realpath reports it).
1982
+ function isSameFile(a, b) {
1983
+ const normalize = (p) => {
1984
+ let real;
1985
+ try {
1986
+ real = realpathSync.native(p);
1987
+ } catch {
1988
+ real = path.resolve(p);
1989
+ }
1990
+ return process.platform === "win32" ? real.toLowerCase() : real;
1991
+ };
1992
+ return normalize(a) === normalize(b);
1993
+ }
1994
+
1995
+ const isMain = (() => {
1996
+ if (!process.argv[1]) return false;
1997
+ try {
1998
+ if (isSameFile(process.argv[1], fileURLToPath(import.meta.url)))
1999
+ return true;
2000
+ } catch {
2001
+ // fall through to the basename fallback below
2002
+ }
2003
+ // Belt-and-suspenders fallback for install shapes realpath can't reconcile
2004
+ // (a shim invoking through an intermediate wrapper, a path realpath can't
2005
+ // stat, etc.): recognize this file by its own known bin names rather than
2006
+ // failing closed into silent no-output.
2007
+ const base = path.basename(process.argv[1]).toLowerCase();
2008
+ return base === "hs.mjs" || base === "hs" || base === "heyspex";
2009
+ })();
2010
+
2011
+ if (isMain) {
2012
+ main(process.argv.slice(2));
2013
+ }