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,496 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * hs view <surface> [spec-id] — open ONE console surface in a chromeless
4
+ * app-mode browser window (the "solo surface" console gauge).
5
+ *
6
+ * Per [[HS-RU2R-I87U-RES-solo-surface-cli-launcher-single]]: launches
7
+ * Edge/Chrome with `--app=<url>` (NOT `--kiosk`, which forces fullscreen and
8
+ * ignores window sizing) pointed at the console's `/solo/<surface>` route.
9
+ * Probes ground-truth dev-server status first (same core as `hs server
10
+ * status`) so a down console yields a clear message, never a blank window.
11
+ *
12
+ * Open = run it; close = close the window; re-run = reopen (idempotent).
13
+ *
14
+ * Usage:
15
+ * hs view # inspector, Pipeline tab, following the focus pin
16
+ * hs view HLFJ # same, anchored on HS-HLFJ (any case, HS- optional)
17
+ * hs view pipeline HS-I87U-CAP # same — "pipeline" is the inspector's Pipeline tab
18
+ * hs view inspector HS-I87U-CAP # inspector on its default tab
19
+ * hs view docs HS-NP1Y # doc + inspector Pipeline tab side by side
20
+ * hs view pipeline --follow capture # track the capture-on-deck pin
21
+ * hs view pipeline --follow capture --url https://console.example.com
22
+ * hs view pipeline --width 510 --height 900 --browser edge|chrome
23
+ */
24
+ import { spawn } from 'node:child_process';
25
+ import path from 'node:path';
26
+ import { fileURLToPath, pathToFileURL } from 'node:url';
27
+
28
+ import { getDevServerStatus } from './dev-server-status.mjs';
29
+ import { resolveConsoleUrl, DEFAULT_CONSOLE_URL } from './lib/console-url-resolver.mjs';
30
+ // The runnable form of a HeySpex command on THIS machine — `hs …` when the bin
31
+ // is on PATH, else an absolute-path `node "<install>/bin/hs.mjs" …`. Never an
32
+ // `npx` form: npx resolves from the READER's working directory, and outside a
33
+ // checkout `npx hs` installs the unrelated public package hs@2.0.1
34
+ // ([[HS-I6HT-EKYU-DIAG-every-message-tells-the-user-to-run-hs]]).
35
+ import { hsCommand } from './lib/hs-command.mjs';
36
+ import {
37
+ complementarySide,
38
+ computeWindowPlacement,
39
+ getPrimaryWorkArea,
40
+ parsePlaceSide,
41
+ placementFlags,
42
+ } from './lib/window-placement.mjs';
43
+ import { collectOccupiedFamilyKeysFromVault, parseSpecId } from '../vendor/heyspex-mcp/spec-id-lib.mjs';
44
+
45
+ function defaultVaultRoot() {
46
+ // This file lives at <vaultRoot>/scripts/, so one level up is the root.
47
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
48
+ }
49
+
50
+ /** Levenshtein edit distance — small local helper, no dependency pulled in for it. */
51
+ function editDistance(a, b) {
52
+ const m = a.length;
53
+ const n = b.length;
54
+ const dp = Array.from({ length: m + 1 }, (_, i) => [i, ...Array(n).fill(0)]);
55
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
56
+ for (let i = 1; i <= m; i++) {
57
+ for (let j = 1; j <= n; j++) {
58
+ dp[i][j] =
59
+ a[i - 1] === b[j - 1]
60
+ ? dp[i - 1][j - 1]
61
+ : 1 + Math.min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]);
62
+ }
63
+ }
64
+ return dp[m][n];
65
+ }
66
+
67
+ /**
68
+ * Nearest occupied family keys to a typo'd spec token — walks the vault
69
+ * (fresh every call, unlike `.heyspex/vault-index.json`) via the shared
70
+ * spec-id-lib helper, never re-implements the vault scan.
71
+ */
72
+ function suggestFamilyKeys(rawSpec, vaultRoot, limit = 5) {
73
+ const parsed = parseSpecId(rawSpec);
74
+ const needle = (parsed?.familyKey ?? rawSpec.replace(/^HS-/i, '')).toUpperCase();
75
+ const occupied = [...collectOccupiedFamilyKeysFromVault(vaultRoot)];
76
+ return occupied
77
+ .map((key) => ({ key, dist: editDistance(needle, key) }))
78
+ .sort((x, y) => x.dist - y.dist || x.key.localeCompare(y.key))
79
+ .slice(0, limit)
80
+ .filter((c) => c.dist <= Math.max(2, Math.ceil(needle.length / 2)))
81
+ .map((c) => `HS-${c.key}`);
82
+ }
83
+
84
+ /**
85
+ * Resolve a spec id/token against the running console before opening a
86
+ * browser window (same resolution rules as `GET /api/specs/:id`, including
87
+ * its bare-family-token prefix fallback). Returns `{ ok: true }`,
88
+ * `{ ok: false, kind: 'ambiguous', candidates }`, or
89
+ * `{ ok: false, kind: 'not_found', suggestions }`.
90
+ */
91
+ async function resolveSpec(base, rawSpec, vaultRoot) {
92
+ let res;
93
+ try {
94
+ res = await fetch(`${base}/api/specs/${encodeURIComponent(rawSpec)}`);
95
+ } catch (err) {
96
+ // Network hiccup against a console we already confirmed is up — don't
97
+ // block the window on it, just open unvalidated.
98
+ return { ok: true, skipped: true, reason: err?.message || String(err) };
99
+ }
100
+ if (res.ok) return { ok: true };
101
+ let body = null;
102
+ try {
103
+ body = await res.json();
104
+ } catch {
105
+ // non-JSON error body — fall through to generic not_found handling
106
+ }
107
+ if (res.status === 404 && body?.error === 'ambiguous_id' && Array.isArray(body.candidates)) {
108
+ return { ok: false, kind: 'ambiguous', candidates: body.candidates };
109
+ }
110
+ if (res.status !== 404) {
111
+ // Not a verdict on the spec — a hosted console answers an unauthenticated
112
+ // probe with a sign-in redirect or a 401/403. Open unvalidated rather than
113
+ // claiming the doc does not exist.
114
+ return { ok: true, skipped: true, reason: `HTTP ${res.status}` };
115
+ }
116
+ return { ok: false, kind: 'not_found', suggestions: suggestFamilyKeys(rawSpec, vaultRoot) };
117
+ }
118
+
119
+ /**
120
+ * The solo surfaces `/solo/<surface>` mounts — mirror of the registry in
121
+ * spex-console/src/app/solo/[surface]/page.tsx. A first positional that is not
122
+ * one of these is read as a spec id, opened on the pipeline surface.
123
+ */
124
+ export const SOLO_SURFACES = new Set([
125
+ 'pipeline',
126
+ 'docs',
127
+ 'inspector',
128
+ 'whats-next',
129
+ 'captures',
130
+ 'session-log',
131
+ 'settings',
132
+ ]);
133
+
134
+ /**
135
+ * The view a bare `hs view` / `hs view <spec-id>` opens. There is no
136
+ * standalone pipeline window: the inspector already carries the Pipeline tab,
137
+ * so "pipeline" resolves to the inspector opened on that tab (owner ruling
138
+ * 2026-10-10) — see {@link INSPECTOR_TAB_ALIASES}.
139
+ */
140
+ export const DEFAULT_SPEC_SURFACE = 'pipeline';
141
+
142
+ /**
143
+ * Surface words that are really an inspector tab. `hs view pipeline <id>` and
144
+ * every older caller still work — they land on the inspector, that tab open.
145
+ */
146
+ export const INSPECTOR_TAB_ALIASES = { pipeline: 'pipeline' };
147
+
148
+ /**
149
+ * Canonicalise a typed spec token: `hlfj`, `hs-hlfj` and `HS-HLFJ` all become
150
+ * `HS-HLFJ`. A missing `HS-` prefix is added (the console's family-token
151
+ * fallback only matches `HS-<token>-…` prefixes, so a bare `hlfj` found
152
+ * nothing). Only a short key form (`HLFJ`, `HLFJ-AB12`, `HLFJ-TASK`) is
153
+ * upper-cased; a full basename keeps its lower-case slug so it still matches
154
+ * exactly.
155
+ */
156
+ export function normalizeSpecToken(raw) {
157
+ const token = String(raw ?? '').trim();
158
+ if (!token) return token;
159
+ const body = token.replace(/^hs-/i, '');
160
+ if (/^[a-z0-9]{4}(?:-[a-z0-9]{2,8}){0,2}$/i.test(body)) return `HS-${body.toUpperCase()}`;
161
+ return `HS-${body}`;
162
+ }
163
+
164
+ /**
165
+ * Split positionals into `{ surface, panel, spec }`. `hs view <surface> [spec]`
166
+ * keeps its surface; `hs view <spec>` and bare `hs view` open the inspector on
167
+ * its Pipeline tab, as does the `pipeline` word. The spec token is normalised.
168
+ */
169
+ export function resolveSurfaceAndSpec(positionals) {
170
+ let [surface = null, spec = null] = positionals;
171
+ if (surface && !SOLO_SURFACES.has(surface.toLowerCase()) && !spec) {
172
+ spec = surface;
173
+ surface = null;
174
+ }
175
+ surface = (surface ?? DEFAULT_SPEC_SURFACE).toLowerCase();
176
+ let panel = null;
177
+ if (INSPECTOR_TAB_ALIASES[surface]) {
178
+ panel = INSPECTOR_TAB_ALIASES[surface];
179
+ surface = 'inspector';
180
+ }
181
+ return { surface, panel, spec: spec ? normalizeSpecToken(spec) : null };
182
+ }
183
+
184
+ /** Anchor sources the window can follow live. `pin` is the default behavior. */
185
+ const FOLLOW_MODES = new Set(['pin', 'capture']);
186
+
187
+ /** Opening width for a surface with nothing better to say. */
188
+ export const DEFAULT_WINDOW_WIDTH = 510;
189
+
190
+ /**
191
+ * Surfaces that want a different opening width than the default.
192
+ *
193
+ * `inspector` is one narrow pane meant to sit beside a terminal — at 510 it
194
+ * takes screen it does not need. It opens at 350 and grows leftward to the
195
+ * `docs` width when a doc pane opens
196
+ * ([[HS-I2YN-ST07-EPIC-two-pane-layout-pipeline-view-on-the]]).
197
+ */
198
+ export const SURFACE_WINDOW_WIDTH = {
199
+ inspector: 350,
200
+ // Two panes: the doc, then the inspector rail (360) to its right.
201
+ docs: 960,
202
+ };
203
+
204
+ /**
205
+ * Resolve the window width: an explicit `--width` always wins, otherwise the
206
+ * surface's own default, otherwise {@link DEFAULT_WINDOW_WIDTH}.
207
+ *
208
+ * `explicitWidth` is null/undefined when the flag was not passed. Deciding that
209
+ * by COMPARING against the fallback would silently drop `--width 510` on the
210
+ * inspector back to 350, so the caller tracks "was it passed" instead.
211
+ */
212
+ export function resolveWindowWidth(surface, explicitWidth) {
213
+ if (typeof explicitWidth === 'number' && Number.isFinite(explicitWidth) && explicitWidth > 0) {
214
+ return explicitWidth;
215
+ }
216
+ return SURFACE_WINDOW_WIDTH[surface] ?? DEFAULT_WINDOW_WIDTH;
217
+ }
218
+
219
+ /**
220
+ * Which half of the primary monitor's work area the window takes.
221
+ *
222
+ * Explicit `--place` wins, then `HEYSPEX_VIEW_PLACE`, then the default: the
223
+ * two-pane capture view (`docs --follow capture`) sits on the RIGHT so the
224
+ * SpexCode agent window can take the left
225
+ * ([[HS-I2YN-ST07-EPIC-two-pane-layout-pipeline-view-on-the]]). Every other
226
+ * window keeps the OS's own placement (`none`), as before.
227
+ */
228
+ export function resolvePlaceSide({ place, surface, follow }, env = process.env) {
229
+ const explicit = parsePlaceSide(place);
230
+ if (explicit) return explicit;
231
+ const fromEnv = parsePlaceSide(env.HEYSPEX_VIEW_PLACE);
232
+ if (fromEnv) return fromEnv;
233
+ return surface === 'docs' && follow === 'capture' ? 'right' : 'none';
234
+ }
235
+
236
+ /** The side the paired agent (terminal) window should take for this view. */
237
+ export const agentSideFor = (viewSide) => complementarySide(viewSide);
238
+
239
+ function parseArgs(argv) {
240
+ const args = {
241
+ place: null,
242
+ printOnly: false,
243
+ surface: null,
244
+ spec: null,
245
+ // null until `--width` is seen; resolved per-surface once the surface is known.
246
+ width: null,
247
+ height: 900,
248
+ browser: 'edge',
249
+ follow: null,
250
+ url: null,
251
+ };
252
+ const positionals = [];
253
+ for (let i = 0; i < argv.length; i++) {
254
+ const a = argv[i];
255
+ if (a === '--help' || a === '-h') return { help: true };
256
+ if (a === '--width') { args.width = Number(argv[++i]) || args.width; continue; }
257
+ if (a === '--height') { args.height = Number(argv[++i]) || args.height; args.heightExplicit = true; continue; }
258
+ if (a === '--place') {
259
+ const side = parsePlaceSide(argv[++i]);
260
+ if (!side) {
261
+ process.stderr.write('hs view: --place expects left, right or none\n');
262
+ process.exitCode = 2;
263
+ return { help: true };
264
+ }
265
+ args.place = side;
266
+ continue;
267
+ }
268
+ if (a === '--print' || a === '--dry-run') { args.printOnly = true; continue; }
269
+ if (a === '--browser') { args.browser = String(argv[++i] || 'edge'); continue; }
270
+ if (a === '--url') {
271
+ const raw = String(argv[++i] || '').trim();
272
+ if (!/^https?:\/\/\S+$/.test(raw)) {
273
+ process.stderr.write(`hs view: --url expects an http(s) URL (got "${raw || '(nothing)'}")\n`);
274
+ process.exitCode = 2;
275
+ return { help: true };
276
+ }
277
+ args.url = raw.replace(/\/+$/, '');
278
+ continue;
279
+ }
280
+ if (a === '--follow') {
281
+ const mode = String(argv[++i] || '').trim().toLowerCase();
282
+ if (!FOLLOW_MODES.has(mode)) {
283
+ process.stderr.write(
284
+ `hs view: --follow expects ${[...FOLLOW_MODES].join(' or ')} (got "${mode || '(nothing)'}")\n`,
285
+ );
286
+ process.exitCode = 2;
287
+ return { help: true };
288
+ }
289
+ args.follow = mode;
290
+ continue;
291
+ }
292
+ if (a.startsWith('--')) {
293
+ process.stderr.write(`hs view: unknown flag ${a}\n`);
294
+ process.exitCode = 2;
295
+ return { help: true };
296
+ }
297
+ positionals.push(a);
298
+ }
299
+ return { ...args, ...resolveSurfaceAndSpec(positionals) };
300
+ }
301
+
302
+ function usage() {
303
+ process.stdout.write(
304
+ [
305
+ 'hs view <surface> [spec-id] [--follow pin|capture] [--url https://…]',
306
+ ' [--width N] [--height N] [--browser edge|chrome]',
307
+ ' [--place left|right|none] [--print]',
308
+ 'hs view [spec-id] # inspector on its Pipeline tab',
309
+ '',
310
+ 'Open one console surface in a chromeless app-mode browser window.',
311
+ 'Surfaces: inspector, docs, whats-next, captures, session-log, settings',
312
+ ' ("pipeline" = the inspector opened on its Pipeline tab; there is no',
313
+ ' standalone pipeline window.)',
314
+ ' (registry: src/app/solo/[surface]/page.tsx).',
315
+ 'Spec ids are forgiving: hlfj, hs-hlfj and HS-HLFJ are the same spec.',
316
+ 'Without a spec-id the inspector and docs views follow the focus pin live.',
317
+ '--follow capture tracks the capture-on-deck pin instead, so the window',
318
+ ' re-anchors itself onto each new capture rather than opening another.',
319
+ '--url opens against a named console (a hosted one) instead of probing for',
320
+ ' a local dev server — the cloud-relay path the desktop companion uses.',
321
+ '--place pins the window to the left or right half of the primary monitor',
322
+ ' work area (taskbar excluded; never another monitor). Default: right for',
323
+ ' `docs --follow capture` (the two-pane capture view, leaving the left half',
324
+ ' for the SpexCode agent window), none otherwise. Env: HEYSPEX_VIEW_PLACE.',
325
+ '--print shows the browser command line and does not open a window.',
326
+ '',
327
+ ].join('\n'),
328
+ );
329
+ }
330
+
331
+ /**
332
+ * Only a localhost/loopback resolution still requires a running local dev
333
+ * server; anything else (a paired or `hs dev mode:cloud` hosted console) is
334
+ * opened directly. An unparseable URL is treated as local so it still goes
335
+ * through the dev-server probe instead of being launched blind.
336
+ */
337
+ function isLocalConsoleUrl(url) {
338
+ try {
339
+ const host = new URL(url).hostname;
340
+ return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
341
+ } catch {
342
+ return true;
343
+ }
344
+ }
345
+
346
+ /**
347
+ * The exact command line a launch would run, as `{ file, args }`. Pure, so
348
+ * `--print` and the tests see precisely what `launchAppWindow` spawns.
349
+ */
350
+ export function buildLaunchCommand(url, { width, height, browser }, placement = null, platform = process.platform) {
351
+ const exe = browser === 'chrome' ? 'chrome' : 'msedge';
352
+ const flags = [`--app=${url}`, ...(placement ? placementFlags(placement) : [`--window-size=${width},${height}`])];
353
+ if (platform === 'win32') return { file: 'cmd.exe', args: ['/c', 'start', '', exe, ...flags] };
354
+ if (platform === 'darwin') {
355
+ const app = browser === 'chrome' ? 'Google Chrome' : 'Microsoft Edge';
356
+ return { file: 'open', args: ['-na', app, '--args', ...flags] };
357
+ }
358
+ return { file: browser === 'chrome' ? 'google-chrome' : 'microsoft-edge', args: flags };
359
+ }
360
+
361
+ function launchAppWindow(url, opts, placement = null) {
362
+ const { width, height, browser } = opts;
363
+ const exe = browser === 'chrome' ? 'chrome' : 'msedge';
364
+ const flags = [`--app=${url}`, ...(placement ? placementFlags(placement) : [`--window-size=${width},${height}`])];
365
+ let child;
366
+ if (process.platform === 'win32') {
367
+ // `start` resolves msedge/chrome via the App Paths registry; the empty ''
368
+ // is start's window-title slot, without it the exe would be eaten as title.
369
+ child = spawn('cmd.exe', ['/c', 'start', '', exe, ...flags], {
370
+ detached: true,
371
+ stdio: 'ignore',
372
+ });
373
+ } else if (process.platform === 'darwin') {
374
+ const app = browser === 'chrome' ? 'Google Chrome' : 'Microsoft Edge';
375
+ child = spawn('open', ['-na', app, '--args', ...flags], { detached: true, stdio: 'ignore' });
376
+ } else {
377
+ const bin = browser === 'chrome' ? 'google-chrome' : 'microsoft-edge';
378
+ child = spawn(bin, flags, { detached: true, stdio: 'ignore' });
379
+ }
380
+ child.unref();
381
+ }
382
+
383
+ async function main() {
384
+ const args = parseArgs(process.argv.slice(2));
385
+ if (args.help) {
386
+ usage();
387
+ return;
388
+ }
389
+
390
+ // Now that the surface is known, settle the width: an explicit `--width`
391
+ // stands, otherwise the surface's own opening width.
392
+ args.width = resolveWindowWidth(args.surface, args.width);
393
+
394
+ // `--url` names the console explicitly — the cloud-relay path, where the
395
+ // companion opens a window against the hosted console it is paired with and
396
+ // there is no local dev server to probe. Without it, the machine's own
397
+ // console resolution decides: `hs dev mode:cloud` / `hs mcp pair` stamp a
398
+ // hosted URL into ~/.heyspex/credentials.yaml, and a non-local resolution
399
+ // needs no dev server either. Only a localhost resolution keeps the old
400
+ // behavior of requiring (and probing for) a running local console.
401
+ const vaultRoot = process.env.HS_VAULT_ROOT || defaultVaultRoot();
402
+ let base = args.url;
403
+ if (!base) {
404
+ const resolved = resolveConsoleUrl({ repoRoot: vaultRoot });
405
+ if (!isLocalConsoleUrl(resolved)) {
406
+ base = resolved.replace(/\/+$/, '');
407
+ } else {
408
+ const status = await getDevServerStatus({ vaultRoot: process.env.HS_VAULT_ROOT });
409
+ if (!status.running) {
410
+ process.stderr.write(
411
+ 'hs view: this machine points at a local console but no spex-console dev server is running.\n' +
412
+ ' Start it first (npm run dev), then re-run this command.\n' +
413
+ ` (Meant the hosted console? Run \`${hsCommand('dev mode:cloud')}\`, or pass --url https://…)\n`,
414
+ );
415
+ process.exitCode = 1;
416
+ return;
417
+ }
418
+ const consoleServer = (status.servers || []).find((s) => s.probe === 'console');
419
+ base = (consoleServer?.url || DEFAULT_CONSOLE_URL).replace(/\/+$/, '');
420
+ }
421
+ }
422
+
423
+ if (args.spec) {
424
+ const resolution = await resolveSpec(base, args.spec, vaultRoot);
425
+ if (!resolution.ok) {
426
+ if (resolution.kind === 'ambiguous') {
427
+ process.stderr.write(
428
+ `hs view: "${args.spec}" is ambiguous — it matches more than one spec:\n` +
429
+ resolution.candidates.map((c) => ` ${c}\n`).join('') +
430
+ 'Pass the full basename to disambiguate.\n',
431
+ );
432
+ } else {
433
+ process.stderr.write(`hs view: no spec found for "${args.spec}".\n`);
434
+ if (resolution.suggestions.length) {
435
+ process.stderr.write(
436
+ 'Did you mean:\n' + resolution.suggestions.map((s) => ` ${s}\n`).join(''),
437
+ );
438
+ }
439
+ }
440
+ process.exitCode = 1;
441
+ return;
442
+ }
443
+ }
444
+
445
+ // A fixed `?spec=` wins over any follow mode (both here and in the route),
446
+ // so only send `follow` when there is no fixed anchor to override it.
447
+ const params = new URLSearchParams();
448
+ if (args.spec) params.set('spec', args.spec);
449
+ else if (args.follow) params.set('follow', args.follow);
450
+ if (args.panel) params.set('panel', args.panel);
451
+ const search = params.toString();
452
+ const query = search ? `?${search}` : '';
453
+ const url = `${base}/solo/${encodeURIComponent(args.surface)}${query}`;
454
+
455
+ // Placement is best-effort: no work area (non-Windows, probe failure) means
456
+ // no position flags and today's behavior.
457
+ const side = resolvePlaceSide(args);
458
+ const placement =
459
+ side === 'none'
460
+ ? null
461
+ : computeWindowPlacement({
462
+ workArea: getPrimaryWorkArea(),
463
+ side,
464
+ width: args.width,
465
+ // Full work-area height unless --height was passed.
466
+ height: args.heightExplicit ? args.height : undefined,
467
+ });
468
+
469
+ if (args.printOnly) {
470
+ const cmd = buildLaunchCommand(url, args, placement);
471
+ process.stdout.write(
472
+ `hs view (--print, nothing opened): place=${side}${placement ? '' : ' (no placement)'}\n` +
473
+ ` ${cmd.file} ${cmd.args.join(' ')}\n` +
474
+ (placement ? ` agent window should take: ${agentSideFor(side)}\n` : ''),
475
+ );
476
+ return;
477
+ }
478
+
479
+ launchAppWindow(url, args, placement);
480
+ const size = placement
481
+ ? `${placement.width}x${placement.height} at ${placement.x},${placement.y}`
482
+ : `${args.width}x${args.height}`;
483
+ process.stdout.write(
484
+ `hs view: opened ${url} in an app-mode ${args.browser === 'chrome' ? 'Chrome' : 'Edge'} window ` +
485
+ `(${size}). Close the window to close; re-run to reopen.\n`,
486
+ );
487
+ }
488
+
489
+ // Run only as the CLI entry point. Without this guard, importing the module —
490
+ // which is how `resolveWindowWidth` is tested — would spawn a browser window.
491
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
492
+ main().catch((err) => {
493
+ process.stderr.write(`hs view: ${err?.message || err}\n`);
494
+ process.exitCode = 1;
495
+ });
496
+ }
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+ // scripts/hs-worktree.mjs — `hs worktree list|alloc|release|reap|rebind`.
3
+ // Task: HS-V9XH (epic HS-03DO "Claim-bound worktree isolation for parallel agent
4
+ // sessions"). Thin shell: parse argv (scripts/lib/hs-worktree-args.mjs), then call
5
+ // dev-factory/lib/worktree-allocator.mjs's exported functions IN-PROCESS — never
6
+ // shells out to the allocator's own CLI mode, so there is exactly one process per
7
+ // invocation and JSON output never has to round-trip through a child's stdout.
8
+ //
9
+ // Exit codes:
10
+ // 0 ok
11
+ // 1 usage (unknown verb/flag — from the parser)
12
+ // 2 check-failed — ONLY `reap --expect-removed` that removed nothing
13
+ // 11 bad-arg (missing required flag / value-flag with no value — from the parser)
14
+ // 13 write-failed (the allocator threw on a git/fs write — alloc or rebind)
15
+
16
+ import { fileURLToPath } from "node:url";
17
+ import path from "node:path";
18
+
19
+ import { parseWorktreeArgs } from "./lib/hs-worktree-args.mjs";
20
+ import {
21
+ makeCtx,
22
+ DEFAULT_DEPS,
23
+ keyInfoFor,
24
+ worktreePathFor,
25
+ allocateForClaim,
26
+ markReleased,
27
+ rebind,
28
+ listWorktrees,
29
+ reapWorktrees,
30
+ } from "../dev-factory/lib/worktree-allocator.mjs";
31
+
32
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
33
+ const REPO_ROOT = path.resolve(__dirname, "..");
34
+
35
+ const EXIT_OK = 0;
36
+ const EXIT_CHECK_FAILED = 2;
37
+ const EXIT_WRITE_FAILED = 13;
38
+
39
+ // ── human-output formatting ─────────────────────────────────────────────────
40
+
41
+ function formatAge(ms) {
42
+ if (ms == null || !Number.isFinite(ms) || ms < 0) return "-";
43
+ const s = Math.floor(ms / 1000);
44
+ if (s < 60) return `${s}s`;
45
+ const m = Math.floor(s / 60);
46
+ if (m < 60) return `${m}m`;
47
+ const h = Math.floor(m / 60);
48
+ if (h < 24) return `${h}h`;
49
+ const d = Math.floor(h / 24);
50
+ return `${d}d`;
51
+ }
52
+
53
+ function formatPid(pid, alive) {
54
+ if (pid == null) return "-";
55
+ return `${pid}(${alive ? "alive" : "dead"})`;
56
+ }
57
+
58
+ function renderTable(rows) {
59
+ const headers = ["PATH", "BRANCH", "KIND", "SPEC", "SESSION", "PID", "AGE", "DIRTY", "RELEASED"];
60
+ const data = rows.map((w) => [
61
+ w.path,
62
+ w.branch || "(detached)",
63
+ w.kind,
64
+ w.specId || "-",
65
+ w.sessionId || "-",
66
+ formatPid(w.pid, w.pidAlive),
67
+ formatAge(w.ageMs),
68
+ w.dirtyCount == null ? "?" : String(w.dirtyCount),
69
+ w.releasedAt ? "yes" : "-",
70
+ ]);
71
+ const widths = headers.map((h, i) => Math.max(h.length, ...data.map((r) => String(r[i]).length)));
72
+ const line = (cols) => cols.map((c, i) => String(c).padEnd(widths[i])).join(" ").trimEnd();
73
+ return [line(headers), ...data.map(line)];
74
+ }
75
+
76
+ // ── agent identity ──────────────────────────────────────────────────────────
77
+
78
+ /**
79
+ * Session id default per task brief: env only, never the MCP's fuller
80
+ * detectAgentIdentity precedence (Cursor/CI vars, etc) — a --session flag
81
+ * always wins.
82
+ */
83
+ function resolveSessionId(flags) {
84
+ return (
85
+ flags.session
86
+ || process.env.CLAUDE_CODE_SESSION_ID
87
+ || process.env.HEYSPEX_SESSION_ID
88
+ || "cli"
89
+ );
90
+ }
91
+
92
+ /**
93
+ * Agent default per task brief: the same detection the MCP uses
94
+ * (vendor/heyspex-mcp/mcp-config.mjs detectAgentIdentity) when it is
95
+ * trivially importable, else HEYSPEX_AGENT env or "cli". A --agent flag
96
+ * always wins over both.
97
+ */
98
+ async function resolveAgent(flags, sessionId) {
99
+ if (flags.agent) return flags.agent;
100
+ try {
101
+ const mod = await import("../vendor/heyspex-mcp/mcp-config.mjs");
102
+ if (typeof mod.detectAgentIdentity === "function") {
103
+ const identity = mod.detectAgentIdentity(undefined, sessionId);
104
+ if (identity?.agent) return identity.agent;
105
+ }
106
+ } catch {
107
+ // Not importable in this install shape (e.g. a published package without
108
+ // vendor/heyspex-mcp) — fall back below.
109
+ }
110
+ return process.env.HEYSPEX_AGENT || "cli";
111
+ }
112
+
113
+ // ── main ─────────────────────────────────────────────────────────────────────
114
+
115
+ async function main() {
116
+ const parsed = parseWorktreeArgs(process.argv.slice(2));
117
+ if (!parsed.ok) {
118
+ process.stderr.write(`hs worktree: ${parsed.message}\n`);
119
+ process.exitCode = parsed.code;
120
+ return;
121
+ }
122
+ const { verb, flags } = parsed;
123
+ const ctx = makeCtx({ repo: REPO_ROOT, worktreeRoot: flags.root || undefined });
124
+
125
+ const emit = (obj, lines) => {
126
+ if (flags.json) process.stdout.write(`${JSON.stringify(obj, null, 2)}\n`);
127
+ else for (const l of lines) process.stdout.write(`${l}\n`);
128
+ };
129
+
130
+ if (verb === "list") {
131
+ const worktrees = listWorktrees(ctx);
132
+ emit({ worktrees }, renderTable(worktrees));
133
+ process.exitCode = EXIT_OK;
134
+ return;
135
+ }
136
+
137
+ if (verb === "alloc") {
138
+ const info = keyInfoFor(flags.spec);
139
+ if (flags.dryRun) {
140
+ const wtPath = worktreePathFor(ctx, flags.spec);
141
+ const baseSha = (DEFAULT_DEPS.git(ctx, ctx.repo, "rev-parse", "HEAD").stdout || "").trim() || null;
142
+ emit(
143
+ { dryRun: true, path: wtPath, branch: info.branch, baseSha },
144
+ [`would allocate ${wtPath} on ${info.branch} from ${baseSha ?? "(unknown)"}`],
145
+ );
146
+ process.exitCode = EXIT_OK;
147
+ return;
148
+ }
149
+ const sessionId = resolveSessionId(flags);
150
+ const agent = await resolveAgent(flags, sessionId);
151
+ try {
152
+ const r = allocateForClaim(ctx, { specId: flags.spec, sessionId, agent, pid: process.pid });
153
+ emit(r, [`${r.created ? "allocated" : "re-attached"} ${r.path} on ${r.branch}`]);
154
+ process.exitCode = EXIT_OK;
155
+ } catch (e) {
156
+ process.stderr.write(`hs worktree alloc: ${e.message}\n`);
157
+ process.exitCode = EXIT_WRITE_FAILED;
158
+ }
159
+ return;
160
+ }
161
+
162
+ if (verb === "release") {
163
+ const r = markReleased(ctx, { specId: flags.spec });
164
+ emit(r, [
165
+ r.found
166
+ ? `${r.alreadyReleased ? "already released" : "released"} ${worktreePathFor(ctx, flags.spec)}`
167
+ : `no worktree for ${flags.spec}`,
168
+ ]);
169
+ process.exitCode = EXIT_OK;
170
+ return;
171
+ }
172
+
173
+ if (verb === "rebind") {
174
+ try {
175
+ const r = rebind(ctx, { fromSpecId: flags.spec, toSpecId: flags.to });
176
+ emit(r, [r.found ? `rebound ${flags.spec} -> ${flags.to} on ${r.branch}` : `no worktree for ${flags.spec}`]);
177
+ process.exitCode = EXIT_OK;
178
+ } catch (e) {
179
+ process.stderr.write(`hs worktree rebind: ${e.message}\n`);
180
+ process.exitCode = EXIT_WRITE_FAILED;
181
+ }
182
+ return;
183
+ }
184
+
185
+ // reap
186
+ const r = await reapWorktrees(ctx, {
187
+ dryRun: flags.dryRun,
188
+ includeForeign: flags.includeForeign,
189
+ yes: flags.yes,
190
+ });
191
+ emit(r, [
192
+ `${r.dryRun ? "would remove" : "removed"} ${r.removed.length}: ${r.removed.join(", ") || "(none)"}`,
193
+ ...r.skipped.map((s) => ` skipped ${s.path} - ${s.reason}`),
194
+ ]);
195
+ process.exitCode = flags.expectRemoved && r.removed.length === 0 ? EXIT_CHECK_FAILED : EXIT_OK;
196
+ }
197
+
198
+ main().catch((err) => {
199
+ process.stderr.write(`hs worktree: ${err?.stack ?? err}\n`);
200
+ process.exitCode = 1;
201
+ });