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
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * hs auto — resolve /hs-auto [filename] and print dispatch plan.
4
+ */
5
+ import process from 'node:process';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { resolveHsAutoEntry } from './hs-auto-resolve.mjs';
9
+
10
+ const HELP = `hs auto — autonomous COO dispatch resolver
11
+
12
+ Usage:
13
+ hs auto [filename] [--json] [--dry-run]
14
+ hs auto --help
15
+
16
+ Arguments:
17
+ filename Optional. One of:
18
+ - vault spec id (HS-…-TASK-… / HS-…-EPIC-… / HS-…-DIAG-… / HS-…-RES-…)
19
+ - path to a spec .md under specs/
20
+ - path to a source file (matched via task target: frontmatter)
21
+ Diagnose + research specs enter the source_plan → epic → tasks → dispatch pipeline.
22
+ Omit to enter discovery mode (backlog-selector after pre-flight).
23
+
24
+ Flags:
25
+ --json Machine-readable JSON on stdout
26
+ --dry-run Same as default (never dispatches — prints plan only)
27
+ --help, -h Show this help
28
+ `;
29
+
30
+ export const EXIT = {
31
+ OK: 0,
32
+ USAGE: 1,
33
+ RESOLVE_FAILED: 2,
34
+ };
35
+
36
+ /** @param {string[]} argv tokens after "auto" */
37
+ export function parseAutoArgs(argv) {
38
+ let filename;
39
+ let json = false;
40
+ for (let i = 0; i < argv.length; i += 1) {
41
+ const a = argv[i];
42
+ if (a === '--help' || a === '-h') return { help: true };
43
+ if (a === '--json') {
44
+ json = true;
45
+ continue;
46
+ }
47
+ if (a === '--dry-run') continue;
48
+ if (a.startsWith('-')) {
49
+ return { error: { code: EXIT.USAGE, message: `unknown flag: ${a}` } };
50
+ }
51
+ if (!filename) {
52
+ filename = a;
53
+ continue;
54
+ }
55
+ return { error: { code: EXIT.USAGE, message: `unexpected argument: ${a}` } };
56
+ }
57
+ return { args: { filename, json } };
58
+ }
59
+
60
+ const PREFLIGHT = [
61
+ 'node dev-factory/fleet/manager.mjs --status',
62
+ 'node dev-factory/pipeline/backlog-selector.mjs --real',
63
+ 'node dev-factory/router.mjs --self-test',
64
+ 'node dev-factory/lib/coo-detect.mjs --self-test',
65
+ ];
66
+
67
+ async function main() {
68
+ const parsed = parseAutoArgs(process.argv.slice(2));
69
+ if (parsed.help) {
70
+ process.stdout.write(`${HELP}\n`);
71
+ process.exit(EXIT.OK);
72
+ }
73
+ if (parsed.error) {
74
+ process.stderr.write(`hs auto: ${parsed.error.message}\n`);
75
+ process.exit(parsed.error.code);
76
+ }
77
+
78
+ const vaultRoot = process.env.HS_VAULT_ROOT;
79
+ if (!vaultRoot) {
80
+ process.stderr.write('hs auto: HS_VAULT_ROOT is not set (the dispatcher must pass it).\n');
81
+ process.exit(EXIT.USAGE);
82
+ }
83
+
84
+ const resolved = resolveHsAutoEntry(vaultRoot, parsed.args.filename);
85
+ const payload = {
86
+ ...resolved,
87
+ preflight: PREFLIGHT,
88
+ discover_command: 'node dev-factory/pipeline/backlog-selector.mjs --real',
89
+ overnight_command:
90
+ 'node dev-factory/pipeline/run-overnight.mjs --concurrent --max-tasks 5 --max-concurrent 4',
91
+ };
92
+
93
+ if (parsed.args.json) {
94
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
95
+ process.exit(resolved.error ? EXIT.RESOLVE_FAILED : EXIT.OK);
96
+ }
97
+
98
+ if (resolved.error) {
99
+ process.stderr.write(`hs auto: ${resolved.error} — ${resolved.message}\n`);
100
+ process.exit(EXIT.RESOLVE_FAILED);
101
+ }
102
+
103
+ const lines = ['# /hs-auto plan', ''];
104
+ lines.push('## Pre-flight');
105
+ for (const cmd of PREFLIGHT) lines.push(`- \`${cmd}\``);
106
+ lines.push('');
107
+ if (resolved.mode === 'discover') {
108
+ lines.push('## Discovery (no filename)');
109
+ lines.push(`- \`${payload.discover_command}\``);
110
+ } else if (resolved.mode === 'source_plan') {
111
+ lines.push(`## Source plan — \`${resolved.spec_id}\` (${resolved.source_kind})`);
112
+ lines.push(resolved.message ?? 'File remediation epic + tasks, then re-resolve.');
113
+ if (resolved.re_resolve_command) lines.push(`- Re-resolve: \`${resolved.re_resolve_command}\``);
114
+ } else if (resolved.mode === 'breakdown') {
115
+ lines.push(`## Breakdown — \`${resolved.spec_id}\``);
116
+ lines.push(`- \`${resolved.breakdown_command}\``);
117
+ } else if (resolved.mode === 'dispatch') {
118
+ lines.push(`## Dispatch — \`${resolved.spec_id}\``);
119
+ lines.push(`- target: \`${resolved.target}\``);
120
+ lines.push(`- \`${resolved.dispatch_command}\``);
121
+ }
122
+ process.stdout.write(`${lines.join('\n')}\n`);
123
+ process.exit(EXIT.OK);
124
+ }
125
+
126
+ const invoked = process.argv[1]
127
+ && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
128
+
129
+ if (invoked) {
130
+ main().catch((err) => {
131
+ process.stderr.write(`hs auto: ${err instanceof Error ? err.message : String(err)}\n`);
132
+ process.exit(EXIT.RESOLVE_FAILED);
133
+ });
134
+ }
@@ -0,0 +1,326 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * hs capture — CLI capture into the HeySpex console drafts inbox.
4
+ *
5
+ * The CLI face of the multi-capture workflow (companion voice + browser are
6
+ * the other faces). Posts a heyspex.capture/0.1 envelope to the console's
7
+ * POST /api/drafts — the same wire shape the companion's
8
+ * DraftsSpexConsoleClient sends — so CLI captures land in the same inbox and
9
+ * can join the same visual capture groups.
10
+ *
11
+ * Usage:
12
+ * hs capture "<text>" [--title <t>] [--add] [--json]
13
+ * hs capture --clipboard [--title <t>] [--add] [--json]
14
+ * hs capture --file <path> [--title <t>] [--add] [--json]
15
+ *
16
+ * --add chains this capture into the PREVIOUS CLI capture's group (within a
17
+ * 2-minute window) — the "add another?" gesture. Group state lives in
18
+ * .heyspex/cli-capture-group.json.
19
+ */
20
+ import { execFileSync } from "node:child_process";
21
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
22
+ import path from "node:path";
23
+ import process from "node:process";
24
+ import { randomUUID } from "node:crypto";
25
+ import { pathToFileURL } from "node:url";
26
+
27
+ import { resolveConsoleUrl, DEFAULT_CONSOLE_URL } from "./lib/console-url-resolver.mjs";
28
+ import { heyspexHome } from "./lib/heyspex-home.mjs";
29
+ import { stashCaptureArgs } from "../vendor/heyspex-mcp/capture-outbox.mjs";
30
+ import { readBearerToken } from "../vendor/heyspex-mcp/credentials.mjs";
31
+
32
+ /**
33
+ * The bearer for a paired cloud console (`/api/drafts`-adjacent surfaces need
34
+ * it; a local dev console ignores it) — the ONE shared reader
35
+ * (`vendor/heyspex-mcp/credentials.mjs`,
36
+ * [[HS-HL0D-WW9R-TASK-share-one-bearer-reader-and-one-console]]), with
37
+ * `HEYSPEX_HOME` honoured through `heyspexHome()`.
38
+ */
39
+ function readCredentialsMcpToken() {
40
+ return readBearerToken({ homeDir: heyspexHome() });
41
+ }
42
+
43
+ const CHAIN_WINDOW_MS = 2 * 60 * 1000; // keep in sync with CaptureGroupTracker.ChainWindow
44
+
45
+ function usage() {
46
+ process.stdout.write(
47
+ [
48
+ "hs capture — capture text/clipboard/file into the console drafts inbox",
49
+ "",
50
+ "Usage:",
51
+ ' hs capture "<text>" [--title <t>] [--add] [--json]',
52
+ " hs capture --clipboard [--title <t>] [--add] [--json]",
53
+ " hs capture --file <path> [--title <t>] [--add] [--json]",
54
+ "",
55
+ " --add Chain into the previous CLI capture's group (within 2 minutes)",
56
+ " --title Optional capture title",
57
+ " --url Base console URL (default: shared resolver — HEYSPEX_CONSOLE_URL, then paired console_url in ~/.heyspex/credentials.yaml, then the repo discovery file, then http://localhost:3000)",
58
+ " --json Machine-readable result",
59
+ "",
60
+ ].join("\n"),
61
+ );
62
+ }
63
+
64
+ function parseArgs(argv) {
65
+ const opts = {
66
+ text: null, clipboard: false, file: null,
67
+ title: null, add: false, json: false,
68
+ url: null, // resolved via the shared console-url resolver unless --url overrides
69
+ };
70
+ for (let i = 0; i < argv.length; i++) {
71
+ const a = argv[i];
72
+ if (a === "--help" || a === "-h") return { help: true };
73
+ else if (a === "--clipboard") opts.clipboard = true;
74
+ else if (a === "--file") opts.file = argv[++i];
75
+ else if (a === "--title") opts.title = argv[++i];
76
+ else if (a === "--url") opts.url = argv[++i];
77
+ else if (a === "--add") opts.add = true;
78
+ else if (a === "--json") opts.json = true;
79
+ else if (!a.startsWith("--") && opts.text === null) opts.text = a;
80
+ else return { error: `unknown argument: ${a}` };
81
+ }
82
+ return { opts };
83
+ }
84
+
85
+ function readClipboard() {
86
+ if (process.platform !== "win32") {
87
+ throw new Error("--clipboard is only implemented on Windows for now");
88
+ }
89
+ return execFileSync(
90
+ "powershell.exe",
91
+ ["-NoProfile", "-Command", "Get-Clipboard -Raw"],
92
+ { encoding: "utf8", windowsHide: true },
93
+ );
94
+ }
95
+
96
+ /** Same semantics as the companion's CaptureGroupTracker: every capture gets
97
+ * a group id; --add joins the previous group (window permitting). */
98
+ function nextGroup(vaultRoot, chain) {
99
+ const stateDir = path.join(vaultRoot, ".heyspex");
100
+ const stateFile = path.join(stateDir, "cli-capture-group.json");
101
+ let prev = null;
102
+ try {
103
+ prev = JSON.parse(readFileSync(stateFile, "utf8"));
104
+ } catch { /* first run / corrupt — start fresh */ }
105
+
106
+ const now = Date.now();
107
+ let group;
108
+ if (chain && prev?.groupId && now - (prev.at ?? 0) <= CHAIN_WINDOW_MS) {
109
+ group = { groupId: prev.groupId, seq: (prev.seq ?? 1) + 1, at: now };
110
+ } else {
111
+ group = { groupId: randomUUID().replaceAll("-", "").slice(0, 12), seq: 1, at: now };
112
+ }
113
+ try {
114
+ if (!existsSync(stateDir)) mkdirSync(stateDir, { recursive: true });
115
+ writeFileSync(stateFile, JSON.stringify(group));
116
+ } catch { /* stateless fallback — capture still goes through */ }
117
+ return group;
118
+ }
119
+
120
+ /** True when `url`'s hostname is a loopback address — localhost, 127.0.0.1,
121
+ * or [::1] (bracketed or not). Non-loopback hosts (a paired cloud console,
122
+ * a LAN address, etc.) never get the hosted-console retry: if a deliberately
123
+ * configured console is unreachable, that's a real outage to surface, not a
124
+ * dead local dev server to route around. */
125
+ function isLoopback(url) {
126
+ try {
127
+ const hostname = new URL(url).hostname.toLowerCase();
128
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1";
129
+ } catch {
130
+ return false;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * POST a capture to `url`, with a local-dead-console fallback ladder:
136
+ * 1. POST to `url` (the resolved console — often the local dev server).
137
+ * 2. If that fails to CONNECT (not an HTTP error) and `url` is loopback,
138
+ * retry once against `hostedUrl` with the identical body/headers/trace id.
139
+ * 3. If the hosted retry also fails to connect, stash the payload to the
140
+ * vault's disk outbox via `stash` and report that instead of throwing.
141
+ *
142
+ * A non-loopback URL that fails to connect is never retried or stashed — the
143
+ * caller configured that console on purpose, so an outage there is reported
144
+ * as-is. An HTTP error response (4xx/5xx) from either console is likewise
145
+ * reported as-is, never retried or stashed — the console IS reachable.
146
+ *
147
+ * `fetchImpl` and `stash` are injectable so tests never open a real socket
148
+ * or touch the real outbox directory.
149
+ */
150
+ export async function postCaptureWithFallback({
151
+ url,
152
+ payload,
153
+ token,
154
+ vaultRoot,
155
+ fetchImpl = fetch,
156
+ hostedUrl = DEFAULT_CONSOLE_URL,
157
+ stash = stashCaptureArgs,
158
+ }) {
159
+ const traceId = payload.trace_id;
160
+ const body = JSON.stringify({ traceId, payload, errors: [] });
161
+ const headers = {
162
+ "content-type": "application/json",
163
+ "x-heyspex-trace-id": traceId,
164
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
165
+ };
166
+
167
+ const post = (targetUrl) =>
168
+ fetchImpl(`${targetUrl.replace(/\/$/, "")}/api/drafts`, { method: "POST", headers, body });
169
+
170
+ let res;
171
+ let consoleUrl = url;
172
+ try {
173
+ res = await post(url);
174
+ } catch (err) {
175
+ if (!isLoopback(url)) {
176
+ return { kind: "connect-error", url, err };
177
+ }
178
+ try {
179
+ res = await post(hostedUrl);
180
+ consoleUrl = hostedUrl;
181
+ } catch (err2) {
182
+ const stashed = stash(vaultRoot, { user_quote: payload.article_text });
183
+ return {
184
+ kind: "stashed",
185
+ files: stashed.map((e) => e.path),
186
+ primaryErr: err,
187
+ hostedErr: err2,
188
+ };
189
+ }
190
+ }
191
+
192
+ if (!res.ok) {
193
+ const errBody = await res.text().catch(() => "");
194
+ return { kind: "http-error", status: res.status, body: errBody, url: consoleUrl };
195
+ }
196
+
197
+ const result = await res.json().catch(() => ({}));
198
+ return { kind: "ok", consoleUrl, retried: consoleUrl !== url, result };
199
+ }
200
+
201
+ async function main() {
202
+ const parsed = parseArgs(process.argv.slice(2));
203
+ if (parsed.help) { usage(); return; }
204
+ if (parsed.error) {
205
+ process.stderr.write(`hs capture: ${parsed.error}\n`);
206
+ process.exit(2);
207
+ }
208
+ const opts = parsed.opts;
209
+
210
+ let content, contentSource;
211
+ if (opts.clipboard) {
212
+ content = readClipboard();
213
+ contentSource = "cli-clipboard";
214
+ } else if (opts.file) {
215
+ // hs.mjs runs this script with cwd=PKG_ROOT (the platform checkout) and
216
+ // forwards the caller's real invocation directory as HS_INVOKED_CWD —
217
+ // resolve the user-typed --file against THAT, not this process's own
218
+ // (wrong) cwd, so `hs capture --file <relative>` finds the file where
219
+ // the caller stood rather than reporting it missing at a platform path.
220
+ const invokedCwd = process.env.HS_INVOKED_CWD?.trim() || process.cwd();
221
+ const p = path.resolve(invokedCwd, opts.file);
222
+ if (!existsSync(p)) {
223
+ process.stderr.write(`hs capture: file not found: ${p}\n`);
224
+ process.exit(1);
225
+ }
226
+ content = readFileSync(p, "utf8");
227
+ contentSource = "cli-file";
228
+ opts.title ??= path.basename(p);
229
+ } else if (opts.text) {
230
+ content = opts.text;
231
+ contentSource = "cli";
232
+ } else {
233
+ usage();
234
+ process.exit(2);
235
+ }
236
+
237
+ if (!content || !content.trim()) {
238
+ process.stderr.write("hs capture: nothing to capture (empty content)\n");
239
+ process.exit(1);
240
+ }
241
+
242
+ const vaultRoot = process.env.HS_VAULT_ROOT ?? process.cwd();
243
+ opts.url ??= resolveConsoleUrl({ repoRoot: vaultRoot });
244
+ const group = nextGroup(vaultRoot, opts.add);
245
+ const traceId = `cli-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
246
+
247
+ const payload = {
248
+ schema: "heyspex.capture/0.1",
249
+ trace_id: traceId,
250
+ tier: 3,
251
+ captured_at: new Date().toISOString(),
252
+ capture_channel: "cli",
253
+ source: contentSource,
254
+ content_source: contentSource,
255
+ title: opts.title,
256
+ article_text: content,
257
+ degraded: null,
258
+ errors: null,
259
+ capture_group_id: group.groupId,
260
+ capture_group_seq: group.seq,
261
+ };
262
+
263
+ const mcpToken = readCredentialsMcpToken();
264
+ const outcome = await postCaptureWithFallback({
265
+ url: opts.url,
266
+ payload,
267
+ token: mcpToken,
268
+ vaultRoot,
269
+ });
270
+
271
+ if (outcome.kind === "connect-error") {
272
+ process.stderr.write(
273
+ `hs capture: console unreachable at ${outcome.url} (${outcome.err.cause?.code ?? outcome.err.message})\n`,
274
+ );
275
+ process.exit(1);
276
+ }
277
+
278
+ if (outcome.kind === "http-error") {
279
+ process.stderr.write(
280
+ `hs capture: POST /api/drafts failed ${outcome.status}: ${outcome.body.slice(0, 300)}\n`,
281
+ );
282
+ process.exit(1);
283
+ }
284
+
285
+ if (outcome.kind === "stashed") {
286
+ if (opts.json) {
287
+ process.stdout.write(JSON.stringify({
288
+ ok: true, stashed: true, traceId, files: outcome.files,
289
+ }, null, 2) + "\n");
290
+ } else {
291
+ process.stdout.write(
292
+ `hs capture: local and hosted consoles both unreachable — saved to disk outbox instead\n` +
293
+ outcome.files.map((f) => ` ${f}`).join("\n") + "\n",
294
+ );
295
+ }
296
+ return;
297
+ }
298
+
299
+ // outcome.kind === "ok"
300
+ if (opts.json) {
301
+ process.stdout.write(JSON.stringify({
302
+ ok: true, traceId, consoleUrl: outcome.consoleUrl,
303
+ group: group.groupId, seq: group.seq,
304
+ chars: content.length, response: outcome.result,
305
+ }, null, 2) + "\n");
306
+ } else {
307
+ const chained = group.seq > 1 ? ` (group ${group.groupId} · #${group.seq})` : "";
308
+ const via = outcome.retried
309
+ ? ` — local console unreachable, sent to ${outcome.consoleUrl} instead`
310
+ : "";
311
+ process.stdout.write(
312
+ `captured ${content.length} chars → drafts${chained}${via}\n` +
313
+ ` add another: hs capture --clipboard --add (within 2 minutes)\n`,
314
+ );
315
+ }
316
+ }
317
+
318
+ // Guard so this module can be `import`ed (e.g. by hs-capture-cli.test.mjs)
319
+ // without running main() — bin/hs.mjs spawns this file as a child process
320
+ // with the file path as argv[1], so this only fires under that spawn.
321
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
322
+ main().catch((err) => {
323
+ process.stderr.write(`hs capture: ${err.message}\n`);
324
+ process.exit(1);
325
+ });
326
+ }
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * hs classifier stats — read-only view of HeySpex Intent Classifier decisions.
4
+ * Dispatched from bin/hs.mjs. Reads .heyspex/intent-classifier-history.jsonl (the
5
+ * append-only decision log the classifier hooks write) and prints counts per behavior
6
+ * and layer plus the most recent fires. Pure read; never writes, never networks.
7
+ *
8
+ * Usage:
9
+ * hs classifier stats [--json] [--recent N]
10
+ */
11
+
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import process from 'node:process';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
18
+
19
+ const HELP = `hs classifier stats — HeySpex Intent Classifier decision stats (read-only)
20
+
21
+ Usage:
22
+ hs classifier stats [--json] [--recent N]
23
+
24
+ Reads .heyspex/intent-classifier-history.jsonl. Shows totals per behavior + layer
25
+ (lexical vs semantic) + decision (fire / self_check_injected / below_threshold /
26
+ error), and the N most recent fires (default 10). self_check_injected is the default
27
+ semantic-layer signal: an OFFER count (how often the deterministic self-check block was
28
+ injected), not an agent-fire count — the agent's own judgment decides whether to act.
29
+ `;
30
+
31
+ function historyPath() {
32
+ const root = process.env.HS_VAULT_ROOT || process.env.HEYSPEX_VAULT_ROOT || REPO_ROOT;
33
+ return path.join(root, '.heyspex', 'intent-classifier-history.jsonl');
34
+ }
35
+
36
+ function readRows() {
37
+ let raw;
38
+ try {
39
+ raw = fs.readFileSync(historyPath(), 'utf8');
40
+ } catch {
41
+ return [];
42
+ }
43
+ const rows = [];
44
+ for (const line of raw.split(/\r?\n/)) {
45
+ const t = line.trim();
46
+ if (!t) continue;
47
+ try {
48
+ const r = JSON.parse(t);
49
+ if (r && r.event_kind === 'intent_classifier.decision') rows.push(r);
50
+ } catch {
51
+ /* skip a corrupt line — never throw */
52
+ }
53
+ }
54
+ return rows;
55
+ }
56
+
57
+ function main(argv) {
58
+ const args = argv[0] === 'classifier' ? argv.slice(1) : argv.slice();
59
+ if (args.includes('--help') || args.includes('-h')) {
60
+ process.stdout.write(HELP);
61
+ process.exit(0);
62
+ }
63
+ const verb = args.find((a) => !a.startsWith('--')) || 'stats';
64
+ if (verb !== 'stats') {
65
+ process.stderr.write(`hs classifier: unknown subcommand \`${verb}\` (expected: stats)\n${HELP}`);
66
+ process.exit(2);
67
+ }
68
+ const asJson = args.includes('--json');
69
+ const recentIdx = args.indexOf('--recent');
70
+ const recentN = recentIdx >= 0 && args[recentIdx + 1] ? Math.max(0, parseInt(args[recentIdx + 1], 10) || 0) : 10;
71
+
72
+ const rows = readRows();
73
+
74
+ // Aggregate: behavior(rule) → { fire, self_check_injected, below_threshold, error, lexical, semantic }.
75
+ // self_check_injected is the DEFAULT semantic-layer signal ([[HS-M4PN-TASK]]): the
76
+ // hook injected a deterministic self-check block and the agent (not a model) decided
77
+ // whether to act. There is no synchronous confidence/verdict, so this is an OFFER
78
+ // count (fire rate of the injection), not a fire count of a behavior. Actual fires by
79
+ // the agent are observable downstream (capture-history, settings-history, defer edits).
80
+ const byBehavior = {};
81
+ const layerTotals = { lexical: 0, semantic: 0 };
82
+ const decisionTotals = { fire: 0, self_check_injected: 0, below_threshold: 0, error: 0 };
83
+ for (const r of rows) {
84
+ const key = r.rule || 'unknown';
85
+ const b = (byBehavior[key] ??= { fire: 0, self_check_injected: 0, below_threshold: 0, error: 0, lexical: 0, semantic: 0 });
86
+ if (r.decision && b[r.decision] !== undefined) b[r.decision] += 1;
87
+ if (r.layer && b[r.layer] !== undefined) b[r.layer] += 1;
88
+ if (r.layer && layerTotals[r.layer] !== undefined) layerTotals[r.layer] += 1;
89
+ if (r.decision && decisionTotals[r.decision] !== undefined) decisionTotals[r.decision] += 1;
90
+ }
91
+
92
+ const recentFires = rows
93
+ .filter((r) => r.decision === 'fire')
94
+ .slice(-recentN)
95
+ .reverse()
96
+ .map((r) => ({ timestamp: r.timestamp, behavior: r.rule, layer: r.layer, confidence: r.confidence ?? null }));
97
+
98
+ if (asJson) {
99
+ process.stdout.write(
100
+ JSON.stringify(
101
+ { total: rows.length, layerTotals, decisionTotals, byBehavior, recentFires },
102
+ null,
103
+ 2,
104
+ ) + '\n',
105
+ );
106
+ process.exit(0);
107
+ }
108
+
109
+ const out = [];
110
+ out.push(`HeySpex Intent Classifier — ${rows.length} decision(s) logged`);
111
+ out.push(` layers: lexical ${layerTotals.lexical} · semantic ${layerTotals.semantic}`);
112
+ out.push(` decisions: fire ${decisionTotals.fire} · self_check_injected ${decisionTotals.self_check_injected} · below_threshold ${decisionTotals.below_threshold} · error ${decisionTotals.error}`);
113
+ out.push('');
114
+ out.push(' per behavior:');
115
+ const names = Object.keys(byBehavior).sort();
116
+ if (names.length === 0) {
117
+ out.push(' (no decisions logged yet)');
118
+ } else {
119
+ for (const name of names) {
120
+ const b = byBehavior[name];
121
+ out.push(` ${name.padEnd(26)} fire ${b.fire} · self-check ${b.self_check_injected} · below ${b.below_threshold} · error ${b.error} (lexical ${b.lexical} / semantic ${b.semantic})`);
122
+ }
123
+ }
124
+ if (recentFires.length) {
125
+ out.push('');
126
+ out.push(` recent fires (last ${recentFires.length}):`);
127
+ for (const f of recentFires) {
128
+ const conf = f.confidence !== null ? ` conf=${f.confidence}` : '';
129
+ out.push(` ${f.timestamp} ${String(f.behavior).padEnd(26)} ${f.layer}${conf}`);
130
+ }
131
+ }
132
+ process.stdout.write(out.join('\n') + '\n');
133
+ process.exit(0);
134
+ }
135
+
136
+ main(process.argv.slice(2));
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `hs companion ...` — the desktop companion's CLI face.
4
+ *
5
+ * start|stop|status|build tray-host lifecycle → companion/start.ps1 (PowerShell 7),
6
+ * which launches companion/src/HeySpex.Companion.Host directly
7
+ * (the versioned-folder indirection was retired 2026-08-12).
8
+ * settings list|get|set every Companion setting, via the console's
9
+ * GET/PUT /api/companion/settings ([[HS-UDH1]]); plus the
10
+ * short aliases (mute, volume, orb, wake, ...).
11
+ *
12
+ * Parsing, the key/alias tables, value coercion, and the request shape live in
13
+ * scripts/lib/hs-companion-settings.mjs so they are unit-testable without a
14
+ * process; this file only spawns, prints, and exits.
15
+ */
16
+ import { spawn } from "node:child_process";
17
+ import { existsSync } from "node:fs";
18
+ import path from "node:path";
19
+ import process from "node:process";
20
+ import { fileURLToPath } from "node:url";
21
+
22
+ import {
23
+ makeCompanionConsoleFetch,
24
+ parseCompanionArgs,
25
+ renderHelp,
26
+ runCompanionSettings,
27
+ } from "./lib/hs-companion-settings.mjs";
28
+
29
+ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
30
+ const START_PS1 = path.join(PKG_ROOT, "companion", "start.ps1");
31
+
32
+ function resolvePwsh() {
33
+ return process.env.HEYSPEX_COMPANION_POWERSHELL?.trim() || "pwsh";
34
+ }
35
+
36
+ function runHost(sub, rest) {
37
+ if (!existsSync(START_PS1)) {
38
+ process.stderr.write(`hs companion: start script not found at ${START_PS1}\n`);
39
+ process.exit(1);
40
+ }
41
+
42
+ const pwshArgs = ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", START_PS1, "-Action", sub];
43
+ if (sub === "start" && rest.includes("--build")) {
44
+ pwshArgs.push("-Build");
45
+ }
46
+
47
+ const child = spawn(resolvePwsh(), pwshArgs, { cwd: PKG_ROOT, stdio: "inherit" });
48
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : code ?? 0));
49
+ child.on("error", (err) => {
50
+ process.stderr.write(
51
+ `hs companion: failed to run ${resolvePwsh()}: ${err.message}\n` +
52
+ "Install PowerShell 7 (pwsh) or set HEYSPEX_COMPANION_POWERSHELL to its path.\n",
53
+ );
54
+ process.exit(1);
55
+ });
56
+ }
57
+
58
+ async function main(argv) {
59
+ const parsed = parseCompanionArgs(argv);
60
+
61
+ if (parsed.error) {
62
+ process.stderr.write(`hs companion: ${parsed.error}\n\n${renderHelp("companion")}\n`);
63
+ process.exit(2);
64
+ }
65
+ if (parsed.kind === "help") {
66
+ process.stdout.write(`${renderHelp(parsed.topic)}\n`);
67
+ process.exit(0);
68
+ }
69
+ if (parsed.kind === "host") {
70
+ runHost(parsed.sub, parsed.rest);
71
+ return;
72
+ }
73
+
74
+ // settings — resolved console (or --url) + the shared bearer ladder.
75
+ const consoleFetch = makeCompanionConsoleFetch({ url: parsed.url, repoRoot: process.env.HS_VAULT_ROOT });
76
+ const result = await runCompanionSettings(parsed, { consoleFetch });
77
+ if (result.ok) {
78
+ process.stdout.write(`${result.stdout}\n`);
79
+ process.exit(0);
80
+ }
81
+ process.stderr.write(`hs companion: ${result.stderr}\n`);
82
+ process.exit(result.exitCode);
83
+ }
84
+
85
+ main(process.argv.slice(2)).catch((err) => {
86
+ process.stderr.write(`hs companion: ${err.message}\n`);
87
+ process.exit(1);
88
+ });