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,280 @@
1
+ /**
2
+ * search-specs.mjs — `heyspex_search_specs`, the vault's text search for agents
3
+ * ([[HS-YFLS]] fix 2).
4
+ *
5
+ * THE DEFECT THIS CLOSES
6
+ * ----------------------
7
+ * The bug-filing protocol tells an agent to "search specs/diagnoses/ before
8
+ * filing" — and no MCP tool searched specs by text. In PROD mode the vault is
9
+ * CLOUD-primary and the local mirror is incomplete (537 files on disk against
10
+ * 559 in the system of record), so a `grep` over the checkout is not a search
11
+ * of the vault: it answers "not on this disk", which is not the same claim.
12
+ * `heyspex_resolve_focus_intent` is the nearest thing and it is built for a
13
+ * different job — given the AC2 symptom sentence it returns 20 specs matched on
14
+ * a broad `security` tag and neither of the two that actually describe it.
15
+ *
16
+ * WHAT THIS IS
17
+ * ------------
18
+ * A duplicate check. It reads the CLOUD vault through `GET /api/search` — the
19
+ * system of record, never local disk — asks a handful of bounded angles
20
+ * ({@link ./symptom-query.mjs}, see that module for why one query cannot work),
21
+ * merges the windows, and returns enough per row to JUDGE a duplicate without a
22
+ * second call: id, name, type, status, a marked-up snippet, and which angles
23
+ * matched.
24
+ *
25
+ * A NEGATIVE MUST MEAN "NOT THERE", NEVER "COULD NOT LOOK" ([[HS-YFLS]] AC3)
26
+ * -------------------------------------------------------------------------
27
+ * Every way of not finding something is reported as a DIFFERENT thing:
28
+ *
29
+ * result: "no_match" — the corpus WAS searched and holds nothing. `searched`
30
+ * is true and `queries_tried` lists exactly what was
31
+ * asked. This is the only shape an agent may act on by
32
+ * filing a new spec.
33
+ * result: "no_query" — nothing searchable was extracted from the input.
34
+ * isError envelopes — CONSOLE_UNREACHABLE / CONSOLE_NOT_SIGNED_IN /
35
+ * VAULT_SCOPE_UNRESOLVED (a transient 503, retry) /
36
+ * VAULT_SCOPE_UNAUTHORIZED (the caller's vault could
37
+ * not be resolved, so the endpoint returned an empty
38
+ * page that means nothing about the corpus) /
39
+ * SEARCH_FAILED / SEARCH_RESPONSE_UNRECOGNIZED.
40
+ *
41
+ * An angle that fails fails the WHOLE call. A partial answer is indistinguishable
42
+ * from a complete one at the call site, and a silently partial duplicate check is
43
+ * exactly how the duplicate gets filed.
44
+ */
45
+
46
+ import { consoleFetch as defaultConsoleFetch, controlOk } from './console-transport.mjs';
47
+ import { buildSymptomQueries } from './symptom-query.mjs';
48
+
49
+ /** Rows requested per angle. Bounded: this tool never walks the vault. */
50
+ const ANGLE_LIMIT = 20;
51
+
52
+ /** Default / maximum rows returned to the agent. */
53
+ const DEFAULT_TOP_K = 10;
54
+ const MAX_TOP_K = 30;
55
+
56
+ /** Snippets are for judging a duplicate, not for reading the spec. */
57
+ const MAX_SNIPPET_CHARS = 320;
58
+
59
+ function errorResult(payload) {
60
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
61
+ }
62
+
63
+ /**
64
+ * Classify a non-OK `/api/search` response. Each code sends the agent somewhere
65
+ * different, which is the entire point of not collapsing them into "no results".
66
+ */
67
+ function searchFailure(res, query) {
68
+ const status = res?.status ?? 0;
69
+ const code =
70
+ status === 0
71
+ ? 'CONSOLE_UNREACHABLE'
72
+ : status === 401
73
+ ? 'CONSOLE_NOT_SIGNED_IN'
74
+ : status === 503
75
+ ? 'VAULT_SCOPE_UNRESOLVED'
76
+ : 'SEARCH_FAILED';
77
+ const message =
78
+ code === 'CONSOLE_UNREACHABLE'
79
+ ? 'The console did not answer, so the vault was NOT searched. Check heyspex_dev_server_status (or console reachability in cloud mode) — do NOT read this as "no matching spec".'
80
+ : code === 'CONSOLE_NOT_SIGNED_IN'
81
+ ? 'The console refused this caller (401), so the vault was NOT searched. Re-pair the MCP bearer — do NOT read this as "no matching spec".'
82
+ : code === 'VAULT_SCOPE_UNRESOLVED'
83
+ ? 'This console instance has not resolved its active vault yet (transient). Retry shortly — the vault was NOT searched.'
84
+ : `GET /api/search returned ${status}, so the vault was NOT searched.`;
85
+ return errorResult({
86
+ error: code,
87
+ message,
88
+ searched: false,
89
+ status,
90
+ url: res?.url ?? null,
91
+ attempted_query: query ?? null,
92
+ console_response: res?.body ?? null,
93
+ });
94
+ }
95
+
96
+ /** `<mark>`s are kept — they show WHERE the match landed — but the row is capped. */
97
+ function normalizeSnippet(value) {
98
+ if (typeof value !== 'string') return '';
99
+ const flat = value.replace(/\s+/g, ' ').trim();
100
+ return flat.length > MAX_SNIPPET_CHARS ? `${flat.slice(0, MAX_SNIPPET_CHARS)}…` : flat;
101
+ }
102
+
103
+ function normalizeRow(row) {
104
+ const id = typeof row?.id === 'string' ? row.id : null;
105
+ if (!id) return null;
106
+ const rank = Number(row?.rank);
107
+ return {
108
+ id,
109
+ name: String(row?.name ?? id),
110
+ type: String(row?.type ?? 'unknown'),
111
+ status: String(row?.status ?? 'unknown').toLowerCase(),
112
+ rank: Number.isFinite(rank) && rank > 0 ? rank : 0,
113
+ snippet: normalizeSnippet(row?.snippet),
114
+ };
115
+ }
116
+
117
+ /**
118
+ * One bounded page for one angle. Returns `{ rows, mode }` on success, or a
119
+ * ready-to-return error envelope under `{ failure }` — never an empty page
120
+ * standing in for a failure.
121
+ */
122
+ async function fetchAngle(fetchImpl, { q, limit, type, status }) {
123
+ const params = new URLSearchParams();
124
+ params.set('q', q);
125
+ params.set('limit', String(limit));
126
+ params.set('scope', 'body');
127
+ if (type) params.set('type', type);
128
+ if (status) params.set('status', status);
129
+
130
+ let res;
131
+ try {
132
+ res = await fetchImpl(`/api/search?${params.toString()}`);
133
+ } catch (err) {
134
+ return {
135
+ failure: errorResult({
136
+ error: 'CONSOLE_UNREACHABLE',
137
+ message: `The console search endpoint threw (${err?.message ?? err}), so the vault was NOT searched. Do NOT read this as "no matching spec".`,
138
+ searched: false,
139
+ attempted_query: q,
140
+ }),
141
+ };
142
+ }
143
+ if (!res || !res.ok) return { failure: searchFailure(res, q) };
144
+
145
+ const body = res.body && typeof res.body === 'object' ? res.body : {};
146
+ // The route answers a caller whose vault scope it could not authorize with a
147
+ // 200 and an EMPTY page carrying `vaultScope.authorized: false`. That page is
148
+ // a permission verdict, not a statement about the corpus — the one shape that
149
+ // would otherwise read as a clean "no duplicate exists".
150
+ if (body.vaultScope && body.vaultScope.authorized === false) {
151
+ return {
152
+ failure: errorResult({
153
+ error: 'VAULT_SCOPE_UNAUTHORIZED',
154
+ message: `The console could not resolve a vault for this caller (${body.vaultScope.reason ?? 'unknown'}), so the empty page it returned says nothing about the vault. Do NOT read this as "no matching spec".`,
155
+ searched: false,
156
+ reason: body.vaultScope.reason ?? null,
157
+ attempted_query: q,
158
+ }),
159
+ };
160
+ }
161
+ const rows = Array.isArray(body.rows) ? body.rows : Array.isArray(body.results) ? body.results : null;
162
+ // `mode` ("fts" | "bm25" | "trgm" | "memory") is the endpoint's proof that it
163
+ // actually ran a query. Its absence alongside no rows is an unrecognized
164
+ // shape, and an unrecognized shape is never a negative result.
165
+ if (rows === null || typeof body.mode !== 'string') {
166
+ return {
167
+ failure: errorResult({
168
+ error: 'SEARCH_RESPONSE_UNRECOGNIZED',
169
+ message: 'GET /api/search answered 200 with a body this tool cannot read as a search result (no `rows`/`mode`), so the vault was NOT searched.',
170
+ searched: false,
171
+ attempted_query: q,
172
+ console_response: body,
173
+ }),
174
+ };
175
+ }
176
+ return { rows: rows.map(normalizeRow).filter(Boolean), mode: body.mode };
177
+ }
178
+
179
+ /**
180
+ * `heyspex_search_specs` — find specs in the CLOUD vault by symptom text.
181
+ *
182
+ * args: { query, top_k?, type?, status? }
183
+ * deps: { fetchImpl } (tests inject; production uses the shared consoleFetch)
184
+ */
185
+ export async function handleSearchSpecs(args = {}, { fetchImpl = defaultConsoleFetch } = {}) {
186
+ const query = typeof args.query === 'string' ? args.query.trim() : '';
187
+ if (!query) {
188
+ return errorResult({
189
+ error: 'SEARCH_QUERY_REQUIRED',
190
+ message: '`query` is required — pass the symptom sentence (or the concept) you would otherwise grep for.',
191
+ searched: false,
192
+ });
193
+ }
194
+ const topKRaw = Number(args.top_k);
195
+ const topK = Number.isFinite(topKRaw) && topKRaw > 0 ? Math.min(MAX_TOP_K, Math.trunc(topKRaw)) : DEFAULT_TOP_K;
196
+ const type = typeof args.type === 'string' && args.type.trim() ? args.type.trim() : undefined;
197
+ const status = typeof args.status === 'string' && args.status.trim() ? args.status.trim() : undefined;
198
+
199
+ const { terms, queries } = buildSymptomQueries(query);
200
+ if (queries.length === 0) {
201
+ return controlOk({
202
+ query,
203
+ terms,
204
+ queries_tried: [],
205
+ searched: false,
206
+ result: 'no_query',
207
+ match_count: 0,
208
+ matches: [],
209
+ message: 'No searchable term survived normalization (the input was punctuation or stopwords only) — the vault was NOT searched. Rephrase with the concrete nouns from the symptom.',
210
+ });
211
+ }
212
+
213
+ /** id → merged candidate. */
214
+ const byId = new Map();
215
+ const queriesTried = [];
216
+ let mode = null;
217
+
218
+ for (const angle of queries) {
219
+ // eslint-disable-next-line no-await-in-loop -- a handful of bounded angles, deliberately sequential so a failure stops the walk.
220
+ const page = await fetchAngle(fetchImpl, { q: angle.q, limit: ANGLE_LIMIT, type, status });
221
+ if (page.failure) return page.failure;
222
+ mode = mode ?? page.mode;
223
+ queriesTried.push({ label: angle.label, q: angle.q, hits: page.rows.length, mode: page.mode });
224
+ page.rows.forEach((row, position) => {
225
+ const existing = byId.get(row.id);
226
+ if (!existing) {
227
+ byId.set(row.id, { ...row, matched_by: [angle.label], best_position: position });
228
+ return;
229
+ }
230
+ // Keep the strongest evidence each angle produced for this spec.
231
+ existing.rank = Math.max(existing.rank, row.rank);
232
+ existing.best_position = Math.min(existing.best_position, position);
233
+ if (!existing.snippet && row.snippet) existing.snippet = row.snippet;
234
+ if (!existing.matched_by.includes(angle.label)) existing.matched_by.push(angle.label);
235
+ });
236
+ }
237
+
238
+ // Agreement across angles first (a spec the quoted concept AND the loose term
239
+ // window both surfaced is the likeliest duplicate), then the endpoint's own
240
+ // `ts_rank_cd`, then the order it handed rows back — which is the only signal
241
+ // left on the disk read tier, where every rank is 0.
242
+ const matches = [...byId.values()]
243
+ .sort(
244
+ (a, b) =>
245
+ b.matched_by.length - a.matched_by.length ||
246
+ b.rank - a.rank ||
247
+ a.best_position - b.best_position ||
248
+ a.id.localeCompare(b.id),
249
+ )
250
+ .slice(0, topK)
251
+ .map(({ best_position: _position, ...row }) => row);
252
+
253
+ if (matches.length === 0) {
254
+ return controlOk({
255
+ query,
256
+ terms,
257
+ queries_tried: queriesTried,
258
+ searched: true,
259
+ corpus: 'cloud-vault (GET /api/search — the system of record, not local disk)',
260
+ result: 'no_match',
261
+ match_count: 0,
262
+ matches: [],
263
+ mode,
264
+ message: `Searched the vault on ${queriesTried.length} angle(s) and found NOTHING. This is an explicit negative: the corpus was read and holds no spec matching these terms — it is safe to file.`,
265
+ });
266
+ }
267
+
268
+ return controlOk({
269
+ query,
270
+ terms,
271
+ queries_tried: queriesTried,
272
+ searched: true,
273
+ corpus: 'cloud-vault (GET /api/search — the system of record, not local disk)',
274
+ result: 'match',
275
+ match_count: matches.length,
276
+ mode,
277
+ matches,
278
+ message: 'Read each match before filing: a spec that already describes this symptom must be commented on or reopened, not duplicated.',
279
+ });
280
+ }
@@ -0,0 +1,113 @@
1
+ // vendor/heyspex-mcp/seat-block-ledger.mjs
2
+ //
3
+ // Per-session "has the DEV-FACTORY SEAT banner already gone out this session"
4
+ // decision for the two vendor surfaces that used to each keep their OWN flag:
5
+ // - tool-dispatch.mjs handleInitialize() → surface 'mcp_instructions'
6
+ // - cli.mjs maybePrependSeatBanner() → surface 'preflight' (when the
7
+ // first tool called is heyspex_preflight) or 'tool_result' otherwise
8
+ //
9
+ // Before this module, `initialize` always appended the banner to `instructions`
10
+ // AND, independently, the first tools/call response ALSO got the banner
11
+ // prepended (a separate `seatBannerSeenSessions` Set tracked only THAT surface) —
12
+ // so a caller could see "DEV-FACTORY SEAT: ..." twice in one session. That is
13
+ // exactly the bug spexharness/src/bootstrap-injection.mjs's engine contract
14
+ // exists to close for every surface (its own header calls out wiring
15
+ // vendor/heyspex-mcp as the deferred follow-up — HS-8U8X).
16
+ //
17
+ // spexharness/ is dev-only tooling: it is deliberately NOT part of the published
18
+ // `heyspex` npm package (see dev-factory-bridge.mjs for the identical open-core
19
+ // concern with dev-factory/spexcrawl/). scripts/verify-mcp-package.mjs's
20
+ // `closure` check fails the build if the server's STATIC import graph reaches a
21
+ // file the package doesn't ship, so the engine contract is reached the same way
22
+ // spexcrawl is: a memoized `await import()`, never a static import. A
23
+ // published install (no spexharness/ tree on disk) gets the fallback ledger
24
+ // below instead — same "at most one surface, across the whole session" contract,
25
+ // just without a shared spexharness bootstrap ledger to defer to.
26
+
27
+ /** @type {Promise<typeof import('../../spexharness/src/bootstrap-injection.mjs')>|undefined} */
28
+ let enginePromise;
29
+
30
+ function loadEngine() {
31
+ if (!enginePromise) {
32
+ enginePromise = import('../../spexharness/src/bootstrap-injection.mjs').catch(() => null);
33
+ }
34
+ return enginePromise;
35
+ }
36
+
37
+ /**
38
+ * Local, dependency-free ledger with the exact `shouldIncludeSeatBlock` /
39
+ * `markSeatBlockSent` shape bootstrap-injection.mjs's `withSeatBlockLedger`
40
+ * exposes — used when the engine module is not reachable (a published install,
41
+ * or any import failure). "No bootstrap yet" degrades to "emit once, guarded",
42
+ * never to "emit unguarded".
43
+ */
44
+ function createFallbackLedger() {
45
+ const sent = new Set();
46
+ return {
47
+ shouldIncludeSeatBlock(surface) {
48
+ if (sent.has(surface)) return false;
49
+ return sent.size === 0;
50
+ },
51
+ markSeatBlockSent(surface) {
52
+ sent.add(surface);
53
+ },
54
+ };
55
+ }
56
+
57
+ function fallbackSeatBlockForSurface(ledger, surface, seatLine) {
58
+ if (!ledger.shouldIncludeSeatBlock(surface)) return null;
59
+ ledger.markSeatBlockSent(surface);
60
+ return seatLine ?? null;
61
+ }
62
+
63
+ /** One ledger controller per session id — two sessions sharing this process (e.g. two
64
+ * fleet workers spawned back-to-back) never see each other's sends. */
65
+ const controllersBySession = new Map();
66
+
67
+ async function getSeatBlockController(sessionId) {
68
+ const key = sessionId || 'local';
69
+ const existing = controllersBySession.get(key);
70
+ if (existing) return existing;
71
+
72
+ const engine = await loadEngine();
73
+ const ledger = engine?.withSeatBlockLedger ? engine.withSeatBlockLedger({}) : createFallbackLedger();
74
+ const forSurface = engine?.seatBlockForSurface
75
+ ? (surface, seatLine) => engine.seatBlockForSurface(ledger, surface, seatLine)
76
+ : (surface, seatLine) => fallbackSeatBlockForSurface(ledger, surface, seatLine);
77
+
78
+ const controller = { forSurface };
79
+ controllersBySession.set(key, controller);
80
+ return controller;
81
+ }
82
+
83
+ /**
84
+ * Decide whether `surface` should emit the seat banner for `sessionId` this
85
+ * session — sourced from the engine contract's per-session ledger when
86
+ * spexharness/ is reachable (memoized per session id), else a local fallback
87
+ * with identical "exactly once, across every surface" semantics. Returns the
88
+ * banner text when `surface` is cleared to send it, else `null`. Never throws;
89
+ * a falsy `seatLine` (not enabled / unknown caller) short-circuits to `null`
90
+ * WITHOUT touching the ledger, so an ineligible caller never "uses up" the
91
+ * session's one send.
92
+ *
93
+ * @param {string|null|undefined} sessionId
94
+ * @param {string} surface one of bootstrap-injection.mjs's SEAT_BLOCK_SURFACES
95
+ * ('mcp_instructions', 'preflight', 'tool_result', 'bootstrap').
96
+ * @param {string|null|undefined} seatLine
97
+ * @returns {Promise<string|null>}
98
+ */
99
+ export async function seatBlockForSessionSurface(sessionId, surface, seatLine) {
100
+ if (!seatLine) return null;
101
+ try {
102
+ const controller = await getSeatBlockController(sessionId);
103
+ return controller.forSurface(surface, seatLine);
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ /** Test-only: forget every per-session ledger and the memoized engine probe. */
110
+ export function resetSeatBlockLedgerForTests() {
111
+ controllersBySession.clear();
112
+ enginePromise = undefined;
113
+ }
@@ -19,7 +19,7 @@
19
19
 
20
20
  import { readFileSync } from "node:fs";
21
21
  import path from "node:path";
22
- import { fileURLToPath } from "node:url";
22
+ import { PACKAGE_ROOT } from "./package-root.mjs";
23
23
 
24
24
  /**
25
25
  * Read and parse <vaultRoot>/dev-factory/seats.json.
@@ -169,11 +169,7 @@ function selfTest() {
169
169
  };
170
170
 
171
171
  // Repo root is two dirs up from this file (vendor/heyspex-mcp/ → repo root).
172
- const repoRoot = path.resolve(
173
- path.dirname(fileURLToPath(import.meta.url)),
174
- "..",
175
- "..",
176
- );
172
+ const repoRoot = PACKAGE_ROOT;
177
173
 
178
174
  // 1. Real registry smoke-read.
179
175
  const real = readSeatsRegistry(repoRoot);
@@ -0,0 +1,109 @@
1
+ // session-owner.mjs — the ONE place the MCP server learns which account owns
2
+ // the documents it files ([[HS-WE87-WW9R-TASK-thread-the-gated-caller-through-every]],
3
+ // epic [[HS-P1AW-WW9R-EPIC-identity-threading-and-the-end-of-the]] — invariant 1).
4
+ //
5
+ // Until this module the three frontmatter writers (`capture-focus.mjs`,
6
+ // `research-focus.mjs`, `defer-spec.mjs`) each stamped the HeySpex owner's
7
+ // handle as a literal — on a customer machine that put the platform owner's
8
+ // name on the customer's own documents. They now ask here, and when nothing
9
+ // resolves the `owner:` key is OMITTED rather than guessed.
10
+ //
11
+ // Resolution order (no network — this runs inside synchronous writers and
12
+ // under `node --test`):
13
+ // 1. `HS_ACCOUNT_ID` env — the same override the CLI's account reader honours.
14
+ // 2. the credentials store's current account (`credentials.mjs`
15
+ // `readCurrentAccountId`, [[HS-UA9S-WW9R-TASK-store-credentials-per-account-login]]):
16
+ // `current_account_id` of `~/.heyspex/credentials.yaml`, set by `hs login` /
17
+ // `hs mcp pair` from the account GET /api/mcp/session confirmed for the
18
+ // stored bearer and moved by `hs login --switch-account` / `hs logout`.
19
+ // An existing single-account (v1) file has no such key: its account is
20
+ // the `~/.heyspex/config.yaml` `account_id:` the earlier `hs login` wrote
21
+ // ([[HS-630L-WW9R-TASK-one-strict-cli-account-reader-hs-login]]) — read
22
+ // for that case only, never written any more (one account-id store).
23
+ // 3. null — the writer omits `owner:`.
24
+ //
25
+ // This is ALSO the CLI's account reader: `resolveConfiguredAccountId` in
26
+ // spex-console/src/lib/cli/vault-workspace-shared.ts and `readConfiguredAccountId`
27
+ // in spex-console/src/lib/cli/install-identity.ts both delegate to
28
+ // `resolveSessionOwnerUncached`, so one precedence serves `hs vault`, `hs connect`,
29
+ // `hs install` and every MCP frontmatter writer. Its bearer twin is
30
+ // `./credentials.mjs` (HS-HL0D).
31
+ //
32
+ // Cached per process once resolved from the real environment (the MCP server
33
+ // is spawned once per session; pairing changes restart it). Callers inject
34
+ // `env` / `homeDir` / `readFile` for tests, which bypasses the cache.
35
+
36
+ import { heyspexHome } from '../../scripts/lib/heyspex-home.mjs';
37
+ import { readFileSync } from 'node:fs';
38
+ import { homedir } from 'node:os';
39
+ import { join } from 'node:path';
40
+
41
+ import { readCurrentAccountId } from './credentials.mjs';
42
+
43
+ const ACCOUNT_ID_LINE_RE = /^account_id:\s*["']?([^"'\r\n#]+)/m;
44
+
45
+ /**
46
+ * Read the paired account id from `~/.heyspex/config.yaml`, or null.
47
+ * @param {{ homeDir?: string, readFile?: (p: string) => string }} [opts]
48
+ */
49
+ export function readConfigAccountId({ homeDir = heyspexHome(), readFile } = {}) {
50
+ const read = readFile ?? ((p) => readFileSync(p, 'utf8'));
51
+ try {
52
+ const raw = read(join(homeDir, '.heyspex', 'config.yaml'));
53
+ const match = String(raw ?? '').match(ACCOUNT_ID_LINE_RE);
54
+ const value = match?.[1]?.trim() ?? '';
55
+ return value || null;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Resolve the session owner without caching — the pure core.
63
+ * @param {{ env?: NodeJS.ProcessEnv | Record<string, string | undefined>, homeDir?: string, readFile?: (p: string) => string }} [opts]
64
+ * @returns {string | null}
65
+ */
66
+ export function resolveSessionOwnerUncached({ env = process.env, homeDir = heyspexHome(), readFile } = {}) {
67
+ const fromEnv = typeof env?.HS_ACCOUNT_ID === 'string' ? env.HS_ACCOUNT_ID.trim() : '';
68
+ if (fromEnv) return fromEnv;
69
+ return readCurrentAccountId({ homeDir, readFile });
70
+ }
71
+
72
+ let cached = null;
73
+ let cacheFilled = false;
74
+
75
+ /**
76
+ * The account every MCP-authored document is stamped with, or null when the
77
+ * machine has no resolvable account (the writers then OMIT `owner:`).
78
+ *
79
+ * The real-environment answer is memoised per process; any injected option
80
+ * bypasses the cache so tests stay hermetic.
81
+ * @param {{ env?: NodeJS.ProcessEnv | Record<string, string | undefined>, homeDir?: string, readFile?: (p: string) => string }} [opts]
82
+ * @returns {string | null}
83
+ */
84
+ export function resolveSessionOwner(opts) {
85
+ if (opts && Object.keys(opts).length > 0) return resolveSessionOwnerUncached(opts);
86
+ if (!cacheFilled) {
87
+ cached = resolveSessionOwnerUncached();
88
+ cacheFilled = true;
89
+ }
90
+ return cached;
91
+ }
92
+
93
+ /** Test seam: forget the per-process answer. */
94
+ export function __resetSessionOwnerCacheForTests() {
95
+ cached = null;
96
+ cacheFilled = false;
97
+ }
98
+
99
+ /**
100
+ * The `owner:` frontmatter entry for an object-shaped frontmatter — `{ owner }`
101
+ * when an account resolved, `{}` when none did (the key is omitted, never
102
+ * guessed). `explicit` (an `args.owner` the caller supplied) wins.
103
+ * @param {{ explicit?: unknown, resolve?: () => string | null }} [opts]
104
+ */
105
+ export function ownerFrontmatterEntry({ explicit, resolve = resolveSessionOwner } = {}) {
106
+ const chosen =
107
+ typeof explicit === 'string' && explicit.trim() ? explicit.trim() : (resolve() ?? null);
108
+ return chosen ? { owner: chosen } : {};
109
+ }
@@ -0,0 +1,162 @@
1
+ // Session hand-off and agent identity tools: heyspex_handoff_record, handoff_load,
2
+ // import_agent. Extracted from cli.mjs under
3
+ // [[HS-BLNF-6W61-DIAG-cli-mjs-has-grown-into-a-13-665-line]] (no behavior change).
4
+
5
+ import { consoleFetch } from './console-transport.mjs';
6
+ import { consoleErrorCode, consoleErrorMessage } from './console-fetch.mjs';
7
+ import { detectAgentIdentity } from './mcp-config.mjs';
8
+ import { resolveReadTier } from './read-path.mjs';
9
+ import { deferControlWrite } from './write-outbox.mjs';
10
+
11
+ /**
12
+ * Import a community agent role via console `POST /api/agents/import` — the MCP
13
+ * face of agent import ([[HS-TASK-Z666]]). Same core as `hs agent import`; no
14
+ * field-mapping logic duplicated in JS.
15
+ */
16
+ export async function handleImportAgent(args = {}) {
17
+ if (!args.collection || typeof args.collection !== 'string') {
18
+ return {
19
+ isError: true,
20
+ content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: 'collection is required' }) }],
21
+ };
22
+ }
23
+ if (!args.role || typeof args.role !== 'string') {
24
+ return {
25
+ isError: true,
26
+ content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: 'role is required' }) }],
27
+ };
28
+ }
29
+ const body = {
30
+ collection: args.collection,
31
+ role: args.role,
32
+ ...(args.dry_run === true || args.dry_run === 'true' ? { dry_run: true } : {}),
33
+ };
34
+ const result = await consoleFetch('/api/agents/import', { method: 'POST', body });
35
+ if (!result.ok) {
36
+ const payload = {
37
+ error: consoleErrorCode(result, 'IMPORT_AGENT_FAILED'),
38
+ message: consoleErrorMessage(result, `POST /api/agents/import returned ${result.status}`),
39
+ status: result.status,
40
+ url: result.url,
41
+ console_response: result.body,
42
+ attempted: body,
43
+ };
44
+ return {
45
+ isError: true,
46
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
47
+ };
48
+ }
49
+ const responseBody = result.body ?? {};
50
+ const payload = {
51
+ spec_id: responseBody.spec_id ?? null,
52
+ path: responseBody.path ?? null,
53
+ warnings: Array.isArray(responseBody.warnings) ? responseBody.warnings : [],
54
+ ...(responseBody.dry_run ? { dry_run: true } : {}),
55
+ console_response: responseBody,
56
+ };
57
+ return {
58
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Upsert the current resume context for this agent/lane into the Neon control
64
+ * plane — [[HS-EPIC-NCPS]] handoff_context / [[HS-EPIC-CLMW]] `hs handoff record`.
65
+ *
66
+ * Self-purging overwrite-in-place (PUT /api/control/handoff-context). The lane
67
+ * defaults to this agent's session id so each session is its own lane. Coordi-
68
+ * nation only — the endpoint rejects durable-knowledge fields.
69
+ */
70
+ export async function handleHandoffRecord(args = {}) {
71
+ const identity = detectAgentIdentity(args.agent, args.session_id);
72
+ const laneKey =
73
+ typeof args.lane === 'string' && args.lane.trim()
74
+ ? args.lane.trim()
75
+ : identity.sessionId;
76
+ const body = {
77
+ lane_key: laneKey,
78
+ agent: identity.agent,
79
+ ...(typeof args.spec_id === 'string' ? { spec_id: args.spec_id } : {}),
80
+ ...(typeof args.phase === 'string' ? { phase: args.phase } : {}),
81
+ ...(typeof args.estimated_time === 'string' ? { estimated_time: args.estimated_time } : {}),
82
+ ...(Array.isArray(args.blockers) ? { blockers: args.blockers.map(String) } : {}),
83
+ ...(Array.isArray(args.links) ? { links: args.links.map(String) } : {}),
84
+ };
85
+ const result = await consoleFetch('/api/control/handoff-context', {
86
+ method: 'PUT',
87
+ body,
88
+ });
89
+ if (!result.ok) {
90
+ const deferred = await deferControlWrite(result, {
91
+ route: '/api/control/handoff-context',
92
+ method: 'PUT',
93
+ request_body: body,
94
+ acting_agent: identity.agent,
95
+ session_id: identity.sessionId,
96
+ }, { isCloud: async () => (await resolveReadTier()) === 'neon' });
97
+ if (deferred) return deferred;
98
+ const payload = {
99
+ error: consoleErrorCode(result, 'HANDOFF_RECORD_FAILED'),
100
+ message: consoleErrorMessage(result, `Handoff-context endpoint returned ${result.status}`),
101
+ status: result.status,
102
+ url: result.url,
103
+ console_response: result.body,
104
+ attempted: { lane: laneKey },
105
+ };
106
+ return {
107
+ isError: true,
108
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
109
+ };
110
+ }
111
+ return {
112
+ content: [{
113
+ type: 'text',
114
+ text: JSON.stringify({ lane: laneKey, console_response: result.body }, null, 2),
115
+ }],
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Read the current resume context for a lane from the Neon control plane —
121
+ * [[HS-EPIC-NCPS]] handoff_context / [[HS-EPIC-CLMW]] `hs handoff load`.
122
+ *
123
+ * Defaults to this agent's own lane (its session); `all: true` lists every
124
+ * active lane (parallel-agent off-on-deck). Self-purging — no per-session
125
+ * history; expired rows read as absent.
126
+ */
127
+ export async function handleHandoffLoad(args = {}) {
128
+ const identity = detectAgentIdentity(args.agent, args.session_id);
129
+ const wantAll = args.all === true;
130
+ const laneKey =
131
+ typeof args.lane === 'string' && args.lane.trim()
132
+ ? args.lane.trim()
133
+ : identity.sessionId;
134
+ const path = wantAll
135
+ ? '/api/control/handoff-context'
136
+ : `/api/control/handoff-context?lane=${encodeURIComponent(laneKey)}`;
137
+ const result = await consoleFetch(path);
138
+ if (!result.ok) {
139
+ const payload = {
140
+ error: consoleErrorCode(result, 'HANDOFF_LOAD_FAILED'),
141
+ message: consoleErrorMessage(result, `Handoff-context endpoint returned ${result.status}`),
142
+ status: result.status,
143
+ url: result.url,
144
+ console_response: result.body,
145
+ attempted: wantAll ? { all: true } : { lane: laneKey },
146
+ };
147
+ return {
148
+ isError: true,
149
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
150
+ };
151
+ }
152
+ return {
153
+ content: [{
154
+ type: 'text',
155
+ text: JSON.stringify(
156
+ wantAll ? result.body : { lane: laneKey, ...result.body },
157
+ null,
158
+ 2,
159
+ ),
160
+ }],
161
+ };
162
+ }