heyspex 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (352) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/README.md +21 -0
  3. package/bin/hs.mjs +1089 -104
  4. package/bin/spexcode.mjs +262 -0
  5. package/dev-factory/engine/worktree.mjs +740 -0
  6. package/dev-factory/lib/code-repo-root.mjs +67 -0
  7. package/dev-factory/lib/context-budget.mjs +84 -0
  8. package/dev-factory/lib/epic-detect.mjs +190 -0
  9. package/dev-factory/lib/fleet-queue-file.mjs +846 -0
  10. package/dev-factory/lib/fleet-queue.mjs +426 -0
  11. package/dev-factory/lib/fleet-state.mjs +14 -18
  12. package/dev-factory/lib/model-catalog.mjs +338 -0
  13. package/dev-factory/lib/seat-context.mjs +199 -0
  14. package/dev-factory/lib/spec-io.mjs +93 -0
  15. package/dev-factory/lib/sufficiency-score.mjs +8 -2
  16. package/dev-factory/lib/worktree-allocator.mjs +744 -0
  17. package/dev-factory/model-catalog.json +222 -0
  18. package/dist/cli/hs-check.mjs +1127 -103
  19. package/dist/cli/hs-claim.mjs +981 -63
  20. package/dist/cli/hs-connect.mjs +10059 -1729
  21. package/dist/cli/hs-defer.mjs +5507 -3072
  22. package/dist/cli/hs-dev.mjs +576 -0
  23. package/dist/cli/hs-env.mjs +416 -302
  24. package/dist/cli/hs-index-rebuild.mjs +7334 -0
  25. package/dist/cli/hs-index-reconcile.mjs +8138 -0
  26. package/dist/cli/hs-init.mjs +2840 -149
  27. package/dist/cli/hs-install.mjs +7214 -594
  28. package/dist/cli/hs-join.mjs +10640 -2421
  29. package/dist/cli/hs-vault-pull.mjs +8157 -6370
  30. package/dist/cli/hs-vault-push.mjs +6433 -0
  31. package/dist/cli/hs-vault.mjs +4029 -1555
  32. package/dist/cli/hs-workspace.mjs +2702 -538
  33. package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
  34. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
  35. package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
  36. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
  37. package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
  38. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
  39. package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
  40. package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
  41. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
  42. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
  43. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
  44. package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
  45. package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
  46. package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -0
  47. package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
  48. package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
  49. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
  50. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
  51. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
  52. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
  53. package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
  54. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
  55. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
  56. package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -0
  57. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
  58. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
  59. package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
  60. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
  61. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
  62. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
  63. package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
  64. package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
  65. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +33 -0
  66. package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
  67. package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
  68. package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
  69. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
  70. package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
  71. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
  72. package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
  73. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
  74. package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
  75. package/dist/cli-templates/harness-commands/cursor/hs-view.md +32 -0
  76. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
  77. package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
  78. package/docs/copilot-plugin-install.md +162 -0
  79. package/docs/driving-heyspex-from-copilot.md +165 -0
  80. package/docs/mcp-client-config.md +259 -6
  81. package/package.json +249 -124
  82. package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
  83. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
  84. package/plugins/heyspex/.mcp.json +12 -0
  85. package/plugins/heyspex/README.md +309 -0
  86. package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
  87. package/plugins/heyspex/bin/run-hook.mjs +80 -0
  88. package/plugins/heyspex/hooks/cursor-hooks.json +308 -0
  89. package/plugins/heyspex/hooks/hooks.json +418 -0
  90. package/plugins/heyspex/mcp.json +13 -0
  91. package/scripts/check-mcp-freshness.mjs +17 -9
  92. package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
  93. package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
  94. package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
  95. package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -0
  96. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +356 -0
  97. package/scripts/claude-hooks/_run.mjs +413 -0
  98. package/scripts/claude-hooks/_transcript-text.mjs +60 -0
  99. package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
  100. package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
  101. package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
  102. package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
  103. package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
  104. package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
  105. package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
  106. package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
  107. package/scripts/claude-hooks/claim-liveness-shared.mjs +577 -0
  108. package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
  109. package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
  110. package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
  111. package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
  112. package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
  113. package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
  114. package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
  115. package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
  116. package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
  117. package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
  118. package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
  119. package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
  120. package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
  121. package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
  122. package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
  123. package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
  124. package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
  125. package/scripts/claude-hooks/inject-communication-style.mjs +86 -0
  126. package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
  127. package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
  128. package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
  129. package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
  130. package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -0
  131. package/scripts/claude-hooks/log-session-activity.mjs +341 -0
  132. package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
  133. package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
  134. package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
  135. package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
  136. package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
  137. package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
  138. package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
  139. package/scripts/claude-hooks/research-method-guard.mjs +398 -0
  140. package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
  141. package/scripts/claude-hooks/session-end.mjs +451 -0
  142. package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
  143. package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
  144. package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
  145. package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
  146. package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
  147. package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
  148. package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
  149. package/scripts/claude-hooks/wip-checkpoint.mjs +302 -0
  150. package/scripts/cursor-hooks/manifest.mjs +281 -0
  151. package/scripts/dev-server-status.mjs +34 -13
  152. package/scripts/dev-server-stop.mjs +313 -0
  153. package/scripts/heyspex-mode.mjs +288 -0
  154. package/scripts/hs-auto-resolve.mjs +240 -0
  155. package/scripts/hs-auto-source.mjs +398 -0
  156. package/scripts/hs-auto.mjs +134 -0
  157. package/scripts/hs-capture-cli.mjs +326 -0
  158. package/scripts/hs-classifier.mjs +136 -0
  159. package/scripts/hs-companion.mjs +88 -0
  160. package/scripts/hs-gateway.mjs +115 -0
  161. package/scripts/hs-mode.mjs +125 -0
  162. package/scripts/hs-next.mjs +441 -0
  163. package/scripts/hs-settings.mjs +338 -0
  164. package/scripts/hs-view.mjs +496 -0
  165. package/scripts/hs-worktree.mjs +201 -0
  166. package/scripts/install-claude-code-hooks.mjs +272 -0
  167. package/scripts/install-cursor-hooks.mjs +471 -0
  168. package/scripts/lib/agent-env-markers.mjs +38 -0
  169. package/scripts/lib/console-url-resolver.mjs +199 -61
  170. package/scripts/lib/customer-kit.mjs +1203 -0
  171. package/scripts/lib/dev-mode.mjs +249 -32
  172. package/scripts/lib/directive-obligations.mjs +349 -0
  173. package/scripts/lib/heyspex-home.mjs +72 -0
  174. package/scripts/lib/hook-dispositions.mjs +210 -0
  175. package/scripts/lib/hook-vault-header.mjs +30 -0
  176. package/scripts/lib/hs-command.mjs +277 -0
  177. package/scripts/lib/hs-companion-settings.mjs +472 -0
  178. package/scripts/lib/hs-source-branch.mjs +196 -0
  179. package/scripts/lib/hs-source-commit.mjs +59 -5
  180. package/scripts/lib/hs-source-identity.mjs +526 -0
  181. package/scripts/lib/hs-source-log.mjs +13 -22
  182. package/scripts/lib/hs-source-preflight.mjs +18 -4
  183. package/scripts/lib/hs-source-push.mjs +386 -307
  184. package/scripts/lib/hs-worktree-args.mjs +115 -0
  185. package/scripts/lib/shared-path-helpers.mjs +134 -0
  186. package/scripts/lib/standalone-doctor.mjs +540 -3
  187. package/scripts/lib/subagent-git-lock.mjs +109 -0
  188. package/scripts/lib/template-pin.mjs +34 -0
  189. package/scripts/lib/window-placement.mjs +109 -0
  190. package/scripts/provenance-lib.mjs +91 -0
  191. package/scripts/stamp-provenance-on-edit.mjs +222 -0
  192. package/scripts/wip-ledger-health.mjs +653 -0
  193. package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
  194. package/spexharness/src/adapters/audit.mjs +197 -0
  195. package/spexharness/src/adapters/claude-code.mjs +449 -0
  196. package/spexharness/src/adapters/copilot.mjs +236 -0
  197. package/spexharness/src/adapters/detect-harness.mjs +34 -0
  198. package/spexharness/src/chat/tui/ansi.mjs +448 -0
  199. package/spexharness/src/chat/tui/palette.mjs +22 -0
  200. package/spexharness/src/chat/tui/select.mjs +208 -0
  201. package/spexharness/src/config-root.mjs +54 -0
  202. package/spexharness/src/context-budget.mjs +141 -0
  203. package/spexharness/src/dev-server-control.mjs +389 -0
  204. package/spexharness/src/doctrine/behaviour.mjs +371 -0
  205. package/spexharness/src/doctrine/corpus.mjs +733 -0
  206. package/spexharness/src/doctrine/inject.mjs +338 -0
  207. package/spexharness/src/doctrine/merge.mjs +184 -0
  208. package/spexharness/src/doctrine/sources.mjs +252 -0
  209. package/spexharness/src/doctrine/synthesize.mjs +401 -0
  210. package/spexharness/src/doctrine/vault-source.mjs +574 -0
  211. package/spexharness/src/doctrine/writer.mjs +176 -0
  212. package/spexharness/src/events.mjs +84 -0
  213. package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
  214. package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
  215. package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
  216. package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
  217. package/spexharness/src/gates/deep-research-cost-gate.mjs +275 -0
  218. package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
  219. package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
  220. package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
  221. package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
  222. package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
  223. package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
  224. package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
  225. package/spexharness/src/gates/git-identity-guard.mjs +61 -0
  226. package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
  227. package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
  228. package/spexharness/src/gates/git-stash-gate.mjs +159 -0
  229. package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
  230. package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
  231. package/spexharness/src/gates/inject-vault-context.mjs +239 -0
  232. package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
  233. package/spexharness/src/gates/plan-review-gate.mjs +233 -0
  234. package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
  235. package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
  236. package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
  237. package/spexharness/src/gates/settings-write-gate.mjs +104 -0
  238. package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
  239. package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
  240. package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
  241. package/spexharness/src/git-reflog-audit.mjs +294 -0
  242. package/spexharness/src/intent-classifier.mjs +196 -0
  243. package/spexharness/src/intent-directive.mjs +626 -0
  244. package/spexharness/src/profile/learn.mjs +551 -0
  245. package/spexharness/src/profile/proposals.mjs +357 -0
  246. package/spexharness/src/profile/vault-inclusion.mjs +386 -0
  247. package/spexharness/src/redact.mjs +151 -0
  248. package/spexharness/src/rule-table.mjs +849 -0
  249. package/spexharness/src/seat-context.mjs +122 -0
  250. package/spexharness/src/session-bootstrap.mjs +512 -0
  251. package/spexharness/src/session-record.mjs +815 -0
  252. package/spexharness/src/token-inventory.mjs +207 -0
  253. package/vendor/heyspex-mcp/account-switch.mjs +96 -0
  254. package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
  255. package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
  256. package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
  257. package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
  258. package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
  259. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +77 -8
  260. package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
  261. package/vendor/heyspex-mcp/capture-console-client.mjs +117 -3
  262. package/vendor/heyspex-mcp/capture-focus.mjs +1061 -31
  263. package/vendor/heyspex-mcp/capture-outbox.mjs +15 -5
  264. package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
  265. package/vendor/heyspex-mcp/capture-tools.mjs +1264 -0
  266. package/vendor/heyspex-mcp/claim-tools.mjs +1147 -0
  267. package/vendor/heyspex-mcp/cli-verbs.mjs +2074 -0
  268. package/vendor/heyspex-mcp/cli.mjs +207 -15524
  269. package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
  270. package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
  271. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +602 -0
  272. package/vendor/heyspex-mcp/config-seed.yaml +15 -0
  273. package/vendor/heyspex-mcp/config-settings.mjs +290 -7
  274. package/vendor/heyspex-mcp/console-fetch.mjs +822 -17
  275. package/vendor/heyspex-mcp/console-transport.mjs +172 -0
  276. package/vendor/heyspex-mcp/credentials.mjs +585 -0
  277. package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
  278. package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
  279. package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
  280. package/vendor/heyspex-mcp/design-tools.mjs +4 -1
  281. package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
  282. package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
  283. package/vendor/heyspex-mcp/dev-tools.mjs +131 -0
  284. package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
  285. package/vendor/heyspex-mcp/events-tools.mjs +207 -0
  286. package/vendor/heyspex-mcp/filing-tools.mjs +723 -0
  287. package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
  288. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +71 -15
  289. package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
  290. package/vendor/heyspex-mcp/folder-trust-store.mjs +60 -0
  291. package/vendor/heyspex-mcp/http-transport.mjs +302 -0
  292. package/vendor/heyspex-mcp/list-tools.mjs +257 -0
  293. package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
  294. package/vendor/heyspex-mcp/mcp-config.mjs +107 -0
  295. package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
  296. package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
  297. package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
  298. package/vendor/heyspex-mcp/occupied-keys.mjs +142 -0
  299. package/vendor/heyspex-mcp/on-deck-brief.mjs +3377 -0
  300. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
  301. package/vendor/heyspex-mcp/package-root.mjs +32 -0
  302. package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
  303. package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
  304. package/vendor/heyspex-mcp/plan-push.mjs +908 -22
  305. package/vendor/heyspex-mcp/plan-review.mjs +124 -30
  306. package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
  307. package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -0
  308. package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -970
  309. package/vendor/heyspex-mcp/promote-capture.mjs +388 -69
  310. package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
  311. package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
  312. package/vendor/heyspex-mcp/promote-research.mjs +666 -53
  313. package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
  314. package/vendor/heyspex-mcp/prompts.mjs +1460 -0
  315. package/vendor/heyspex-mcp/read-path.mjs +191 -15
  316. package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
  317. package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
  318. package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
  319. package/vendor/heyspex-mcp/register-spec.mjs +266 -54
  320. package/vendor/heyspex-mcp/request-context.mjs +83 -0
  321. package/vendor/heyspex-mcp/research-focus.mjs +207 -4
  322. package/vendor/heyspex-mcp/research-handlers.mjs +625 -0
  323. package/vendor/heyspex-mcp/research-tools.mjs +4 -4
  324. package/vendor/heyspex-mcp/response-budget.mjs +348 -0
  325. package/vendor/heyspex-mcp/review-focus.mjs +25 -1
  326. package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
  327. package/vendor/heyspex-mcp/search-specs.mjs +280 -0
  328. package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
  329. package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
  330. package/vendor/heyspex-mcp/session-owner.mjs +109 -0
  331. package/vendor/heyspex-mcp/session-tools.mjs +162 -0
  332. package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
  333. package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
  334. package/vendor/heyspex-mcp/spec-edit-tools.mjs +453 -0
  335. package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
  336. package/vendor/heyspex-mcp/split-research.mjs +304 -56
  337. package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
  338. package/vendor/heyspex-mcp/submit-for-review.mjs +150 -7
  339. package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
  340. package/vendor/heyspex-mcp/summary-write-tools.mjs +228 -0
  341. package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
  342. package/vendor/heyspex-mcp/test-helpers.mjs +123 -1
  343. package/vendor/heyspex-mcp/tool-catalog.mjs +2819 -0
  344. package/vendor/heyspex-mcp/tool-dispatch.mjs +657 -0
  345. package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
  346. package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
  347. package/vendor/heyspex-mcp/vault-io.mjs +720 -0
  348. package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
  349. package/vendor/heyspex-mcp/vault-resolve.mjs +554 -68
  350. package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
  351. package/vendor/heyspex-mcp/whoami.mjs +92 -0
  352. package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
