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
@@ -19,6 +19,15 @@
19
19
  * (~/.heyspex/dev-mode.json) → cloud admin setting (account_settings.dev_mode,
20
20
  * injected by the caller — this module never talks to Neon) → default prod.
21
21
  *
22
+ * ONE HOME ([[HS-R0YM-WW9R-TASK-publish-the-routing-fact-table-test]], routing
23
+ * fact G in `spex-console/src/lib/cli/routing-facts.ts`): the cloud
24
+ * `account_settings.dev_mode` row is the home of the mode. `dev-mode.json` is a
25
+ * machine-local CACHE of it (owner decision: kept as a cache, not deleted),
26
+ * validated by the `hs doctor` `mode-override` row, which names the file's
27
+ * writer and date and fails when it disagrees with a known cloud setting.
28
+ * Both implementations are held to the one precedence table in
29
+ * `./dev-mode.fixtures.mjs`.
30
+ *
22
31
  * MACHINE-LOCAL RECORD — CROSS-COMPONENT CONTRACT (the Windows Companion
23
32
  * reads the same file; keep the shape exactly this):
24
33
  * ~/.heyspex/dev-mode.json
@@ -28,19 +37,29 @@
28
37
  * Env overrides:
29
38
  * HEYSPEX_DEV_MODE = prod | dev | local | cloud (local/cloud imply dev)
30
39
  * HEYSPEX_DEV_RELIANCE = local | cloud (refines HEYSPEX_DEV_MODE=dev)
40
+ * `HEYSPEX_DEV_MODE=dev` with no reliance still resolves local, but the
41
+ * envelope reports `sources.reliance: "assumed-local"` — never silently.
42
+ *
43
+ * Any active switch (env or file) is reported as ONE structured object by
44
+ * `modeOverrideOf(resolved)` — `hs dev --json`, `hs doctor --json` and the MCP
45
+ * `heyspex_dev_mode` all carry it as `mode_override`
46
+ * ([[HS-EVVI-WW9R-TASK-show-any-dev-mode-override-in-hs-dev]]).
31
47
  *
32
48
  * Zero deps beyond node builtins (same rule as console-url-resolver.mjs).
33
49
  */
50
+ import { heyspexHome } from './heyspex-home.mjs';
34
51
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
35
52
  import { homedir } from 'node:os';
36
53
  import { join } from 'node:path';
37
54
 
55
+ import { assertNotRealHomeUnderTest, readBearer } from '../../vendor/heyspex-mcp/credentials.mjs';
56
+
38
57
  export const CLOUD_CONSOLE_URL = 'https://app.heyspex.com';
39
58
  export const LOCAL_CONSOLE_URL = 'http://localhost:3000';
40
59
  export const DEV_MODE_FILENAME = 'dev-mode.json';
41
60
 
42
61
  /** Absolute path of the machine-local record. */
43
- export function devModeFilePath(homeDir = homedir()) {
62
+ export function devModeFilePath(homeDir = heyspexHome()) {
44
63
  return join(homeDir, '.heyspex', DEV_MODE_FILENAME);
45
64
  }
46
65
 
@@ -70,14 +89,19 @@ export function deriveSemantics(mode, reliance) {
70
89
  };
71
90
  }
72
91
 
92
+ /** Default file reader — injectable so tests never touch the real HOME. */
93
+ const defaultReadFile = (p) => readFileSync(p, 'utf8');
94
+
73
95
  /**
74
96
  * Read + validate ~/.heyspex/dev-mode.json. Never throws:
75
97
  * returns { record, parseError } — record null when missing/invalid.
98
+ * `readFile` is an injectable seam (same shape as the TS twin's) so the
99
+ * parity test can feed both implementations the SAME in-memory fixture.
76
100
  */
