heyspex 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/README.md +21 -0
  2. package/bin/hs.mjs +931 -98
  3. package/bin/spexcode.mjs +262 -0
  4. package/dev-factory/engine/worktree.mjs +740 -0
  5. package/dev-factory/lib/code-repo-root.mjs +67 -0
  6. package/dev-factory/lib/context-budget.mjs +84 -0
  7. package/dev-factory/lib/epic-detect.mjs +190 -0
  8. package/dev-factory/lib/fleet-queue-file.mjs +846 -0
  9. package/dev-factory/lib/fleet-queue.mjs +426 -0
  10. package/dev-factory/lib/fleet-state.mjs +14 -18
  11. package/dev-factory/lib/model-catalog.mjs +338 -0
  12. package/dev-factory/lib/seat-context.mjs +199 -0
  13. package/dev-factory/lib/spec-io.mjs +93 -0
  14. package/dev-factory/lib/sufficiency-score.mjs +8 -2
  15. package/dev-factory/lib/worktree-allocator.mjs +744 -0
  16. package/dev-factory/model-catalog.json +222 -0
  17. package/dist/cli/hs-check.mjs +1085 -103
  18. package/dist/cli/hs-claim.mjs +936 -63
  19. package/dist/cli/hs-connect.mjs +9786 -1910
  20. package/dist/cli/hs-defer.mjs +5267 -3053
  21. package/dist/cli/hs-dev.mjs +564 -0
  22. package/dist/cli/hs-env.mjs +385 -299
  23. package/dist/cli/hs-index-rebuild.mjs +7129 -0
  24. package/dist/cli/hs-index-reconcile.mjs +7852 -0
  25. package/dist/cli/hs-init.mjs +777 -138
  26. package/dist/cli/hs-install.mjs +6583 -485
  27. package/dist/cli/hs-join.mjs +10137 -2368
  28. package/dist/cli/hs-vault-pull.mjs +4659 -5882
  29. package/dist/cli/hs-vault.mjs +3793 -1537
  30. package/dist/cli/hs-workspace.mjs +2550 -524
  31. package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
  32. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
  33. package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
  34. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
  35. package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
  36. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
  37. package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
  38. package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
  39. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
  40. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
  41. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
  42. package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
  43. package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
  44. package/dist/cli-templates/harness-commands/claude/hs-ground.md +28 -0
  45. package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
  46. package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
  47. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
  48. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
  49. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
  50. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
  51. package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
  52. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
  53. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
  54. package/dist/cli-templates/harness-commands/claude/hs-view.md +26 -0
  55. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
  56. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
  57. package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
  58. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
  59. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
  60. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
  61. package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
  62. package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
  63. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +31 -0
  64. package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
  65. package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
  66. package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
  67. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
  68. package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
  69. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
  70. package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
  71. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
  72. package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
  73. package/dist/cli-templates/harness-commands/cursor/hs-view.md +30 -0
  74. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
  75. package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
  76. package/docs/mcp-client-config.md +259 -6
  77. package/package.json +231 -124
  78. package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
  79. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
  80. package/plugins/heyspex/.mcp.json +12 -0
  81. package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
  82. package/plugins/heyspex/bin/run-hook.mjs +62 -0
  83. package/plugins/heyspex/hooks/cursor-hooks.json +227 -0
  84. package/plugins/heyspex/hooks/hooks.json +361 -0
  85. package/plugins/heyspex/mcp.json +13 -0
  86. package/scripts/check-mcp-freshness.mjs +17 -9
  87. package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
  88. package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
  89. package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
  90. package/scripts/claude-hooks/_intent-classifier-router.mjs +410 -0
  91. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +351 -0
  92. package/scripts/claude-hooks/_run.mjs +120 -0
  93. package/scripts/claude-hooks/_transcript-text.mjs +60 -0
  94. package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
  95. package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
  96. package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
  97. package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
  98. package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
  99. package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
  100. package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
  101. package/scripts/claude-hooks/claim-liveness-shared.mjs +575 -0
  102. package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
  103. package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
  104. package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
  105. package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
  106. package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
  107. package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
  108. package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
  109. package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
  110. package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
  111. package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
  112. package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
  113. package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
  114. package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
  115. package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
  116. package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
  117. package/scripts/claude-hooks/inject-communication-style.mjs +66 -0
  118. package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
  119. package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
  120. package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
  121. package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
  122. package/scripts/claude-hooks/intent-classifier-trigger.mjs +247 -0
  123. package/scripts/claude-hooks/log-session-activity.mjs +341 -0
  124. package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
  125. package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
  126. package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
  127. package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
  128. package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
  129. package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
  130. package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
  131. package/scripts/claude-hooks/research-method-guard.mjs +398 -0
  132. package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
  133. package/scripts/claude-hooks/session-end.mjs +449 -0
  134. package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
  135. package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
  136. package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
  137. package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
  138. package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
  139. package/scripts/claude-hooks/wip-checkpoint.mjs +298 -0
  140. package/scripts/cursor-hooks/manifest.mjs +235 -0
  141. package/scripts/dev-server-status.mjs +34 -13
  142. package/scripts/dev-server-stop.mjs +313 -0
  143. package/scripts/heyspex-mode.mjs +268 -0
  144. package/scripts/hs-auto-resolve.mjs +240 -0
  145. package/scripts/hs-auto-source.mjs +398 -0
  146. package/scripts/hs-auto.mjs +134 -0
  147. package/scripts/hs-capture-cli.mjs +326 -0
  148. package/scripts/hs-classifier.mjs +136 -0
  149. package/scripts/hs-companion.mjs +88 -0
  150. package/scripts/hs-gateway.mjs +115 -0
  151. package/scripts/hs-mode.mjs +125 -0
  152. package/scripts/hs-next.mjs +441 -0
  153. package/scripts/hs-settings.mjs +338 -0
  154. package/scripts/hs-view.mjs +333 -0
  155. package/scripts/hs-worktree.mjs +201 -0
  156. package/scripts/install-claude-code-hooks.mjs +270 -0
  157. package/scripts/install-cursor-hooks.mjs +471 -0
  158. package/scripts/lib/agent-env-markers.mjs +38 -0
  159. package/scripts/lib/console-url-resolver.mjs +197 -60
  160. package/scripts/lib/customer-kit.mjs +886 -0
  161. package/scripts/lib/dev-mode.mjs +244 -28
  162. package/scripts/lib/heyspex-home.mjs +72 -0
  163. package/scripts/lib/hs-command.mjs +277 -0
  164. package/scripts/lib/hs-companion-settings.mjs +472 -0
  165. package/scripts/lib/hs-source-commit.mjs +59 -5
  166. package/scripts/lib/hs-source-identity.mjs +526 -0
  167. package/scripts/lib/hs-source-log.mjs +11 -21
  168. package/scripts/lib/hs-source-push.mjs +107 -58
  169. package/scripts/lib/hs-worktree-args.mjs +115 -0
  170. package/scripts/lib/shared-path-helpers.mjs +134 -0
  171. package/scripts/lib/standalone-doctor.mjs +540 -3
  172. package/scripts/lib/subagent-git-lock.mjs +109 -0
  173. package/scripts/lib/template-pin.mjs +34 -0
  174. package/scripts/provenance-lib.mjs +91 -0
  175. package/scripts/stamp-provenance-on-edit.mjs +222 -0
  176. package/scripts/wip-ledger-health.mjs +653 -0
  177. package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
  178. package/spexharness/src/adapters/audit.mjs +197 -0
  179. package/spexharness/src/adapters/claude-code.mjs +441 -0
  180. package/spexharness/src/chat/tui/ansi.mjs +448 -0
  181. package/spexharness/src/chat/tui/palette.mjs +22 -0
  182. package/spexharness/src/chat/tui/select.mjs +208 -0
  183. package/spexharness/src/config-root.mjs +54 -0
  184. package/spexharness/src/context-budget.mjs +141 -0
  185. package/spexharness/src/dev-server-control.mjs +389 -0
  186. package/spexharness/src/doctrine/behaviour.mjs +371 -0
  187. package/spexharness/src/doctrine/corpus.mjs +733 -0
  188. package/spexharness/src/doctrine/inject.mjs +338 -0
  189. package/spexharness/src/doctrine/merge.mjs +184 -0
  190. package/spexharness/src/doctrine/sources.mjs +252 -0
  191. package/spexharness/src/doctrine/synthesize.mjs +401 -0
  192. package/spexharness/src/doctrine/vault-source.mjs +574 -0
  193. package/spexharness/src/doctrine/writer.mjs +176 -0
  194. package/spexharness/src/events.mjs +84 -0
  195. package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
  196. package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
  197. package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
  198. package/spexharness/src/gates/deep-research-cost-gate.mjs +274 -0
  199. package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
  200. package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
  201. package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
  202. package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
  203. package/spexharness/src/gates/git-identity-guard.mjs +61 -0
  204. package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
  205. package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
  206. package/spexharness/src/gates/git-stash-gate.mjs +159 -0
  207. package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
  208. package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
  209. package/spexharness/src/gates/inject-vault-context.mjs +239 -0
  210. package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
  211. package/spexharness/src/gates/plan-review-gate.mjs +233 -0
  212. package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
  213. package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
  214. package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
  215. package/spexharness/src/gates/settings-write-gate.mjs +104 -0
  216. package/spexharness/src/git-reflog-audit.mjs +294 -0
  217. package/spexharness/src/intent-classifier.mjs +194 -0
  218. package/spexharness/src/intent-directive.mjs +541 -0
  219. package/spexharness/src/profile/learn.mjs +551 -0
  220. package/spexharness/src/profile/proposals.mjs +357 -0
  221. package/spexharness/src/profile/vault-inclusion.mjs +386 -0
  222. package/spexharness/src/redact.mjs +151 -0
  223. package/spexharness/src/rule-table.mjs +819 -0
  224. package/spexharness/src/seat-context.mjs +122 -0
  225. package/spexharness/src/session-bootstrap.mjs +512 -0
  226. package/spexharness/src/session-record.mjs +815 -0
  227. package/spexharness/src/token-inventory.mjs +207 -0
  228. package/vendor/heyspex-mcp/account-switch.mjs +95 -0
  229. package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
  230. package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
  231. package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
  232. package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
  233. package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
  234. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +46 -5
  235. package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
  236. package/vendor/heyspex-mcp/capture-console-client.mjs +80 -1
  237. package/vendor/heyspex-mcp/capture-focus.mjs +1035 -29
  238. package/vendor/heyspex-mcp/capture-outbox.mjs +5 -1
  239. package/vendor/heyspex-mcp/capture-tools.mjs +1219 -0
  240. package/vendor/heyspex-mcp/claim-tools.mjs +1124 -0
  241. package/vendor/heyspex-mcp/cli-verbs.mjs +2076 -0
  242. package/vendor/heyspex-mcp/cli.mjs +201 -15524
  243. package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
  244. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +412 -0
  245. package/vendor/heyspex-mcp/config-seed.yaml +15 -0
  246. package/vendor/heyspex-mcp/config-settings.mjs +290 -7
  247. package/vendor/heyspex-mcp/console-fetch.mjs +657 -16
  248. package/vendor/heyspex-mcp/console-transport.mjs +166 -0
  249. package/vendor/heyspex-mcp/credentials.mjs +584 -0
  250. package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
  251. package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
  252. package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
  253. package/vendor/heyspex-mcp/design-tools.mjs +4 -1
  254. package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
  255. package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
  256. package/vendor/heyspex-mcp/dev-tools.mjs +124 -0
  257. package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
  258. package/vendor/heyspex-mcp/events-tools.mjs +207 -0
  259. package/vendor/heyspex-mcp/filing-tools.mjs +688 -0
  260. package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
  261. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +69 -14
  262. package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
  263. package/vendor/heyspex-mcp/folder-trust-store.mjs +59 -0
  264. package/vendor/heyspex-mcp/http-transport.mjs +300 -0
  265. package/vendor/heyspex-mcp/list-tools.mjs +257 -0
  266. package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
  267. package/vendor/heyspex-mcp/mcp-config.mjs +94 -0
  268. package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
  269. package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
  270. package/vendor/heyspex-mcp/occupied-keys.mjs +141 -0
  271. package/vendor/heyspex-mcp/on-deck-brief.mjs +3360 -0
  272. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
  273. package/vendor/heyspex-mcp/package-root.mjs +32 -0
  274. package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
  275. package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
  276. package/vendor/heyspex-mcp/plan-push.mjs +906 -21
  277. package/vendor/heyspex-mcp/plan-review.mjs +124 -30
  278. package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
  279. package/vendor/heyspex-mcp/procedure-handlers.mjs +325 -0
  280. package/vendor/heyspex-mcp/procedure-tools.mjs +540 -351
  281. package/vendor/heyspex-mcp/promote-capture.mjs +386 -69
  282. package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
  283. package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
  284. package/vendor/heyspex-mcp/promote-research.mjs +660 -53
  285. package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
  286. package/vendor/heyspex-mcp/prompts.mjs +1460 -0
  287. package/vendor/heyspex-mcp/read-path.mjs +29 -9
  288. package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
  289. package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
  290. package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
  291. package/vendor/heyspex-mcp/register-spec.mjs +266 -54
  292. package/vendor/heyspex-mcp/request-context.mjs +59 -0
  293. package/vendor/heyspex-mcp/research-focus.mjs +207 -4
  294. package/vendor/heyspex-mcp/research-handlers.mjs +623 -0
  295. package/vendor/heyspex-mcp/research-tools.mjs +4 -4
  296. package/vendor/heyspex-mcp/response-budget.mjs +348 -0
  297. package/vendor/heyspex-mcp/review-focus.mjs +25 -1
  298. package/vendor/heyspex-mcp/search-specs.mjs +280 -0
  299. package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
  300. package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
  301. package/vendor/heyspex-mcp/session-owner.mjs +108 -0
  302. package/vendor/heyspex-mcp/session-tools.mjs +152 -0
  303. package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
  304. package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
  305. package/vendor/heyspex-mcp/spec-edit-tools.mjs +446 -0
  306. package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
  307. package/vendor/heyspex-mcp/split-research.mjs +301 -56
  308. package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
  309. package/vendor/heyspex-mcp/submit-for-review.mjs +132 -6
  310. package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
  311. package/vendor/heyspex-mcp/summary-write-tools.mjs +224 -0
  312. package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
  313. package/vendor/heyspex-mcp/test-helpers.mjs +108 -1
  314. package/vendor/heyspex-mcp/tool-catalog.mjs +2813 -0
  315. package/vendor/heyspex-mcp/tool-dispatch.mjs +639 -0
  316. package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
  317. package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
  318. package/vendor/heyspex-mcp/vault-io.mjs +724 -0
  319. package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
  320. package/vendor/heyspex-mcp/vault-resolve.mjs +366 -68
  321. package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
  322. package/vendor/heyspex-mcp/whoami.mjs +91 -0
