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
@@ -46,16 +46,19 @@
46
46
  * import) so tests can stub scripts/lib/dev-mode.mjs and prove the verdict
47
47
  * comes from `resolveDevMode` itself, not from string-matching this file.
48
48
  */
49
- import { dirname, join } from 'node:path';
50
- import { fileURLToPath, pathToFileURL } from 'node:url';
51
- import { getActiveConfig } from './vault-resolve.mjs';
52
-
53
- const __dirname = dirname(fileURLToPath(import.meta.url));
49
+ import { AsyncLocalStorage } from 'node:async_hooks';
50
+ import { execFileSync } from 'node:child_process';
51
+ import { existsSync } from 'node:fs';
52
+ import { join } from 'node:path';
53
+ import { importDevModeCore } from './dev-mode-core.mjs';
54
54
  // THIS PACKAGE's own root (vendor/heyspex-mcp/read-path.mjs -> two levels
55
55
  // up) — repo root in the monorepo, `node_modules/heyspex` in a packed
56
56
  // install. Used to locate this package's OWN sibling code (dev-mode.mjs);
57
- // see the module doc above for why this is never the vault root.
58
- const PACKAGE_ROOT = join(__dirname, '..', '..');
57
+ // see the module doc above for why this is never the vault root. Derived in
58
+ // ONE place (package-root.mjs) so a bundler bakes one build-machine path
59
+ // into the console's remote-MCP chunk instead of one per module.
60
+ import { PACKAGE_ROOT } from './package-root.mjs';
61
+ import { getActiveConfig } from './vault-resolve.mjs';
59
62
 
60
63
  /**
61
64
  * Resolve the VAULT root via the same shared 5-tier resolver every other MCP
@@ -78,8 +81,11 @@ export function resolveVaultRoot() {
78
81
 
79
82
  /** Real loader: dynamic-imports the shared dev-mode core from THIS PACKAGE's own root. */
80
83
  export async function loadRealDevMode() {
81
- const coreUrl = pathToFileURL(join(PACKAGE_ROOT, 'scripts', 'lib', 'dev-mode.mjs')).href;
82
- return import(coreUrl);
84
+ // Path candidate first (stdio / packed installs), then the static
85
+ // specifier a bundler can trace — see dev-mode-core.mjs. In a bundled
86
+ // deployment `import.meta.url` no longer points at a real checkout, so the
87
+ // path-built URL misses and the fallback is what actually loads.
88
+ return importDevModeCore([join(PACKAGE_ROOT, 'scripts', 'lib', 'dev-mode.mjs')]);
83
89
  }
84
90
 
