heyspex 0.1.0 → 0.2.0

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 (322) hide show
  1. package/README.md +21 -0
  2. package/bin/hs.mjs +931 -98
  3. package/bin/spexcode.mjs +262 -0
  4. package/dev-factory/engine/worktree.mjs +740 -0
  5. package/dev-factory/lib/code-repo-root.mjs +67 -0
  6. package/dev-factory/lib/context-budget.mjs +84 -0
  7. package/dev-factory/lib/epic-detect.mjs +190 -0
  8. package/dev-factory/lib/fleet-queue-file.mjs +846 -0
  9. package/dev-factory/lib/fleet-queue.mjs +426 -0
  10. package/dev-factory/lib/fleet-state.mjs +14 -18
  11. package/dev-factory/lib/model-catalog.mjs +338 -0
  12. package/dev-factory/lib/seat-context.mjs +199 -0
  13. package/dev-factory/lib/spec-io.mjs +93 -0
  14. package/dev-factory/lib/sufficiency-score.mjs +8 -2
  15. package/dev-factory/lib/worktree-allocator.mjs +744 -0
  16. package/dev-factory/model-catalog.json +222 -0
  17. package/dist/cli/hs-check.mjs +1085 -103
  18. package/dist/cli/hs-claim.mjs +936 -63
  19. package/dist/cli/hs-connect.mjs +9786 -1910
  20. package/dist/cli/hs-defer.mjs +5267 -3053
  21. package/dist/cli/hs-dev.mjs +564 -0
  22. package/dist/cli/hs-env.mjs +385 -299
  23. package/dist/cli/hs-index-rebuild.mjs +7129 -0
  24. package/dist/cli/hs-index-reconcile.mjs +7852 -0
  25. package/dist/cli/hs-init.mjs +777 -138
  26. package/dist/cli/hs-install.mjs +6583 -485
  27. package/dist/cli/hs-join.mjs +10137 -2368
  28. package/dist/cli/hs-vault-pull.mjs +4659 -5882
  29. package/dist/cli/hs-vault.mjs +3793 -1537
  30. package/dist/cli/hs-workspace.mjs +2550 -524
  31. package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
  32. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
  33. package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
  34. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
  35. package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
  36. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
  37. package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
  38. package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
  39. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
  40. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
  41. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
  42. package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
  43. package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
  44. package/dist/cli-templates/harness-commands/claude/hs-ground.md +28 -0
  45. package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
  46. package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
  47. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
  48. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
  49. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
  50. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
  51. package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
  52. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
  53. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
  54. package/dist/cli-templates/harness-commands/claude/hs-view.md +26 -0
  55. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
  56. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
  57. package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
  58. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
  59. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
  60. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
  61. package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
  62. package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
  63. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +31 -0
  64. package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
  65. package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
  66. package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
  67. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
  68. package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
  69. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
  70. package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
  71. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
  72. package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
  73. package/dist/cli-templates/harness-commands/cursor/hs-view.md +30 -0
  74. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
  75. package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
  76. package/docs/mcp-client-config.md +259 -6
  77. package/package.json +231 -124
  78. package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
  79. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
  80. package/plugins/heyspex/.mcp.json +12 -0
  81. package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
  82. package/plugins/heyspex/bin/run-hook.mjs +62 -0
  83. package/plugins/heyspex/hooks/cursor-hooks.json +227 -0
  84. package/plugins/heyspex/hooks/hooks.json +361 -0
  85. package/plugins/heyspex/mcp.json +13 -0
  86. package/scripts/check-mcp-freshness.mjs +17 -9
  87. package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
  88. package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
  89. package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
  90. package/scripts/claude-hooks/_intent-classifier-router.mjs +410 -0
  91. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +351 -0
  92. package/scripts/claude-hooks/_run.mjs +120 -0
  93. package/scripts/claude-hooks/_transcript-text.mjs +60 -0
  94. package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
  95. package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
  96. package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
  97. package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
  98. package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
  99. package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
  100. package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
  101. package/scripts/claude-hooks/claim-liveness-shared.mjs +575 -0
  102. package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
  103. package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
  104. package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
  105. package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
  106. package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
  107. package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
  108. package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
  109. package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
  110. package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
  111. package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
  112. package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
  113. package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
  114. package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
  115. package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
  116. package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
  117. package/scripts/claude-hooks/inject-communication-style.mjs +66 -0
  118. package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
  119. package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
  120. package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
  121. package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
  122. package/scripts/claude-hooks/intent-classifier-trigger.mjs +247 -0
  123. package/scripts/claude-hooks/log-session-activity.mjs +341 -0
  124. package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
  125. package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
  126. package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
  127. package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
  128. package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
  129. package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
  130. package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
  131. package/scripts/claude-hooks/research-method-guard.mjs +398 -0
  132. package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
  133. package/scripts/claude-hooks/session-end.mjs +449 -0
  134. package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
  135. package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
  136. package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
  137. package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
  138. package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
  139. package/scripts/claude-hooks/wip-checkpoint.mjs +298 -0
  140. package/scripts/cursor-hooks/manifest.mjs +235 -0
  141. package/scripts/dev-server-status.mjs +34 -13
  142. package/scripts/dev-server-stop.mjs +313 -0
  143. package/scripts/heyspex-mode.mjs +268 -0
  144. package/scripts/hs-auto-resolve.mjs +240 -0
  145. package/scripts/hs-auto-source.mjs +398 -0
  146. package/scripts/hs-auto.mjs +134 -0
  147. package/scripts/hs-capture-cli.mjs +326 -0
  148. package/scripts/hs-classifier.mjs +136 -0
  149. package/scripts/hs-companion.mjs +88 -0
  150. package/scripts/hs-gateway.mjs +115 -0
  151. package/scripts/hs-mode.mjs +125 -0
  152. package/scripts/hs-next.mjs +441 -0
  153. package/scripts/hs-settings.mjs +338 -0
  154. package/scripts/hs-view.mjs +333 -0
  155. package/scripts/hs-worktree.mjs +201 -0
  156. package/scripts/install-claude-code-hooks.mjs +270 -0
  157. package/scripts/install-cursor-hooks.mjs +471 -0
  158. package/scripts/lib/agent-env-markers.mjs +38 -0
  159. package/scripts/lib/console-url-resolver.mjs +197 -60
  160. package/scripts/lib/customer-kit.mjs +886 -0
  161. package/scripts/lib/dev-mode.mjs +244 -28
  162. package/scripts/lib/heyspex-home.mjs +72 -0
  163. package/scripts/lib/hs-command.mjs +277 -0
  164. package/scripts/lib/hs-companion-settings.mjs +472 -0
  165. package/scripts/lib/hs-source-commit.mjs +59 -5
  166. package/scripts/lib/hs-source-identity.mjs +526 -0
  167. package/scripts/lib/hs-source-log.mjs +11 -21
  168. package/scripts/lib/hs-source-push.mjs +107 -58
  169. package/scripts/lib/hs-worktree-args.mjs +115 -0
  170. package/scripts/lib/shared-path-helpers.mjs +134 -0
  171. package/scripts/lib/standalone-doctor.mjs +540 -3
  172. package/scripts/lib/subagent-git-lock.mjs +109 -0
  173. package/scripts/lib/template-pin.mjs +34 -0
  174. package/scripts/provenance-lib.mjs +91 -0
  175. package/scripts/stamp-provenance-on-edit.mjs +222 -0
  176. package/scripts/wip-ledger-health.mjs +653 -0
  177. package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
  178. package/spexharness/src/adapters/audit.mjs +197 -0
  179. package/spexharness/src/adapters/claude-code.mjs +441 -0
  180. package/spexharness/src/chat/tui/ansi.mjs +448 -0
  181. package/spexharness/src/chat/tui/palette.mjs +22 -0
  182. package/spexharness/src/chat/tui/select.mjs +208 -0
  183. package/spexharness/src/config-root.mjs +54 -0
  184. package/spexharness/src/context-budget.mjs +141 -0
  185. package/spexharness/src/dev-server-control.mjs +389 -0
  186. package/spexharness/src/doctrine/behaviour.mjs +371 -0
  187. package/spexharness/src/doctrine/corpus.mjs +733 -0
  188. package/spexharness/src/doctrine/inject.mjs +338 -0
  189. package/spexharness/src/doctrine/merge.mjs +184 -0
  190. package/spexharness/src/doctrine/sources.mjs +252 -0
  191. package/spexharness/src/doctrine/synthesize.mjs +401 -0
  192. package/spexharness/src/doctrine/vault-source.mjs +574 -0
  193. package/spexharness/src/doctrine/writer.mjs +176 -0
  194. package/spexharness/src/events.mjs +84 -0
  195. package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
  196. package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
  197. package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
  198. package/spexharness/src/gates/deep-research-cost-gate.mjs +274 -0
  199. package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
  200. package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
  201. package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
  202. package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
  203. package/spexharness/src/gates/git-identity-guard.mjs +61 -0
  204. package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
  205. package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
  206. package/spexharness/src/gates/git-stash-gate.mjs +159 -0
  207. package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
  208. package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
  209. package/spexharness/src/gates/inject-vault-context.mjs +239 -0
  210. package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
  211. package/spexharness/src/gates/plan-review-gate.mjs +233 -0
  212. package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
  213. package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
  214. package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
  215. package/spexharness/src/gates/settings-write-gate.mjs +104 -0
  216. package/spexharness/src/git-reflog-audit.mjs +294 -0
  217. package/spexharness/src/intent-classifier.mjs +194 -0
  218. package/spexharness/src/intent-directive.mjs +541 -0
  219. package/spexharness/src/profile/learn.mjs +551 -0
  220. package/spexharness/src/profile/proposals.mjs +357 -0
  221. package/spexharness/src/profile/vault-inclusion.mjs +386 -0
  222. package/spexharness/src/redact.mjs +151 -0
  223. package/spexharness/src/rule-table.mjs +819 -0
  224. package/spexharness/src/seat-context.mjs +122 -0
  225. package/spexharness/src/session-bootstrap.mjs +512 -0
  226. package/spexharness/src/session-record.mjs +815 -0
  227. package/spexharness/src/token-inventory.mjs +207 -0
  228. package/vendor/heyspex-mcp/account-switch.mjs +95 -0
  229. package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
  230. package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
  231. package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
  232. package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
  233. package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
  234. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +46 -5
  235. package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
  236. package/vendor/heyspex-mcp/capture-console-client.mjs +80 -1
  237. package/vendor/heyspex-mcp/capture-focus.mjs +1035 -29
  238. package/vendor/heyspex-mcp/capture-outbox.mjs +5 -1
  239. package/vendor/heyspex-mcp/capture-tools.mjs +1219 -0
  240. package/vendor/heyspex-mcp/claim-tools.mjs +1124 -0
  241. package/vendor/heyspex-mcp/cli-verbs.mjs +2076 -0
  242. package/vendor/heyspex-mcp/cli.mjs +201 -15524
  243. package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
  244. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +412 -0
  245. package/vendor/heyspex-mcp/config-seed.yaml +15 -0
  246. package/vendor/heyspex-mcp/config-settings.mjs +290 -7
  247. package/vendor/heyspex-mcp/console-fetch.mjs +657 -16
  248. package/vendor/heyspex-mcp/console-transport.mjs +166 -0
  249. package/vendor/heyspex-mcp/credentials.mjs +584 -0
  250. package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
  251. package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
  252. package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
  253. package/vendor/heyspex-mcp/design-tools.mjs +4 -1
  254. package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
  255. package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
  256. package/vendor/heyspex-mcp/dev-tools.mjs +124 -0
  257. package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
  258. package/vendor/heyspex-mcp/events-tools.mjs +207 -0
  259. package/vendor/heyspex-mcp/filing-tools.mjs +688 -0
  260. package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
  261. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +69 -14
  262. package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
  263. package/vendor/heyspex-mcp/folder-trust-store.mjs +59 -0
  264. package/vendor/heyspex-mcp/http-transport.mjs +300 -0
  265. package/vendor/heyspex-mcp/list-tools.mjs +257 -0
  266. package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
  267. package/vendor/heyspex-mcp/mcp-config.mjs +94 -0
  268. package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
  269. package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
  270. package/vendor/heyspex-mcp/occupied-keys.mjs +141 -0
  271. package/vendor/heyspex-mcp/on-deck-brief.mjs +3360 -0
  272. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
  273. package/vendor/heyspex-mcp/package-root.mjs +32 -0
  274. package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
  275. package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
  276. package/vendor/heyspex-mcp/plan-push.mjs +906 -21
  277. package/vendor/heyspex-mcp/plan-review.mjs +124 -30
  278. package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
  279. package/vendor/heyspex-mcp/procedure-handlers.mjs +325 -0
  280. package/vendor/heyspex-mcp/procedure-tools.mjs +540 -351
  281. package/vendor/heyspex-mcp/promote-capture.mjs +386 -69
  282. package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
  283. package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
  284. package/vendor/heyspex-mcp/promote-research.mjs +660 -53
  285. package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
  286. package/vendor/heyspex-mcp/prompts.mjs +1460 -0
  287. package/vendor/heyspex-mcp/read-path.mjs +29 -9
  288. package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
  289. package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
  290. package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
  291. package/vendor/heyspex-mcp/register-spec.mjs +266 -54
  292. package/vendor/heyspex-mcp/request-context.mjs +59 -0
  293. package/vendor/heyspex-mcp/research-focus.mjs +207 -4
  294. package/vendor/heyspex-mcp/research-handlers.mjs +623 -0
  295. package/vendor/heyspex-mcp/research-tools.mjs +4 -4
  296. package/vendor/heyspex-mcp/response-budget.mjs +348 -0
  297. package/vendor/heyspex-mcp/review-focus.mjs +25 -1
  298. package/vendor/heyspex-mcp/search-specs.mjs +280 -0
  299. package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
  300. package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
  301. package/vendor/heyspex-mcp/session-owner.mjs +108 -0
  302. package/vendor/heyspex-mcp/session-tools.mjs +152 -0
  303. package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
  304. package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
  305. package/vendor/heyspex-mcp/spec-edit-tools.mjs +446 -0
  306. package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
  307. package/vendor/heyspex-mcp/split-research.mjs +301 -56
  308. package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
  309. package/vendor/heyspex-mcp/submit-for-review.mjs +132 -6
  310. package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
  311. package/vendor/heyspex-mcp/summary-write-tools.mjs +224 -0
  312. package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
  313. package/vendor/heyspex-mcp/test-helpers.mjs +108 -1
  314. package/vendor/heyspex-mcp/tool-catalog.mjs +2813 -0
  315. package/vendor/heyspex-mcp/tool-dispatch.mjs +639 -0
  316. package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
  317. package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
  318. package/vendor/heyspex-mcp/vault-io.mjs +724 -0
  319. package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
  320. package/vendor/heyspex-mcp/vault-resolve.mjs +366 -68
  321. package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
  322. package/vendor/heyspex-mcp/whoami.mjs +91 -0