@@ -19,6 +19,15 @@
19
19
  * (~/.heyspex/dev-mode.json) → cloud admin setting (account_settings.dev_mode,
20
20
  * injected by the caller — this module never talks to Neon) → default prod.
21
21
  *
22
+ * ONE HOME ([[HS-R0YM-WW9R-TASK-publish-the-routing-fact-table-test]], routing
23
+ * fact G in `spex-console/src/lib/cli/routing-facts.ts`): the cloud
24
+ * `account_settings.dev_mode` row is the home of the mode. `dev-mode.json` is a
25
+ * machine-local CACHE of it (owner decision: kept as a cache, not deleted),
26
+ * validated by the `hs doctor` `mode-override` row, which names the file's
27
+ * writer and date and fails when it disagrees with a known cloud setting.
28
+ * Both implementations are held to the one precedence table in
29
+ * `./dev-mode.fixtures.mjs`.
30
+ *
22
31
  * MACHINE-LOCAL RECORD — CROSS-COMPONENT CONTRACT (the Windows Companion
23
32
  * reads the same file; keep the shape exactly this):
24
33
  * ~/.heyspex/dev-mode.json
@@ -28,6 +37,13 @@
28
37
  * Env overrides:
29
38
  * HEYSPEX_DEV_MODE = prod | dev | local | cloud (local/cloud imply dev)