85
91
  /**
@@ -94,6 +100,27 @@ export class ConsoleUnreachableCloudModeError extends Error {
94
100
  }
95
101
  }
96
102
 
103
+ /**
104
+ * `{ degraded:true, source:'cloud', warning }` when the resolved vault is a
105
+ * satellite binding whose vault clone is absent on this machine (marker with
106
+ * vault_path, no clone), else null. Same record shape as the outage fallback.
107
+ */
108
+ export function vaultNotClonedInfo() {
109
+ try {
110
+ const cfg = getActiveConfig();
111
+ if (cfg && cfg.vaultCloned === false) {
112
+ return {
113
+ degraded: true,
114
+ source: 'cloud',
115
+ warning: `vault ${cfg.vaultId} is bound by this repo's heyspex.yaml but is not cloned at ${cfg.vaultPath ?? 'its vault_path'} on this machine; answered from the cloud (cloud-only mode). Clone the vault repo there for local reads.`,
116
+ };
117
+ }
118
+ } catch {
119
+ /* resolver failure: not a clone problem */
120
+ }
121
+ return null;
122
+ }
123
+
97
124
  /**
98
125
  * resolveReadTier({ env, homeDir, loadDevMode? }) -> "disk" | "neon"
99
126
  *
@@ -112,23 +139,172 @@ export async function resolveReadTier({ env, homeDir, loadDevMode = loadRealDevM
112
139
  }
113
140
 
114
141
  /**
115
- * readViaSelector({ tier, diskRead, neonRead, routeLabel, consoleUrl })
142
+ * resolveVaultWriteMode({ env, homeDir, loadDevMode? }) -> "local" | "cloud"
143
+ *
144
+ * The write-side twin of {@link resolveReadTier}: whatever
145
+ * `resolveDevMode(...).vaultWriteMode` says — `local` only for dev mode with
146
+ * local reliance, `cloud` for prod and for dev with cloud reliance. A
147
+ * create-flow that writes the vault directly (save_research) consults THIS
148
+ * before touching disk, so a cloud-reliant session never leaves a file the
149
+ * console's index has never heard of (the split-brain a disk write in cloud
150
+ * mode produces). Same injectable loader as the read tier.
151
+ */
152
+ export async function resolveVaultWriteMode({ env, homeDir, loadDevMode = loadRealDevMode } = {}) {
153
+ const mod = await loadDevMode();
154
+ const resolved = mod.resolveDevMode({ env, homeDir });
155
+ return resolved.vaultWriteMode === 'local' ? 'local' : 'cloud';
156
+ }
157
+
158
+ /**
159
+ * A console read that failed, carrying the transport verdict so
160
+ * {@link readViaSelector} can tell an OUTAGE (status 0, 5xx, timeout/network)
161
+ * from an ANSWER (401/403/404/409). Thrown by the `neonRead` closures; build
162
+ * one from a consoleFetch result with {@link consoleReadErrorFrom}.
163
+ */
164
+ export class ConsoleReadError extends Error {
165
+ constructor(message, { status, errorKind, route } = {}) {
166
+ super(message);
167
+ this.name = 'ConsoleReadError';
168
+ this.status = status;
169
+ this.errorKind = errorKind;
170
+ this.route = route;
171
+ }
172
+ }
173
+
174
+ /** Build a {@link ConsoleReadError} from a consoleFetch result `{status, errorKind}`. */
175
+ export function consoleReadErrorFrom(res, message, route) {
176
+ const e = new ConsoleReadError(message, { status: res?.status, errorKind: res?.errorKind, route });
177
+ if (res?.authProblem || res?.refusal) e.authProblem = true;
178
+ return e;
179
+ }
180
+
181
+ /**
182
+ * True when `err` says the console was unreachable or broken (status 0, any
183
+ * 5xx, errorKind timeout/abort/network). False for answered 4xx verdicts
184
+ * (401/403/404/409) and for anything unclassified.
185
+ */
186
+ export function isOutageClass(err) {
187
+ if (!err || typeof err !== 'object') return false;
188
+ if (err.authProblem) return false; // sign-in / scope refusals are not outages
189
+ const { status, errorKind } = err;
190
+ if (errorKind === 'timeout' || errorKind === 'abort' || errorKind === 'network') return true;
191
+ if (typeof status === 'number' && (status === 0 || status >= 500)) return true;
192
+ return false;
193
+ }
194
+
195
+ // Per-call collector so callers whose reader returns a non-object (Set, array,
196
+ // null) can still learn that the answer came from the local clone.
197
+ const degradationScope = new AsyncLocalStorage();
198
+
199
+ /**
200
+ * Run `fn`, collecting a {@link readViaSelector} degradation record for every
201
+ * disk-fallback read made inside it. Resolves `{ value, degraded }` where
202
+ * `degraded` is null or `{ degraded:true, source:'disk', cloud_error, warning }`.
203
+ */
204
+ export async function withDegradationScope(fn) {
205
+ const records = [];
206
+ const value = await degradationScope.run(records, fn);
207
+ return { value, degraded: records[0] ?? null };
208
+ }
209
+
210
+ function lastCommitIso(root) {
211
+ try {
212
+ const out = execFileSync('git', ['-C', root, 'log', '-1', '--format=%cI'], {
213
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000,
214
+ }).trim();
215
+ return out || null;
216
+ } catch {
217
+ return null;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Stamp a degradation record onto a reader's result. Plain objects get the
223
+ * fields spread in; an MCP tool result (`{content:[{type:'text',text:<json>}]}`)
224
+ * gets them merged into the JSON payload inside `text` (and spread on the
225
+ * envelope too); any other shape (Set/array/null/primitive) is returned
226
+ * unchanged — it rides the {@link withDegradationScope} collector instead.
227
+ */
228
+ export function mergeDegradation(result, info) {
229
+ if (!isPlainObject(result) || !info) return result;
230
+ const first = Array.isArray(result.content) ? result.content[0] : null;
231
+ if (first && first.type === 'text' && typeof first.text === 'string') {
232
+ try {
233
+ const parsed = JSON.parse(first.text);
234
+ if (isPlainObject(parsed)) {
235
+ return { ...result, ...info, content: [{ ...first, text: JSON.stringify({ ...parsed, ...info }, null, 2) }, ...result.content.slice(1)] };
236
+ }
237
+ } catch {
238
+ /* not JSON — fall through to the envelope spread */
239
+ }
240
+ }
241
+ return { ...result, ...info };
242
+ }
243
+
244
+ function isPlainObject(v) {
245
+ return v !== null && typeof v === 'object' && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype;
246
+ }
247
+
248
+ /**
249
+ * readViaSelector({ tier, diskRead, neonRead, routeLabel, consoleUrl, vaultRoot?, allowDiskFallback? })
116
250
  *
117
251
  * Runs the read matching `tier`:
118
252
  * - "disk": calls `diskRead()` (`neonRead` is never invoked).
119
- * - "neon": calls `neonRead()`. On failure, wraps the error into a plain
120
- * {@link ConsoleUnreachableCloudModeError} naming `consoleUrl` and
121
- * `routeLabel`; `diskRead` is never called (no silent local fallback
122
- * out of cloud reliance).
253
+ * - "neon": calls `neonRead()`. On failure:
254
+ * * DEGRADE ("degrade, don't die"): when `diskRead` exists, the failure
255
+ * is outage-class ({@link isOutageClass}), a local clone exists
256
+ * (`<vaultRoot>/specs`), and HEYSPEX_NO_DISK_FALLBACK !== '1', answer
257
+ * from `diskRead()`. A plain-object result is returned spread with
258
+ * `degraded: true, source: 'disk', cloud_error, warning`; any other
259
+ * shape (Set/array/null) is returned unchanged and the same record is
260
+ * pushed to the active {@link withDegradationScope} collector.
261
+ * * otherwise wraps the error into {@link ConsoleUnreachableCloudModeError}
262
+ * naming `consoleUrl` and `routeLabel` (4xx, no clone, no diskRead,
263
+ * opt-out env, or `allowDiskFallback: false` for write preconditions).
123
264
  */
124
- export async function readViaSelector({ tier, diskRead, neonRead, routeLabel, consoleUrl }) {
265
+ export async function readViaSelector({ tier, diskRead, neonRead, routeLabel, consoleUrl, vaultRoot, allowDiskFallback = true }) {
266
+ // A satellite whose marker names a vault that is not cloned here is
267
+ // CLOUD-ONLY: a disk read would silently answer empty. Serve the cloud read
268
+ // (when this read has one) and declare it - never a silent empty answer.
269
+ const uncloned = vaultNotClonedInfo();
270
+ if (uncloned && typeof neonRead === 'function') {
271
+ const result = await neonRead();
272
+ const store = degradationScope.getStore();
273
+ if (store) store.push(uncloned);
274
+ return mergeDegradation(result, uncloned);
275
+ }
125
276
  if (tier === 'disk') {
277
+ if (uncloned) {
278
+ throw new ConsoleUnreachableCloudModeError(
279
+ `VAULT_NOT_CLONED_LOCALLY: route "${routeLabel}" has no cloud read and ${uncloned.warning}`,
280
+ );
281
+ }
126
282
  return diskRead();
127
283
  }
128
284
  if (tier === 'neon') {
129
285
  try {
130
286
  return await neonRead();
131
287
  } catch (err) {
288
+ const root = vaultRoot ?? resolveVaultRoot();
289
+ if (
290
+ allowDiskFallback &&
291
+ typeof diskRead === 'function' &&
292
+ isOutageClass(err) &&
293
+ process.env.HEYSPEX_NO_DISK_FALLBACK !== '1' &&
294
+ root && existsSync(join(root, 'specs'))
295
+ ) {
296
+ const result = await diskRead();
297
+ const kind = err.status ? `HTTP ${err.status}` : (err.errorKind ?? 'unreachable');
298
+ const info = {
299
+ degraded: true,
300
+ source: 'disk',
301
+ cloud_error: String(err?.message ?? err),
302
+ warning: `Console ${consoleUrl} failed (${kind}); answered from the local clone at ${root} (last commit ${lastCommitIso(root)}). Writes still require the console.`,
303
+ };
304
+ const store = degradationScope.getStore();
305
+ if (store) store.push(info);
306
+ return mergeDegradation(result, info);
307
+ }
132
308
  throw new ConsoleUnreachableCloudModeError(
133
309
  `CONSOLE_UNREACHABLE_CLOUD_MODE: route "${routeLabel}" failed against console ${consoleUrl} — ${String(err?.message ?? err)}`,
134
310
  );
@@ -0,0 +1,146 @@
1
+ // The read-back receipt envelope for `hs` verbs that live in plain .mjs under
2
+ // vendor/heyspex-mcp/ and so cannot import the TypeScript receipt
3
+ // (spex-console/src/lib/cli/receipt.ts, [[HS-STCY-WW9R-TASK-add-a-shared-receipt-that-reads-back]]).
4
+ // Same envelope, same words: `{ local, cloud, verified, agree, diffs, changed,
5
+ // failed?, retry? }`, a two-column "local (this machine) | cloud (console)"
6
+ // table and one "Result:" line with the same anchors (verified / unverified /
7
+ // disagree / failed). Only the side fields differ per verb, so a verb passes a
8
+ // row table naming which local field and which cloud field each row shows
9
+ // and whether the row must agree. `hs login` is the first user
10
+ // ([[HS-CRNE-WW9R-TASK-give-hs-install-and-hs-login-the]], epic
11
+ // [[HS-L6HW-WW9R-EPIC-one-http-transport-and-measured]] invariant 6).
12
+ //
13
+ // Pure: no IO, never throws.
14
+
15
+ /** `EXIT.ACCOUNT_MISMATCH` in spex-console/src/lib/cli/control-shared.ts — the console confirmed another account. */
16
+ export const EXIT_ACCOUNT_MISMATCH = 20;
17
+ /** `EXIT.RECEIPT_DISAGREE` in control-shared.ts — both sides were read back and disagree. */
18
+ export const EXIT_RECEIPT_DISAGREE = 21;
19
+
20
+ /**
21
+ * @typedef {{ read: boolean, reason?: string, [field: string]: unknown }} ReceiptSide
22
+ * @typedef {{ label: string, local?: string, cloud?: string, compare?: boolean }} ReceiptRow
23
+ * One table row: the local side's field, the cloud side's field, and
24
+ * whether the two must be equal for `agree` (a diff is named by `label`).
25
+ */
26
+
27
+ /** A side that was not read back, with the reason. */
28
+ export function unreadSide(reason) {
29
+ return { read: false, reason };
30
+ }
31
+
32
+ const META = new Set(['read', 'reason']);
33
+
34
+ function valueOf(side, field) {
35
+ if (!side || !field) return null;
36
+ const v = side[field];
37
+ return v === undefined ? null : v;
38
+ }
39
+
40
+ /**
41
+ * Build the receipt from the two read-backs.
42
+ * - `verified`: both sides were read back;
43
+ * - `diffs`: every `compare` row whose two values differ (only when verified);
44
+ * - `agree`: verified with no diffs;
45
+ * - `changed`: per side, every field that differs from the `before` read of
46
+ * that side (`[]` on an unchanged re-run; a side with no before read, or
47
+ * not read after, contributes nothing).
48
+ * @param {{ local: ReceiptSide | null, cloud: ReceiptSide | null, rows: ReceiptRow[],
49
+ * before?: { local?: ReceiptSide | null, cloud?: ReceiptSide | null },
50
+ * failed?: { side: 'local' | 'cloud', reason?: string }, retry?: string }} input
51
+ */
52
+ export function buildSidesReceipt({ local, cloud, rows, before = {}, failed, retry }) {
53
+ const l = local ?? unreadSide('not read back');
54
+ const c = cloud ?? unreadSide('not read back');
55
+ if (failed?.reason) (failed.side === 'cloud' ? c : l).reason = failed.reason;
56
+ const verified = l.read === true && c.read === true;
57
+
58
+ const diffs = [];
59
+ if (verified) {
60
+ for (const row of rows) {
61
+ if (!row.compare) continue;
62
+ const lv = valueOf(l, row.local);
63
+ const cv = valueOf(c, row.cloud);
64
+ if (lv !== cv) diffs.push({ field: row.label, local: lv, cloud: cv });
65
+ }
66
+ }
67
+
68
+ const changed = [];
69
+ for (const [name, after] of [['local', l], ['cloud', c]]) {
70
+ const prior = before[name];
71
+ if (!prior?.read || !after.read) continue;
72
+ const fields = new Set([...Object.keys(prior), ...Object.keys(after)].filter((f) => !META.has(f)));
73
+ for (const field of fields) {
74
+ const from = valueOf(prior, field);
75
+ const to = valueOf(after, field);
76
+ if (from !== to) changed.push({ side: name, field, from, to });
77
+ }
78
+ }
79
+
80
+ return {
81
+ local: l,
82
+ cloud: c,
83
+ verified,
84
+ agree: verified && diffs.length === 0,
85
+ diffs,
86
+ changed,
87
+ ...(failed ? { failed: failed.side } : {}),
88
+ ...(retry ? { retry } : {}),
89
+ };
90
+ }
91
+
92
+ /** The one-word state a script or test can branch on (same as receipt.ts `receiptStatus`). */
93
+ export function sidesReceiptStatus(receipt) {
94
+ if (receipt.failed) return 'failed';
95
+ if (!receipt.verified) return 'unverified';
96
+ if (!receipt.agree) return 'disagree';
97
+ return 'verified';
98
+ }
99
+
100
+ function cell(side, field) {
101
+ if (!field) return '';
102
+ if (!side.read) return '(not read)';
103
+ const v = valueOf(side, field);
104
+ if (v === null) return '(none)';
105
+ if (typeof v === 'boolean') return v ? 'yes' : 'no';
106
+ return String(v);
107
+ }
108
+
109
+ const show = (v) => (v === null || v === undefined ? '(none)' : String(v));
110
+
111
+ /**
112
+ * The text sink: the same two-column table and "Result:" line as receipt.ts
113
+ * `formatReceipt`, rows taken from `rows`.
114
+ * @param {ReturnType<typeof buildSidesReceipt>} receipt
115
+ * @param {ReceiptRow[]} rows
116
+ */
117
+ export function formatSidesReceipt(receipt, rows) {
118
+ const table = [
119
+ ['', 'local (this machine)', 'cloud (console)'],
120
+ ...rows.map((r) => [r.label, cell(receipt.local, r.local), cell(receipt.cloud, r.cloud)]),
121
+ ];
122
+ const w0 = Math.max(...table.map((r) => r[0].length));
123
+ const w1 = Math.max(...table.map((r) => r[1].length));
124
+ const lines = [
125
+ 'Read back after the write:',
126
+ ...table.map(([a, b, c]) => ` ${a.padEnd(w0)} ${b.padEnd(w1)} ${c}`.trimEnd()),
127
+ ];
128
+ const status = sidesReceiptStatus(receipt);
129
+ const retry = receipt.retry ? ` Retry: \`${receipt.retry}\`` : '';
130
+ if (status === 'failed') {
131
+ const reason = receipt[receipt.failed].reason;
132
+ lines.push(`Result: failed -- the ${receipt.failed} half did not land${reason ? ` (${reason})` : ''}.${retry}`);
133
+ } else if (status === 'unverified') {
134
+ const unread = ['local', 'cloud'].filter((s) => !receipt[s].read);
135
+ const why = unread.map((s) => `${s}${receipt[s].reason ? `: ${receipt[s].reason}` : ''}`).join('; ');
136
+ lines.push(`Result: unverified -- the ${unread.join(' and ')} side was not read back (${why}).`);
137
+ } else if (status === 'disagree') {
138
+ lines.push(
139
+ `Result: the two sides disagree on ${receipt.diffs.map((d) => d.field).join(', ')}.${retry}`,
140
+ ...receipt.diffs.map((d) => ` ${d.field}: local ${show(d.local)} | cloud ${show(d.cloud)}`),
141
+ );
142
+ } else {
143
+ lines.push('Result: verified -- both sides were read back and agree.');
144
+ }
145
+ return lines.join('\n');
146
+ }
@@ -0,0 +1,117 @@
1
+ // heyspex_start_recipe and heyspex_list_recipes handlers (recipe-tools.mjs holds the store). Extracted from cli.mjs under
2
+ // [[HS-BLNF-6W61-DIAG-cli-mjs-has-grown-into-a-13-665-line]] (no behavior change).
3
+
4
+ import { wrapDesignResult } from './tool-helpers.mjs';
5
+ import { executeListRecipes, executeStartRecipe } from './recipe-tools.mjs';
6
+ import { detectAgentIdentity } from './mcp-config.mjs';
7
+ import { consoleErrorMessage } from './console-fetch.mjs';
8
+ import { createCaptureSessionOnConsole, writeCaptureFocusVendor } from './capture-tools.mjs';
9
+
10
+ // heyspex_list_recipes — pure catalog scan wrapped for the tool surface.
11
+ export function handleListRecipes(vaultRoot, args) {
12
+ return wrapDesignResult(executeListRecipes(vaultRoot, args));
13
+ }
14
+
15
+ // heyspex_start_recipe — resolve the recipe (pure) then, when the console is
16
+ // reachable, create + pin a capture session tied to it (mirrors
17
+ // handlePinCapture). The recipe start context is returned in every case so the
18
+ // flow can be walked even when the capture could not be created (offline).
19
+ export async function handleStartRecipe(vaultRoot, args) {
20
+ const resolved = executeStartRecipe(vaultRoot, args);
21
+ if (resolved.error) {
22
+ return {
23
+ isError: true,
24
+ content: [{ type: 'text', text: JSON.stringify(resolved, null, 2) }],
25
+ };
26
+ }
27
+ const recipe = resolved.recipe;
28
+ const identity = detectAgentIdentity();
29
+
30
+ const createResult = await createCaptureSessionOnConsole({
31
+ opened_by: 'agent',
32
+ opened_by_session: identity.sessionId,
33
+ mode: 'attended',
34
+ status: 'pending',
35
+ });
36
+
37
+ if (createResult.ok && createResult.session) {
38
+ const session = createResult.session;
39
+ const pin = await writeCaptureFocusVendor(vaultRoot, {
40
+ specId: session.id,
41
+ specPath: session.specPath || '',
42
+ reason: `Started workflow-recipe ${recipe.recipe_id} (${recipe.name})`,
43
+ claim: { claimed_by: identity.agent, claim_session: identity.sessionId },
44
+ });
45
+ if (!pin.pinned) {
46
+ // The session row WAS created and the recipe context is intact — both are
47
+ // returned so nothing is lost and no second session gets minted. But the
48
+ // message may not say "pinned" over a pin that never persisted
49
+ // ([[HS-IOJW-5YDH-DIAG]] sweep: report the result, not the request).
50
+ return {
51
+ isError: true,
52
+ content: [{
53
+ type: 'text',
54
+ text: JSON.stringify({
55
+ started: true,
56
+ pinned: false,
57
+ error: 'CAPTURE_PIN_NOT_WRITTEN',
58
+ source: 'database',
59
+ capture: { spec_id: session.id, spec_path: session.specPath || null },
60
+ pin,
61
+ recipe,
62
+ message:
63
+ `Capture session ${session.id} was created for workflow-recipe ${recipe.recipe_id}, but the ` +
64
+ `capture-on-deck pin was NOT written (${pin.reason}). A later /hs-capture with no id will ` +
65
+ 'resolve to a DIFFERENT capture. Do not re-run heyspex_start_recipe (it would mint a second ' +
66
+ `session) — re-pin with heyspex_pin_capture({ spec_id: "${session.id}" }) once the console is healthy.`,
67
+ }, null, 2),
68
+ }],
69
+ };
70
+ }
71
+ return {
72
+ content: [{
73
+ type: 'text',
74
+ text: JSON.stringify({
75
+ started: true,
76
+ pinned: true,
77
+ pin,
78
+ source: 'database',
79
+ capture: { spec_id: session.id, spec_path: session.specPath || null },
80
+ recipe,
81
+ message: `Capture session ${session.id} created and pinned (${pin.tier}) for workflow-recipe ${recipe.recipe_id}. Walk the capture_sequence, then file the spec pre-filled from spec_template (recipe_id + tool_scope stamp provenance).`,
82
+ }, null, 2),
83
+ }],
84
+ };
85
+ }
86
+
87
+ // The create failed → the recipe context is still fully returned so the flow
88
+ // can be walked / filed later. No divergent local capture file is minted
89
+ // (capture is online-only, [[heyspex-is-online-only-no-offline]]).
90
+ //
91
+ // HS-I6HT: this used to hardcode "HeySpex can't reach the server right now"
92
+ // for EVERY failure, so a sign-in failure printed an outage sentence next to
93
+ // a CONSOLE_NOT_SIGNED_IN code — two contradictory stories in one payload.
94
+ // `consoleErrorMessage` already writes the right sentence per class (sign-in,
95
+ // 401, wrong port, timeout, genuinely down); use it whenever it has one, and
96
+ // keep the old wording only as the last-resort fallback.
97
+ const answered =
98
+ typeof createResult.result?.body?.message === 'string'
99
+ ? createResult.result.body.message
100
+ : null;
101
+ const whatHappened =
102
+ (createResult.result ? consoleErrorMessage(createResult.result, answered) : null)
103
+ || "HeySpex can't reach the server right now.";
104
+ return {
105
+ content: [{
106
+ type: 'text',
107
+ text: JSON.stringify({
108
+ started: false,
109
+ status: 'deferred',
110
+ error: createResult.error || 'CONSOLE_UNREACHABLE',
111
+ recipe,
112
+ message:
113
+ `${whatHappened} The capture wasn't created, but the recipe's steps are returned below and nothing is lost. Fix the cause above, then call again to pin the capture.`,
114
+ }, null, 2),
115
+ }],
116
+ };
117
+ }