@@ -12,23 +12,25 @@
12
12
  * heyspex_design_* family is the canonical template this module mirrors).
13
13
  */
14
14
 
15
- import { existsSync, readFileSync } from 'node:fs';
16
- import { join } from 'node:path';
15
+ import { existsSync, readFileSync } from "node:fs";
16
+ import { dirname, join, resolve } from "node:path";
17
+ import { fileURLToPath } from "node:url";
17
18
 
18
- import { readViaSelector } from './read-path.mjs';
19
+ import { readViaSelector } from "./read-path.mjs";
20
+ import { pageListToolResult } from "./list-tools.mjs";
19
21
 
20
- const CATALOG_FILE = ['specs', 'protocols', '.catalog.json'];
21
- const PROTOCOLS_DIR = ['specs', 'protocols'];
22
+ const CATALOG_FILE = ["specs", "protocols", ".catalog.json"];
23
+ const PROTOCOLS_DIR = ["specs", "protocols"];
22
24
 
23
25
  function normalize(value) {
24
- return String(value ?? '').toLowerCase();
26
+ return String(value ?? "").toLowerCase();
25
27
  }
26
28
 
27
29
  export function loadCatalog(vaultRoot) {
28
30
  const path = join(vaultRoot, ...CATALOG_FILE);
29
31
  if (!existsSync(path)) return null;
30
32
  try {
31
- return JSON.parse(readFileSync(path, 'utf8'));
33
+ return JSON.parse(readFileSync(path, "utf8"));
32
34
  } catch {
33
35
  return null;
34
36
  }
@@ -41,7 +43,7 @@ function summarize(entry) {
41
43
  title: entry.title,
42
44
  tags: Array.isArray(entry.tags) ? entry.tags : [],
43
45
  triggers: Array.isArray(entry.triggers) ? entry.triggers : [],
44
- summary: entry.summary ?? '',
46
+ summary: entry.summary ?? "",
45
47
  path: entry.path,
46
48
  };
47
49
  }