30
39
  * HEYSPEX_DEV_RELIANCE = local | cloud (refines HEYSPEX_DEV_MODE=dev)
40
+ * `HEYSPEX_DEV_MODE=dev` with no reliance still resolves local, but the
41
+ * envelope reports `sources.reliance: "assumed-local"` — never silently.
42
+ *
43
+ * Any active switch (env or file) is reported as ONE structured object by
44
+ * `modeOverrideOf(resolved)` — `hs dev --json`, `hs doctor --json` and the MCP
45
+ * `heyspex_dev_mode` all carry it as `mode_override`
46
+ * ([[HS-EVVI-WW9R-TASK-show-any-dev-mode-override-in-hs-dev]]).
31
47
  *
32
48
  * Zero deps beyond node builtins (same rule as console-url-resolver.mjs).
33
49
  */
@@ -35,6 +51,8 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
35
51
  import { homedir } from 'node:os';
36
52
  import { join } from 'node:path';
37
53
 
54
+ import { assertNotRealHomeUnderTest, readBearer } from '../../vendor/heyspex-mcp/credentials.mjs';
55
+
38
56
  export const CLOUD_CONSOLE_URL = 'https://app.heyspex.com';
39
57
  export const LOCAL_CONSOLE_URL = 'http://localhost:3000';