@@ -18,7 +18,9 @@
18
18
  * [[closed-trackable-specs-are-historical-record-not-living-docs]]) and
19
19
  * classify each hit SUPERSEDED | SUBSUMED | SIBLING | UPSTREAM | PARENT.
20
20
  * 3. Code audit — resolve the literal files/symbols the plan names against
21
- * the codebase: unresolved → BLOCKER, resolved → INFO.
21
+ * the CODE checkout first and the vault checkout second
22
+ * ({@link resolveCodeAuditRoots}): unresolved in both → BLOCKER,
23
+ * resolved → INFO.
22
24
  * 4. Canonical + supersession — borrow the SRP test from
23
25
  * [[doc-canonicalization-protocol]] and the relationship language from
24
26
  * [[supersession-protocol]].
@@ -31,7 +33,10 @@
31
33
 
32
34
  import { createHash } from 'node:crypto';
33
35
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
34
- import { join } from 'node:path';
36
+ import { isAbsolute, join, resolve, sep } from 'node:path';
37
+ import { PACKAGE_ROOT } from './package-root.mjs';
38
+ import { assertPathUnderRoot } from '../../scripts/lib/shared-path-helpers.mjs';
39
+ import { resolveCodeAndVaultRoots } from '../../scripts/lib/shared-path-helpers.mjs';
35
40
 