@@ -54,15 +56,18 @@ function summarize(entry) {
54
56
  * preserves today's disk-only behavior byte-for-byte.
55
57
  */
56
58
  export function executeListProcedures(vaultRoot, args = {}, injectedCatalog) {
57
- const catalog = injectedCatalog !== undefined ? injectedCatalog : loadCatalog(vaultRoot);
59
+ const catalog =
60
+ injectedCatalog !== undefined ? injectedCatalog : loadCatalog(vaultRoot);
58
61
  if (!catalog) {
59
62
  return {
60
- error: 'CATALOG_NOT_FOUND',
63
+ error: "CATALOG_NOT_FOUND",
61
64
  message:
62
- 'specs/protocols/.catalog.json not found. Run `node scripts/build-procedure-catalog.mjs`.',
65
+ "specs/protocols/.catalog.json not found. Run `node scripts/build-procedure-catalog.mjs`.",
63
66
  };
64
67
  }
65
- const procedures = Array.isArray(catalog.procedures) ? catalog.procedures : [];
68
+ const procedures = Array.isArray(catalog.procedures)
69
+ ? catalog.procedures
70
+ : [];
66
71
  const tagFilter = args.tag ? normalize(args.tag) : null;
67
72
  const triggerFilter = args.trigger ? normalize(args.trigger) : null;
68
73
  const queryFilter = args.query ? normalize(args.query) : null;
@@ -80,59 +85,68 @@ export function executeListProcedures(vaultRoot, args = {}, injectedCatalog) {
80
85
  if (queryFilter) {
81
86
  const haystack = [entry.name, entry.title, entry.summary]
82
87
  .map(normalize)
83
- .join(' ');
88
+ .join(" ");
84
89
  if (!haystack.includes(queryFilter)) continue;
85
90
  }
86
91
  matches.push(summarize(entry));
87
92
  }
88
93
 
89
- matches.sort((a, b) => a.name.localeCompare(b.name));
90
-
91
- return {
92
- procedures: matches,
93
- count: matches.length,
94
- total: procedures.length,
95
- filters_applied: {
96
- tag: args.tag ?? null,
97
- trigger: args.trigger ?? null,
98
- query: args.query ?? null,
94
+ // One page on a (name, id) keyset cursor, fitted to the byte budget of the
95
+ // tool — never the whole catalog, never a total count
96
+ // ([[HS-E66W-8KK5-TASK-mcp-list-tools-page-with-a-cursor-and-a]]).
97
+ const paged = pageListToolResult("heyspex_list_procedures", matches, args, {
98
+ keyOf: (p) => p.name,
99
+ idOf: (p) => p.id ?? p.name,
100
+ extra: {
101
+ filters_applied: {
102
+ tag: args.tag ?? null,
103
+ trigger: args.trigger ?? null,
104
+ query: args.query ?? null,
105
+ },
99
106
  },
100
- };
107
+ });
108
+ if (paged.error) return paged.error;
109
+ return paged.result;
101
110
  }
102
111
 
103
112
  function findEntry(catalog, name) {
104
- const procedures = Array.isArray(catalog.procedures) ? catalog.procedures : [];
113
+ const procedures = Array.isArray(catalog.procedures)
114
+ ? catalog.procedures
115
+ : [];
105
116
  return procedures.find((p) => p.name === name) || null;
106
117
  }
107
118
 
108
119
  export function executeGetProcedure(vaultRoot, args = {}) {
109
- const name = String(args.name ?? '').trim();
120
+ const name = String(args.name ?? "").trim();
110
121
  if (!name) {
111
- return { error: 'MISSING_NAME', message: 'name is required (e.g. "spec-hierarchy")' };
122
+ return {
123
+ error: "MISSING_NAME",
124
+ message: 'name is required (e.g. "spec-hierarchy")',
125
+ };
112
126
  }
113
127
  const catalog = loadCatalog(vaultRoot);
114
128
  if (!catalog) {
115
129
  return {
116
- error: 'CATALOG_NOT_FOUND',
130
+ error: "CATALOG_NOT_FOUND",
117
131
  message:
118
- 'specs/protocols/.catalog.json not found. Run `node scripts/build-procedure-catalog.mjs`.',
132
+ "specs/protocols/.catalog.json not found. Run `node scripts/build-procedure-catalog.mjs`.",
119
133
  };
120
134
  }
121
135
  const entry = findEntry(catalog, name);
122
136
  if (!entry) {
123
137
  return {
124
- error: 'PROCEDURE_NOT_FOUND',
138
+ error: "PROCEDURE_NOT_FOUND",
125
139
  message: `No procedure named "${name}" in catalog. Call heyspex_list_procedures to browse.`,
126
140
  };
127
141
  }
128
142
  const bodyPath = join(vaultRoot, ...PROTOCOLS_DIR, `${name}.md`);
129
143
  if (!existsSync(bodyPath)) {
130
144
  return {
131
- error: 'BODY_FILE_MISSING',
145
+ error: "BODY_FILE_MISSING",
132
146
  message: `Catalog entry exists but ${entry.path} not found on disk. Regenerate the catalog.`,
133
147
  };
134
148
  }
135
- const content = readFileSync(bodyPath, 'utf8');
149
+ const content = readFileSync(bodyPath, "utf8");
136
150
  return {
137
151
  procedure: {
138
152
  name: entry.name,
@@ -140,7 +154,7 @@ export function executeGetProcedure(vaultRoot, args = {}) {
140
154
  title: entry.title,
141
155
  tags: Array.isArray(entry.tags) ? entry.tags : [],
142
156
  triggers: Array.isArray(entry.triggers) ? entry.triggers : [],
143
- summary: entry.summary ?? '',
157
+ summary: entry.summary ?? "",
144
158
  path: entry.path,
145
159
  status: entry.status ?? null,
146
160
  created: entry.created ?? null,
@@ -152,395 +166,471 @@ export function executeGetProcedure(vaultRoot, args = {}) {
152
166
  };
153
167
  }
154
168
 
169
+ /**
170
+ * Procedures every customer vault ships (the universal template protocols; a
171
+ * drift test pins this list to the template manifest's protocol dests). Every
172
+ * other PREFLIGHT_INTENT_MAP procedure is platform-only: a vault whose runtime
173
+ * catalog lacks it gets the row DEMOTED to advisory by {@link resolveRow}, never
174
+ * deleted. Promoting more procedures to universal is a later template version.
175
+ */
176
+ export const UNIVERSAL_PROCEDURES = Object.freeze([
177
+ "canonical-frontmatter-templates",
178
+ "spec-hierarchy",
179
+ "memory-protocol",
180
+ "session-close-protocol",
181
+ "spec-summary-authoring",
182
+ "destructive-action-checklist",
183
+ ]);
184
+
185
+ /** Audience of a preflight row, by procedure name. */
186
+ export function procedureAudience(name) {
187
+ return UNIVERSAL_PROCEDURES.includes(name) ? "universal" : "platform";
188
+ }
189
+
190
+ const DEMOTED_REASON = "not in this vault's catalog (platform-only procedure)";
191
+
192
+ /**
193
+ * Resolve one preflight row against the supplied (runtime) catalog. Shared by
194
+ * the static intent path AND the close_session thread-type path. A row whose
195
+ * procedure is present is returned exactly as before; an absent one is demoted
196
+ * to `advisory` (original severity kept in `demoted_from`) with
197
+ * `missing_from_catalog: true` still set for consumers.
198
+ */
199
+ export function resolveRow(catalog, row) {
200
+ const entry = findEntry(catalog, row.procedure);
201
+ const base = {
202
+ procedure: row.procedure,
203
+ ...(row.why !== undefined ? { why: row.why } : {}),
204
+ severity: row.severity,
205
+ path: entry?.path ?? null,
206
+ title: entry?.title ?? null,
207
+ missing_from_catalog: entry ? undefined : true,
208
+ };
209
+ if (entry) return base;
210
+ return {
211
+ ...base,
212
+ severity: "advisory",
213
+ demoted_from: row.severity,
214
+ reason: DEMOTED_REASON,
215
+ };
216
+ }
217
+
155
218
  // Bootstrap intent → procedure map (eight intents per HS-EPIC-3QS8 §Open
156
219
  // questions "Trigger taxonomy"). Each row names the procedure slug (matches
157
220
  // specs/protocols/<slug>.md), a `why` blurb the agent can quote, and a
158
221
  // severity. Extension rule: add a new intent when a third unique use case
159
222
  // appears — new intents land in their own tasks, not edits to this map.
160
223
  const PREFLIGHT_INTENT_MAP = {
224
+ // [[HS-K0ZP]]: "about to do research" is a governed trigger. Running this
225
+ // preflight writes the proof-of-use marker that unlocks WebSearch / WebFetch /
226
+ // heyspex_save_research for the session (spexharness/src/gates/research-protocol-gate.mjs
227
+ // on every PreToolUse front, preflight-gate.mjs inside the MCP server). Before
228
+ // it, those calls are refused -- the protocol was prose an agent could skip.
229
+ research: [
230
+ {
231
+ procedure: "research-capture-protocol",
232
+ why: "Method triage FIRST (known-target-retrieval vs the confidence ladder), ground-first (heyspex_recall_memories + /hs-ground) before any external pass, and the offer-before-save step: a web search offers to save findings, it does not auto-file a research doc.",
233
+ severity: "required",
234
+ },
235
+ {
236
+ procedure: "capture-guardrails",
237
+ why: "A known-target lookup never enters the research-doc pipeline; the exit for a quick retrieval is a memory-reference or a fold into the consuming spec.",
238
+ severity: "recommended",
239
+ },
240
+ ],
161
241
  promote_research: [
162
242
  {
163
- procedure: 'research-capture-protocol',
164
- why: 'Confirm the research doc is complete (prerequisites, conclusion, sources) before promotion.',
165
- severity: 'required',
243
+ procedure: "research-capture-protocol",
244
+ why: "Confirm the research doc is complete (prerequisites, conclusion, sources) before promotion.",
245
+ severity: "required",
166
246
  },
167
247
  {
168
- procedure: 'spec-hierarchy',
169
- why: 'Mandatory default: target_milestone NEW. Existing-milestone attach only when status: open and thesis clearly extends; in_progress requires explicit owner override.',
170
- severity: 'required',
248
+ procedure: "spec-hierarchy",
249
+ why: "Mandatory default: target_milestone NEW. Existing-milestone attach only when status: open and thesis clearly extends; in_progress requires explicit owner override.",
250
+ severity: "required",
171
251
  },
172
252
  {
173
- procedure: 'pillar-assignment-protocol',
174
- why: 'Every spawned epic must name a pillar before execution breakdown.',
175
- severity: 'recommended',
253
+ procedure: "pillar-assignment-protocol",
254
+ why: "Every spawned epic must name a pillar before execution breakdown.",
255
+ severity: "recommended",
176
256
  },
177
257
  ],
178
258
  file_iteration: [
179
259
  {
180
- procedure: 'supersession-protocol',
181
- why: '§Iterates-on: choose the right tier; a re-attempt is neither supersession nor amends.',
182
- severity: 'required',
260
+ procedure: "supersession-protocol",
261
+ why: "§Iterates-on: choose the right tier; a re-attempt is neither supersession nor amends.",
262
+ severity: "required",
183
263
  },
184
264
  {
185
- procedure: 'research-capture-protocol',
265
+ procedure: "research-capture-protocol",
186
266
  why: 'Promote flow asks "is this a re-attempt?" and populates iterates_on: — confirm iteration lineage is recorded.',
187
- severity: 'required',
267
+ severity: "required",
188
268
  },
189
269
  {
190
- procedure: 'internal-linking-protocol',
191
- why: 'Iteration structure and tier-based lineage tracking; see entity-model §Iteration lineage for tier semantics.',
192
- severity: 'recommended',
270
+ procedure: "internal-linking-protocol",
271
+ why: "Iteration structure and tier-based lineage tracking; see entity-model §Iteration lineage for tier semantics.",
272
+ severity: "recommended",
193
273
  },
194
274
  ],
195
275
  file_milestone: [
196
276
  {
197
- procedure: 'spec-hierarchy',
198
- why: 'Defines which level a new spec belongs at and the milestone frontmatter shape.',
199
- severity: 'required',
277
+ procedure: "spec-hierarchy",
278
+ why: "Defines which level a new spec belongs at and the milestone frontmatter shape.",
279
+ severity: "required",
200
280
  },
201
281
  {
202
- procedure: 'spec-summary-authoring',
203
- why: 'Every milestone needs a plain-language summary: at filing time — pass summary to heyspex_file_spec / POST /api/specs.',
204
- severity: 'required',
282
+ procedure: "spec-summary-authoring",
283
+ why: "Every milestone needs a plain-language summary: at filing time — pass summary to heyspex_file_spec / POST /api/specs.",
284
+ severity: "required",
205
285
  },
206
286
  {
207
- procedure: 'vault-id-assignment-protocol',
208
- why: 'Allocates a unique HS-MIL-<suffix> id and prevents duplicates.',
209
- severity: 'required',
287
+ procedure: "vault-id-assignment-protocol",
288
+ why: "Allocates a unique HS-MIL-<suffix> id and prevents duplicates.",
289
+ severity: "required",
210
290
  },
211
291
  ],
212
292
  file_epic: [
213
293
  {
214
- procedure: 'spec-hierarchy',
215
- why: 'Defines epic frontmatter, required parent-milestone link, and child layout.',
216
- severity: 'required',
294
+ procedure: "spec-hierarchy",
295
+ why: "Defines epic frontmatter, required parent-milestone link, and child layout.",
296
+ severity: "required",
217
297
  },
218
298
  {
219
- procedure: 'spec-summary-authoring',
220
- why: 'Every epic needs a plain-language summary at plan-ingest filing time — pass summary to heyspex_push_plan / heyspex_file_spec / POST /api/specs. Do not file tasks in the same pass.',
221
- severity: 'required',
299
+ procedure: "spec-summary-authoring",
300
+ why: "Every epic needs a plain-language summary at plan-ingest filing time — pass summary to heyspex_push_plan / heyspex_file_spec / POST /api/specs. Do not file tasks in the same pass.",
301
+ severity: "required",
222
302
  },
223
303
  {
224
- procedure: 'pillar-assignment-protocol',
225
- why: 'Every epic must name a pillar (hard-blocked at filing). Covers picking an existing HS-PILR theme or authoring a net-new one.',
226
- severity: 'required',
304
+ procedure: "pillar-assignment-protocol",
305
+ why: "Every epic must name a pillar (hard-blocked at filing). Covers picking an existing HS-PILR theme or authoring a net-new one.",
306
+ severity: "required",
227
307
  },
228
308
  {
229
- procedure: 'vault-id-assignment-protocol',
230
- why: 'Allocates a unique HS-EPIC-<suffix> id and prevents duplicates.',
231
- severity: 'required',
309
+ procedure: "vault-id-assignment-protocol",
310
+ why: "Allocates a unique HS-EPIC-<suffix> id and prevents duplicates.",
311
+ severity: "required",
232
312
  },
233
313
  {
234
- procedure: 'plan-ingest-agent-workflow',
235
- why: 'Linkage + manifest registration steps when ingesting an external plan into the epic.',
236
- severity: 'recommended',
314
+ procedure: "plan-ingest-agent-workflow",
315
+ why: "Linkage + manifest registration steps when ingesting an external plan into the epic.",
316
+ severity: "recommended",
237
317
  },
238
318
  {
239
- procedure: 'builder-fleet-capability-reference',
240
- why: 'Optional builder_profile + ## Builder fleet notes at epic filing — anticipate local builder model class and context tier.',
241
- severity: 'recommended',
319
+ procedure: "builder-fleet-capability-reference",
320
+ why: "Optional builder_profile + ## Builder fleet notes at epic filing — anticipate local builder model class and context tier.",
321
+ severity: "recommended",
242
322
  },
243
323
  ],
244
324
  file_task: [
245
325
  {
246
- procedure: 'spec-hierarchy',
247
- why: 'Defines task frontmatter, required parent-epic link, and child-task fan-out rules.',
248
- severity: 'required',
326
+ procedure: "spec-hierarchy",
327
+ why: "Defines task frontmatter, required parent-epic link, and child-task fan-out rules.",
328
+ severity: "required",
249
329
  },
250
330
  {
251
- procedure: 'builder-fleet-capability-reference',
252
- why: 'Required ## Builder brief + dispatch frontmatter (suggested_model, recommended_num_ctx, context_files) for fleet-calibrated tasks.',
253
- severity: 'required',
331
+ procedure: "builder-fleet-capability-reference",
332
+ why: "Required ## Builder brief + dispatch frontmatter (suggested_model, recommended_num_ctx, context_files) for fleet-calibrated tasks.",
333
+ severity: "required",
254
334
  },
255
335
  {
256
- procedure: 'spec-summary-authoring',
257
- why: 'Every task needs a plain-language summary at the /hs-next breakdown moment — pass summary to heyspex_file_spec / POST /api/specs. Tasks are never filed during plan ingest.',
258
- severity: 'required',
336
+ procedure: "spec-summary-authoring",
337
+ why: "Every task needs a plain-language summary at the /hs-next breakdown moment — pass summary to heyspex_file_spec / POST /api/specs. Tasks are never filed during plan ingest.",
338
+ severity: "required",
259
339
  },
260
340
  {
261
- procedure: 'vault-id-assignment-protocol',
262
- why: 'Allocates a unique HS-TASK-<suffix> id and prevents duplicates.',
263
- severity: 'required',
341
+ procedure: "vault-id-assignment-protocol",
342
+ why: "Allocates a unique HS-TASK-<suffix> id and prevents duplicates.",
343
+ severity: "required",
264
344
  },
265
345
  {
266
- procedure: 'checks-manifest-authoring',
267
- why: 'Every task ships a FROZEN specs/checks/<task-id>.checks.json — each AC classified machine|property|human with named runnable checks, authored at breakdown before any builder sees the brief. A task without one is not ready to dispatch.',
268
- severity: 'required',
346
+ procedure: "checks-manifest-authoring",
347
+ why: "Every task ships a FROZEN specs/checks/<task-id>.checks.json — each AC classified machine|property|human with named runnable checks, authored at breakdown before any builder sees the brief. A task without one is not ready to dispatch.",
348
+ severity: "required",
269
349
  },
270
350
  {
271
- procedure: 'unit-test-authoring-protocol',
272
- why: 'How the tests behind the manifest checks are written: pure-core/thin-shell, colocation, the five case families, and the mutation gate as the quality oracle.',
273
- severity: 'recommended',
351
+ procedure: "unit-test-authoring-protocol",
352
+ why: "How the tests behind the manifest checks are written: pure-core/thin-shell, colocation, the five case families, and the mutation gate as the quality oracle.",
353
+ severity: "recommended",
274
354
  },
275
355
  ],
276
356
  file_diagnose: [
277
357
  {
278
- procedure: 'spec-hierarchy',
279
- why: 'Defines diagnose frontmatter, the standalone-default / optional epic-or-milestone parent rule, and severity.',
280
- severity: 'required',
358
+ procedure: "spec-hierarchy",
359
+ why: "Defines diagnose frontmatter, the standalone-default / optional epic-or-milestone parent rule, and severity.",
360
+ severity: "required",
281
361
  },
282
362
  {
283
- procedure: 'vault-id-assignment-protocol',
284
- why: 'Allocates a unique HS-DIAG-<suffix> id and prevents duplicates.',
285
- severity: 'required',
363
+ procedure: "vault-id-assignment-protocol",
364
+ why: "Allocates a unique HS-DIAG-<suffix> id and prevents duplicates.",
365
+ severity: "required",
286
366
  },
287
367
  {
288
- procedure: 'resolution-diagnose-workflow',
289
- why: 'The end-to-end bug→diagnose procedure: intake, §Step 2 symptom-vs-core classification (if fixing exactly this leaves the same class of failure possible, you are still on the symptom), severity ranking, §Step 6 sibling sweep — which now owes a receipt in the body\'s ## Sweep section — and close-out.',
290
- severity: 'required',
368
+ procedure: "resolution-diagnose-workflow",
369
+ why: "The end-to-end bug→diagnose procedure: intake, §Step 2 symptom-vs-core classification (if fixing exactly this leaves the same class of failure possible, you are still on the symptom), severity ranking, §Step 6 sibling sweep — which now owes a receipt in the body's ## Sweep section — and close-out.",
370
+ severity: "required",
291
371
  },
292
372
  {
293
- procedure: 'checks-manifest-authoring',
373
+ procedure: "checks-manifest-authoring",
294
374
  why: 'A bug is the case where a regression check is worth the most and costs the least — the reproduction is already written. Freeze one runnable check per acceptance criterion via `checks:` on heyspex_file_spec, and make at least one of them FAIL when the fix is reverted; "the test passes now" is also what a symptom-level fix produces. REQUIRED WITH AN OUT: a trivial bug may file with no manifest — filing still succeeds and returns a `no_checks_manifest` warning — but skipping is a decision to state, not a step to forget.',
295
- severity: 'required',
375
+ severity: "required",
296
376
  },
297
377
  ],
298
378
  file_research: [
299
379
  {
300
- procedure: 'spec-hierarchy',
301
- why: 'Confirms research is a standalone entity (HS-RES-<id>), how it links back to consumers, and that promotion defaults to a NEW milestone.',
302
- severity: 'required',
380
+ procedure: "capture-guardrails",
381
+ why: "A brand-new idea surfacing in conversation (no existing HS-CAP-*/HS-RES-* id already in play) goes through Capture → Ground=Expand → Promote-to-research first via heyspex_active_capture — never straight to file_research/research dispatch tooling. Skip only when the research target already has an existing capture or research id.",
382
+ severity: "recommended",
383
+ },
384
+ {
385
+ procedure: "spec-hierarchy",
386
+ why: "Confirms research is a standalone entity (HS-RES-<id>), how it links back to consumers, and that promotion defaults to a NEW milestone.",
387
+ severity: "required",
303
388
  },
304
389
  {
305
- procedure: 'research-capture-protocol',
306
- why: 'Required template (Executive Summary + Sources) and the back-wikilink + maintenance rules for a research doc.',
307
- severity: 'required',
390
+ procedure: "research-capture-protocol",
391
+ why: "Required template (Executive Summary + Sources) and the back-wikilink + maintenance rules for a research doc.",
392
+ severity: "required",
308
393
  },
309
394
  {
310
- procedure: 'vault-id-assignment-protocol',
311
- why: 'Allocates a unique HS-RES-<suffix> id and prevents duplicates.',
312
- severity: 'required',
395
+ procedure: "vault-id-assignment-protocol",
396
+ why: "Allocates a unique HS-RES-<suffix> id and prevents duplicates.",
397
+ severity: "required",
313
398
  },
314
399
  ],
315
400
  file_memory_lesson: [
316
401
  {
317
- procedure: 'memory-protocol',
318
- why: 'How to file a memory-lesson and link it back to the originating spec; the four-axis capture rubric.',
319
- severity: 'required',
402
+ procedure: "memory-protocol",
403
+ why: "How to file a memory-lesson and link it back to the originating spec; the four-axis capture rubric.",
404
+ severity: "required",
320
405
  },
321
406
  {
322
- procedure: 'vault-id-assignment-protocol',
323
- why: 'Allocates a unique memory slug/id and prevents duplicates.',
324
- severity: 'required',
407
+ procedure: "vault-id-assignment-protocol",
408
+ why: "Allocates a unique memory slug/id and prevents duplicates.",
409
+ severity: "required",
325
410
  },
326
411
  ],
327
412
  file_memory_decision: [
328
413
  {
329
- procedure: 'memory-protocol',
330
- why: 'How to file a memory-decision and link it back to the originating spec; the four-axis capture rubric.',
331
- severity: 'required',
414
+ procedure: "memory-protocol",
415
+ why: "How to file a memory-decision and link it back to the originating spec; the four-axis capture rubric.",
416
+ severity: "required",
332
417
  },
333
418
  {
334
- procedure: 'vault-id-assignment-protocol',
335
- why: 'Allocates a unique memory slug/id and prevents duplicates.',
336
- severity: 'required',
419
+ procedure: "vault-id-assignment-protocol",
420
+ why: "Allocates a unique memory slug/id and prevents duplicates.",
421
+ severity: "required",
337
422
  },
338
423
  ],
339
424
  file_memory_reference: [
340
425
  {
341
- procedure: 'memory-protocol',
342
- why: 'How to file a memory-reference and link it back to the originating spec; the four-axis capture rubric.',
343
- severity: 'required',
426
+ procedure: "memory-protocol",
427
+ why: "How to file a memory-reference and link it back to the originating spec; the four-axis capture rubric.",
428
+ severity: "required",
344
429
  },
345
430
  {
346
- procedure: 'vault-id-assignment-protocol',
347
- why: 'Allocates a unique memory slug/id and prevents duplicates.',
348
- severity: 'required',
431
+ procedure: "vault-id-assignment-protocol",
432
+ why: "Allocates a unique memory slug/id and prevents duplicates.",
433
+ severity: "required",
349
434
  },
350
435
  ],
351
436
  file_memory_ui_pattern: [
352
437
  {
353
- procedure: 'memory-protocol',
354
- why: 'How to file a memory-ui-pattern and link it back to the originating spec; the four-axis capture rubric.',
355
- severity: 'required',
438
+ procedure: "memory-protocol",
439
+ why: "How to file a memory-ui-pattern and link it back to the originating spec; the four-axis capture rubric.",
440
+ severity: "required",
356
441
  },
357
442
  {
358
- procedure: 'vault-id-assignment-protocol',
359
- why: 'Allocates a unique memory slug/id and prevents duplicates.',
360
- severity: 'required',
443
+ procedure: "vault-id-assignment-protocol",
444
+ why: "Allocates a unique memory slug/id and prevents duplicates.",
445
+ severity: "required",
361
446
  },
362
447
  ],
363
448
  file_review: [
364
449
  {
365
- procedure: 'session-close-protocol',
366
- why: 'The HS-REV companion doc shape (four required sections) and the close-out flow it belongs to.',
367
- severity: 'required',
450
+ procedure: "session-close-protocol",
451
+ why: "The HS-REV companion doc shape (four required sections) and the close-out flow it belongs to.",
452
+ severity: "required",
368
453
  },
369
454
  {
370
- procedure: 'spec-hierarchy',
455
+ procedure: "spec-hierarchy",
371
456
  why: 'Confirms the review parents the spec under review (parent: "[[<spec>]]" is required).',
372
- severity: 'recommended',
457
+ severity: "recommended",
373
458
  },
374
459
  ],
375
460
  file_capture: [
376
461
  {
377
- procedure: 'capture-guardrails',
378
- why: 'Captures are session-based and NOT research — human words stay verbatim in `## Notes`; AI authors separate layers on an explicit pass. Drive them via heyspex_active_capture, not heyspex_file_spec.',
379
- severity: 'required',
462
+ procedure: "capture-guardrails",
463
+ why: "Captures are session-based and NOT research — human words stay verbatim in `## Notes`; AI authors separate layers on an explicit pass. Drive them via heyspex_active_capture, not heyspex_file_spec.",
464
+ severity: "required",
380
465
  },
381
466
  ],
382
467
  destructive_action: [
383
468
  {
384
- procedure: 'destructive-action-checklist',
385
- why: 'Run before any PR that deletes, archives, or routes data-destroying actions.',
386
- severity: 'required',
469
+ procedure: "destructive-action-checklist",
470
+ why: "Run before any PR that deletes, archives, or routes data-destroying actions.",
471
+ severity: "required",
387
472
  },
388
473
  {
389
- procedure: 'dev-server-lifecycle-and-db-safety',
390
- why: 'If the action is a DB migration / Neon branch reset / .next delete, stop the running dev server first — check ground-truth status via heyspex_dev_server_status.',
391
- severity: 'recommended',
474
+ procedure: "dev-server-lifecycle-and-db-safety",
475
+ why: "If the action is a DB migration / Neon branch reset / .next delete, stop the running dev server first — check ground-truth status via heyspex_dev_server_status.",
476
+ severity: "recommended",
392
477
  },
393
478
  ],
394
479
  protocol_edit: [
395
480
  {
396
- procedure: 'spec-hierarchy',
481
+ procedure: "spec-hierarchy",
397
482
  why: '§Agent autonomy row "Modify a protocol doc" — protocol edits require explicit user approval.',
398
- severity: 'required',
483
+ severity: "required",
399
484
  },
400
485
  ],
401
486
  memory_creation: [
402
487
  {
403
- procedure: 'memory-protocol',
404
- why: 'How to file a lesson/decision/reference/ui-pattern memory and link it back to the originating spec.',
405
- severity: 'required',
488
+ procedure: "memory-protocol",
489
+ why: "How to file a lesson/decision/reference/ui-pattern memory and link it back to the originating spec.",
490
+ severity: "required",
406
491
  },
407
492
  ],
408
493
  close_session: [
409
494
  {
410
- procedure: 'session-close-protocol',
411
- why: 'Required agent close-out checklist (status flips, review-doc creation, hand-off).',
412
- severity: 'required',
495
+ procedure: "session-close-protocol",
496
+ why: "Required agent close-out checklist (status flips, review-doc creation, hand-off).",
497
+ severity: "required",
413
498
  },
414
499
  {
415
- procedure: 'memory-protocol',
416
- why: 'Self-check for learnings is step 0 of close-out — capture any non-obvious lesson/decision as a memory spec.',
417
- severity: 'recommended',
500
+ procedure: "memory-protocol",
501
+ why: "Self-check for learnings is step 0 of close-out — capture any non-obvious lesson/decision as a memory spec.",
502
+ severity: "recommended",
418
503
  },
419
504
  ],
420
505
  context_rollover: [
421
506
  {
422
- procedure: 'context-rollover-checkpoint',
423
- why: 'Lightweight mid-flight checkpoint when compaction interrupts implementation — record WIP + handoff, then resume. Do NOT flip status / write HS-REV (that is session close, not rollover).',
424
- severity: 'required',
507
+ procedure: "context-rollover-checkpoint",
508
+ why: "Lightweight mid-flight checkpoint when compaction interrupts implementation — record WIP + handoff, then resume. Do NOT flip status / write HS-REV (that is session close, not rollover).",
509
+ severity: "required",
425
510
  },
426
511
  ],
427
512
  claim_spec: [
428
513
  {
429
- procedure: 'spec-hierarchy',
430
- why: 'A spec is claimable iff it is its own current ClaimTarget — read §Claim eligibility before claiming an Epic/Milestone with open descendants; the console endpoint returns 409 claim_blocked_by_descendants otherwise ([[HS-TASK-0RVD]]).',
431
- severity: 'required',
514
+ procedure: "spec-hierarchy",
515
+ why: "A spec is claimable iff it is its own current ClaimTarget — read §Claim eligibility before claiming an Epic/Milestone with open descendants; the console endpoint returns 409 claim_blocked_by_descendants otherwise ([[HS-TASK-0RVD]]).",
516
+ severity: "required",
432
517
  },
433
518
  {
434
- procedure: 'pre-execution-alignment-gate',
435
- why: 'The /hs-next surface routes the claim through this gate; the brief sections (world_state_new / world_state_modified / graph_delta) and the 4-option menu are documented here.',
436
- severity: 'recommended',
519
+ procedure: "pre-execution-alignment-gate",
520
+ why: "The /hs-next surface routes the claim through this gate; the brief sections (world_state_new / world_state_modified / graph_delta) and the 4-option menu are documented here.",
521
+ severity: "recommended",
437
522
  },
438
523
  ],
439
524
  execute_spec: [
440
525
  {
441
- procedure: 'dev-factory-delegation-routing',
442
- why: 'The COO operating contract: delegation is table-driven. Before delegating build/breakdown/critic work, consult dev-factory/routing-table.json — build → free local fleet, breakdown/critic → DeepSeek V4, cloud/Opus escalation-only. NEVER spawn a Claude subagent on your own (expensive) tier for mechanical, gate-verified build.',
443
- severity: 'required',
526
+ procedure: "dev-factory-delegation-routing",
527
+ why: "The COO operating contract: delegation is table-driven. Before delegating build/breakdown/critic work, consult dev-factory/routing-table.json — build → free local fleet, breakdown/critic → DeepSeek V4, cloud/Opus escalation-only. NEVER spawn a Claude subagent on your own (expensive) tier for mechanical, gate-verified build.",
528
+ severity: "required",
444
529
  },
445
530
  {
446
- procedure: 'pre-execution-alignment-gate',
447
- why: 'The first pre-flight before executing an epic/milestone — break down a childless node or revisit a drifted breakdown; hard gate with override-with-reason.',
448
- severity: 'required',
531
+ procedure: "pre-execution-alignment-gate",
532
+ why: "The first pre-flight before executing an epic/milestone — break down a childless node or revisit a drifted breakdown; hard gate with override-with-reason.",
533
+ severity: "required",
449
534
  },
450
535
  {
451
- procedure: 'spec-hierarchy',
452
- why: 'Confirms the level you are executing and the parent/child linkage the breakdown must respect.',
453
- severity: 'recommended',
536
+ procedure: "spec-hierarchy",
537
+ why: "Confirms the level you are executing and the parent/child linkage the breakdown must respect.",
538
+ severity: "recommended",
454
539
  },
455
540
  {
456
- procedure: 'builder-fleet-capability-reference',
457
- why: 'Context tier ladder + task Builder brief contract when breaking down or dispatching to the local fleet.',
458
- severity: 'recommended',
541
+ procedure: "builder-fleet-capability-reference",
542
+ why: "Context tier ladder + task Builder brief contract when breaking down or dispatching to the local fleet.",
543
+ severity: "recommended",
459
544
  },
460
545
  {
461
- procedure: 'unit-test-authoring-protocol',
462
- why: 'Before writing any test: pure-core/thin-shell structure, colocated <module>.test.ts, the five case families every pure module covers, and the mutation gate as the test-quality oracle.',
463
- severity: 'recommended',
546
+ procedure: "unit-test-authoring-protocol",
547
+ why: "Before writing any test: pure-core/thin-shell structure, colocated <module>.test.ts, the five case families every pure module covers, and the mutation gate as the test-quality oracle.",
548
+ severity: "recommended",
464
549
  },
465
550
  {
466
- procedure: 'checks-manifest-authoring',
467
- why: 'The frozen specs/checks/<task-id>.checks.json is the external completion gate — builders satisfy its named checks and never edit it; hs check run <task-id> is the per-criterion verdict.',
468
- severity: 'recommended',
551
+ procedure: "checks-manifest-authoring",
552
+ why: "The frozen specs/checks/<task-id>.checks.json is the external completion gate — builders satisfy its named checks and never edit it; hs check run <task-id> is the per-criterion verdict.",
553
+ severity: "recommended",
469
554
  },
470
555
  {
471
- procedure: 'dev-server-lifecycle-and-db-safety',
472
- why: 'Execution often involves DB migrations / dependency installs / .next deletes that break a running dev server — check ground-truth status (heyspex_dev_server_status) and stop the server first.',
473
- severity: 'recommended',
556
+ procedure: "dev-server-lifecycle-and-db-safety",
557
+ why: "Execution often involves DB migrations / dependency installs / .next deletes that break a running dev server — check ground-truth status (heyspex_dev_server_status) and stop the server first.",
558
+ severity: "recommended",
474
559
  },
475
560
  ],
476
561
  exit_plan_mode: [
477
562
  {
478
- procedure: 'plan-quality-canonical-check',
479
- why: 'Run dual overlap+code audit and canonical/supersession check before exit; revise plan to V2.',
480
- severity: 'required',
563
+ procedure: "plan-quality-canonical-check",
564
+ why: "Run dual overlap+code audit and canonical/supersession check before exit; revise plan to V2.",
565
+ severity: "required",
481
566
  },
482
567
  {
483
- procedure: 'plan-ingest-agent-workflow',
484
- why: 'After the plan is approved, do NOT start coding. First ingest the approved plan into the vault as an epic (file it with children:[]; tasks fan out later via /hs-next) AND claim it via heyspex_claim_spec before executing. ExitPlanMode is the plan→execute seam — without this step agents skip filing+claiming and go straight to code.',
485
- severity: 'required',
568
+ procedure: "plan-ingest-agent-workflow",
569
+ why: "After the plan is approved, do NOT start coding. First ingest the approved plan into the vault as an epic (file it with children:[]; tasks fan out later via /hs-next) AND claim it via heyspex_claim_spec before executing. ExitPlanMode is the plan→execute seam — without this step agents skip filing+claiming and go straight to code.",
570
+ severity: "required",
486
571
  },
487
572
  {
488
- procedure: 'supersession-protocol',
489
- why: 'If the plan deprecates an existing spec, file the supersession edge per protocol.',
490
- severity: 'recommended',
573
+ procedure: "supersession-protocol",
574
+ why: "If the plan deprecates an existing spec, file the supersession edge per protocol.",
575
+ severity: "recommended",
491
576
  },
492
577
  ],
493
578
  recommend_location: [
494
579
  {
495
- procedure: 'rule-placement-protocol',
496
- why: 'Cost ranking for where a new rule belongs: procedure ≥ memory > spec > CLAUDE.md. CLAUDE.md is the most expensive surface (always-loaded, customer-inherited) — never the default.',
497
- severity: 'required',
580
+ procedure: "rule-placement-protocol",
581
+ why: "Cost ranking for where a new rule belongs: procedure ≥ memory > spec > CLAUDE.md. CLAUDE.md is the most expensive surface (always-loaded, customer-inherited) — never the default.",
582
+ severity: "required",
498
583
  },
499
584
  {
500
- procedure: 'memory-protocol',
501
- why: 'When the right home is a memory (rank 2 in rule-placement-protocol), this defines the lesson/decision/reference/ui-pattern shape and tagging conventions.',
502
- severity: 'recommended',
585
+ procedure: "memory-protocol",
586
+ why: "When the right home is a memory (rank 2 in rule-placement-protocol), this defines the lesson/decision/reference/ui-pattern shape and tagging conventions.",
587
+ severity: "recommended",
503
588
  },
504
589
  {
505
- procedure: 'foundations-placement-protocol',
506
- why: 'When the thing to place is a high-level vision/strategy concept (a principle, a north-star idea, a business claim) rather than an agent-behavior rule: ranks the foundations homes (North Star > Operating Model > Principles > Architecture > Positioning > Research) and gates a foundational edit behind user sign-off.',
507
- severity: 'recommended',
590
+ procedure: "foundations-placement-protocol",
591
+ why: "When the thing to place is a high-level vision/strategy concept (a principle, a north-star idea, a business claim) rather than an agent-behavior rule: ranks the foundations homes (North Star > Operating Model > Principles > Architecture > Positioning > Research) and gates a foundational edit behind user sign-off.",
592
+ severity: "recommended",
508
593
  },
509
594
  ],
510
595
  focus_pivot: [
511
596
  {
512
- procedure: 'focus-intent-routing',
597
+ procedure: "focus-intent-routing",
513
598
  why: 'How to detect a roadmap focus-pivot utterance and route it through heyspex_set_focus_areas — covers negation handling ("stop working on X" must NOT add X), the closed-tag-vocabulary constraint (heyspex_list_known_tags first), and the confirmation beat before applying.',
514
- severity: 'required',
599
+ severity: "required",
515
600
  },
516
601
  ],
517
602
  server_impacting_action: [
518
603
  {
519
- procedure: 'dev-server-lifecycle-and-db-safety',
520
- why: 'Before a DB migration, Neon branch reset, dependency install, .next delete, or starting a second dev server: check ground-truth status (heyspex_dev_server_status / `hs server status`) and stop a live server first — running a migration under a live server strands its in-memory schema and forces a restart.',
521
- severity: 'required',
604
+ procedure: "dev-server-lifecycle-and-db-safety",
605
+ why: "Before a DB migration, Neon branch reset, dependency install, .next delete, or starting a second dev server: check ground-truth status (heyspex_dev_server_status / `hs server status`) and stop a live server first — running a migration under a live server strands its in-memory schema and forces a restart.",
606
+ severity: "required",
522
607
  },
523
608
  ],
524
609
  restructure_content: [
525
610
  {
526
- procedure: 'internal-linking-protocol',
527
- why: 'Before re-parenting an epic, editing a milestone epics[] roster, or moving trackable work to a new canonical home — leave a summary + wikilink stub at the old home so wikilinks and lexical retrieval signal are preserved.',
528
- severity: 'required',
611
+ procedure: "internal-linking-protocol",
612
+ why: "Before re-parenting an epic, editing a milestone epics[] roster, or moving trackable work to a new canonical home — leave a summary + wikilink stub at the old home so wikilinks and lexical retrieval signal are preserved.",
613
+ severity: "required",
529
614
  },
530
615
  {
531
- procedure: 'spec-hierarchy',
532
- why: 'Confirms bidirectional linkage shape (epics: ⇄ milestone:, children: ⇄ parent:) that re-parent edits must maintain.',
533
- severity: 'recommended',
616
+ procedure: "section-supersede-protocol",
617
+ why: "Before editing or deleting a section that has gone stale: the three-way test for edit-in-place vs supersede-the-section vs retire-the-whole-document, the stub properties a reviewer checks, and the rule that the repair stays a proposal a person approves — a linked heading is never silently rewritten.",
618
+ severity: "required",
534
619
  },
535
620
  {
536
- procedure: 'supersession-protocol',
537
- why: 'If the move replaces dead guidance (not merely relocates live work), use supersession instead of a cross-reference stub.',
538
- severity: 'recommended',
621
+ procedure: "spec-hierarchy",
622
+ why: "Confirms bidirectional linkage shape (epics: ⇄ milestone:, children: ⇄ parent:) that re-parent edits must maintain.",
623
+ severity: "recommended",
539
624
  },
540
625
  {
541
- procedure: 'doc-canonicalization-protocol',
542
- why: 'If the restructure is an evergreen-doc split/merge rather than plan-side re-parent, use doc-canonicalization instead.',
543
- severity: 'recommended',
626
+ procedure: "supersession-protocol",
627
+ why: "If the move replaces dead guidance (not merely relocates live work), use supersession instead of a cross-reference stub.",
628
+ severity: "recommended",
629
+ },
630
+ {
631
+ procedure: "doc-canonicalization-protocol",
632
+ why: "If the restructure is an evergreen-doc split/merge rather than plan-side re-parent, use doc-canonicalization instead.",
633
+ severity: "recommended",
544
634
  },
545
635
  ],
546
636
  };
@@ -559,10 +649,9 @@ const VALID_INTENTS = Object.keys(PREFLIGHT_INTENT_MAP);
559
649
  // close_tool; others carry no filing_tool block.
560
650
  const CLOSE_TOOL_MAP = {
561
651
  close_session: {
562
- tool: 'heyspex_submit_for_review',
563
- minimal_call: { spec_id: '<completed-review-unit-or-task-id>' },
564
- note:
565
- 'Status SSOT is PATCH /api/specs/:id/status — not frontmatter edits. This tool applies review-grain coercion, parent rollup, and releases the claim. Still run session-close-protocol steps 2–7 (change-log, HS-REV or review seed, WIP, export).',
652
+ tool: "heyspex_submit_for_review",
653
+ minimal_call: { spec_id: "<completed-review-unit-or-task-id>" },
654
+ note: "Status SSOT is PATCH /api/specs/:id/status — not frontmatter edits. This tool applies review-grain coercion, parent rollup, and releases the claim. Still run session-close-protocol steps 2–7 (change-log, HS-REV or review seed, WIP, export).",
566
655
  },
567
656
  };
568
657
 
@@ -572,30 +661,38 @@ function buildCloseTool(intent) {
572
661
  }
573
662
 
574
663
  const FILE_SPEC_NOTE =
575
- 'Use this. The server generates frontmatter from the canonical template. Do not compose frontmatter from a sibling spec.';
664
+ "Use this. The server generates frontmatter from the canonical template. Do not compose frontmatter from a sibling spec.";
576
665
  const FILING_TOOL_MAP = {
577
- file_milestone: { type: 'milestone' },
578
- file_epic: { type: 'epic', parentKey: 'milestone', parentExample: '[[HS-MIL-...]]' },
579
- file_task: { type: 'task', parentKey: 'parent', parentExample: '[[HS-EPIC-...]]' },
666
+ file_milestone: { type: "milestone" },
667
+ file_epic: {
668
+ type: "epic",
669
+ parentKey: "milestone",
670
+ parentExample: "[[HS-MIL-...]]",
671
+ },
672
+ file_task: {
673
+ type: "task",
674
+ parentKey: "parent",
675
+ parentExample: "[[HS-EPIC-...]]",
676
+ },
580
677
  file_diagnose: {
581
- type: 'diagnose',
678
+ type: "diagnose",
582
679
  note: `${FILE_SPEC_NOTE} By default a diagnose is filed standalone — it has no parent and does not nest under any epic or milestone, so it lives on its own in the Resolution lane rather than inside a planned work item. Keep it standalone for a cross-cutting bug, or one that isn't part of any single epic's or milestone's scope (the common case). Only set parent: "[[HS-EPIC-...]]" or "[[HS-MIL-...]]" when the bug clearly belongs to that epic's or milestone's work and you want it tracked under it.`,
583
680
  },
584
- file_research: { type: 'research' },
585
- file_memory_lesson: { type: 'memory-lesson' },
586
- file_memory_decision: { type: 'memory-decision' },
587
- file_memory_reference: { type: 'memory-reference' },
588
- file_memory_ui_pattern: { type: 'memory-ui-pattern' },
681
+ file_research: { type: "research" },
682
+ file_memory_lesson: { type: "memory-lesson" },
683
+ file_memory_decision: { type: "memory-decision" },
684
+ file_memory_reference: { type: "memory-reference" },
685
+ file_memory_ui_pattern: { type: "memory-ui-pattern" },
589
686
  file_review: {
590
- type: 'review',
591
- parentKey: 'parent',
592
- parentExample: '[[<spec-under-review>]]',
687
+ type: "review",
688
+ parentKey: "parent",
689
+ parentExample: "[[<spec-under-review>]]",
593
690
  note: `${FILE_SPEC_NOTE} A review's parent is the spec it reviews — parent is required.`,
594
691
  },
595
692
  file_capture: {
596
- tool: 'heyspex_active_capture',
597
- minimal_call: { spec_id: '<HS-CAP-...>' },
598
- note: 'Captures are session-based; use heyspex_active_capture, NOT heyspex_file_spec.',
693
+ tool: "heyspex_active_capture",
694
+ minimal_call: { spec_id: "<HS-CAP-...>" },
695
+ note: "Captures are session-based; use heyspex_active_capture, NOT heyspex_file_spec.",
599
696
  },
600
697
  };
601
698
 
@@ -606,13 +703,17 @@ function buildFilingTool(intent) {
606
703
  if (!spec) return null;
607
704
  // Captures (and any future bespoke tool) carry an explicit shape.
608
705
  if (spec.tool) {
609
- return { tool: spec.tool, minimal_call: spec.minimal_call, note: spec.note };
706
+ return {
707
+ tool: spec.tool,
708
+ minimal_call: spec.minimal_call,
709
+ note: spec.note,
710
+ };
610
711
  }
611
712
  const minimal_call = { type: spec.type };
612
713
  if (spec.parentKey) minimal_call[spec.parentKey] = spec.parentExample;
613
- minimal_call.name = '<title>';
714
+ minimal_call.name = "<title>";
614
715
  return {
615
- tool: 'heyspex_file_spec',
716
+ tool: "heyspex_file_spec",
616
717
  minimal_call,
617
718
  note: spec.note ?? FILE_SPEC_NOTE,
618
719
  };
@@ -630,81 +731,81 @@ function buildFilingTool(intent) {
630
731
  // ─────────────────────────────────────────────────────────────────────────────
631
732
  const CLOSE_SESSION_THREAD_PATHS = {
632
733
  capture_only: {
633
- label: 'Capture-only — brain-offload via /hs-capture; no claim, no code',
734
+ label: "Capture-only — brain-offload via /hs-capture; no claim, no code",
634
735
  is_close: false,
635
736
  close_tool: false,
636
- procedures: ['research-capture-protocol'],
737
+ procedures: ["research-capture-protocol"],
637
738
  steps: [
638
- 'No session close-out is owed — a capture is not a review unit.',
639
- 'Exit = promote to research (heyspex_promote_capture) or leave the capture pinned.',
640
- 'Do NOT submit_for_review, mint an HS-REV, record a WIP ledger, or flush an audit export.',
739
+ "No session close-out is owed — a capture is not a review unit.",
740
+ "Exit = promote to research (heyspex_promote_capture) or leave the capture pinned.",
741
+ "Do NOT submit_for_review, mint an HS-REV, record a WIP ledger, or flush an audit export.",
641
742
  ],
642
743
  },
643
744
  research_only: {
644
- label: 'Research-only — saved an HS-RES; no claimed spec',
745
+ label: "Research-only — saved an HS-RES; no claimed spec",
645
746
  is_close: false,
646
747
  close_tool: false,
647
- procedures: ['research-capture-protocol'],
748
+ procedures: ["research-capture-protocol"],
648
749
  steps: [
649
- 'Save findings via heyspex_save_research / heyspex_complete_research (external scope needs >=1 cited URL).',
650
- 'Research is evergreen: do NOT submit_for_review or flip status, and do NOT mint an HS-REV.',
651
- 'No execution WIP ledger is owed. Audit export (heyspex_export_events) is optional.',
750
+ "Save findings via heyspex_save_research / heyspex_complete_research (external scope needs >=1 cited URL).",
751
+ "Research is evergreen: do NOT submit_for_review or flip status, and do NOT mint an HS-REV.",
752
+ "No execution WIP ledger is owed. Audit export (heyspex_export_events) is optional.",
652
753
  ],
653
754
  },
654
755
  plan_ingest_only: {
655
- label: 'Plan/ingest-only — filed milestone+epics; no task execution',
756
+ label: "Plan/ingest-only — filed milestone+epics; no task execution",
656
757
  is_close: false,
657
758
  close_tool: false,
658
- procedures: ['plan-ingest-agent-workflow', 'spec-hierarchy'],
759
+ procedures: ["plan-ingest-agent-workflow", "spec-hierarchy"],
659
760
  steps: [
660
- 'Confirm every new spec is registered + linked: heyspex_validate_linkage (heyspex_register_spec for any orphan).',
661
- 'Confirm each milestone/epic carries an authored summary and a re-derived ## Intended outcome.',
662
- 'No task close, no HS-REV, no execution WIP ledger — epics are filed with children: [].',
761
+ "Confirm every new spec is registered + linked: heyspex_validate_linkage (heyspex_register_spec for any orphan).",
762
+ "Confirm each milestone/epic carries an authored summary and a re-derived ## Intended outcome.",
763
+ "No task close, no HS-REV, no execution WIP ledger — epics are filed with children: [].",
663
764
  ],
664
765
  },
665
766
  one_task: {
666
- label: 'One-task execution — claimed and completed a single task',
767
+ label: "One-task execution — claimed and completed a single task",
667
768
  is_close: true,
668
769
  close_tool: true,
669
- procedures: ['session-close-protocol', 'memory-protocol'],
770
+ procedures: ["session-close-protocol", "memory-protocol"],
670
771
  steps: [
671
- 'heyspex_submit_for_review({ spec_id }) on the completed unit (status API SSOT — never hand-edit status:). Grain-coercion, parent rollup, and claim release happen server-side.',
672
- 'Append a change-log roll-up / review seed to the task.',
673
- 'Create an HS-REV companion + parent ## Review back-link ONLY if the active review grain is task; otherwise the change-log seed is enough.',
674
- 'heyspex_record_wip for the worked epic (skip only if the epic closed this session).',
675
- 'heyspex_export_events to flush the audit log. Do NOT git commit / push.',
772
+ "heyspex_submit_for_review({ spec_id }) on the completed unit (status API SSOT — never hand-edit status:). Grain-coercion, parent rollup, and claim release happen server-side.",
773
+ "Append a change-log roll-up / review seed to the task.",
774
+ "Create an HS-REV companion + parent ## Review back-link ONLY if the active review grain is task; otherwise the change-log seed is enough.",
775
+ "heyspex_record_wip for the worked epic (skip only if the epic closed this session).",
776
+ "heyspex_export_events to flush the audit log. Do NOT git commit / push.",
676
777
  ],
677
778
  },
678
779
  multi_task_epic: {
679
- label: 'Multi-task / epic push — completed several tasks under an epic',
780
+ label: "Multi-task / epic push — completed several tasks under an epic",
680
781
  is_close: true,
681
782
  close_tool: true,
682
- procedures: ['session-close-protocol', 'memory-protocol'],
783
+ procedures: ["session-close-protocol", "memory-protocol"],
683
784
  steps: [
684
- 'heyspex_submit_for_review on each completed unit; parent rollup is automatic when the last child lands.',
685
- 'One consolidated change-log + the HS-REV companion at the ACTIVE review grain (epic in steady-state, milestone in MVP).',
686
- 'heyspex_record_wip once for the worked epic; heyspex_export_events; do NOT git commit / push.',
785
+ "heyspex_submit_for_review on each completed unit; parent rollup is automatic when the last child lands.",
786
+ "One consolidated change-log + the HS-REV companion at the ACTIVE review grain (epic in steady-state, milestone in MVP).",
787
+ "heyspex_record_wip once for the worked epic; heyspex_export_events; do NOT git commit / push.",
687
788
  ],
688
789
  },
689
790
  resolve_diagnose: {
690
- label: 'Resolve / diagnose — worked an HS-DIAG or a Resolution item',
791
+ label: "Resolve / diagnose — worked an HS-DIAG or a Resolution item",
691
792
  is_close: true,
692
793
  close_tool: true,
693
- procedures: ['session-close-protocol', 'resolution-diagnose-workflow'],
794
+ procedures: ["session-close-protocol", "resolution-diagnose-workflow"],
694
795
  steps: [
695
- 'If the flag was a stale-cache false positive (the file on disk contradicts it): re-stamp via heyspex_record_alignment — do NOT re-file work that already exists.',
696
- 'If a real diagnose/fix completed: heyspex_submit_for_review on it + change-log; HS-REV per active grain.',
697
- 'heyspex_record_wip if anything remains open; heyspex_export_events.',
796
+ "If the flag was a stale-cache false positive (the file on disk contradicts it): re-stamp via heyspex_record_alignment — do NOT re-file work that already exists.",
797
+ "If a real diagnose/fix completed: heyspex_submit_for_review on it + change-log; HS-REV per active grain.",
798
+ "heyspex_record_wip if anything remains open; heyspex_export_events.",
698
799
  ],
699
800
  },
700
801
  mid_rollover: {
701
- label: 'Mid-implementation rollover — claimed but not finished',
802
+ label: "Mid-implementation rollover — claimed but not finished",
702
803
  is_close: false,
703
804
  close_tool: false,
704
- procedures: ['context-rollover-checkpoint'],
805
+ procedures: ["context-rollover-checkpoint"],
705
806
  steps: [
706
- 'This is NOT a session close. Do NOT flip status, mint an HS-REV, or submit_for_review.',
707
- 'Record a lightweight WIP checkpoint (heyspex_record_wip) so the next session resumes; leave the spec in_progress.',
807
+ "This is NOT a session close. Do NOT flip status, mint an HS-REV, or submit_for_review.",
808
+ "Record a lightweight WIP checkpoint (heyspex_record_wip) so the next session resumes; leave the spec in_progress.",
708
809
  'See heyspex_preflight({ intent: "context_rollover" }) / the context-rollover-checkpoint procedure for the full checkpoint shape.',
709
810
  ],
710
811
  },
@@ -713,37 +814,31 @@ const CLOSE_SESSION_THREAD_PATHS = {
713
814
  const CLOSE_SESSION_THREAD_TYPES = Object.keys(CLOSE_SESSION_THREAD_PATHS);
714
815
 
715
816
  const CLOSE_SESSION_MIXED_RULE =
716
- 'If the session spanned MORE THAN ONE thread type (e.g. filed a plan AND finished a task), request each applicable type and run the UNION of their steps — ordered, idempotent (re-running submit_for_review / export_events is safe), and surface the ambiguity. Never drop an owed obligation; never duplicate one.';
817
+ "If the session spanned MORE THAN ONE thread type (e.g. filed a plan AND finished a task), request each applicable type and run the UNION of their steps — ordered, idempotent (re-running submit_for_review / export_events is safe), and surface the ambiguity. Never drop an owed obligation; never duplicate one.";
717
818
 
718
819
  function buildCloseSessionPreflight(vaultRoot, threadType, injectedCatalog) {
719
820
  const path = CLOSE_SESSION_THREAD_PATHS[threadType];
720
821
  if (!path) {
721
822
  return {
722
- error: 'UNKNOWN_THREAD_TYPE',
723
- message: `Unknown thread_type "${threadType}" for close_session. Valid thread types: ${CLOSE_SESSION_THREAD_TYPES.join(', ')}.`,
823
+ error: "UNKNOWN_THREAD_TYPE",
824
+ message: `Unknown thread_type "${threadType}" for close_session. Valid thread types: ${CLOSE_SESSION_THREAD_TYPES.join(", ")}.`,
724
825
  };
725
826
  }
726
- const catalog = injectedCatalog !== undefined ? injectedCatalog : loadCatalog(vaultRoot);
827
+ const catalog =
828
+ injectedCatalog !== undefined ? injectedCatalog : loadCatalog(vaultRoot);
727
829
  if (!catalog) {
728
830
  return {
729
- error: 'CATALOG_NOT_FOUND',
831
+ error: "CATALOG_NOT_FOUND",
730
832
  message:
731
- 'specs/protocols/.catalog.json not found. Run `node scripts/build-procedure-catalog.mjs`.',
833
+ "specs/protocols/.catalog.json not found. Run `node scripts/build-procedure-catalog.mjs`.",
732
834
  };
733
835
  }
734
- const procedures = path.procedures.map((name) => {
735
- const entry = findEntry(catalog, name);
736
- return {
737
- procedure: name,
738
- severity: 'required',
739
- path: entry?.path ?? null,
740
- title: entry?.title ?? null,
741
- missing_from_catalog: entry ? undefined : true,
742
- };
743
- });
744
- const close_tool = path.close_tool ? buildCloseTool('close_session') : null;
836
+ const procedures = path.procedures.map((name) =>
837
+ resolveRow(catalog, { procedure: name, severity: "required" }),
838
+ );
839
+ const close_tool = path.close_tool ? buildCloseTool("close_session") : null;
745
840
  return {
746
- intent: 'close_session',
841
+ intent: "close_session",
747
842
  thread_type: threadType,
748
843
  thread_label: path.label,
749
844
  is_close: path.is_close,
@@ -760,46 +855,37 @@ function buildCloseSessionPreflight(vaultRoot, threadType, injectedCatalog) {
760
855
  * thread-type path too, so a single HTTP fetch covers both branches).
761
856
  */
762
857
  export function executeRunPreflight(vaultRoot, args = {}, injectedCatalog) {
763
- const intent = String(args.intent ?? '').trim();
858
+ const intent = String(args.intent ?? "").trim();
764
859
  if (!intent) {
765
860
  return {
766
- error: 'MISSING_INTENT',
767
- message: `intent is required. Valid intents: ${VALID_INTENTS.join(', ')}.`,
861
+ error: "MISSING_INTENT",
862
+ message: `intent is required. Valid intents: ${VALID_INTENTS.join(", ")}.`,
768
863
  };
769
864
  }
770
865
  // Thread-type-aware close-out: when a thread_type is declared, return that
771
866
  // type's CONDITIONAL close path instead of the static checklist ([[HS-RES-32MJ]]).
772
- const threadType = String(args.thread_type ?? '').trim();
773
- if (intent === 'close_session' && threadType) {
867
+ const threadType = String(args.thread_type ?? "").trim();
868
+ if (intent === "close_session" && threadType) {
774
869
  return buildCloseSessionPreflight(vaultRoot, threadType, injectedCatalog);
775
870
  }
776
871
  const recommendations = PREFLIGHT_INTENT_MAP[intent];
777
872
  if (!recommendations) {
778
873
  return {
779
- error: 'UNKNOWN_INTENT',
780
- message: `Unknown intent "${intent}". Valid intents: ${VALID_INTENTS.join(', ')}.`,
874
+ error: "UNKNOWN_INTENT",
875
+ message: `Unknown intent "${intent}". Valid intents: ${VALID_INTENTS.join(", ")}.`,
781
876
  };
782
877
  }
783
- const catalog = injectedCatalog !== undefined ? injectedCatalog : loadCatalog(vaultRoot);
878
+ const catalog =
879
+ injectedCatalog !== undefined ? injectedCatalog : loadCatalog(vaultRoot);
784
880
  if (!catalog) {
785
881
  return {
786
- error: 'CATALOG_NOT_FOUND',
882
+ error: "CATALOG_NOT_FOUND",
787
883
  message:
788
- 'specs/protocols/.catalog.json not found. Run `node scripts/build-procedure-catalog.mjs`.',
884
+ "specs/protocols/.catalog.json not found. Run `node scripts/build-procedure-catalog.mjs`.",
789
885
  };
790
886
  }
791
887
 
792
- const procedures = recommendations.map((row) => {
793
- const entry = findEntry(catalog, row.procedure);
794
- return {
795
- procedure: row.procedure,
796
- why: row.why,
797
- severity: row.severity,
798
- path: entry?.path ?? null,
799
- title: entry?.title ?? null,
800
- missing_from_catalog: entry ? undefined : true,
801
- };
802
- });
888
+ const procedures = recommendations.map((row) => resolveRow(catalog, row));
803
889
 
804
890
  const filing_tool = buildFilingTool(intent);
805
891
  const close_tool = buildCloseTool(intent);
@@ -814,15 +900,19 @@ export function executeRunPreflight(vaultRoot, args = {}, injectedCatalog) {
814
900
  // Family-overlap advisory for filing intents: warn agents to check whether
815
901
  // a closed family already covered this goal with no iterates_on edge.
816
902
  const FILING_ADVISORY =
817
- 'Family-overlap check (warn-only): before filing, check whether a CLOSED family already shipped against this goal with no iterates_on: edge — compare both TITLE and JTBD (job-to-be-done) statement, not title alone, since two differently-titled specs can still be the same job (owner-approved 2026-08-23 job-based-duplicate-detection extension) — if so this may be round 2; see entity-model §Iteration lineage. Never blocks filing.';
818
- const FILING_INTENTS_WITH_ADVISORY = ['file_epic', 'file_task', 'promote_research'];
903
+ "Family-overlap check (warn-only): before filing, check whether a CLOSED family already shipped against this goal with no iterates_on: edge — compare both TITLE and JTBD (job-to-be-done) statement, not title alone, since two differently-titled specs can still be the same job (owner-approved 2026-08-23 job-based-duplicate-detection extension) — if so this may be round 2; see entity-model §Iteration lineage. Never blocks filing.";
904
+ const FILING_INTENTS_WITH_ADVISORY = [
905
+ "file_epic",
906
+ "file_task",
907
+ "promote_research",
908
+ ];
819
909
  if (FILING_INTENTS_WITH_ADVISORY.includes(intent)) {
820
910
  result.advisory = FILING_ADVISORY;
821
911
  }
822
912
 
823
913
  // Static close_session call (no thread_type declared): nudge toward the
824
914
  // conditional path so the agent runs the right minimal-complete close-out.
825
- if (intent === 'close_session') {
915
+ if (intent === "close_session") {
826
916
  result.thread_type_hint = {
827
917
  note: 'Close-out is thread-type-aware. Re-call heyspex_preflight({ intent: "close_session", thread_type: "<type>" }) to get the conditional close path for what this session actually did.',
828
918
  thread_types: CLOSE_SESSION_THREAD_TYPES.map((t) => ({
@@ -834,9 +924,64 @@ export function executeRunPreflight(vaultRoot, args = {}, injectedCatalog) {
834
924
  };
835
925
  }
836
926
 
927
+ // [[HS-EN4F]]: research intents carry the absolute hs-res route.
928
+ if (RESEARCH_TOOLING_INTENTS.includes(intent)) {
929
+ const research_tooling = buildResearchTooling();
930
+ if (research_tooling) result.research_tooling = research_tooling;
931
+ }
932
+
837
933
  return result;
838
934
  }
839
935
 
936
+ // ─────────────────────────────────────────────────────────────────────────────
937
+ // [[HS-EN4F]] — the hs-res route, in absolute paths. The /hs-res command and
938
+ // its SKILL.md spell the gather/synthesis CLIs repo-relative
939
+ // (`node dev-factory/spexcrawl/...`), which only resolves inside the platform
940
+ // checkout. An agent in a customer repo (sales-coach, 2026-10-02) looked for
941
+ // hs-res there, did not find it, concluded "the tooling isn't installed" and
942
+ // fanned out Sonnet subagents on WebSearch. The research preflight is the one
943
+ // call every research path must make (the HS-K0ZP gate refuses WebSearch /
944
+ // WebFetch until it runs), so it carries the route: absolute commands derived
945
+ // from where THIS module sits — the MCP server serving the customer repo runs
946
+ // out of the platform checkout, so these paths resolve from any working
947
+ // directory. Null when the platform does not ship the CLIs (published package).
948
+ // ─────────────────────────────────────────────────────────────────────────────
949
+ const RESEARCH_TOOLING_INTENTS = [
950
+ "research",
951
+ "file_research",
952
+ "promote_research",
953
+ ];
954
+ const DEFAULT_PLATFORM_ROOT = resolve(
955
+ dirname(fileURLToPath(import.meta.url)),
956
+ "..",
957
+ "..",
958
+ );
959
+
960
+ export function buildResearchTooling(
961
+ platformRoot = DEFAULT_PLATFORM_ROOT,
962
+ exists = existsSync,
963
+ ) {
964
+ const abs = (...parts) => join(platformRoot, ...parts).replace(/\\/g, "/");
965
+ const spexcrawl = abs("dev-factory", "spexcrawl", "spexcrawl-cli.mjs");
966
+ const firecrawl = abs("dev-factory", "spexcrawl", "firecrawl-cli.mjs");
967
+ const synthesize = abs("dev-factory", "spexcrawl", "synthesize-cli.mjs");
968
+ const skill = abs(".commandcode", "skills", "hs-res", "SKILL.md");
969
+ if (![spexcrawl, firecrawl, synthesize].every((p) => exists(p))) return null;
970
+ return {
971
+ method:
972
+ "hs-res (the confidence ladder) — SpexCrawl via the Companion is primary, self-hosted Firecrawl is the automatic fallback, DeepSeek synthesizes. Zero Claude/Anthropic sub-agents.",
973
+ note: "These commands run from ANY repo. A project with no /hs-res command or skill of its own does NOT mean the tooling is missing — use these absolute paths. Native WebSearch/WebFetch is the last resort only when both gather backends report unavailable; never fan out Sonnet sub-agents for research.",
974
+ skill_path: exists(skill) ? skill : null,
975
+ gather_primary: `node "${spexcrawl}" search "<query>" --json`,
976
+ scrape_primary: `node "${spexcrawl}" scrape "<url>" --json`,
977
+ gather_fallback: `node "${firecrawl}" search "<query>" --json`,
978
+ scrape_fallback: `node "${firecrawl}" scrape "<url>" --json`,
979
+ synthesize: `node "${synthesize}" --prompt-file <path> --json`,
980
+ grounding_first:
981
+ "Before any gather: method triage (known-target-retrieval vs the ladder), ground-first (heyspex_recall_memories), and check for a working reference the owner already runs (e.g. `docker ps` for a local Firecrawl on :3002). Record the four-part grounding result (already built / already decided / owner reference / residual delta).",
982
+ };
983
+ }
984
+
840
985
  export const PREFLIGHT_INTENTS = VALID_INTENTS;
841
986
  export { CLOSE_SESSION_THREAD_TYPES };
842
987
 
@@ -871,12 +1016,17 @@ function catalogFromProceduresRoute(body) {
871
1016
  * Exported for callers that need the raw catalog object outside the three
872
1017
  * higher-level wrappers below (none today; kept for symmetry / testability).
873
1018
  */
874
- export async function loadCatalogViaTier({ vaultRoot, tier, consoleFetch, consoleUrl }) {
1019
+ export async function loadCatalogViaTier({
1020
+ vaultRoot,
1021
+ tier,
1022
+ consoleFetch,
1023
+ consoleUrl,
1024
+ }) {
875
1025
  return readViaSelector({
876
1026
  tier,
877
1027
  diskRead: () => loadCatalog(vaultRoot),
878
1028
  neonRead: async () => {
879
- const res = await consoleFetch('/api/procedures');
1029
+ const res = await consoleFetch("/api/procedures");
880
1030
  if (!res.ok) {
881
1031
  throw new Error(
882
1032
  `GET /api/procedures returned ${res.status}: ${res.error || JSON.stringify(res.body ?? {})}`,
@@ -884,22 +1034,34 @@ export async function loadCatalogViaTier({ vaultRoot, tier, consoleFetch, consol
884
1034
  }
885
1035
  return catalogFromProceduresRoute(res.body);
886
1036
  },
887
- routeLabel: 'GET /api/procedures',
1037
+ routeLabel: "GET /api/procedures",
888
1038
  consoleUrl,
889
1039
  });
890
1040
  }
891
1041
 
892
1042
  /** Cloud-reliant sibling of {@link executeListProcedures}. */
893
- export async function executeListProceduresViaTier({ vaultRoot, args, tier, consoleFetch, consoleUrl }) {
1043
+ export async function executeListProceduresViaTier({
1044
+ vaultRoot,
1045
+ args,
1046
+ tier,
1047
+ consoleFetch,
1048
+ consoleUrl,
1049
+ }) {
894
1050
  return readViaSelector({
895
1051
  tier,
896
1052
  diskRead: () => executeListProcedures(vaultRoot, args),
897
1053
  neonRead: async () => {
898
- const catalog = await loadCatalogViaTier({ vaultRoot, tier: 'neon', consoleFetch, consoleUrl });
1054
+ const catalog = await loadCatalogViaTier({
1055
+ vaultRoot,
1056
+ tier: "neon",
1057
+ consoleFetch,
1058
+ consoleUrl,
1059
+ });
899
1060
  if (!catalog) {
900
1061
  return {
901
- error: 'CATALOG_NOT_FOUND',
902
- message: 'GET /api/procedures returned no catalog (console reports it missing).',
1062
+ error: "CATALOG_NOT_FOUND",
1063
+ message:
1064
+ "GET /api/procedures returned no catalog (console reports it missing).",
903
1065
  };
904
1066
  }
905
1067
  // Filtering happens HERE, client-side — the route has no tag/trigger/query
@@ -908,24 +1070,39 @@ export async function executeListProceduresViaTier({ vaultRoot, args, tier, cons
908
1070
  // catalog as `injectedCatalog` runs the SAME filter logic either way.
909
1071
  return executeListProcedures(vaultRoot, args, catalog);
910
1072
  },
911
- routeLabel: 'GET /api/procedures',
1073
+ routeLabel: "GET /api/procedures",
912
1074
  consoleUrl,
913
1075
  });
914
1076
  }
915
1077
 
916
1078
  /** Cloud-reliant sibling of {@link executeGetProcedure}. Single round trip: `GET /api/procedures/<name>` returns catalog entry + full body together. */
917
- export async function executeGetProcedureViaTier({ vaultRoot, args, tier, consoleFetch, consoleUrl }) {
1079
+ export async function executeGetProcedureViaTier({
1080
+ vaultRoot,
1081
+ args,
1082
+ tier,
1083
+ consoleFetch,
1084
+ consoleUrl,
1085
+ }) {
918
1086
  return readViaSelector({
919
1087
  tier,
920
1088
  diskRead: () => executeGetProcedure(vaultRoot, args),
921
1089
  neonRead: async () => {
922
- const name = String(args?.name ?? '').trim();
1090
+ const name = String(args?.name ?? "").trim();
923
1091
  if (!name) {
924
- return { error: 'MISSING_NAME', message: 'name is required (e.g. "spec-hierarchy")' };
1092
+ return {
1093
+ error: "MISSING_NAME",
1094
+ message: 'name is required (e.g. "spec-hierarchy")',
1095
+ };
925
1096
  }
926
- const res = await consoleFetch(`/api/procedures/${encodeURIComponent(name)}`);
1097
+ const res = await consoleFetch(
1098
+ `/api/procedures/${encodeURIComponent(name)}`,
1099
+ );
927
1100
  if (res.ok) {
928
- return { procedure: res.body?.procedure, content: res.body?.content, source: res.body?.source ?? null };
1101
+ return {
1102
+ procedure: res.body?.procedure,
1103
+ content: res.body?.content,
1104
+ source: res.body?.source ?? null,
1105
+ };
929
1106
  }
930
1107
  // Aligned with the sibling *ViaTier functions ([[HS-DJWP-MCPC-TASK]]
931
1108
  // round 3 fix — a 500 {error:'boom'} was previously swallowed as a real
@@ -937,34 +1114,46 @@ export async function executeGetProcedureViaTier({ vaultRoot, args, tier, consol
937
1114
  // selector surfaces CONSOLE_UNREACHABLE_CLOUD_MODE rather than silently
938
1115
  // degrading; the epic's no-silent-degrade rule treats "console answered,
939
1116
  // but the answer means something is broken" the same as unreachable.
940
- if (res.status === 404 && res.body?.error === 'PROCEDURE_NOT_FOUND') {
1117
+ if (res.status === 404 && res.body?.error === "PROCEDURE_NOT_FOUND") {
941
1118
  return { error: res.body.error, message: res.body.message };
942
1119
  }
943
1120
  throw new Error(
944
- `GET /api/procedures/${name} returned ${res.status}: ${res.body?.error ? `${res.body.error}${res.body.message ? ` — ${res.body.message}` : ''}` : res.error || JSON.stringify(res.body ?? {})}`,
1121
+ `GET /api/procedures/${name} returned ${res.status}: ${res.body?.error ? `${res.body.error}${res.body.message ? ` — ${res.body.message}` : ""}` : res.error || JSON.stringify(res.body ?? {})}`,
945
1122
  );
946
1123
  },
947
- routeLabel: 'GET /api/procedures/[name]',
1124
+ routeLabel: "GET /api/procedures/[name]",
948
1125
  consoleUrl,
949
1126
  });
950
1127
  }
951
1128
 
952
1129
  /** Cloud-reliant sibling of {@link executeRunPreflight}. One catalog fetch covers both the static intent map AND the close_session thread-type path. */
953
- export async function executeRunPreflightViaTier({ vaultRoot, args, tier, consoleFetch, consoleUrl }) {
1130
+ export async function executeRunPreflightViaTier({
1131
+ vaultRoot,
1132
+ args,
1133
+ tier,
1134
+ consoleFetch,
1135
+ consoleUrl,
1136
+ }) {
954
1137
  return readViaSelector({
955
1138
  tier,
956
1139
  diskRead: () => executeRunPreflight(vaultRoot, args),
957
1140
  neonRead: async () => {
958
- const catalog = await loadCatalogViaTier({ vaultRoot, tier: 'neon', consoleFetch, consoleUrl });
1141
+ const catalog = await loadCatalogViaTier({
1142
+ vaultRoot,
1143
+ tier: "neon",
1144
+ consoleFetch,
1145
+ consoleUrl,
1146
+ });
959
1147
  if (!catalog) {
960
1148
  return {
961
- error: 'CATALOG_NOT_FOUND',
962
- message: 'GET /api/procedures returned no catalog (console reports it missing).',
1149
+ error: "CATALOG_NOT_FOUND",
1150
+ message:
1151
+ "GET /api/procedures returned no catalog (console reports it missing).",
963
1152
  };
964
1153
  }
965
1154
  return executeRunPreflight(vaultRoot, args, catalog);
966
1155
  },
967
- routeLabel: 'GET /api/procedures',
1156
+ routeLabel: "GET /api/procedures",
968
1157
  consoleUrl,
969
1158
  });
970
1159
  }