77
- export function readLocalDevModeRecord({ homeDir = homedir() } = {}) {
101
+ export function readLocalDevModeRecord({ homeDir = heyspexHome(), readFile = defaultReadFile } = {}) {
78
102
  let raw;
79
103
  try {
80
- raw = readFileSync(devModeFilePath(homeDir), 'utf8');
104
+ raw = readFile(devModeFilePath(homeDir));
81
105
  } catch {
82
106
  return { record: null, parseError: null }; // absent — not an error
83
107
  }
@@ -112,10 +136,11 @@ export function readLocalDevModeRecord({ homeDir = homedir() } = {}) {
112
136
  */
113
137
  export function resolveDevMode({
114
138
  env = process.env,
115
- homeDir = homedir(),
139
+ homeDir = heyspexHome(),
140
+ readFile = defaultReadFile,
116
141
  cloudSetting = undefined,
117
142
  } = {}) {
118
- const local = readLocalDevModeRecord({ homeDir });
143
+ const local = readLocalDevModeRecord({ homeDir, readFile });
119
144
  const cloudKnown = cloudSetting !== undefined;
120
145
  const cloudValue =
121
146
  cloudKnown && cloudSetting
@@ -128,11 +153,19 @@ export function resolveDevMode({
128
153
  let mode;
129
154
  let reliance;
130
155
  let source;
156
+ // Where the RELIANCE came from. Equals `source` except in one case:
157
+ // `HEYSPEX_DEV_MODE=dev` with no `HEYSPEX_DEV_RELIANCE` used to mean local
158
+ // silently; it still means local, but the envelope now says the reliance
159
+ // was ASSUMED ([[HS-EVVI-WW9R-TASK-show-any-dev-mode-override-in-hs-dev]]
160
+ // AC1, invariant 10 — no hidden mode switch).
161
+ let relianceSource;
131
162
  const envParsed = normalizeMode(env.HEYSPEX_DEV_MODE);
132
163
  if (envParsed) {
133
164
  mode = envParsed.mode;
134
- reliance = envParsed.reliance ?? normalizeReliance(env.HEYSPEX_DEV_RELIANCE) ?? 'local';
165
+ const envReliance = envParsed.reliance ?? normalizeReliance(env.HEYSPEX_DEV_RELIANCE);
166
+ reliance = envReliance ?? 'local';
135
167
  source = 'env';
168
+ relianceSource = envReliance ? 'env' : 'assumed-local';
136
169
  } else if (local.record) {
137
170
  mode = local.record.mode;
138
171
  reliance = local.record.reliance;
@@ -167,7 +200,7 @@ export function resolveDevMode({
167
200
  vaultWriteMode: semantics.vaultWriteMode,
168
201
  sources: {
169
202
  mode: source,
170
- reliance: source,
203
+ reliance: relianceSource ?? source,
171
204
  consoleUrl:
172
205
  source === 'local-override' && local.record?.console_url ? 'local-override' : 'derived',
173
206
  readTier: 'derived',
@@ -183,6 +216,131 @@ export function resolveDevMode({
183
216
  };
184
217
  }
185
218
 
219
+ /** The `source` values {@link modeOverrideOf} can report. */
220
+ export const MODE_OVERRIDE_SOURCE_ENV = 'env';
221
+ export const MODE_OVERRIDE_SOURCE_FILE = 'dev-mode.json';
222
+
223
+ /**
224
+ * The one structured answer to "is a mode switch active on this machine?" —
225
+ * what `hs dev --json`, `hs doctor --json` and `heyspex_dev_mode` all report
226
+ * as `mode_override` ([[HS-EVVI-WW9R-TASK-show-any-dev-mode-override-in-hs-dev]]
227
+ * AC1/AC2; epic [[HS-P1AW-WW9R-EPIC-identity-threading-and-the-end-of-the]]
228
+ * criterion 4). Pure: reads only the resolved envelope, never the file.
229
+ *
230
+ * null — nothing overrides the cloud/default
231
+ * { source: "env", written_at: null, written_by: "HEYSPEX_DEV_MODE", disagrees_with_cloud }
232
+ * { source: "dev-mode.json", written_at: <ISO>, written_by: <record.source>, disagrees_with_cloud }
233
+ *
234
+ * `written_at` / `written_by` are the record's `updated_at` / `source` (the
235
+ * cross-component file contract keeps its own key names; the Companion reads
236
+ * that file). `disagrees_with_cloud` is only ever true when the cloud setting
237
+ * is KNOWN and differs — an unreachable cloud is never a disagreement.
238
+ */
239
+ export function modeOverrideOf(resolved) {
240
+ if (!resolved) return null;
241
+ const cloud = resolved.cloudSetting?.value ?? null;
242
+ if (resolved.sources?.mode === 'env') {
243
+ const disagrees =
244
+ cloud !== null &&
245
+ (cloud.mode !== resolved.mode ||
246
+ (resolved.mode === 'dev' && cloud.reliance !== resolved.reliance));
247
+ return {
248
+ source: MODE_OVERRIDE_SOURCE_ENV,
249
+ written_at: null,
250
+ written_by: 'HEYSPEX_DEV_MODE',
251
+ disagrees_with_cloud: disagrees,
252
+ };
253
+ }
254
+ if (resolved.localOverride?.present) {
255
+ const record = resolved.localOverride.record;
256
+ return {
257
+ source: MODE_OVERRIDE_SOURCE_FILE,
258
+ written_at: record?.updated_at ?? null,
259
+ written_by: record?.source ?? null,
260
+ disagrees_with_cloud: resolved.localOverride.staleAgainstCloud === true,
261
+ };
262
+ }
263
+ return null;
264
+ }
265
+
266
+ /** `prod` / `dev+local` / `dev+cloud` — the label doctor and hs dev print. */
267
+ export function describeModeLabel(resolved) {
268
+ return `${resolved.mode}${resolved.mode === 'dev' ? `+${resolved.reliance}` : ''}`;
269
+ }
270
+
271
+ /**
272
+ * The `hs doctor` row for {@link modeOverrideOf} (row id `mode-override`).
273
+ * Pure — the caller resolves ONCE and passes the envelope in; this never
274
+ * re-reads the file. Shape matches every other doctor row
275
+ * (`{ id, label, status, detail, human? }`) plus the structured
276
+ * `mode_override` so `--json` carries the same object `hs dev --json` does.
277
+ *
278
+ * ok no override; or one that pins prod (the default) and does not
279
+ * disagree with a known cloud setting — the file is then just a
280
+ * cache of the normal mode, and the detail still names its date and
281
+ * writer so nothing on disk stays invisible.
282
+ * warn an override puts this machine in dev mode (cloud setting unknown
283
+ * or agreeing) — names the file, date and writer, repair `hs dev prod`.
284
+ * fail the override DISAGREES with a known cloud setting — same repair.
285
+ *
286
+ * `repairCommand` is rendered by the caller (`hsCommand('dev prod')`) so the
287
+ * advice is in the form THIS machine can run.
288
+ */
289
+ export function modeOverrideDoctorRow(resolved, { recordPath, repairCommand = 'hs dev prod' } = {}) {
290
+ const id = 'mode-override';
291
+ const label = 'Dev-mode override (env / ~/.heyspex/dev-mode.json)';
292
+ const override = modeOverrideOf(resolved);
293
+ const modeLabel = describeModeLabel(resolved);
294
+ if (!override) {
295
+ return {
296
+ id,
297
+ label,
298
+ status: 'ok',
299
+ detail: `none — ${modeLabel} from ${resolved.sources.mode}`,
300
+ mode_override: null,
301
+ };
302
+ }
303
+ const where =
304
+ override.source === MODE_OVERRIDE_SOURCE_ENV
305
+ ? `HEYSPEX_DEV_MODE env override`
306
+ : `${recordPath ?? devModeFilePath()}`;
307
+ const stamp =
308
+ override.source === MODE_OVERRIDE_SOURCE_ENV
309
+ ? ''
310
+ : ` (written ${override.written_at ?? 'unknown date'} by ${override.written_by ?? 'unknown writer'})`;
311
+ const cloud = resolved.cloudSetting?.value;
312
+ const cloudLabel = cloud ? describeModeLabel({ mode: cloud.mode, reliance: cloud.reliance }) : null;
313
+ const repair = `Run \`${repairCommand}\` to put this machine back on the cloud console, or set the mode on purpose with \`${repairCommand.replace(/\bdev prod$/, 'dev <mode>')}\`.`;
314
+
315
+ if (override.disagrees_with_cloud) {
316
+ return {
317
+ id,
318
+ label,
319
+ status: 'fail',
320
+ detail: `${modeLabel} from ${where}${stamp} — DISAGREES with the cloud setting (${cloudLabel})`,
321
+ human: repair,
322
+ mode_override: override,
323
+ };
324
+ }
325
+ if (resolved.mode === 'dev') {
326
+ return {
327
+ id,
328
+ label,
329
+ status: 'warn',
330
+ detail: `${modeLabel} from ${where}${stamp}${cloudLabel ? ` — matches the cloud setting (${cloudLabel})` : ' — cloud setting not checked (offline)'}`,
331
+ human: repair,
332
+ mode_override: override,
333
+ };
334
+ }
335
+ return {
336
+ id,
337
+ label,
338
+ status: 'ok',
339
+ detail: `${where}${stamp} pins ${modeLabel} — the default, nothing overridden`,
340
+ mode_override: override,
341
+ };
342
+ }
343
+
186
344
  /**
187
345
  * Upsert top-level `key: value` lines in ~/.heyspex/credentials.yaml without a
188
346
  * full YAML round-trip (same targeted approach as the vendor MCP `pair`
@@ -192,8 +350,11 @@ export function resolveDevMode({
192
350
  */
193
351
  export function upsertCredentialsYamlKeys(homeDir, updates) {
194
352
  const dir = join(homeDir, '.heyspex');
195
- mkdirSync(dir, { recursive: true, mode: 0o700 });
196
353
  const filePath = join(dir, 'credentials.yaml');
354
+ // The real-home guard the credentials and registry writers carry: under a
355
+ // test runner, refuse to touch the REAL ~/.heyspex (checked before mkdir).
356
+ assertNotRealHomeUnderTest(filePath);
357
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
197
358
  let content = '';
198
359
  try {
199
360
  content = readFileSync(filePath, 'utf8');
@@ -214,14 +375,57 @@ export function upsertCredentialsYamlKeys(homeDir, updates) {
214
375
  return filePath;
215
376
  }
216
377
 
217
- /** Targeted read of `mcp_token:` from credentials.yaml (bearer for cloud calls). */
218
- export function readCredentialsMcpToken({ homeDir = homedir() } = {}) {
378
+ /**
379
+ * The bearer for cloud calls — `HEYSPEX_MCP_TOKEN` env, then `mcp_token:` in
380
+ * credentials.yaml — via the ONE shared reader
381
+ * (`vendor/heyspex-mcp/credentials.mjs`,
382
+ * [[HS-HL0D-WW9R-TASK-share-one-bearer-reader-and-one-console]]). Kept under
383
+ * its historical name for the hook scripts that import it; the precedence is
384
+ * the shared module's, never re-implemented here.
385
+ */
386
+ export function readCredentialsMcpToken({ env = process.env, homeDir = heyspexHome() } = {}) {
387
+ return readBearer({ env, homeDir }).token;
388
+ }
389
+
390
+ /**
391
+ * Run `fn(signal)` under a request timeout that is ALWAYS cleared and can
392
+ * never fire after the request has settled.
393
+ *
394
+ * Why not `AbortSignal.timeout(ms)`: that signal owns a timer nothing can
395
+ * cancel — it stays armed for the whole window even once the response has
396
+ * resolved, so a late abort can land on an already-settled request. Here the
397
+ * controller is ours: the timer is cleared in `finally` (rule 1 of
398
+ * [[HS-SWW3-Z9WP-DIAG-hs-dev-crashes-with-a-libuv-assertion]]) and `unref`'d
399
+ * so it can never hold the event loop open on its own.
400
+ */
401
+ async function withRequestTimeout(timeoutMs, fn) {
402
+ const controller = new AbortController();
403
+ const state = { timedOut: false };
404
+ const timer = setTimeout(() => {
405
+ state.timedOut = true;
406
+ controller.abort();
407
+ }, timeoutMs);
408
+ timer.unref?.();
409
+ try {
410
+ return await fn(controller.signal);
411
+ } catch (err) {
412
+ if (state.timedOut) throw new Error(`timed out after ${timeoutMs}ms`);
413
+ throw err;
414
+ } finally {
415
+ clearTimeout(timer);
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Release a response we are not going to read. An undrained body keeps its
421
+ * socket half-read in the keep-alive pool; cancelling hands it back so the
422
+ * process can tear down cleanly. Never throws (the body may already be gone).
423
+ */
424
+ async function discardResponseBody(res) {
219
425
  try {
220
- const raw = readFileSync(join(homeDir, '.heyspex', 'credentials.yaml'), 'utf8');
221
- const match = raw.match(/^mcp_token:\s*["']?([^"'\r\n#]+)/m);
222
- return match?.[1]?.trim() || null;
426
+ await res?.body?.cancel();
223
427
  } catch {
224
- return null;
428
+ /* already consumed, closed, or bodyless — nothing to release */
225
429
  }
226
430
  }
227
431
 
@@ -229,17 +433,22 @@ export function readCredentialsMcpToken({ homeDir = homedir() } = {}) {
229
433
  export async function fetchCloudDevMode({
230
434
  consoleUrl,
231
435
  fetchImpl = fetch,
232
- homeDir = homedir(),
436
+ homeDir = heyspexHome(),
233
437
  timeoutMs = 4000,
234
438
  } = {}) {
235
439
  if (!consoleUrl) return { ok: false, setting: undefined, error: 'no console url' };
236
440
  try {
237
441
  const token = readCredentialsMcpToken({ homeDir });
238
- const res = await fetchImpl(`${consoleUrl.replace(/\/+$/, '')}/api/admin/dev-mode`, {
239
- headers: token ? { Authorization: `Bearer ${token}` } : {},
240
- signal: AbortSignal.timeout(timeoutMs),
241
- });
242
- if (!res.ok) return { ok: false, setting: undefined, error: `HTTP ${res.status}` };
442
+ const res = await withRequestTimeout(timeoutMs, (signal) =>
443
+ fetchImpl(`${consoleUrl.replace(/\/+$/, '')}/api/admin/dev-mode`, {
444
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
445
+ signal,
446
+ }),
447
+ );
448
+ if (!res.ok) {
449
+ await discardResponseBody(res);
450
+ return { ok: false, setting: undefined, error: `HTTP ${res.status}` };
451
+ }
243
452
  const body = await res.json();
244
453
  return { ok: true, setting: body?.cloud ?? null, error: null };
245
454
  } catch (err) {
@@ -280,7 +489,7 @@ export class DevModeNotAdminError extends Error {
280
489
  export async function applyDevMode(
281
490
  target,
282
491
  {
283
- homeDir = homedir(),
492
+ homeDir = heyspexHome(),
284
493
  fetchImpl = fetch,
285
494
  source = 'cli',
286
495
  now = () => new Date().toISOString(),
@@ -297,18 +506,24 @@ export async function applyDevMode(
297
506
  parsed.reliance ?? normalizeReliance(target?.reliance) ?? 'local';
298
507
  const mode = parsed.mode;
299
508
  const semantics = deriveSemantics(mode, reliance);
509
+ // Under a test runner, refuse a real-home target BEFORE the cloud precheck
510
+ // or any write (the same guard the credentials and registry writers carry).
511
+ assertNotRealHomeUnderTest(devModeFilePath(homeDir));
300
512
 
513
+ // Timer-safe on purpose — never `AbortSignal.timeout` here
514
+ // ([[HS-SWW3-Z9WP-DIAG-hs-dev-crashes-with-a-libuv-assertion]]).
301
515
  async function postCloud(token) {
302
- const res = await fetchImpl(`${semantics.consoleUrl}/api/admin/dev-mode`, {
303
- method: 'POST',
304
- headers: {
305
- 'content-type': 'application/json',
306
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
307
- },
308
- body: JSON.stringify({ mode, reliance: semantics.reliance, source }),
309
- signal: AbortSignal.timeout(5000),
310
- });
311
- return res;
516
+ return withRequestTimeout(5000, (signal) =>
517
+ fetchImpl(`${semantics.consoleUrl}/api/admin/dev-mode`, {
518
+ method: 'POST',
519
+ headers: {
520
+ 'content-type': 'application/json',
521
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
522
+ },
523
+ body: JSON.stringify({ mode, reliance: semantics.reliance, source }),
524
+ signal,
525
+ }),
526
+ );
312
527
  }
313
528
 
314
529
  // 0. Authorization precheck — see the doc comment above.
@@ -318,6 +533,7 @@ export async function applyDevMode(
318
533
  if (token) {
319
534
  try {
320
535
  const res = await postCloud(token);
536
+ await discardResponseBody(res);
321
537
  if (res.status === 403) throw new DevModeNotAdminError();
322
538
  cloud = { attempted: true, updated: res.ok, error: res.ok ? null : `HTTP ${res.status}` };
323
539
  } catch (err) {
@@ -336,8 +552,8 @@ export async function applyDevMode(
336
552
  source,
337
553
  };
338
554
  const dir = join(homeDir, '.heyspex');
339
- mkdirSync(dir, { recursive: true, mode: 0o700 });
340
555
  const recordPath = devModeFilePath(homeDir);
556
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
341
557
  writeFileSync(recordPath, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
342
558
 
343
559
  // 2. credentials.yaml console_url (rung 2 of the shared resolver).
@@ -354,6 +570,7 @@ export async function applyDevMode(
354
570
  const res = await postCloud(token);
355
571
  if (res.ok) cloud.updated = true;
356
572
  else cloud.error = `HTTP ${res.status}`;
573
+ await discardResponseBody(res);
357
574
  } catch (err) {
358
575
  cloud.error = String(err?.message ?? err);
359
576
  }