40
58
  export const DEV_MODE_FILENAME = 'dev-mode.json';
@@ -70,14 +88,19 @@ export function deriveSemantics(mode, reliance) {
70
88
  };
71
89
  }
72
90
 
91
+ /** Default file reader — injectable so tests never touch the real HOME. */
92
+ const defaultReadFile = (p) => readFileSync(p, 'utf8');
93
+
73
94
  /**
74
95
  * Read + validate ~/.heyspex/dev-mode.json. Never throws:
75
96
  * returns { record, parseError } — record null when missing/invalid.
97
+ * `readFile` is an injectable seam (same shape as the TS twin's) so the
98
+ * parity test can feed both implementations the SAME in-memory fixture.
76
99
  */
77
- export function readLocalDevModeRecord({ homeDir = homedir() } = {}) {
100
+ export function readLocalDevModeRecord({ homeDir = homedir(), readFile = defaultReadFile } = {}) {
78
101
  let raw;
79
102
  try {
80
- raw = readFileSync(devModeFilePath(homeDir), 'utf8');
103
+ raw = readFile(devModeFilePath(homeDir));
81
104
  } catch {
82
105
  return { record: null, parseError: null }; // absent — not an error
83
106
  }
@@ -113,9 +136,10 @@ export function readLocalDevModeRecord({ homeDir = homedir() } = {}) {
113
136
  export function resolveDevMode({
114
137
  env = process.env,
115
138
  homeDir = homedir(),
139
+ readFile = defaultReadFile,
116
140
  cloudSetting = undefined,
117
141
  } = {}) {
118
- const local = readLocalDevModeRecord({ homeDir });
142
+ const local = readLocalDevModeRecord({ homeDir, readFile });
119
143
  const cloudKnown = cloudSetting !== undefined;
120
144
  const cloudValue =
121
145
  cloudKnown && cloudSetting
@@ -128,11 +152,19 @@ export function resolveDevMode({
128
152
  let mode;
129
153
  let reliance;
130
154
  let source;
155
+ // Where the RELIANCE came from. Equals `source` except in one case:
156
+ // `HEYSPEX_DEV_MODE=dev` with no `HEYSPEX_DEV_RELIANCE` used to mean local
157
+ // silently; it still means local, but the envelope now says the reliance
158
+ // was ASSUMED ([[HS-EVVI-WW9R-TASK-show-any-dev-mode-override-in-hs-dev]]
159
+ // AC1, invariant 10 — no hidden mode switch).
160
+ let relianceSource;
131
161
  const envParsed = normalizeMode(env.HEYSPEX_DEV_MODE);
132
162
  if (envParsed) {
133
163
  mode = envParsed.mode;
134
- reliance = envParsed.reliance ?? normalizeReliance(env.HEYSPEX_DEV_RELIANCE) ?? 'local';
164
+ const envReliance = envParsed.reliance ?? normalizeReliance(env.HEYSPEX_DEV_RELIANCE);
165
+ reliance = envReliance ?? 'local';
135
166
  source = 'env';
167
+ relianceSource = envReliance ? 'env' : 'assumed-local';
136
168
  } else if (local.record) {
137
169
  mode = local.record.mode;
138
170
  reliance = local.record.reliance;
@@ -167,7 +199,7 @@ export function resolveDevMode({
167
199
  vaultWriteMode: semantics.vaultWriteMode,
168
200
  sources: {
169
201
  mode: source,
170
- reliance: source,
202
+ reliance: relianceSource ?? source,
171
203
  consoleUrl:
172
204
  source === 'local-override' && local.record?.console_url ? 'local-override' : 'derived',
173
205
  readTier: 'derived',
@@ -183,6 +215,131 @@ export function resolveDevMode({
183
215
  };
184
216
  }
185
217
 
218
+ /** The `source` values {@link modeOverrideOf} can report. */
219
+ export const MODE_OVERRIDE_SOURCE_ENV = 'env';
220
+ export const MODE_OVERRIDE_SOURCE_FILE = 'dev-mode.json';
221
+
222
+ /**
223
+ * The one structured answer to "is a mode switch active on this machine?" —
224
+ * what `hs dev --json`, `hs doctor --json` and `heyspex_dev_mode` all report
225
+ * as `mode_override` ([[HS-EVVI-WW9R-TASK-show-any-dev-mode-override-in-hs-dev]]
226
+ * AC1/AC2; epic [[HS-P1AW-WW9R-EPIC-identity-threading-and-the-end-of-the]]
227
+ * criterion 4). Pure: reads only the resolved envelope, never the file.
228
+ *
229
+ * null — nothing overrides the cloud/default
230
+ * { source: "env", written_at: null, written_by: "HEYSPEX_DEV_MODE", disagrees_with_cloud }
231
+ * { source: "dev-mode.json", written_at: <ISO>, written_by: <record.source>, disagrees_with_cloud }
232
+ *
233
+ * `written_at` / `written_by` are the record's `updated_at` / `source` (the
234
+ * cross-component file contract keeps its own key names; the Companion reads
235
+ * that file). `disagrees_with_cloud` is only ever true when the cloud setting
236
+ * is KNOWN and differs — an unreachable cloud is never a disagreement.
237
+ */
238
+ export function modeOverrideOf(resolved) {
239
+ if (!resolved) return null;
240
+ const cloud = resolved.cloudSetting?.value ?? null;
241
+ if (resolved.sources?.mode === 'env') {
242
+ const disagrees =
243
+ cloud !== null &&
244
+ (cloud.mode !== resolved.mode ||
245
+ (resolved.mode === 'dev' && cloud.reliance !== resolved.reliance));
246
+ return {
247
+ source: MODE_OVERRIDE_SOURCE_ENV,
248
+ written_at: null,
249
+ written_by: 'HEYSPEX_DEV_MODE',
250
+ disagrees_with_cloud: disagrees,
251
+ };
252
+ }
253
+ if (resolved.localOverride?.present) {
254
+ const record = resolved.localOverride.record;
255
+ return {
256
+ source: MODE_OVERRIDE_SOURCE_FILE,
257
+ written_at: record?.updated_at ?? null,
258
+ written_by: record?.source ?? null,
259
+ disagrees_with_cloud: resolved.localOverride.staleAgainstCloud === true,
260
+ };
261
+ }
262
+ return null;
263
+ }
264
+
265
+ /** `prod` / `dev+local` / `dev+cloud` — the label doctor and hs dev print. */
266
+ export function describeModeLabel(resolved) {
267
+ return `${resolved.mode}${resolved.mode === 'dev' ? `+${resolved.reliance}` : ''}`;
268
+ }
269
+
270
+ /**
271
+ * The `hs doctor` row for {@link modeOverrideOf} (row id `mode-override`).
272
+ * Pure — the caller resolves ONCE and passes the envelope in; this never
273
+ * re-reads the file. Shape matches every other doctor row
274
+ * (`{ id, label, status, detail, human? }`) plus the structured
275
+ * `mode_override` so `--json` carries the same object `hs dev --json` does.
276
+ *
277
+ * ok no override; or one that pins prod (the default) and does not
278
+ * disagree with a known cloud setting — the file is then just a
279
+ * cache of the normal mode, and the detail still names its date and
280
+ * writer so nothing on disk stays invisible.
281
+ * warn an override puts this machine in dev mode (cloud setting unknown
282
+ * or agreeing) — names the file, date and writer, repair `hs dev prod`.
283
+ * fail the override DISAGREES with a known cloud setting — same repair.
284
+ *
285
+ * `repairCommand` is rendered by the caller (`hsCommand('dev prod')`) so the
286
+ * advice is in the form THIS machine can run.
287
+ */
288
+ export function modeOverrideDoctorRow(resolved, { recordPath, repairCommand = 'hs dev prod' } = {}) {
289
+ const id = 'mode-override';
290
+ const label = 'Dev-mode override (env / ~/.heyspex/dev-mode.json)';
291
+ const override = modeOverrideOf(resolved);
292
+ const modeLabel = describeModeLabel(resolved);
293
+ if (!override) {
294
+ return {
295
+ id,
296
+ label,
297
+ status: 'ok',
298
+ detail: `none — ${modeLabel} from ${resolved.sources.mode}`,
299
+ mode_override: null,
300
+ };
301
+ }
302
+ const where =
303
+ override.source === MODE_OVERRIDE_SOURCE_ENV
304
+ ? `HEYSPEX_DEV_MODE env override`
305
+ : `${recordPath ?? devModeFilePath()}`;
306
+ const stamp =
307
+ override.source === MODE_OVERRIDE_SOURCE_ENV
308
+ ? ''
309
+ : ` (written ${override.written_at ?? 'unknown date'} by ${override.written_by ?? 'unknown writer'})`;
310
+ const cloud = resolved.cloudSetting?.value;
311
+ const cloudLabel = cloud ? describeModeLabel({ mode: cloud.mode, reliance: cloud.reliance }) : null;
312
+ const repair = `Run \`${repairCommand}\` to put this machine back on the cloud console, or set the mode on purpose with \`${repairCommand.replace(/\bdev prod$/, 'dev <mode>')}\`.`;
313
+
314
+ if (override.disagrees_with_cloud) {
315
+ return {
316
+ id,
317
+ label,
318
+ status: 'fail',
319
+ detail: `${modeLabel} from ${where}${stamp} — DISAGREES with the cloud setting (${cloudLabel})`,
320
+ human: repair,
321
+ mode_override: override,
322
+ };
323
+ }
324
+ if (resolved.mode === 'dev') {
325
+ return {
326
+ id,
327
+ label,
328
+ status: 'warn',
329
+ detail: `${modeLabel} from ${where}${stamp}${cloudLabel ? ` — matches the cloud setting (${cloudLabel})` : ' — cloud setting not checked (offline)'}`,
330
+ human: repair,
331
+ mode_override: override,
332
+ };
333
+ }
334
+ return {
335
+ id,
336
+ label,
337
+ status: 'ok',
338
+ detail: `${where}${stamp} pins ${modeLabel} — the default, nothing overridden`,
339
+ mode_override: override,
340
+ };
341
+ }
342
+
186
343
  /**
187
344
  * Upsert top-level `key: value` lines in ~/.heyspex/credentials.yaml without a
188
345
  * full YAML round-trip (same targeted approach as the vendor MCP `pair`
@@ -192,8 +349,11 @@ export function resolveDevMode({
192
349
  */
193
350
  export function upsertCredentialsYamlKeys(homeDir, updates) {
194
351
  const dir = join(homeDir, '.heyspex');
195
- mkdirSync(dir, { recursive: true, mode: 0o700 });
196
352
  const filePath = join(dir, 'credentials.yaml');
353
+ // The real-home guard the credentials and registry writers carry: under a
354
+ // test runner, refuse to touch the REAL ~/.heyspex (checked before mkdir).
355
+ assertNotRealHomeUnderTest(filePath);
356
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
197
357
  let content = '';
198
358
  try {
199
359
  content = readFileSync(filePath, 'utf8');
@@ -214,14 +374,57 @@ export function upsertCredentialsYamlKeys(homeDir, updates) {
214
374
  return filePath;
215
375
  }
216
376
 
217
- /** Targeted read of `mcp_token:` from credentials.yaml (bearer for cloud calls). */
218
- export function readCredentialsMcpToken({ homeDir = homedir() } = {}) {
377
+ /**
378
+ * The bearer for cloud calls — `HEYSPEX_MCP_TOKEN` env, then `mcp_token:` in
379
+ * credentials.yaml — via the ONE shared reader
380
+ * (`vendor/heyspex-mcp/credentials.mjs`,
381
+ * [[HS-HL0D-WW9R-TASK-share-one-bearer-reader-and-one-console]]). Kept under
382
+ * its historical name for the hook scripts that import it; the precedence is
383
+ * the shared module's, never re-implemented here.
384
+ */
385
+ export function readCredentialsMcpToken({ env = process.env, homeDir = homedir() } = {}) {
386
+ return readBearer({ env, homeDir }).token;
387
+ }
388
+
389
+ /**
390
+ * Run `fn(signal)` under a request timeout that is ALWAYS cleared and can
391
+ * never fire after the request has settled.
392
+ *
393
+ * Why not `AbortSignal.timeout(ms)`: that signal owns a timer nothing can
394
+ * cancel — it stays armed for the whole window even once the response has
395
+ * resolved, so a late abort can land on an already-settled request. Here the
396
+ * controller is ours: the timer is cleared in `finally` (rule 1 of
397
+ * [[HS-SWW3-Z9WP-DIAG-hs-dev-crashes-with-a-libuv-assertion]]) and `unref`'d
398
+ * so it can never hold the event loop open on its own.
399
+ */
400
+ async function withRequestTimeout(timeoutMs, fn) {
401
+ const controller = new AbortController();
402
+ const state = { timedOut: false };
403
+ const timer = setTimeout(() => {
404
+ state.timedOut = true;
405
+ controller.abort();
406
+ }, timeoutMs);
407
+ timer.unref?.();
408
+ try {
409
+ return await fn(controller.signal);
410
+ } catch (err) {
411
+ if (state.timedOut) throw new Error(`timed out after ${timeoutMs}ms`);
412
+ throw err;
413
+ } finally {
414
+ clearTimeout(timer);
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Release a response we are not going to read. An undrained body keeps its
420
+ * socket half-read in the keep-alive pool; cancelling hands it back so the
421
+ * process can tear down cleanly. Never throws (the body may already be gone).
422
+ */
423
+ async function discardResponseBody(res) {
219
424
  try {
220
- const raw = readFileSync(join(homeDir, '.heyspex', 'credentials.yaml'), 'utf8');
221
- const match = raw.match(/^mcp_token:\s*["']?([^"'\r\n#]+)/m);
222
- return match?.[1]?.trim() || null;
425
+ await res?.body?.cancel();
223
426
  } catch {
224
- return null;
427
+ /* already consumed, closed, or bodyless — nothing to release */
225
428
  }
226
429
  }
227
430
 
@@ -235,11 +438,16 @@ export async function fetchCloudDevMode({
235
438
  if (!consoleUrl) return { ok: false, setting: undefined, error: 'no console url' };
236
439
  try {
237
440
  const token = readCredentialsMcpToken({ homeDir });
238
- const res = await fetchImpl(`${consoleUrl.replace(/\/+$/, '')}/api/admin/dev-mode`, {
239
- headers: token ? { Authorization: `Bearer ${token}` } : {},
240
- signal: AbortSignal.timeout(timeoutMs),
241
- });
242
- if (!res.ok) return { ok: false, setting: undefined, error: `HTTP ${res.status}` };
441
+ const res = await withRequestTimeout(timeoutMs, (signal) =>
442
+ fetchImpl(`${consoleUrl.replace(/\/+$/, '')}/api/admin/dev-mode`, {
443
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
444
+ signal,
445
+ }),
446
+ );
447
+ if (!res.ok) {
448
+ await discardResponseBody(res);
449
+ return { ok: false, setting: undefined, error: `HTTP ${res.status}` };
450
+ }
243
451
  const body = await res.json();
244
452
  return { ok: true, setting: body?.cloud ?? null, error: null };
245
453
  } catch (err) {
@@ -297,18 +505,24 @@ export async function applyDevMode(
297
505
  parsed.reliance ?? normalizeReliance(target?.reliance) ?? 'local';
298
506
  const mode = parsed.mode;
299
507
  const semantics = deriveSemantics(mode, reliance);
508
+ // Under a test runner, refuse a real-home target BEFORE the cloud precheck
509
+ // or any write (the same guard the credentials and registry writers carry).
510
+ assertNotRealHomeUnderTest(devModeFilePath(homeDir));
300
511
 
512
+ // Timer-safe on purpose — never `AbortSignal.timeout` here
513
+ // ([[HS-SWW3-Z9WP-DIAG-hs-dev-crashes-with-a-libuv-assertion]]).
301
514
  async function postCloud(token) {
302
- const res = await fetchImpl(`${semantics.consoleUrl}/api/admin/dev-mode`, {
303
- method: 'POST',
304
- headers: {
305
- 'content-type': 'application/json',
306
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
307
- },
308
- body: JSON.stringify({ mode, reliance: semantics.reliance, source }),
309
- signal: AbortSignal.timeout(5000),
310
- });
311
- return res;
515
+ return withRequestTimeout(5000, (signal) =>
516
+ fetchImpl(`${semantics.consoleUrl}/api/admin/dev-mode`, {
517
+ method: 'POST',
518
+ headers: {
519
+ 'content-type': 'application/json',
520
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
521
+ },
522
+ body: JSON.stringify({ mode, reliance: semantics.reliance, source }),
523
+ signal,
524
+ }),
525
+ );
312
526
  }
313
527
 
314
528
  // 0. Authorization precheck — see the doc comment above.
@@ -318,6 +532,7 @@ export async function applyDevMode(
318
532
  if (token) {
319
533
  try {
320
534
  const res = await postCloud(token);
535
+ await discardResponseBody(res);
321
536
  if (res.status === 403) throw new DevModeNotAdminError();
322
537
  cloud = { attempted: true, updated: res.ok, error: res.ok ? null : `HTTP ${res.status}` };
323
538
  } catch (err) {
@@ -336,8 +551,8 @@ export async function applyDevMode(
336
551
  source,
337
552
  };
338
553
  const dir = join(homeDir, '.heyspex');
339
- mkdirSync(dir, { recursive: true, mode: 0o700 });
340
554
  const recordPath = devModeFilePath(homeDir);
555
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
341
556
  writeFileSync(recordPath, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
342
557
 
343
558
  // 2. credentials.yaml console_url (rung 2 of the shared resolver).
@@ -354,6 +569,7 @@ export async function applyDevMode(
354
569
  const res = await postCloud(token);
355
570
  if (res.ok) cloud.updated = true;
356
571
  else cloud.error = `HTTP ${res.status}`;
572
+ await discardResponseBody(res);
357
573
  } catch (err) {
358
574
  cloud.error = String(err?.message ?? err);
359
575
  }
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * heyspex-home.mjs — the ONE resolver for "which home directory does
4
+ * `.heyspex/` live under".
5
+ *
6
+ * Every machine-global HeySpex file (`credentials.yaml`, `registry.json`,
7
+ * `dev-mode.json`, `config.yaml`, `env.keys`, companion pairing state) lives at
8
+ * `<home>/.heyspex/…`, and before this module `<home>` was always a bare
9
+ * `os.homedir()`. That makes the machine single-tenant: one signed-in identity,
10
+ * one vault registry, one console URL. There is no way to run a second,
11
+ * isolated HeySpex identity on the same box — which is exactly what a
12
+ * fresh-customer onboarding rehearsal needs (see
13
+ * [[HS-ONB-customer-install-checklist]] and
14
+ * [[HS-BFXF-DIAG-hs-never-resolves-on-a-fresh-machine]]).
15
+ *
16
+ * `HEYSPEX_HOME` overrides that home directory. It names the directory that
17
+ * CONTAINS `.heyspex/`, not the `.heyspex/` directory itself — so every
18
+ * existing `join(homeDir, '.heyspex', …)` call site keeps its shape and only
19
+ * its default changes:
20
+ *
21
+ * HEYSPEX_HOME=C:\heyspex-test\paul-mg → C:\heyspex-test\paul-mg\.heyspex\credentials.yaml
22
+ *
23
+ * Deliberately NOT an override of the `.heyspex` directory name: the two
24
+ * conventions collide (some callers hold `homeDir`, some hold the full
25
+ * `.heyspex` path), and a home-dir override substitutes cleanly at both.
26
+ *
27
+ * Scope note: this covers HeySpex's own state. It does not redirect `~/.claude`,
28
+ * `~/.gitconfig`, or npm — a full-fidelity clean-machine test still wants a
29
+ * separate OS user profile. What it buys is a second HeySpex identity (account,
30
+ * registry, active vault, console URL) alongside the builder identity on one
31
+ * machine.
32
+ *
33
+ * Plain Node ESM, zero deps beyond `node:os` — importable from the vendored MCP
34
+ * server, the Claude hooks, and the dev-factory dispatchers with no build step,
35
+ * exactly like `console-url-resolver.mjs` next to it.
36
+ */
37
+ import { homedir } from 'node:os';
38
+
39
+ /**
40
+ * The home directory `.heyspex/` lives under.
41
+ *
42
+ * Precedence:
43
+ * 1. `HEYSPEX_HOME` env — an absolute path to the directory containing `.heyspex/`
44
+ * 2. `os.homedir()`
45
+ *
46
+ * A blank or whitespace-only `HEYSPEX_HOME` is treated as unset rather than as
47
+ * an empty path, so `HEYSPEX_HOME=` in a shell profile fails safe to the real
48
+ * home instead of resolving `.heyspex/` against the process CWD.
49
+ *
50
+ * @param {NodeJS.ProcessEnv} [env]
51
+ * @returns {string}
52
+ */
53
+ export function heyspexHome(env = process.env) {
54
+ const override = env?.HEYSPEX_HOME;
55
+ if (typeof override === 'string' && override.trim().length > 0) {
56
+ return override.trim();
57
+ }
58
+ return homedir();
59
+ }
60
+
61
+ /**
62
+ * True when the resolved home came from `HEYSPEX_HOME` rather than the real
63
+ * user profile. Used by `hs doctor` and the login banner so an isolated
64
+ * identity is never mistaken for the machine's default one.
65
+ *
66
+ * @param {NodeJS.ProcessEnv} [env]
67
+ * @returns {boolean}
68
+ */
69
+ export function isHeyspexHomeOverridden(env = process.env) {
70
+ const override = env?.HEYSPEX_HOME;
71
+ return typeof override === 'string' && override.trim().length > 0;
72
+ }