36
41
  // In-flight statuses only. Terminal-status specs (closed / superseded /
37
42
  // cancelled / duplicate / archived) are frozen historical records, filtered
@@ -56,11 +61,17 @@ const OVERLAP_DIRS = [
56
61
  // Memory buckets scanned only for the Axis-B design-supersession pass (Pass 6).
57
62
  const MEMORY_AXIS_DIRS = ['specs/memory/decisions', 'specs/memory/lessons'];
58
63
 
59
- // Code roots scanned by the code-audit pass for symbol resolution. Only those
60
- // that exist under vaultRoot are read; missing roots are skipped silently.
61
- // (specs/ is intentionally excluded — it holds markdown, not code; symbols
62
- // mentioned only in prose specs must not count as "resolved" in code.)
64
+ // Code roots scanned by the code-audit pass for symbol resolution, relative to
65
+ // each checkout in {@link resolveCodeAuditRoots}. Only those that exist are
66
+ // read; missing roots are skipped silently. (specs/ is intentionally excluded —
67
+ // it holds markdown, not code; symbols mentioned only in prose specs must not
68
+ // count as "resolved" in code.)
63
69
  const CODE_ROOTS = ['vendor/heyspex-mcp', 'spex-console/src', 'scripts'];
70
+
71
+ // THIS PACKAGE's own root — `vendor/heyspex-mcp/plan-review.mjs` -> two levels
72
+ // up: the code repo root in the monorepo, `node_modules/heyspex` in a packed
73
+ // install. Same derivation `read-path.mjs` (PACKAGE_ROOT) and `dev-tools.mjs`
74
+ // (CHECKOUT_ROOT) already use for this package's own sibling code.
64
75
  const CODE_EXTS = new Set(['.mjs', '.ts', '.tsx', '.js', '.jsx', '.cjs', '.mts', '.cts']);
65
76
  const SKIP_DIRS = new Set(['node_modules', '.git', '.next', 'dist', 'build', '.turbo', 'coverage']);
66
77
  const MAX_CODE_FILES = 4000; // backstop so the audit can't walk an unbounded tree
@@ -541,9 +552,52 @@ function runOverlapPass(planBody, scope, candidates) {
541
552
  return { in_flight_overlap: overlaps, supersedes, canonical_doc: canonicalDoc };
542
553
  }
543
554
 
544
- /** Recursively collect code files under a root, bounded by MAX_CODE_FILES. */
545
- function collectCodeFiles(vaultRoot, root, acc) {
546
- const full = join(vaultRoot, root);
555
+ /**
556
+ * The ordered checkouts the code audit resolves a plan's `files`/`symbols`
557
+ * against ([[HS-8BHX-OUJ0-DIAG-breakdown-plan-review-resolves-code]]).
558
+ *
559
+ * Since the repo split the vault (`specs/`) and the code (`package.json`,
560
+ * `scripts/`, `spex-console/src`, …) can be two separate checkouts, and the
561
+ * vault holds NO code — so resolving a plan's code paths against the vault root
562
+ * reported every real file as a `BLOCKER`, the highest severity the audit has.
563
+ * The CODE root therefore goes first (a plan's paths are almost always code),
564
+ * and the VAULT root is carried ALONGSIDE it rather than replacing it, so a plan
565
+ * that legitimately names vault content (`specs/…`, `heyspex.yaml`) still
566
+ * resolves. A path present in NEITHER checkout is still a real BLOCKER.
567
+ *
568
+ * Code-root precedence mirrors the two already-fixed sites of this bug family
569
+ * (`spex-console/bin/hs-check.ts`, `spex-console/bin/hs-revalidate.ts`): an
570
+ * explicit caller override, then the `HS_PKG_ROOT` env the `bin/hs.mjs`
571
+ * dispatcher forwards, then this package's own install location. The MCP server
572
+ * is spawned by the editor, not by that dispatcher, so `PACKAGE_ROOT` is the
573
+ * one that normally answers — the same derivation `read-path.mjs` and
574
+ * `dev-tools.mjs` use for this package's sibling code. Single-checkout installs
575
+ * collapse the two roots back to one entry, so nothing changes there.
576
+ *
577
+ * @param {string} vaultRoot resolved vault root (holds `specs/`)
578
+ * @param {string} [codeRootOverride] explicit code root (tests / future callers)
579
+ * @returns {{ label: string, root: string }[]}
580
+ */
581
+ export function resolveCodeAuditRoots(vaultRoot, codeRootOverride) {
582
+ return resolveCodeAndVaultRoots({
583
+ vaultRoot,
584
+ explicit: codeRootOverride,
585
+ packageRoot: PACKAGE_ROOT,
586
+ });
587
+ }
588
+
589
+ /** "the code root (C:\a) or the vault root (C:\b)" — AC3: say where we looked. */
590
+ function describeRoots(roots) {
591
+ return roots.map((r) => `the ${r.label} (${r.root})`).join(' or ');
592
+ }
593
+
594
+ /**
595
+ * Recursively collect code files under `root` inside one checkout, bounded by
596
+ * MAX_CODE_FILES. Each entry keeps the checkout it came from so the reader can
597
+ * be told which root resolved a symbol.
598
+ */
599
+ function collectCodeFiles(base, root, acc) {
600
+ const full = join(base.root, root);
547
601
  if (!existsSync(full)) return;
548
602
  let entries;
549
603
  try {
@@ -555,45 +609,56 @@ function collectCodeFiles(vaultRoot, root, acc) {
555
609
  if (acc.length >= MAX_CODE_FILES) return;
556
610
  if (entry.isDirectory()) {
557
611
  if (SKIP_DIRS.has(entry.name)) continue;
558
- collectCodeFiles(vaultRoot, `${root}/${entry.name}`, acc);
612
+ collectCodeFiles(base, `${root}/${entry.name}`, acc);
559
613
  } else if (entry.isFile()) {
560
614
  const dot = entry.name.lastIndexOf('.');
561
615
  const ext = dot === -1 ? '' : entry.name.slice(dot);
562
616
  if (!CODE_EXTS.has(ext)) continue;
563
- acc.push(`${root}/${entry.name}`);
617
+ acc.push({ base, rel: `${root}/${entry.name}` });
564
618
  }
565
619
  }
566
620
  }
567
621
 
568
622
  /**
569
623
  * Pass 3 — code audit. Resolve each file path and symbol the plan names against
570
- * the codebase. Unresolved → BLOCKER; resolved → INFO.
624
+ * the code checkout first and the vault checkout second
625
+ * ({@link resolveCodeAuditRoots}). Unresolved in BOTH → BLOCKER; resolved → INFO.
626
+ *
627
+ * @returns {{ findings: object[], roots: { label: string, root: string }[] }}
571
628
  */
572
- function runCodeAudit(vaultRoot, scope, planContent) {
629
+ function runCodeAudit(vaultRoot, scope, planContent, codeRootOverride) {
573
630
  const findings = [];
631
+ const roots = resolveCodeAuditRoots(vaultRoot, codeRootOverride);
574
632
 
575
- // File-path resolution — cheap existence check.
633
+ // File-path resolution — cheap existence check, per checkout in order.
576
634
  for (const f of scope.files) {
577
635
  const clean = f.replace(/^\.\//, '');
578
- const resolved = existsSync(join(vaultRoot, clean));
636
+ const hit = roots.find((entry) => existsSync(join(entry.root, clean)));
579
637
  findings.push({
580
638
  kind: 'file',
581
639
  ref: f,
582
- severity: resolved ? 'INFO' : 'BLOCKER',
583
- note: resolved
584
- ? `File exists at ${clean}.`
585
- : `Plan references ${f} but no such file exists under the vault root — fix the path or add it to scope.`,
640
+ severity: hit ? 'INFO' : 'BLOCKER',
641
+ resolved_root: hit ? hit.label : null,
642
+ note: hit
643
+ ? `File exists at ${clean} under the ${hit.label} (${hit.root}).`
644
+ : `Plan references ${f} but no such file exists under ${describeRoots(roots)} — fix the path or add it to scope.`,
586
645
  });
587
646
  }
588
647
 
589
648
  // Symbol resolution — build the code corpus once, then test each symbol.
590
649
  if (scope.symbols.length) {
591
650
  const files = [];
592
- for (const root of CODE_ROOTS) collectCodeFiles(vaultRoot, root, files);
651
+ for (const entry of roots) {
652
+ for (const root of CODE_ROOTS) collectCodeFiles(entry, root, files);
653
+ }
593
654
  const corpus = [];
594
- for (const rel of files) {
655
+ for (const file of files) {
595
656
  try {
596
- corpus.push({ rel, content: readFileSync(join(vaultRoot, rel), 'utf8') });
657
+ corpus.push({
658
+ rel: file.rel,
659
+ base: file.base,
660
+ content: readFileSync(join(file.base.root, file.rel), 'utf8'),
661
+ });
597
662
  } catch {
598
663
  /* skip unreadable */
599
664
  }
@@ -601,9 +666,11 @@ function runCodeAudit(vaultRoot, scope, planContent) {
601
666
  for (const sym of scope.symbols) {
602
667
  const re = new RegExp(`\\b${sym.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`);
603
668
  const where = [];
669
+ const whereRoots = new Set();
604
670
  for (const file of corpus) {
605
671
  if (re.test(file.content)) {
606
672
  where.push(file.rel);
673
+ whereRoots.add(file.base.label);
607
674
  if (where.length >= 5) break;
608
675
  }
609
676
  }
@@ -612,14 +679,15 @@ function runCodeAudit(vaultRoot, scope, planContent) {
612
679
  ref: sym,
613
680
  severity: where.length ? 'INFO' : 'BLOCKER',
614
681
  where,
682
+ resolved_root: where.length ? [...whereRoots].join(', ') : null,
615
683
  note: where.length
616
- ? `Symbol resolves in ${where.length === 5 ? '5+' : where.length} file(s).`
617
- : `Plan references \`${sym}\` but it is not defined anywhere in the scanned code roots (${CODE_ROOTS.join(', ')}) — verify the name or add the missing implementation to scope.`,
684
+ ? `Symbol resolves in ${where.length === 5 ? '5+' : where.length} file(s) under the ${[...whereRoots].join(', ')}.`
685
+ : `Plan references \`${sym}\` but it is not defined anywhere in the scanned code roots (${CODE_ROOTS.join(', ')}) under ${describeRoots(roots)} — verify the name or add the missing implementation to scope.`,
618
686
  });
619
687
  }
620
688
  }
621
689
 
622
- return findings;
690
+ return { findings, roots };
623
691
  }
624
692
 
625
693
  /** Pass 4 — turn findings into actionable V2 patch hints. */
@@ -976,6 +1044,7 @@ export function buildQualityJudgeBrief(proposedTasks = []) {
976
1044
  return lines.join('\n');
977
1045
  }
978
1046
 
1047
+
979
1048
  /**
980
1049
  * Run the full plan review. Accepts either a `planPath` (read from disk,
981
1050
  * relative to vaultRoot or absolute) or inline `planContent`. When
@@ -983,12 +1052,20 @@ export function buildQualityJudgeBrief(proposedTasks = []) {
983
1052
  * synthesize review input from a draft task set. Writes a marker file keyed
984
1053
  * by the sha256 of the plan content and returns the findings.
985
1054
  *
1055
+ * `codeRoot` is an optional override for the CODE checkout the pass-3 audit
1056
+ * resolves file paths and symbols against; it defaults to `HS_PKG_ROOT` / this
1057
+ * package's own root, with the vault root carried alongside it
1058
+ * ({@link resolveCodeAuditRoots} —
1059
+ * [[HS-8BHX-OUJ0-DIAG-breakdown-plan-review-resolves-code]]).
1060
+ *
986
1061
  * @returns { scope_terms, in_flight_overlap, canonical_doc, supersedes,
987
- * code_audit, vocabulary_reconciliation, v2_patch_hints,
988
- * review_marker_path, review_scope, quality_findings? } | { error, message }
1062
+ * code_audit, code_audit_roots, vocabulary_reconciliation,
1063
+ * v2_patch_hints, review_marker_path, review_scope,
1064
+ * quality_findings? } | { error, message }
989
1065
  */
990
1066
  export function runPlanReview({
991
1067
  vaultRoot,
1068
+ codeRoot,
992
1069
  planPath,
993
1070
  planContent,
994
1071
  scope = 'plan',
@@ -1058,7 +1135,12 @@ export function runPlanReview({
1058
1135
  const scopeTerms = extractScopeTerms(analysisText);
1059
1136
  const candidates = loadOverlapCandidates(vaultRoot);
1060
1137
  const { in_flight_overlap, supersedes, canonical_doc } = runOverlapPass(analysisText, scopeTerms, candidates);
1061
- const code_audit = runCodeAudit(vaultRoot, scopeTerms, content);
1138
+ const { findings: code_audit, roots: codeAuditRoots } = runCodeAudit(
1139
+ vaultRoot,
1140
+ scopeTerms,
1141
+ content,
1142
+ codeRoot,
1143
+ );
1062
1144
 
1063
1145
  // Pass 5 — vocabulary reconciliation, scoped to the specs the overlap pass
1064
1146
  // already flagged (high precision). Index candidates by every id form so the
@@ -1088,6 +1170,9 @@ export function runPlanReview({
1088
1170
  canonical_doc,
1089
1171
  supersedes,
1090
1172
  code_audit,
1173
+ // AC3 — say which checkouts the code audit searched, so a future wrong-root
1174
+ // failure is readable from the output instead of guessed at.
1175
+ code_audit_roots: codeAuditRoots.map((entry) => ({ label: entry.label, root: entry.root })),
1091
1176
  vocabulary_reconciliation,
1092
1177
  axis_b_drift: axis_b.axis_b_drift,
1093
1178
  axis_b_decisions_checked: axis_b.axis_b_decisions_checked,
@@ -1123,10 +1208,14 @@ export function runPlanReview({
1123
1208
  const reviewsDirRel = join('.heyspex', 'plan-reviews');
1124
1209
  const reviewsDirAbs = join(vaultRoot, reviewsDirRel);
1125
1210
  const markerRel = join(reviewsDirRel, `${hash}.json`);
1211
+ const markerAbs = join(vaultRoot, markerRel);
1126
1212
  try {
1213
+ // Refuse BEFORE any mkdirSync/writeFileSync — a malformed vaultRoot must
1214
+ // create nothing on disk, not create it somewhere wrong ([[HS-MMR6]]).
1215
+ assertPathUnderRoot(vaultRoot, markerAbs, 'plan-review marker write');
1127
1216
  if (!existsSync(reviewsDirAbs)) mkdirSync(reviewsDirAbs, { recursive: true });
1128
1217
  const marker = { ...result, plan_content_sha256: hash, reviewed_marker: true };
1129
- writeFileSync(join(vaultRoot, markerRel), JSON.stringify(marker, null, 2), 'utf8');
1218
+ writeFileSync(markerAbs, JSON.stringify(marker, null, 2), 'utf8');
1130
1219
  result.review_marker_path = markerRel.replace(/\\/g, '/');
1131
1220
  } catch (err) {
1132
1221
  // Marker write failure is surfaced but doesn't drop the findings — the
@@ -1405,9 +1494,14 @@ export function runPlanQc({
1405
1494
  axis_b_user_confirmed: userConfirmed && axisB.axis_b_drift.length > 0,
1406
1495
  axis_b_confirmation_note: userConfirmed && confirmationNote ? confirmationNote : undefined,
1407
1496
  };
1497
+ const qcMarkerAbs = join(vaultRoot, markerRel);
1408
1498
  try {
1499
+ // Refuse BEFORE any mkdirSync/writeFileSync -- same guard, same reason as
1500
+ // the plan-review marker write above ([[HS-MMR6]]): a malformed vaultRoot
1501
+ // must create nothing on disk, not create it somewhere wrong.
1502
+ assertPathUnderRoot(vaultRoot, qcMarkerAbs, 'plan-review marker write');
1409
1503
  if (!existsSync(qcDirAbs)) mkdirSync(qcDirAbs, { recursive: true });
1410
- writeFileSync(join(vaultRoot, markerRel), JSON.stringify(marker, null, 2), 'utf8');
1504
+ writeFileSync(qcMarkerAbs, JSON.stringify(marker, null, 2), 'utf8');
1411
1505
  return {
1412
1506
  qc_marker_path: markerRel.replace(/\\/g, '/'),
1413
1507
  plan_content_sha256: hash,
@@ -0,0 +1,327 @@
1
+ // heyspex_list_procedures, get_procedure, preflight handlers (disk and tier-aware
2
+ // variants; the procedure store itself is procedure-tools.mjs). Extracted from cli.mjs under
3
+ // [[HS-BLNF-6W61-DIAG-cli-mjs-has-grown-into-a-13-665-line]] (no behavior change).
4
+
5
+ import { executeGetProcedure, executeGetProcedureViaTier, executeListProcedures, executeListProceduresViaTier, executeRunPreflight, executeRunPreflightViaTier } from './procedure-tools.mjs';
6
+ import { detectAgentIdentity, effectiveConsoleUrl } from './mcp-config.mjs';
7
+ import { preflightMarkerNotice, writePreflightMarker } from './preflight-gate.mjs';
8
+
9
+ /**
10
+ * [[HS-K0ZP]]: reading research-capture-protocol directly is the same proof that
11
+ * running heyspex_preflight({ intent: "research" }) gives, so a successful
12
+ * heyspex_get_procedure on it writes the `research` marker too. Only a real body
13
+ * counts -- a NOT_FOUND response taught the agent nothing. Never throws: the
14
+ * marker write is fail-open by contract, and a read must never break on it.
15
+ */
16
+ export const RESEARCH_PROTOCOL_NAME = 'research-capture-protocol';
17
+ export function recordResearchProtocolRead(vaultRoot, args, result) {
18
+ try {
19
+ if (String(args?.name ?? '').trim() !== RESEARCH_PROTOCOL_NAME) return false;
20
+ if (!result || result.error || !result.content) return false;
21
+ const identity = detectAgentIdentity(args.agent, args.session_id);
22
+ return writePreflightMarker(vaultRoot, 'research', {
23
+ sessionId: identity.sessionId,
24
+ agent: identity.agent,
25
+ procedures: [RESEARCH_PROTOCOL_NAME],
26
+ degradedSource: result.degraded ? 'disk' : undefined,
27
+ });
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+ import { matchingRecipes } from './recipe-tools.mjs';
33
+ import { wrapDesignResult } from './tool-helpers.mjs';
34
+ import { MEMORY_BASELINE_TAGS, MEMORY_DESCRIPTION_TRUNCATE, MEMORY_INJECTION_LIMIT, detectMemoryAmendment, scoreRelevantMemories } from './on-deck-brief.mjs';
35
+ import { consoleReadErrorFrom, readViaSelector, resolveReadTier } from './read-path.mjs';
36
+ import { consoleFetch } from './console-transport.mjs';
37
+ import { extractSummary } from './memory-tools.mjs';
38
+
39
+ export function handleListProcedures(vaultRoot, args) {
40
+ return wrapDesignResult(executeListProcedures(vaultRoot, args));
41
+ }
42
+
43
+ export function handleGetProcedure(vaultRoot, args) {
44
+ const result = executeGetProcedure(vaultRoot, args);
45
+ recordResearchProtocolRead(vaultRoot, args, result);
46
+ return wrapDesignResult(result);
47
+ }
48
+
49
+ export function handleRunPreflight(vaultRoot, args) {
50
+ const result = executeRunPreflight(vaultRoot, args);
51
+
52
+ // Proof-of-use ([[preflight-gate]]): record that the questions were actually
53
+ // asked, so the tools those questions govern can refuse when they were not.
54
+ // Only a real answer counts — an UNKNOWN_INTENT / CATALOG_NOT_FOUND response
55
+ // taught the agent nothing and must not clear a gate.
56
+ if (result && !result.error) {
57
+ const identity = detectAgentIdentity(args.agent, args.session_id);
58
+ const wrote = writePreflightMarker(vaultRoot, String(args.intent ?? '').trim(), {
59
+ sessionId: identity.sessionId,
60
+ agent: identity.agent,
61
+ threadType: args.thread_type ?? null,
62
+ procedures: Array.isArray(result.procedures)
63
+ ? result.procedures.map((p) => p?.procedure).filter(Boolean)
64
+ : [],
65
+ });
66
+ // A marker we could not write used to be swallowed silently, so preflight
67
+ // reported success while the gated tools refused forever and told the caller
68
+ // to "call preflight, then retry" — a loop no retry escaped
69
+ // ([[HS-N71O-P3XQ-DIAG-six-new-qc-gates-fail-cold-verification]] item 3).
70
+ // Preflight still succeeds (fail-open); it just stops claiming it recorded
71
+ // something it did not.
72
+ const notice = preflightMarkerNotice(wrote);
73
+ if (notice) Object.assign(result, notice);
74
+ }
75
+ // Match workflow-recipes against the intent so recipes are discoverable the
76
+ // same way procedures are ([[HS-RQ9K-TASK]] / [[HS-5G0P-EPIC]]). Never throws.
77
+ let recipes = [];
78
+ try {
79
+ recipes = matchingRecipes(vaultRoot, args.intent);
80
+ } catch {
81
+ recipes = [];
82
+ }
83
+
84
+ // Recipe-only intent (e.g. "bug_fix"): a valid recipe signature that is NOT a
85
+ // procedure intent returns UNKNOWN_INTENT from the procedure map. Rather than
86
+ // hard-error, surface the matching recipes so the caller still gets a useful
87
+ // answer — recipe intents live in a different namespace than procedure intents.
88
+ if (result && result.error === 'UNKNOWN_INTENT' && recipes.length > 0) {
89
+ return wrapDesignResult({
90
+ intent: String(args.intent ?? '').trim(),
91
+ procedures: [],
92
+ recipes,
93
+ note:
94
+ 'No procedure checklist for this intent, but it matches one or more workflow-recipes. Start one with heyspex_start_recipe, or call heyspex_preflight with a procedure intent (e.g. file_task, execute_spec).',
95
+ });
96
+ }
97
+
98
+ // Attach recalled_memories so high-stakes decision points carry the same
99
+ // memory load /hs-next does. Skipped on error responses ([[HS-TASK-IMMF]]).
100
+ if (result && !result.error) {
101
+ try {
102
+ const recalled = scoreRelevantMemories(vaultRoot, {
103
+ // Use the intent name as a primary signal so an intent like
104
+ // recommend_location surfaces template/saas/architecture memories
105
+ // strongly. The baseline pool still applies on top.
106
+ primaryTags: typeof args.intent === 'string' ? [args.intent] : [],
107
+ });
108
+ if (recalled.items.length > 0) {
109
+ result.recalled_memories = recalled;
110
+ }
111
+ } catch {
112
+ // Recall failure must never break preflight — surface the procedures
113
+ // without memories rather than 500ing the response.
114
+ }
115
+ if (recipes.length > 0) {
116
+ result.recipes = recipes;
117
+ }
118
+ }
119
+ return wrapDesignResult(result);
120
+ }
121
+
122
+ /**
123
+ * Cloud-reliant sibling of `handleListProcedures` ([[HS-DJWP-MCPC-TASK]]
124
+ * "Procedures" family, backing `heyspex_list_procedures`). `handleListProcedures`
125
+ * stays untouched (its `diskRead` branch).
126
+ */
127
+ export async function handleListProceduresViaTier(vaultRoot, args) {
128
+ const tier = await resolveReadTier();
129
+ const result = await executeListProceduresViaTier({
130
+ vaultRoot,
131
+ args,
132
+ tier,
133
+ consoleFetch,
134
+ consoleUrl: effectiveConsoleUrl(),
135
+ });
136
+ return wrapDesignResult(result);
137
+ }
138
+
139
+ /**
140
+ * Cloud-reliant sibling of `handleGetProcedure` ([[HS-DJWP-MCPC-TASK]]
141
+ * "Procedures" family, backing `heyspex_get_procedure`). `handleGetProcedure`
142
+ * stays untouched (its `diskRead` branch).
143
+ */
144
+ export async function handleGetProcedureViaTier(vaultRoot, args) {
145
+ const tier = await resolveReadTier();
146
+ const result = await executeGetProcedureViaTier({
147
+ vaultRoot,
148
+ args,
149
+ tier,
150
+ consoleFetch,
151
+ consoleUrl: effectiveConsoleUrl(),
152
+ });
153
+ recordResearchProtocolRead(vaultRoot, args, result);
154
+ return wrapDesignResult(result);
155
+ }
156
+
157
+ /**
158
+ * Cloud-reliant sibling of `handleRunPreflight` ([[HS-DJWP-MCPC-TASK]]
159
+ * "Procedures" + "Memories" families, backing `heyspex_preflight`).
160
+ * `handleRunPreflight` is left completely untouched — it is imported and
161
+ * called directly (synchronously) by `preflight-gate.test.mjs`, so it must
162
+ * keep returning a plain object, never a Promise. This is a SEPARATE function,
163
+ * used only by the live tool dispatch (`case 'heyspex_preflight'` below),
164
+ * which duplicates the marker-write / recipe-match / memory-recall tail
165
+ * (`writePreflightMarker`, `preflightMarkerNotice`, `matchingRecipes` are
166
+ * reused unchanged — only the tier-aware catalog + memory fetch differ).
167
+ */
168
+ export async function handleRunPreflightViaTier(vaultRoot, args) {
169
+ const tier = await resolveReadTier();
170
+ const consoleUrl = effectiveConsoleUrl();
171
+ const result = await executeRunPreflightViaTier({ vaultRoot, args, tier, consoleFetch, consoleUrl });
172
+
173
+ if (result && !result.error) {
174
+ const identity = detectAgentIdentity(args.agent, args.session_id);
175
+ const wrote = writePreflightMarker(vaultRoot, String(args.intent ?? '').trim(), {
176
+ sessionId: identity.sessionId,
177
+ agent: identity.agent,
178
+ threadType: args.thread_type ?? null,
179
+ procedures: Array.isArray(result.procedures)
180
+ ? result.procedures.map((p) => p?.procedure).filter(Boolean)
181
+ : [],
182
+ degradedSource: result.degraded ? 'disk' : undefined,
183
+ });
184
+ const notice = preflightMarkerNotice(wrote);
185
+ if (notice) Object.assign(result, notice);
186
+ }
187
+
188
+ let recipes = [];
189
+ try {
190
+ recipes = matchingRecipes(vaultRoot, args.intent);
191
+ } catch {
192
+ recipes = [];
193
+ }
194
+
195
+ if (result && result.error === 'UNKNOWN_INTENT' && recipes.length > 0) {
196
+ return wrapDesignResult({
197
+ intent: String(args.intent ?? '').trim(),
198
+ procedures: [],
199
+ recipes,
200
+ note:
201
+ 'No procedure checklist for this intent, but it matches one or more workflow-recipes. Start one with heyspex_start_recipe, or call heyspex_preflight with a procedure intent (e.g. file_task, execute_spec).',
202
+ });
203
+ }
204
+
205
+ if (result && !result.error) {
206
+ try {
207
+ const recalled = await scoreRelevantMemoriesViaTier(
208
+ vaultRoot,
209
+ { primaryTags: typeof args.intent === 'string' ? [args.intent] : [] },
210
+ { tier, consoleUrl },
211
+ );
212
+ if (recalled.items.length > 0) {
213
+ result.recalled_memories = recalled;
214
+ }
215
+ } catch {
216
+ // Recall failure must never break preflight.
217
+ }
218
+ if (recipes.length > 0) {
219
+ result.recipes = recipes;
220
+ }
221
+ }
222
+ return wrapDesignResult(result);
223
+ }
224
+
225
+ /**
226
+ * Cloud-reliant sibling of `scoreRelevantMemories` ([[HS-DJWP-MCPC-TASK]]
227
+ * "Memories" family, backing the preflight memory recall). `scoreRelevantMemories`
228
+ * is left completely untouched (its callers — `handleRunPreflight`,
229
+ * `enrichOnDeckWithRecalledMemories` — keep the exact disk-only behavior they
230
+ * have today); this is a NEW, separate cloud path used only by
231
+ * `handleRunPreflightViaTier` below. Scoring logic (tag-overlap, amendment
232
+ * detection, truncation, sort) is duplicated rather than shared with
233
+ * `scoreRelevantMemories` deliberately — the two operate on differently-shaped
234
+ * inputs (disk: `{frontmatter, body}` per file; cloud: the console's flat
235
+ * `MemoryDoc` rows) and duplicating ~25 lines is a smaller risk than
236
+ * refactoring the already-tested disk function to share a helper.
237
+ *
238
+ * Unlike `handleRecallMemoriesViaTier`, no fidelity gap here — `MemoryDoc`
239
+ * already carries everything this scorer reads (`tags`, `body`, `name`, `id`).
240
+ *
241
+ * Split into a tag-only ranking pass ({@link rankMemoryCandidatesByTags}) and a
242
+ * per-item body-dependent builder ({@link buildScoredMemoryItem}) so the
243
+ * caller ({@link scoreRelevantMemoriesViaTier}) can rank against a
244
+ * summary-only listing (score is pure tag-overlap, never touches body) and
245
+ * only fetch body text for the top `limit` survivors —
246
+ * [[HS-LKV0-FUU4-DIAG-cloud-mode-recall-times-out-the]]: the console's
247
+ * `GET /api/memories` used to always ship a body per doc, which is what made
248
+ * a 896-doc vault blow the 30s fetch timeout on every one of these read paths.
249
+ */
250
+ export function rankMemoryCandidatesByTags(docs, options = {}) {
251
+ const primaryTags = Array.isArray(options.primaryTags) ? options.primaryTags : [];
252
+ const baselineTags = Array.isArray(options.baselineTags) ? options.baselineTags : MEMORY_BASELINE_TAGS;
253
+ const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : MEMORY_INJECTION_LIMIT;
254
+
255
+ const querySet = new Set([...primaryTags, ...baselineTags].map((t) => String(t).toLowerCase()));
256
+ if (querySet.size === 0) return [];
257
+
258
+ const scored = [];
259
+ for (const doc of docs) {
260
+ const tags = Array.isArray(doc.tags) ? doc.tags : [];
261
+ if (tags.length === 0) continue;
262
+ const tagSet = new Set(tags.map((t) => String(t).toLowerCase()));
263
+ let score = 0;
264
+ for (const q of querySet) if (tagSet.has(q)) score += 1;
265
+ if (score === 0) continue;
266
+ scored.push({ doc, score });
267
+ }
268
+
269
+ scored.sort((a, b) => {
270
+ if (b.score !== a.score) return b.score - a.score;
271
+ const aName = a.doc.name || a.doc.id || '';
272
+ const bName = b.doc.name || b.doc.id || '';
273
+ return String(aName).localeCompare(String(bName));
274
+ });
275
+
276
+ return scored.slice(0, limit);
277
+ }
278
+
279
+ /** Builds one scored-memory item from a ranked candidate + its (separately fetched) body. */
280
+ export function buildScoredMemoryItem(doc, score, body) {
281
+ const slug = doc.id;
282
+ const name = doc.name || doc.id || slug;
283
+ let description = extractSummary(body || '');
284
+ if (description.length > MEMORY_DESCRIPTION_TRUNCATE) {
285
+ description = description.slice(0, MEMORY_DESCRIPTION_TRUNCATE - 1) + '…';
286
+ }
287
+ const amended = detectMemoryAmendment(body || '', description);
288
+ if (amended) {
289
+ description = `${description} ⚠ ${amended} — this summary may not reflect the current ruling; READ THE FULL BODY before applying.`;
290
+ }
291
+ return { source: 'vault', name, link: `[[${slug}]]`, description, amended: amended || undefined, score };
292
+ }
293
+
294
+ export async function scoreRelevantMemoriesViaTier(vaultRoot, options, { tier, consoleUrl }) {
295
+ return readViaSelector({
296
+ tier,
297
+ diskRead: () => scoreRelevantMemories(vaultRoot, options),
298
+ neonRead: async () => {
299
+ // Summary-only listing — scoring is pure tag-overlap and never needs body.
300
+ const res = await consoleFetch('/api/memories');
301
+ if (!res.ok) {
302
+ throw consoleReadErrorFrom(res, `GET /api/memories returned ${res.status}: ${res.error || JSON.stringify(res.body ?? {})}`, null);
303
+ }
304
+ const docs = Array.isArray(res.body?.memories) ? res.body.memories : [];
305
+ const ranked = rankMemoryCandidatesByTags(docs, options);
306
+ if (ranked.length === 0) return { header: null, items: [] };
307
+
308
+ // Fetch bodies ONLY for the survivors that ranking already narrowed to
309
+ // (`limit`, default 5) — never one fetch per candidate across the whole set.
310
+ const idsQs = new URLSearchParams({ ids: ranked.map(({ doc }) => doc.id).join(','), fields: 'body' });
311
+ const bodyPath = `/api/memories?${idsQs}`;
312
+ const bodyRes = await consoleFetch(bodyPath);
313
+ if (!bodyRes.ok) {
314
+ throw consoleReadErrorFrom(bodyRes, `GET ${bodyPath} returned ${bodyRes.status}: ${bodyRes.error || JSON.stringify(bodyRes.body ?? {})}`, null);
315
+ }
316
+ const bodyDocs = Array.isArray(bodyRes.body?.memories) ? bodyRes.body.memories : [];
317
+ const bodyById = new Map(bodyDocs.map((d) => [d.id, typeof d.body === 'string' ? d.body : '']));
318
+
319
+ return {
320
+ header: 'Retrieved from prior sessions. Apply at decision points; do not echo verbatim.',
321
+ items: ranked.map(({ doc, score }) => buildScoredMemoryItem(doc, score, bodyById.get(doc.id) || '')),
322
+ };
323
+ },
324
+ routeLabel: 'GET /api/memories',
325
+ consoleUrl,
326
+ });
327
+ }