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,512 @@
1
+ /**
2
+ * Write outbox — "degrade, don't die" for vault WRITES in cloud reliance.
3
+ *
4
+ * Per [[heyspex-is-online-only-no-offline]] (2026-08-20 amendment): when the
5
+ * console is unreachable (status 0 / timeout) or answers 5xx, a spec write is
6
+ * queued durably and replayed on the next successful console call, with a
7
+ * truthful receipt ("Not written to the console yet; queued…") — never a lie
8
+ * that it landed, never a failed workflow.
9
+ *
10
+ * Modelled on capture-outbox.mjs (stash -> list -> replay -> remove, quarantine
11
+ * for what the console structurally refuses, vault-root path guard HS-AI1T).
12
+ *
13
+ * What is queued: spec create / update / ordered batch (promotions) and the
14
+ * append-only control writes (report_progress, record_wip, handoff_record).
15
+ * What is NEVER queued: claims and status flips — the Neon control plane is the
16
+ * mutual-exclusion / transition authority, so those keep failing loudly. 4xx
17
+ * and 401/403 are never queued either (they fail identically on replay, or need
18
+ * `hs login`).
19
+ *
20
+ * Layout: `<vaultRoot>/.heyspex/write-outbox/<ISO>__<NN>__<op>.json`.
21
+ *
22
+ * Replay safety:
23
+ * - the STORED idempotency-key is replayed, never a fresh one ([[HS-2FQY]]);
24
+ * - a stored `create` that failed with a timeout / 5xx first looks the spec up
25
+ * BY NAME and skips the POST when it already landed (`landed_server_side`);
26
+ * - a stored `update` is refused (quarantined, `cloud_newer`) when the cloud
27
+ * copy changed after the pre-image the edit was based on.
28
+ */
29
+
30
+ import {
31
+ readdirSync,
32
+ readFileSync,
33
+ writeFileSync,
34
+ mkdirSync,
35
+ existsSync,
36
+ renameSync,
37
+ rmSync,
38
+ } from 'node:fs';
39
+ import { basename, join, isAbsolute } from 'node:path';
40
+
41
+ import { assertPathUnderRoot } from '../../scripts/lib/shared-path-helpers.mjs';
42
+
43
+ export const DEFERRED_RECEIPT =
44
+ 'Not written to the console yet; queued to .heyspex/write-outbox and replayed on the next successful console call.';
45
+
46
+ /** Process-monotonic within-millisecond tiebreaker (see capture-outbox.mjs). */
47
+ let stashSeq = 0;
48
+
49
+ export function writeOutboxDir(vaultRoot) {
50
+ return join(vaultRoot, '.heyspex', 'write-outbox');
51
+ }
52
+
53
+ export function writeOutboxQuarantineDir(vaultRoot) {
54
+ return join(vaultRoot, '.heyspex', 'write-outbox', 'quarantine');
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Default vault root — set once by the tool dispatcher so deep writers
59
+ // (createSpecViaConsole & co.) that were never handed a vaultRoot can still
60
+ // stash. With no root known, writers keep their old throw-loudly behaviour.
61
+ // ---------------------------------------------------------------------------
62
+ let defaultVaultRoot = null;
63
+
64
+ export function setWriteOutboxRoot(vaultRoot) {
65
+ if (typeof vaultRoot === 'string' && vaultRoot.trim() && isAbsolute(vaultRoot)) {
66
+ defaultVaultRoot = vaultRoot;
67
+ }
68
+ }
69
+
70
+ export function getWriteOutboxRoot() {
71
+ return defaultVaultRoot;
72
+ }
73
+
74
+ /** Test helper. */
75
+ export function resetWriteOutboxRoot() {
76
+ defaultVaultRoot = null;
77
+ drainOnceDone = false;
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Classification
82
+ // ---------------------------------------------------------------------------
83
+
84
+ /**
85
+ * True for the OUTAGE class only: transport failure (status 0 / timeout / abort
86
+ * / network) or a 5xx answer. Sign-in problems, typed scope refusals, 401/403
87
+ * and every other 4xx are NOT outages — they never queue.
88
+ */
89
+ export function isOutageClassResult(result) {
90
+ if (!result || result.ok) return false;
91
+ if (result.authProblem || result.refusal || result.urlMismatch) return false;
92
+ const status = typeof result.status === 'number' ? result.status : 0;
93
+ if (status === 401 || status === 403) return false;
94
+ if (status >= 400 && status < 500) return false;
95
+ if (status >= 500) return true;
96
+ // status 0 / missing: a genuine non-response.
97
+ return true;
98
+ }
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // Stash / list / remove / quarantine
102
+ // ---------------------------------------------------------------------------
103
+
104
+ /**
105
+ * Persist one write. `entry` carries
106
+ * `{ op, route, method, request_body, idempotency_key, acting_agent, session_id,
107
+ * failure: {status, errorKind, message}, pre_image_updated? , steps? }`.
108
+ * Returns `{ outbox_id, path, entry }`, or `null` when no vault root is usable
109
+ * or the write failed (callers then fall back to their loud error).
110
+ */
111
+ export function stashWrite(vaultRoot, entry) {
112
+ try {
113
+ const dir = writeOutboxDir(vaultRoot);
114
+ assertPathUnderRoot(vaultRoot, dir, 'write-outbox write');
115
+ mkdirSync(dir, { recursive: true });
116
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
117
+ const seq = String(stashSeq++).padStart(6, '0');
118
+ const op = ['create', 'update', 'batch', 'control'].includes(entry.op) ? entry.op : 'control';
119
+ const outboxId = `${stamp}__${seq}__${op}`;
120
+ const record = {
121
+ op,
122
+ route: entry.route ?? null,
123
+ method: entry.method ?? null,
124
+ request_body: entry.request_body ?? null,
125
+ idempotency_key: entry.idempotency_key ?? null,
126
+ acting_agent: entry.acting_agent ?? null,
127
+ session_id: entry.session_id ?? null,
128
+ attempted_at: entry.attempted_at ?? new Date().toISOString(),
129
+ failure: entry.failure ?? null,
130
+ ...(entry.pre_image_updated !== undefined ? { pre_image_updated: entry.pre_image_updated } : {}),
131
+ ...(entry.steps ? { steps: entry.steps } : {}),
132
+ ...(entry.completed_steps ? { completed_steps: entry.completed_steps } : {}),
133
+ ...(entry.spec_id ? { spec_id: entry.spec_id } : {}),
134
+ };
135
+ const path = join(dir, `${outboxId}.json`);
136
+ writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
137
+ // A fresh entry re-arms the once-per-process first-success drain.
138
+ drainOnceDone = false;
139
+ return { outbox_id: outboxId, path, entry: record };
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+
145
+ /** Oldest-first list of parsable entries. Unreadable files are skipped. */
146
+ export function listOutbox(vaultRoot) {
147
+ const dir = writeOutboxDir(vaultRoot);
148
+ if (!existsSync(dir)) return [];
149
+ const out = [];
150
+ for (const d of readdirSync(dir, { withFileTypes: true })) {
151
+ if (!d.isFile() || !d.name.endsWith('.json')) continue;
152
+ try {
153
+ const entry = JSON.parse(readFileSync(join(dir, d.name), 'utf8'));
154
+ out.push({ ...entry, outbox_id: d.name.replace(/\.json$/, ''), path: join(dir, d.name) });
155
+ } catch {
156
+ /* skip unreadable */
157
+ }
158
+ }
159
+ return out.sort((a, b) => (a.outbox_id < b.outbox_id ? -1 : a.outbox_id > b.outbox_id ? 1 : 0));
160
+ }
161
+
162
+ export function outboxCount(vaultRoot) {
163
+ try {
164
+ return listOutbox(vaultRoot).length;
165
+ } catch {
166
+ return 0;
167
+ }
168
+ }
169
+
170
+ export function removeOutboxEntry(path) {
171
+ try {
172
+ rmSync(path);
173
+ return true;
174
+ } catch {
175
+ return false;
176
+ }
177
+ }
178
+
179
+ /** Move an entry to quarantine/ with a `<name>.reason.json` sidecar. */
180
+ export function quarantineOutboxEntry(vaultRoot, entryPath, metadata = {}) {
181
+ try {
182
+ const qdir = writeOutboxQuarantineDir(vaultRoot);
183
+ assertPathUnderRoot(vaultRoot, qdir, 'write-outbox write');
184
+ mkdirSync(qdir, { recursive: true });
185
+ const name = basename(entryPath);
186
+ renameSync(entryPath, join(qdir, name));
187
+ writeFileSync(
188
+ join(qdir, `${name}.reason.json`),
189
+ `${JSON.stringify(
190
+ {
191
+ reason: metadata.reason || 'console_refused',
192
+ status: metadata.status ?? null,
193
+ consoleMessage: metadata.consoleMessage ?? null,
194
+ quarantined_at: new Date().toISOString(),
195
+ },
196
+ null,
197
+ 2,
198
+ )}\n`,
199
+ 'utf8',
200
+ );
201
+ return true;
202
+ } catch {
203
+ return false;
204
+ }
205
+ }
206
+
207
+ export function listQuarantinedOutbox(vaultRoot) {
208
+ const dir = writeOutboxQuarantineDir(vaultRoot);
209
+ if (!existsSync(dir)) return [];
210
+ return readdirSync(dir, { withFileTypes: true })
211
+ .filter((d) => d.isFile() && d.name.endsWith('.json') && !d.name.endsWith('.reason.json'))
212
+ .map((d) => d.name)
213
+ .sort()
214
+ .map((f) => {
215
+ let reason = null;
216
+ try {
217
+ reason = JSON.parse(readFileSync(join(dir, `${f}.reason.json`), 'utf8'));
218
+ } catch {
219
+ /* sidecar may be absent */
220
+ }
221
+ return { file: f, path: join(dir, f), reason };
222
+ });
223
+ }
224
+
225
+ // ---------------------------------------------------------------------------
226
+ // Deferred response
227
+ // ---------------------------------------------------------------------------
228
+
229
+ /** The truthful tool response for a queued write. Never `isError`. */
230
+ export function buildDeferredPayload(stashed, vaultRoot, extra = {}) {
231
+ return {
232
+ status: 'deferred',
233
+ outbox_id: stashed.outbox_id,
234
+ pending_sync: outboxCount(vaultRoot),
235
+ receipt: DEFERRED_RECEIPT,
236
+ ...extra,
237
+ };
238
+ }
239
+
240
+ /** MCP tool envelope for {@link buildDeferredPayload}. */
241
+ export function deferredToolResponse(payload) {
242
+ return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
243
+ }
244
+
245
+ /** Result descriptor for a failed consoleFetch, as stored in `failure`. */
246
+ export function failureOf(result, fallbackMessage = null) {
247
+ return {
248
+ status: typeof result?.status === 'number' ? result.status : 0,
249
+ errorKind: result?.errorKind ?? null,
250
+ message: String(result?.error ?? fallbackMessage ?? '').slice(0, 500) || null,
251
+ };
252
+ }
253
+
254
+ /**
255
+ * Stash a single write described by a failed consoleFetch result, when (and only
256
+ * when) it is outage class and a vault root is known. Returns the stash record
257
+ * or null (caller then surfaces its original loud error).
258
+ */
259
+ export function stashIfOutage(result, entry, vaultRoot = defaultVaultRoot) {
260
+ if (!vaultRoot || !isOutageClassResult(result)) return null;
261
+ return stashWrite(vaultRoot, { ...entry, failure: failureOf(result) });
262
+ }
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // Drain
266
+ // ---------------------------------------------------------------------------
267
+
268
+ let draining = false;
269
+ let drainOnceDone = false;
270
+
271
+ const sameName = (a, b) =>
272
+ typeof a === 'string' && typeof b === 'string' && a.trim().toLowerCase() === b.trim().toLowerCase();
273
+
274
+ /** Find an already-landed spec by exact name (case-insensitive) within its type. */
275
+ async function findSpecByName(consoleFetch, type, name) {
276
+ let after = null;
277
+ for (let i = 0; i < 20; i += 1) {
278
+ const params = new URLSearchParams();
279
+ if (type) params.set('type', type);
280
+ params.set('limit', '500');
281
+ if (after) params.set('after', after);
282
+ const res = await consoleFetch(`/api/specs?${params.toString()}`);
283
+ if (!res?.ok) return { error: res };
284
+ const rows = Array.isArray(res.body?.specs) ? res.body.specs : [];
285
+ const hit = rows.find((r) => sameName(r?.name, name) || sameName(r?.title, name));
286
+ if (hit) return { hit };
287
+ const next = typeof res.body?.next_cursor === 'string' && res.body.next_cursor ? res.body.next_cursor : null;
288
+ if (!next || next === after || rows.length === 0) break;
289
+ after = next;
290
+ }
291
+ return { hit: null };
292
+ }
293
+
294
+ function basenameOf(spec) {
295
+ const rel = spec?.relPath ?? spec?.path ?? null;
296
+ if (rel) return String(rel).split(/[\\/]/).pop().replace(/\.md$/i, '');
297
+ return spec?.basename ?? spec?.id ?? null;
298
+ }
299
+
300
+ const needsLandingCheck = (failure) =>
301
+ failure?.errorKind === 'timeout' || failure?.errorKind === 'abort' || (failure?.status ?? 0) >= 500;
302
+
303
+ /**
304
+ * Replay one request. Returns
305
+ * `{ outcome: 'ok'|'skipped'|'quarantine'|'stop', result?, reason?, spec? }`.
306
+ * `outcome: 'stop'` means outage / sign-in: leave the entry (and everything
307
+ * after it) for the next successful call.
308
+ */
309
+ async function replayRequest(consoleFetch, step, parentOp) {
310
+ if (parentOp === 'create' && needsLandingCheck(step.failure ?? null)) {
311
+ const found = await findSpecByName(consoleFetch, step.request_body?.type, step.request_body?.name);
312
+ if (found.error) {
313
+ return isOutageClassResult(found.error) || found.error.status === 401 || found.error.status === 403
314
+ ? { outcome: 'stop', result: found.error }
315
+ : { outcome: 'quarantine', reason: 'console_refused', result: found.error };
316
+ }
317
+ if (found.hit) return { outcome: 'skipped', landed_server_side: true, spec: found.hit };
318
+ }
319
+ if (parentOp === 'update' && step.pre_image_updated) {
320
+ const cur = await consoleFetch(step.route);
321
+ if (!cur?.ok) {
322
+ return isOutageClassResult(cur) || cur?.status === 401 || cur?.status === 403
323
+ ? { outcome: 'stop', result: cur }
324
+ : { outcome: 'quarantine', reason: 'console_refused', result: cur };
325
+ }
326
+ const cloudUpdated = cur.body?.spec?.updated ?? cur.body?.spec?.frontmatter?.updated ?? null;
327
+ const a = Date.parse(String(cloudUpdated));
328
+ const b = Date.parse(String(step.pre_image_updated));
329
+ if (Number.isFinite(a) && Number.isFinite(b) && a > b) {
330
+ return { outcome: 'quarantine', reason: 'cloud_newer', result: cur };
331
+ }
332
+ }
333
+ const headers = step.idempotency_key ? { 'idempotency-key': step.idempotency_key } : undefined;
334
+ const res = await consoleFetch(step.route, {
335
+ method: step.method || 'POST',
336
+ ...(step.request_body !== null && step.request_body !== undefined ? { body: step.request_body } : {}),
337
+ ...(headers ? { headers } : {}),
338
+ ...(parentOp === 'create' ? { retryOnTimeout: false } : {}),
339
+ });
340
+ if (res?.ok) return { outcome: 'ok', result: res };
341
+ if (isOutageClassResult(res) || res?.status === 401 || res?.status === 403 || res?.authProblem) {
342
+ return { outcome: 'stop', result: res };
343
+ }
344
+ return { outcome: 'quarantine', reason: 'console_refused', result: res };
345
+ }
346
+
347
+ /**
348
+ * Replay every queued write, oldest-first, with the STORED idempotency keys.
349
+ * Never stashes (recursion-guarded). 4xx -> quarantine and keep going; 5xx /
350
+ * status 0 / sign-in problem -> stop and leave the rest queued.
351
+ *
352
+ * @returns {Promise<{drained: object[], quarantined: object[], stopped_at: object|null}>}
353
+ */
354
+ export async function drainWriteOutbox({ vaultRoot, consoleFetch } = {}) {
355
+ const out = { drained: [], quarantined: [], stopped_at: null };
356
+ if (draining || !vaultRoot || typeof consoleFetch !== 'function') return out;
357
+ draining = true;
358
+ try {
359
+ for (const entry of listOutbox(vaultRoot)) {
360
+ const stopAt = (res, step = null) => {
361
+ out.stopped_at = {
362
+ outbox_id: entry.outbox_id,
363
+ status: res?.status ?? 0,
364
+ errorKind: res?.errorKind ?? null,
365
+ ...(step ? { step_index: step } : {}),
366
+ };
367
+ };
368
+ const quarantine = (r, extra = {}) => {
369
+ quarantineOutboxEntry(vaultRoot, entry.path, {
370
+ reason: r.reason,
371
+ status: r.result?.status ?? null,
372
+ consoleMessage: r.result?.error ?? JSON.stringify(r.result?.body ?? {}).slice(0, 400),
373
+ });
374
+ out.quarantined.push({
375
+ outbox_id: entry.outbox_id,
376
+ reason: r.reason,
377
+ status: r.result?.status ?? null,
378
+ route: entry.route,
379
+ ...extra,
380
+ });
381
+ };
382
+
383
+ if (entry.op === 'batch') {
384
+ const steps = Array.isArray(entry.steps) ? [...entry.steps] : [];
385
+ let stopped = false;
386
+ let done = 0;
387
+ while (steps.length > 0) {
388
+ const step = steps[0];
389
+ const r = await replayRequest(consoleFetch, { ...step, failure: step.failure ?? (done === 0 ? entry.failure : null) }, step.kind ?? 'batch');
390
+ if (r.outcome === 'stop') {
391
+ stopAt(r.result, done);
392
+ stopped = true;
393
+ break;
394
+ }
395
+ if (r.outcome === 'quarantine') {
396
+ quarantine(r, { step_index: done });
397
+ stopped = 'quarantined';
398
+ break;
399
+ }
400
+ const spec = r.spec ?? r.result?.body?.spec ?? null;
401
+ out.drained.push({
402
+ outbox_id: entry.outbox_id,
403
+ spec_id: spec?.id ?? step.spec_id ?? null,
404
+ basename: basenameOf(spec),
405
+ route: step.route,
406
+ ...(r.landed_server_side ? { landed_server_side: true } : {}),
407
+ });
408
+ steps.shift();
409
+ done += 1;
410
+ if (steps.length > 0) {
411
+ // persist progress so a crash/outage never re-sends a landed step
412
+ try {
413
+ writeFileSync(entry.path, `${JSON.stringify({ ...stripRuntime(entry), steps }, null, 2)}\n`, 'utf8');
414
+ } catch {
415
+ /* best effort */
416
+ }
417
+ }
418
+ }
419
+ if (stopped === 'quarantined') continue;
420
+ if (stopped) break;
421
+ removeOutboxEntry(entry.path);
422
+ continue;
423
+ }
424
+
425
+ const r = await replayRequest(consoleFetch, entry, entry.op);
426
+ if (r.outcome === 'stop') {
427
+ stopAt(r.result);
428
+ break;
429
+ }
430
+ if (r.outcome === 'quarantine') {
431
+ quarantine(r);
432
+ continue;
433
+ }
434
+ const spec = r.spec ?? r.result?.body?.spec ?? null;
435
+ removeOutboxEntry(entry.path);
436
+ out.drained.push({
437
+ outbox_id: entry.outbox_id,
438
+ spec_id: spec?.id ?? entry.spec_id ?? null,
439
+ basename: basenameOf(spec),
440
+ route: entry.route,
441
+ ...(r.landed_server_side ? { landed_server_side: true } : {}),
442
+ });
443
+ }
444
+ } catch {
445
+ /* a drain never throws into a tool call */
446
+ } finally {
447
+ draining = false;
448
+ }
449
+ return out;
450
+ }
451
+
452
+ function stripRuntime(entry) {
453
+ const { outbox_id: _id, path: _p, ...rest } = entry;
454
+ return rest;
455
+ }
456
+
457
+ /**
458
+ * Drain and shape the result for a tool response: `{ drained, pending_sync }`
459
+ * fields only when there is something to say. Failure-safe.
460
+ */
461
+ export async function drainAndSummarize(vaultRoot, consoleFetch) {
462
+ if (!vaultRoot) return {};
463
+ try {
464
+ if (outboxCount(vaultRoot) === 0) return {};
465
+ const d = await drainWriteOutbox({ vaultRoot, consoleFetch });
466
+ const pending = outboxCount(vaultRoot);
467
+ return {
468
+ ...(d.drained.length ? { drained: d.drained } : {}),
469
+ ...(d.quarantined.length ? { quarantined: d.quarantined } : {}),
470
+ ...(pending ? { pending_sync: pending } : {}),
471
+ };
472
+ } catch {
473
+ return {};
474
+ }
475
+ }
476
+
477
+ /**
478
+ * Once per process, after the first successful console call: drain in the
479
+ * background. Non-blocking and failure-safe.
480
+ */
481
+ export function maybeDrainOnFirstSuccess(consoleFetch) {
482
+ if (drainOnceDone || !defaultVaultRoot) return;
483
+ drainOnceDone = true;
484
+ const root = defaultVaultRoot;
485
+ setImmediate(() => {
486
+ try {
487
+ if (outboxCount(root) === 0) return;
488
+ drainWriteOutbox({ vaultRoot: root, consoleFetch }).catch(() => {});
489
+ } catch {
490
+ /* swallow */
491
+ }
492
+ });
493
+ }
494
+
495
+ /**
496
+ * Control-plane append writes (report_progress, record_wip, handoff_record):
497
+ * on outage class in cloud reliance, queue as `op: 'control'` and return the
498
+ * `deferred` tool response; otherwise return null so the caller keeps its loud
499
+ * error envelope. `isCloud` is an injected async predicate (the caller's
500
+ * `resolveReadTier() === 'neon'`), keeping this module free of read-path.
501
+ * Claims and status flips must NEVER be routed through here.
502
+ */
503
+ export async function deferControlWrite(result, entry, { vaultRoot = defaultVaultRoot, isCloud = async () => true } = {}) {
504
+ if (!vaultRoot || !isOutageClassResult(result)) return null;
505
+ try {
506
+ if (!(await isCloud())) return null;
507
+ } catch {
508
+ return null;
509
+ }
510
+ const stashed = stashWrite(vaultRoot, { ...entry, op: 'control', failure: failureOf(result) });
511
+ return stashed ? deferredToolResponse(buildDeferredPayload(stashed, vaultRoot)) : null;
512
+ }