heyspex 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (352) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/README.md +21 -0
  3. package/bin/hs.mjs +1089 -104
  4. package/bin/spexcode.mjs +262 -0
  5. package/dev-factory/engine/worktree.mjs +740 -0
  6. package/dev-factory/lib/code-repo-root.mjs +67 -0
  7. package/dev-factory/lib/context-budget.mjs +84 -0
  8. package/dev-factory/lib/epic-detect.mjs +190 -0
  9. package/dev-factory/lib/fleet-queue-file.mjs +846 -0
  10. package/dev-factory/lib/fleet-queue.mjs +426 -0
  11. package/dev-factory/lib/fleet-state.mjs +14 -18
  12. package/dev-factory/lib/model-catalog.mjs +338 -0
  13. package/dev-factory/lib/seat-context.mjs +199 -0
  14. package/dev-factory/lib/spec-io.mjs +93 -0
  15. package/dev-factory/lib/sufficiency-score.mjs +8 -2
  16. package/dev-factory/lib/worktree-allocator.mjs +744 -0
  17. package/dev-factory/model-catalog.json +222 -0
  18. package/dist/cli/hs-check.mjs +1127 -103
  19. package/dist/cli/hs-claim.mjs +981 -63
  20. package/dist/cli/hs-connect.mjs +10059 -1729
  21. package/dist/cli/hs-defer.mjs +5507 -3072
  22. package/dist/cli/hs-dev.mjs +576 -0
  23. package/dist/cli/hs-env.mjs +416 -302
  24. package/dist/cli/hs-index-rebuild.mjs +7334 -0
  25. package/dist/cli/hs-index-reconcile.mjs +8138 -0
  26. package/dist/cli/hs-init.mjs +2840 -149
  27. package/dist/cli/hs-install.mjs +7214 -594
  28. package/dist/cli/hs-join.mjs +10640 -2421
  29. package/dist/cli/hs-vault-pull.mjs +8157 -6370
  30. package/dist/cli/hs-vault-push.mjs +6433 -0
  31. package/dist/cli/hs-vault.mjs +4029 -1555
  32. package/dist/cli/hs-workspace.mjs +2702 -538
  33. package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
  34. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
  35. package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
  36. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
  37. package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
  38. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
  39. package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
  40. package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
  41. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
  42. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
  43. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
  44. package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
  45. package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
  46. package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -0
  47. package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
  48. package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
  49. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
  50. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
  51. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
  52. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
  53. package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
  54. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
  55. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
  56. package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -0
  57. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
  58. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
  59. package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
  60. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
  61. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
  62. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
  63. package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
  64. package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
  65. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +33 -0
  66. package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
  67. package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
  68. package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
  69. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
  70. package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
  71. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
  72. package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
  73. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
  74. package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
  75. package/dist/cli-templates/harness-commands/cursor/hs-view.md +32 -0
  76. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
  77. package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
  78. package/docs/copilot-plugin-install.md +162 -0
  79. package/docs/driving-heyspex-from-copilot.md +165 -0
  80. package/docs/mcp-client-config.md +259 -6
  81. package/package.json +249 -124
  82. package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
  83. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
  84. package/plugins/heyspex/.mcp.json +12 -0
  85. package/plugins/heyspex/README.md +309 -0
  86. package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
  87. package/plugins/heyspex/bin/run-hook.mjs +80 -0
  88. package/plugins/heyspex/hooks/cursor-hooks.json +308 -0
  89. package/plugins/heyspex/hooks/hooks.json +418 -0
  90. package/plugins/heyspex/mcp.json +13 -0
  91. package/scripts/check-mcp-freshness.mjs +17 -9
  92. package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
  93. package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
  94. package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
  95. package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -0
  96. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +356 -0
  97. package/scripts/claude-hooks/_run.mjs +413 -0
  98. package/scripts/claude-hooks/_transcript-text.mjs +60 -0
  99. package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
  100. package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
  101. package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
  102. package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
  103. package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
  104. package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
  105. package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
  106. package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
  107. package/scripts/claude-hooks/claim-liveness-shared.mjs +577 -0
  108. package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
  109. package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
  110. package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
  111. package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
  112. package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
  113. package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
  114. package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
  115. package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
  116. package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
  117. package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
  118. package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
  119. package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
  120. package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
  121. package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
  122. package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
  123. package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
  124. package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
  125. package/scripts/claude-hooks/inject-communication-style.mjs +86 -0
  126. package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
  127. package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
  128. package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
  129. package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
  130. package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -0
  131. package/scripts/claude-hooks/log-session-activity.mjs +341 -0
  132. package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
  133. package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
  134. package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
  135. package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
  136. package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
  137. package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
  138. package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
  139. package/scripts/claude-hooks/research-method-guard.mjs +398 -0
  140. package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
  141. package/scripts/claude-hooks/session-end.mjs +451 -0
  142. package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
  143. package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
  144. package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
  145. package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
  146. package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
  147. package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
  148. package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
  149. package/scripts/claude-hooks/wip-checkpoint.mjs +302 -0
  150. package/scripts/cursor-hooks/manifest.mjs +281 -0
  151. package/scripts/dev-server-status.mjs +34 -13
  152. package/scripts/dev-server-stop.mjs +313 -0
  153. package/scripts/heyspex-mode.mjs +288 -0
  154. package/scripts/hs-auto-resolve.mjs +240 -0
  155. package/scripts/hs-auto-source.mjs +398 -0
  156. package/scripts/hs-auto.mjs +134 -0
  157. package/scripts/hs-capture-cli.mjs +326 -0
  158. package/scripts/hs-classifier.mjs +136 -0
  159. package/scripts/hs-companion.mjs +88 -0
  160. package/scripts/hs-gateway.mjs +115 -0
  161. package/scripts/hs-mode.mjs +125 -0
  162. package/scripts/hs-next.mjs +441 -0
  163. package/scripts/hs-settings.mjs +338 -0
  164. package/scripts/hs-view.mjs +496 -0
  165. package/scripts/hs-worktree.mjs +201 -0
  166. package/scripts/install-claude-code-hooks.mjs +272 -0
  167. package/scripts/install-cursor-hooks.mjs +471 -0
  168. package/scripts/lib/agent-env-markers.mjs +38 -0
  169. package/scripts/lib/console-url-resolver.mjs +199 -61
  170. package/scripts/lib/customer-kit.mjs +1203 -0
  171. package/scripts/lib/dev-mode.mjs +249 -32
  172. package/scripts/lib/directive-obligations.mjs +349 -0
  173. package/scripts/lib/heyspex-home.mjs +72 -0
  174. package/scripts/lib/hook-dispositions.mjs +210 -0
  175. package/scripts/lib/hook-vault-header.mjs +30 -0
  176. package/scripts/lib/hs-command.mjs +277 -0
  177. package/scripts/lib/hs-companion-settings.mjs +472 -0
  178. package/scripts/lib/hs-source-branch.mjs +196 -0
  179. package/scripts/lib/hs-source-commit.mjs +59 -5
  180. package/scripts/lib/hs-source-identity.mjs +526 -0
  181. package/scripts/lib/hs-source-log.mjs +13 -22
  182. package/scripts/lib/hs-source-preflight.mjs +18 -4
  183. package/scripts/lib/hs-source-push.mjs +386 -307
  184. package/scripts/lib/hs-worktree-args.mjs +115 -0
  185. package/scripts/lib/shared-path-helpers.mjs +134 -0
  186. package/scripts/lib/standalone-doctor.mjs +540 -3
  187. package/scripts/lib/subagent-git-lock.mjs +109 -0
  188. package/scripts/lib/template-pin.mjs +34 -0
  189. package/scripts/lib/window-placement.mjs +109 -0
  190. package/scripts/provenance-lib.mjs +91 -0
  191. package/scripts/stamp-provenance-on-edit.mjs +222 -0
  192. package/scripts/wip-ledger-health.mjs +653 -0
  193. package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
  194. package/spexharness/src/adapters/audit.mjs +197 -0
  195. package/spexharness/src/adapters/claude-code.mjs +449 -0
  196. package/spexharness/src/adapters/copilot.mjs +236 -0
  197. package/spexharness/src/adapters/detect-harness.mjs +34 -0
  198. package/spexharness/src/chat/tui/ansi.mjs +448 -0
  199. package/spexharness/src/chat/tui/palette.mjs +22 -0
  200. package/spexharness/src/chat/tui/select.mjs +208 -0
  201. package/spexharness/src/config-root.mjs +54 -0
  202. package/spexharness/src/context-budget.mjs +141 -0
  203. package/spexharness/src/dev-server-control.mjs +389 -0
  204. package/spexharness/src/doctrine/behaviour.mjs +371 -0
  205. package/spexharness/src/doctrine/corpus.mjs +733 -0
  206. package/spexharness/src/doctrine/inject.mjs +338 -0
  207. package/spexharness/src/doctrine/merge.mjs +184 -0
  208. package/spexharness/src/doctrine/sources.mjs +252 -0
  209. package/spexharness/src/doctrine/synthesize.mjs +401 -0
  210. package/spexharness/src/doctrine/vault-source.mjs +574 -0
  211. package/spexharness/src/doctrine/writer.mjs +176 -0
  212. package/spexharness/src/events.mjs +84 -0
  213. package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
  214. package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
  215. package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
  216. package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
  217. package/spexharness/src/gates/deep-research-cost-gate.mjs +275 -0
  218. package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
  219. package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
  220. package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
  221. package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
  222. package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
  223. package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
  224. package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
  225. package/spexharness/src/gates/git-identity-guard.mjs +61 -0
  226. package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
  227. package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
  228. package/spexharness/src/gates/git-stash-gate.mjs +159 -0
  229. package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
  230. package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
  231. package/spexharness/src/gates/inject-vault-context.mjs +239 -0
  232. package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
  233. package/spexharness/src/gates/plan-review-gate.mjs +233 -0
  234. package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
  235. package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
  236. package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
  237. package/spexharness/src/gates/settings-write-gate.mjs +104 -0
  238. package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
  239. package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
  240. package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
  241. package/spexharness/src/git-reflog-audit.mjs +294 -0
  242. package/spexharness/src/intent-classifier.mjs +196 -0
  243. package/spexharness/src/intent-directive.mjs +626 -0
  244. package/spexharness/src/profile/learn.mjs +551 -0
  245. package/spexharness/src/profile/proposals.mjs +357 -0
  246. package/spexharness/src/profile/vault-inclusion.mjs +386 -0
  247. package/spexharness/src/redact.mjs +151 -0
  248. package/spexharness/src/rule-table.mjs +849 -0
  249. package/spexharness/src/seat-context.mjs +122 -0
  250. package/spexharness/src/session-bootstrap.mjs +512 -0
  251. package/spexharness/src/session-record.mjs +815 -0
  252. package/spexharness/src/token-inventory.mjs +207 -0
  253. package/vendor/heyspex-mcp/account-switch.mjs +96 -0
  254. package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
  255. package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
  256. package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
  257. package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
  258. package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
  259. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +77 -8
  260. package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
  261. package/vendor/heyspex-mcp/capture-console-client.mjs +117 -3
  262. package/vendor/heyspex-mcp/capture-focus.mjs +1061 -31
  263. package/vendor/heyspex-mcp/capture-outbox.mjs +15 -5
  264. package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
  265. package/vendor/heyspex-mcp/capture-tools.mjs +1264 -0
  266. package/vendor/heyspex-mcp/claim-tools.mjs +1147 -0
  267. package/vendor/heyspex-mcp/cli-verbs.mjs +2074 -0
  268. package/vendor/heyspex-mcp/cli.mjs +207 -15524
  269. package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
  270. package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
  271. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +602 -0
  272. package/vendor/heyspex-mcp/config-seed.yaml +15 -0
  273. package/vendor/heyspex-mcp/config-settings.mjs +290 -7
  274. package/vendor/heyspex-mcp/console-fetch.mjs +822 -17
  275. package/vendor/heyspex-mcp/console-transport.mjs +172 -0
  276. package/vendor/heyspex-mcp/credentials.mjs +585 -0
  277. package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
  278. package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
  279. package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
  280. package/vendor/heyspex-mcp/design-tools.mjs +4 -1
  281. package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
  282. package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
  283. package/vendor/heyspex-mcp/dev-tools.mjs +131 -0
  284. package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
  285. package/vendor/heyspex-mcp/events-tools.mjs +207 -0
  286. package/vendor/heyspex-mcp/filing-tools.mjs +723 -0
  287. package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
  288. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +71 -15
  289. package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
  290. package/vendor/heyspex-mcp/folder-trust-store.mjs +60 -0
  291. package/vendor/heyspex-mcp/http-transport.mjs +302 -0
  292. package/vendor/heyspex-mcp/list-tools.mjs +257 -0
  293. package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
  294. package/vendor/heyspex-mcp/mcp-config.mjs +107 -0
  295. package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
  296. package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
  297. package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
  298. package/vendor/heyspex-mcp/occupied-keys.mjs +142 -0
  299. package/vendor/heyspex-mcp/on-deck-brief.mjs +3377 -0
  300. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
  301. package/vendor/heyspex-mcp/package-root.mjs +32 -0
  302. package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
  303. package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
  304. package/vendor/heyspex-mcp/plan-push.mjs +908 -22
  305. package/vendor/heyspex-mcp/plan-review.mjs +124 -30
  306. package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
  307. package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -0
  308. package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -970
  309. package/vendor/heyspex-mcp/promote-capture.mjs +388 -69
  310. package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
  311. package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
  312. package/vendor/heyspex-mcp/promote-research.mjs +666 -53
  313. package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
  314. package/vendor/heyspex-mcp/prompts.mjs +1460 -0
  315. package/vendor/heyspex-mcp/read-path.mjs +191 -15
  316. package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
  317. package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
  318. package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
  319. package/vendor/heyspex-mcp/register-spec.mjs +266 -54
  320. package/vendor/heyspex-mcp/request-context.mjs +83 -0
  321. package/vendor/heyspex-mcp/research-focus.mjs +207 -4
  322. package/vendor/heyspex-mcp/research-handlers.mjs +625 -0
  323. package/vendor/heyspex-mcp/research-tools.mjs +4 -4
  324. package/vendor/heyspex-mcp/response-budget.mjs +348 -0
  325. package/vendor/heyspex-mcp/review-focus.mjs +25 -1
  326. package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
  327. package/vendor/heyspex-mcp/search-specs.mjs +280 -0
  328. package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
  329. package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
  330. package/vendor/heyspex-mcp/session-owner.mjs +109 -0
  331. package/vendor/heyspex-mcp/session-tools.mjs +162 -0
  332. package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
  333. package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
  334. package/vendor/heyspex-mcp/spec-edit-tools.mjs +453 -0
  335. package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
  336. package/vendor/heyspex-mcp/split-research.mjs +304 -56
  337. package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
  338. package/vendor/heyspex-mcp/submit-for-review.mjs +150 -7
  339. package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
  340. package/vendor/heyspex-mcp/summary-write-tools.mjs +228 -0
  341. package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
  342. package/vendor/heyspex-mcp/test-helpers.mjs +123 -1
  343. package/vendor/heyspex-mcp/tool-catalog.mjs +2819 -0
  344. package/vendor/heyspex-mcp/tool-dispatch.mjs +657 -0
  345. package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
  346. package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
  347. package/vendor/heyspex-mcp/vault-io.mjs +720 -0
  348. package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
  349. package/vendor/heyspex-mcp/vault-resolve.mjs +554 -68
  350. package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
  351. package/vendor/heyspex-mcp/whoami.mjs +92 -0
  352. package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
@@ -0,0 +1,723 @@
1
+ // Filing and plan tools: heyspex_file_spec, register_spec, push_plan, ground_plan,
2
+ // plan_review, plan_qc, validate_linkage, validate_user_skills. 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 { buildGatewayReport } from '../../scripts/gateway-skills-lib.mjs';
6
+ import { resolveSpecIdForMutation, wrapDesignResult } from './tool-helpers.mjs';
7
+ import { validateLinkage } from './validate-linkage.mjs';
8
+ import { runPlanQc, runPlanReview } from './plan-review.mjs';
9
+ import { injectNeonFocusPin, writeFocusSelectionVendor } from './focus-tools.mjs';
10
+ import { executeGroundPlan } from './plan-ground.mjs';
11
+ import { executePushPlanCloudAware } from './plan-push.mjs';
12
+ import { registerSpecViaMode } from './register-spec.mjs';
13
+ import { SUMMARY_REQUIRED_FILE_SPEC_TYPES, buildSummaryRequiredMcpError } from './summary-contract.mjs';
14
+ import { buildCaptureStageRefusal, decideCaptureStageGate } from './capture-stage-gate.mjs';
15
+ import { resolveLiveActiveCapture } from './capture-tools.mjs';
16
+ import { detectAgentIdentity } from './mcp-config.mjs';
17
+ import { randomUUID } from 'node:crypto';
18
+ import { consoleFetch, enrichConsoleFailurePayloadAsync } from './console-transport.mjs';
19
+ import { consoleErrorCode, consoleErrorMessage } from './console-fetch.mjs';
20
+ import { resolveReadTier } from './read-path.mjs';
21
+ import {
22
+ buildDeferredPayload,
23
+ deferredToolResponse,
24
+ drainAndSummarize,
25
+ isOutageClassResult,
26
+ stashIfOutage,
27
+ } from './write-outbox.mjs';
28
+ import { join } from 'node:path';
29
+ import { statSync, existsSync } from 'node:fs';
30
+ import { importDevModeCore } from './dev-mode-core.mjs';
31
+ import { PACKAGE_ROOT } from './package-root.mjs';
32
+ import { resolveCodeRoot } from '../../scripts/lib/shared-path-helpers.mjs';
33
+
34
+ // scripts/lib/dev-mode.mjs is a CODE-repo artifact that ships with this
35
+ // package — since the repo split (2026-08-31) the vault root no longer holds
36
+ // it, so resolve it relative to THIS file's checkout first and fall back to
37
+ // the vault root only for pre-split layouts where they were the same repo.
38
+ // `HS_PKG_ROOT` wins when set (same precedence as dev-tools.mjs's
39
+ // CHECKOUT_ROOT / plan-review.mjs's resolveCodeAuditRoots).
40
+ const CHECKOUT_ROOT = resolveCodeRoot({ packageRoot: PACKAGE_ROOT });
41
+
42
+ /**
43
+ * heyspex_validate_linkage — scan the vault for orphan specs, broken wikilinks,
44
+ * missing manifest refs, and parent/child mismatches ([[HS-TASK-0U1F]]).
45
+ * Core lives in validate-linkage.mjs; this is the MCP face.
46
+ */
47
+ export function handleValidateLinkage(vaultRoot, args = {}) {
48
+ const result = validateLinkage(vaultRoot, {
49
+ spec_id: typeof args.spec_id === 'string' && args.spec_id.trim() ? args.spec_id.trim() : null,
50
+ includeWarnings: args.include_warnings !== false,
51
+ });
52
+ return {
53
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
54
+ };
55
+ }
56
+
57
+ /**
58
+ * heyspex_validate_user_skills — validate user-authored gateway skills and
59
+ * their scope coverage (HS-WEC3). Core lives in scripts/gateway-skills-lib.mjs;
60
+ * this is the MCP face (CLI face: `hs gateway validate`; doctor face:
61
+ * `hs doctor --check-gateway`). Read-only by contract.
62
+ */
63
+ export function handleValidateUserSkills(vaultRoot, args = {}) {
64
+ const report = buildGatewayReport(vaultRoot);
65
+ const slug = typeof args.skill === 'string' && args.skill.trim() ? args.skill.trim() : null;
66
+ const result = slug
67
+ ? {
68
+ ...report,
69
+ skills: { ...report.skills, skills: report.skills.skills.filter((s) => s.slug === slug) },
70
+ orphans: { ...report.orphans, orphans: report.orphans.orphans.filter((o) => o.slug === slug) },
71
+ }
72
+ : report;
73
+ if (slug && result.skills.skills.length === 0) {
74
+ return wrapDesignResult({ error: 'skill_not_found', message: `No user skill named "${slug}" under ${report.skills.root}.` });
75
+ }
76
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
77
+ }
78
+
79
+ /**
80
+ * heyspex_register_spec — link an orphan/mismatched spec into the roadmap
81
+ * hierarchy ([[HS-TASK-5K13]]). Core lives in register-spec.mjs; this is the
82
+ * MCP face. The fix counterpart to heyspex_validate_linkage.
83
+ */
84
+ export async function handleRegisterSpec(vaultRoot, args = {}) {
85
+ const registerResolved = await resolveSpecIdForMutation(args.spec_id);
86
+ if (registerResolved.response) return registerResolved.response;
87
+ // Cloud-aware entry ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]
88
+ // sweep): resolves `resolveVaultWriteMode()` ONCE and either runs the
89
+ // untouched synchronous disk path (`local`) or sends each touched spec
90
+ // through `PUT /api/specs/:id` (`cloud`). Awaiting is safe in both — the
91
+ // local branch delegates straight to `registerSpec` and returns the same
92
+ // plain object it always did.
93
+ const result = await registerSpecViaMode(vaultRoot, {
94
+ spec_id: registerResolved.specId,
95
+ parent_id: typeof args.parent_id === 'string' && args.parent_id.trim() ? args.parent_id.trim() : null,
96
+ roadmap_id: typeof args.roadmap_id === 'string' && args.roadmap_id.trim() ? args.roadmap_id.trim() : undefined,
97
+ position: typeof args.position === 'string' && args.position.trim() ? args.position.trim() : undefined,
98
+ dryRun: args.dry_run === true,
99
+ });
100
+ return {
101
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
102
+ };
103
+ }
104
+
105
+ /**
106
+ * File a NEW spec of any non-capture kind ([[HS-EPIC-1EM0]] / [[HS-TASK-B6DG]]).
107
+ * Thin forwarder to console `POST /api/specs` — the server owns id allocation,
108
+ * canonical-template frontmatter, parent-array linkage, and per-kind validation
109
+ * (all extended by [[HS-TASK-U13O]]). The client contract is preserved verbatim
110
+ * ({ type, name, frontmatter?, body? }); no kind/title rename. POST errors
111
+ * (invalid parent, `type: capture` rejection, etc.) surface verbatim so the
112
+ * agent sees the server's guidance (e.g. "use heyspex_active_capture").
113
+ */
114
+ export function resolveFileSpecSummary(args = {}) {
115
+ const top = typeof args.summary === 'string' ? args.summary.trim() : '';
116
+ const fromFm =
117
+ args.frontmatter && typeof args.frontmatter.summary === 'string'
118
+ ? args.frontmatter.summary.trim()
119
+ : '';
120
+ return top || fromFm;
121
+ }
122
+
123
+ /**
124
+ * The only top-level `heyspex_file_spec` arguments the tool actually reads
125
+ * ([[tool-catalog.mjs]]'s declared inputSchema `properties`, kept in
126
+ * lockstep by `file-spec-artifact-field.test.mjs`). Parent/milestone linkage
127
+ * is NOT a top-level argument — it goes inside `frontmatter.parent` /
128
+ * `frontmatter.milestone` (the schema description says so), but nothing
129
+ * enforced that at runtime.
130
+ */
131
+ export const FILE_SPEC_ACCEPTED_ARGS = [
132
+ 'type',
133
+ 'name',
134
+ 'summary',
135
+ 'frontmatter',
136
+ 'body',
137
+ 'recipe_context',
138
+ 'checks',
139
+ 'expect_tier',
140
+ ];
141
+
142
+ /** Top-level keys a caller plausibly means for frontmatter linkage — used only to tailor the refusal message. */
143
+ const FILE_SPEC_FRONTMATTER_HINT_KEYS = new Set(['parent', 'milestone', 'pillar', 'severity', 'id']);
144
+
145
+ /**
146
+ * Pure verdict on a `heyspex_file_spec` argument bag: refuses an unrecognized
147
+ * top-level key BY NAME instead of letting it silently vanish before
148
+ * `buildFileSpecPostBody` even runs — the same failure mode
149
+ * [[additionalproperties-false-in-an-mcp]] documents for
150
+ * `heyspex_update_capture` (`updateCaptureArgsVerdict`), and the exact shape
151
+ * of [[HS-XQMK-A1QE-DIAG-heyspex-file-spec-drops-the-parent]]: a top-level
152
+ * `parent:` argument passed the declared `additionalProperties: false`
153
+ * schema (never enforced at runtime by the MCP transport), then vanished
154
+ * silently because `buildFileSpecPostBody` never read `args.parent` — every
155
+ * filed task landed orphaned with no error anywhere. Call this at the
156
+ * handler's first hop, before any other validation or the POST.
157
+ */
158
+ export function fileSpecArgsVerdict(args = {}) {
159
+ const unknown = Object.keys(args).filter((k) => !FILE_SPEC_ACCEPTED_ARGS.includes(k));
160
+ if (unknown.length > 0) {
161
+ const hinted = unknown.filter((k) => FILE_SPEC_FRONTMATTER_HINT_KEYS.has(k));
162
+ const hint =
163
+ hinted.length > 0
164
+ ? ` Spec-kind linkage/overrides go inside frontmatter, not top-level: pass frontmatter: { ${hinted
165
+ .map((k) => `${k}: ...`)
166
+ .join(', ')} } instead.`
167
+ : '';
168
+ return {
169
+ ok: false,
170
+ error: {
171
+ error: 'UNKNOWN_FIELD',
172
+ message: `heyspex_file_spec does not accept: ${unknown.join(', ')}. NOTHING WAS FILED — re-send using only the accepted arguments.${hint}`,
173
+ field: unknown[0],
174
+ unknown_fields: unknown,
175
+ accepted_fields: FILE_SPEC_ACCEPTED_ARGS,
176
+ },
177
+ };
178
+ }
179
+ return { ok: true };
180
+ }
181
+
182
+ /**
183
+ * Build the POST /api/specs request body from heyspex_file_spec args. Pure and
184
+ * exported so the field-forwarding contract is unit-testable without a live
185
+ * console. `recipe_context` (the heyspex_start_recipe start context) is
186
+ * forwarded verbatim when it is a non-null object — the server reads recipe_id
187
+ * + tool_scope from it and stamps recipe provenance ([[HS-98G6-TASK]] /
188
+ * [[HS-5G0P-EPIC]] AC5); it is omitted entirely when absent so non-recipe
189
+ * filings are byte-identical to before.
190
+ */
191
+ export function buildFileSpecPostBody(args = {}, actingAgent = null) {
192
+ const authoredSummary = resolveFileSpecSummary(args);
193
+ return {
194
+ type: args.type,
195
+ name: args.name,
196
+ ...(authoredSummary ? { summary: authoredSummary } : {}),
197
+ ...(args.frontmatter && typeof args.frontmatter === 'object'
198
+ ? { frontmatter: args.frontmatter }
199
+ : {}),
200
+ ...(typeof args.body === 'string' ? { body: args.body } : {}),
201
+ ...(actingAgent ? { acting_agent: actingAgent } : {}),
202
+ ...(args.recipe_context && typeof args.recipe_context === 'object' && !Array.isArray(args.recipe_context)
203
+ ? { recipe_context: args.recipe_context }
204
+ : {}),
205
+ // The frozen checks manifest ([[checks-manifest-authoring]]). Forwarded only
206
+ // when the caller authored rows, so filings that pass none stay byte-identical
207
+ // to before and pick up the server's `no_checks_manifest` warning instead.
208
+ ...(Array.isArray(args.checks) && args.checks.length > 0 ? { checks: args.checks } : {}),
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Local host names whose console owns THIS machine's disk. A write posted to
214
+ * one of these landed (or should have landed) in the local clone; anything
215
+ * else is a cloud console with no access to it.
216
+ */
217
+ export const LOCAL_CONSOLE_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '[::1]', '::1']);
218
+
219
+ /** True when a console origin points at this machine. Never throws. */
220
+ export function isLocalConsoleOrigin(url) {
221
+ if (typeof url !== 'string' || !url) return false;
222
+ try {
223
+ return LOCAL_CONSOLE_HOSTS.has(new URL(url).hostname.toLowerCase());
224
+ } catch {
225
+ return false;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * WHERE DID THE WRITE LAND — the pure verdict ([[HS-IOJW-5YDH-DIAG]]).
231
+ *
232
+ * The bug this closes: a vault write reported plain success whether the bytes
233
+ * reached a file in this clone or only a Neon row on another machine. Two
234
+ * sessions lost work to it on 2026-08-21 and recovered it only by re-reading
235
+ * the file out of habit.
236
+ *
237
+ * Inputs are all MEASURED by the caller, never assumed:
238
+ * consoleUrl the origin the POST actually went to (consoleFetch result.url)
239
+ * consoleWrite the console's own `write` block when it is new enough to send one
240
+ * relPath vault-relative path the console says it wrote
241
+ * vaultRoot this session's clone, or null when there is none
242
+ * absPath the absolute path actually written, when the console reports one
243
+ * localFileExists a real stat of that path; null when unmeasurable
244
+ * devMode { mode, reliance, source, updated_at } from ~/.heyspex/dev-mode.json
245
+ * expectTier the caller's declared expectation: "disk" | "cloud" | undefined
246
+ *
247
+ * Returns { landed, error } — `error` non-null means the tool must answer
248
+ * isError, WITH the spec id still attached so nobody re-files a duplicate.
249
+ */
250
+ export function fileSpecLandingVerdict({
251
+ consoleUrl = null,
252
+ consoleWrite = null,
253
+ relPath = null,
254
+ vaultRoot = null,
255
+ localFileExists = null,
256
+ devMode = null,
257
+ expectTier = null,
258
+ absPath = null,
259
+ } = {}) {
260
+ // Tier: prefer what the console measured; fall back to the origin we posted
261
+ // to, so this still works against a deployed console that predates the
262
+ // `write` block.
263
+ let tier;
264
+ let reportedBy;
265
+ if (consoleWrite?.mode === 'local' || consoleWrite?.mode === 'cloud') {
266
+ tier = consoleWrite.mode === 'local' ? 'disk' : 'cloud';
267
+ reportedBy = 'console';
268
+ } else {
269
+ tier = isLocalConsoleOrigin(consoleUrl) ? 'disk' : 'cloud';
270
+ reportedBy = 'inferred_from_console_url';
271
+ }
272
+
273
+ const landed = {
274
+ tier,
275
+ reported_by: reportedBy,
276
+ console_url: consoleUrl,
277
+ rel_path: relPath,
278
+ // The console's own absolute path wins. Its vault root is the SPECS dir,
279
+ // not the repo root, and `relPath` is relative to THAT — joining it onto
280
+ // the MCP's vaultRoot points at a file that never existed. Caught live on
281
+ // the first real filing through this code.
282
+ abs_path: absPath ?? (vaultRoot && relPath ? join(vaultRoot, relPath) : null),
283
+ in_local_vault: localFileExists,
284
+ vault_root: vaultRoot,
285
+ console_write: consoleWrite ?? null,
286
+ dev_mode: devMode
287
+ ? {
288
+ mode: devMode.mode ?? null,
289
+ reliance: devMode.reliance ?? null,
290
+ set_by: devMode.source ?? null,
291
+ set_at: devMode.updated_at ?? null,
292
+ }
293
+ : null,
294
+ };
295
+
296
+ const modeNote = landed.dev_mode?.mode
297
+ ? ` Mode is ${landed.dev_mode.mode}/${landed.dev_mode.reliance}, last set by ${landed.dev_mode.set_by ?? 'unknown'} at ${landed.dev_mode.set_at ?? 'unknown time'}.`
298
+ : '';
299
+
300
+ // 1. The caller said where it wanted the write and it went elsewhere.
301
+ if (expectTier && tier !== expectTier) {
302
+ return {
303
+ landed,
304
+ error: {
305
+ error: 'WRITE_TIER_MISMATCH',
306
+ message:
307
+ `The spec was created, but on ${tier} — you asked for ${expectTier}.${modeNote} ` +
308
+ 'The spec id below is real; do NOT file it again. Switch the mode with ' +
309
+ 'heyspex_dev_mode and re-check, or pass expect_tier to match where you want it.',
310
+ },
311
+ };
312
+ }
313
+
314
+ // 2. Undeclared cloud landing while this session has a clone in front of it.
315
+ // This is the exact shape that lost work: agent believes it is writing
316
+ // files, the mode was flipped underneath it, nothing appears on disk.
317
+ if (!expectTier && tier === 'cloud' && vaultRoot) {
318
+ return {
319
+ landed,
320
+ error: {
321
+ error: 'WRITE_LANDED_OFF_DISK',
322
+ message:
323
+ `The spec was created in the cloud index, NOT as a file in this clone (${vaultRoot}).${modeNote} ` +
324
+ 'The spec id below is real; do NOT file it again. Either switch back with ' +
325
+ 'heyspex_dev_mode({ action: "set", mode: "local" }) and re-file, or pass ' +
326
+ 'expect_tier: "cloud" to say you meant it.',
327
+ },
328
+ };
329
+ }
330
+
331
+ // 3. It claims disk, and the file is not there. A write that reports success
332
+ // over a missing file is the defect itself.
333
+ if (tier === 'disk' && localFileExists === false) {
334
+ return {
335
+ landed,
336
+ error: {
337
+ error: 'WRITE_NOT_ON_DISK',
338
+ message:
339
+ `The console answered success but no file exists at ${landed.abs_path ?? relPath}.${modeNote} ` +
340
+ 'The spec id below may still be real in the index — check it before re-filing, and ' +
341
+ 'read the console logs for a swallowed disk error.',
342
+ },
343
+ };
344
+ }
345
+
346
+ return { landed, error: null };
347
+ }
348
+
349
+ /**
350
+ * Shell around {@link fileSpecLandingVerdict}: reads the machine-local dev-mode
351
+ * record and stats the file. Never throws — a landing report that fails must
352
+ * not fail the write it reports on.
353
+ */
354
+ export async function measureFileSpecLanding({ consoleUrl, consoleBody, relPath, vaultRoot, expectTier }) {
355
+ let devMode = null;
356
+ try {
357
+ if (vaultRoot) {
358
+ // Bundler-safe loader (see dev-mode-core.mjs): the runtime paths first,
359
+ // then a static specifier. Without the fallback the bundled remote
360
+ // endpoint skipped the landing probe entirely.
361
+ const mod = await importDevModeCore([
362
+ join(CHECKOUT_ROOT, 'scripts', 'lib', 'dev-mode.mjs'),
363
+ join(vaultRoot, 'scripts', 'lib', 'dev-mode.mjs'),
364
+ ]);
365
+ devMode = mod.readLocalDevModeRecord()?.record ?? null;
366
+ }
367
+ } catch {
368
+ devMode = null;
369
+ }
370
+ // Which absolute path to stat. The console's own `absPath` is authoritative
371
+ // when it sent one — its vault root is <repo>/specs, so `relPath` alone is
372
+ // NOT resolvable against the MCP's repo root. Without it, try both roots
373
+ // rather than guessing one and reporting a phantom missing file.
374
+ const consoleWrite = consoleBody?.write ?? null;
375
+ const candidates = [];
376
+ if (typeof consoleWrite?.absPath === 'string' && consoleWrite.absPath) {
377
+ candidates.push(consoleWrite.absPath);
378
+ } else if (vaultRoot && relPath) {
379
+ candidates.push(join(vaultRoot, relPath), join(vaultRoot, 'specs', relPath));
380
+ }
381
+ let localFileExists = candidates.length > 0 ? false : null;
382
+ let absPath = candidates[0] ?? null;
383
+ for (const candidate of candidates) {
384
+ try {
385
+ if (statSync(candidate).isFile()) {
386
+ localFileExists = true;
387
+ absPath = candidate;
388
+ break;
389
+ }
390
+ } catch {
391
+ // keep looking
392
+ }
393
+ }
394
+ return fileSpecLandingVerdict({
395
+ consoleUrl,
396
+ consoleWrite,
397
+ relPath,
398
+ vaultRoot,
399
+ localFileExists,
400
+ devMode,
401
+ expectTier,
402
+ absPath,
403
+ });
404
+ }
405
+
406
+ export async function handleFileSpec(args = {}, vaultRoot = null) {
407
+ // First hop: refuse an unrecognized top-level arg (e.g. a top-level
408
+ // `parent:`) BY NAME instead of letting it silently vanish before
409
+ // buildFileSpecPostBody even runs [[HS-XQMK-A1QE-DIAG-heyspex-file-spec-drops-the-parent]].
410
+ const argsVerdict = fileSpecArgsVerdict(args);
411
+ if (!argsVerdict.ok) {
412
+ return {
413
+ isError: true,
414
+ content: [{ type: 'text', text: JSON.stringify(argsVerdict.error, null, 2) }],
415
+ };
416
+ }
417
+ if (!args.type || typeof args.type !== 'string') {
418
+ return {
419
+ isError: true,
420
+ content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: 'type is required' }) }],
421
+ };
422
+ }
423
+ if (!args.name || typeof args.name !== 'string') {
424
+ return {
425
+ isError: true,
426
+ content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: 'name is required' }) }],
427
+ };
428
+ }
429
+ // Summary required at filing — planning phase (milestone/epic) or execution
430
+ // phase (task via What's Next `/hs-next` only). Never co-file epics and tasks.
431
+ if (SUMMARY_REQUIRED_FILE_SPEC_TYPES.has(args.type) && !resolveFileSpecSummary(args)) {
432
+ return {
433
+ isError: true,
434
+ content: [{
435
+ type: 'text',
436
+ text: JSON.stringify(buildSummaryRequiredMcpError(args.type), null, 2),
437
+ }],
438
+ };
439
+ }
440
+ // Capture-stage rule ([[capture-stage-gate]]): an open capture must not become
441
+ // an epic/task/milestone. Checked HERE, before the POST, because the hook that
442
+ // used to own this could not read the live pin and could be switched off by a
443
+ // file. Runs on its own resolution failure as "allow" — a gate that refuses on
444
+ // its own uncertainty is worse than no gate.
445
+ const captureStageVerdict = decideCaptureStageGate({
446
+ toolArgs: args,
447
+ activeCapture: vaultRoot ? await resolveLiveActiveCapture(vaultRoot) : null,
448
+ });
449
+ if (captureStageVerdict.decision === 'refuse') {
450
+ return {
451
+ isError: true,
452
+ content: [{ type: 'text', text: JSON.stringify(buildCaptureStageRefusal(captureStageVerdict), null, 2) }],
453
+ };
454
+ }
455
+
456
+ // Acting agent for `assisted_by` auto-stamp ([[HS-EPIC-IKIM-authorship-provenance-frontmatter-owner]]
457
+ // AC #2). Env-detected (same source as claim_spec); the console seeds
458
+ // `assisted_by: [acting_agent]` on the new spec, omit-when-empty if unset.
459
+ const actingAgent = detectAgentIdentity(args.agent, args.session_id).agent;
460
+ const body = buildFileSpecPostBody(args, actingAgent);
461
+ // Idempotency key ([[HS-70TD-DIAG]]): generated ONCE per logical file_spec call
462
+ // and reused across the transport's timeout-retry (the header lives on the
463
+ // shared `init`), so a slow-console retry replays the first create server-side
464
+ // instead of double-allocating a second byte-identical spec.
465
+ const idempotencyKey = randomUUID();
466
+ // Degrade-don't-die: replay anything queued by an earlier outage first (the
467
+ // drain never stashes, and is failure-safe).
468
+ const outboxNote = vaultRoot ? await drainAndSummarize(vaultRoot, consoleFetch) : {};
469
+ const result = await consoleFetch('/api/specs', {
470
+ method: 'POST',
471
+ body,
472
+ headers: { 'idempotency-key': idempotencyKey },
473
+ // [[HS-2FQY]] double-mint guard: against a REMOTE console the blind
474
+ // timeout-retry can land on a different serverless instance, where the
475
+ // in-memory idempotency registry cannot replay the first create — the
476
+ // retry mints a SECOND id for the same spec (observed: HS-2FQY + HS-K70K,
477
+ // identical content, one call). Loopback keeps the retry (single process,
478
+ // registry replays). On a remote timeout, check before re-filing (below).
479
+ retryOnTimeout: false,
480
+ });
481
+ if (!result.ok) {
482
+ // Outage class (status 0 / timeout / 5xx) in CLOUD reliance: queue the create
483
+ // with its idempotency key and answer truthfully "deferred" — never isError,
484
+ // never a claim it landed. 4xx / 401 / sign-in problems keep their loud envelope.
485
+ if (vaultRoot && isOutageClassResult(result) && (await resolveReadTier()) === 'neon') {
486
+ const stashed = stashIfOutage(
487
+ result,
488
+ {
489
+ op: 'create',
490
+ route: '/api/specs',
491
+ method: 'POST',
492
+ request_body: body,
493
+ idempotency_key: idempotencyKey,
494
+ acting_agent: actingAgent,
495
+ session_id: args.session_id ?? null,
496
+ },
497
+ vaultRoot,
498
+ );
499
+ if (stashed) {
500
+ return deferredToolResponse({
501
+ ...buildDeferredPayload(stashed, vaultRoot, { attempted: { type: args.type, name: args.name } }),
502
+ ...(outboxNote.drained ? { drained: outboxNote.drained } : {}),
503
+ });
504
+ }
505
+ }
506
+ const payload = await enrichConsoleFailurePayloadAsync(result, {
507
+ error: consoleErrorCode(result, 'FILE_SPEC_FAILED'),
508
+ message: consoleErrorMessage(result, `POST /api/specs returned ${result.status}`),
509
+ status: result.status,
510
+ url: result.url,
511
+ console_response: result.body,
512
+ attempted: { type: args.type, name: args.name },
513
+ ...(result.errorKind === 'timeout'
514
+ ? {
515
+ hint:
516
+ 'The create may still have completed server-side after this client timed out. '
517
+ + 'Search for a spec with this exact name before re-filing — a blind re-file can mint a duplicate id ([[HS-2FQY]]).',
518
+ }
519
+ : {}),
520
+ });
521
+ return {
522
+ isError: true,
523
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
524
+ };
525
+ }
526
+ // POST returns { spec: { id, ... } }; surface the canonical { spec_id, path }
527
+ // contract alongside the full created spec for callers that want more.
528
+ const spec = result.body?.spec ?? null;
529
+ const relPath = spec?.relPath ?? spec?.path ?? null;
530
+ // [[HS-H227-MCPC-TASK]]: a caller that passed no `expect_tier` at all used
531
+ // to fall through to `WRITE_LANDED_OFF_DISK` the moment this session's own
532
+ // reliance is cloud (fileSpecLandingVerdict's rule 2 — "undeclared cloud
533
+ // landing while a clone is in front of us") even though a cloud-reliant
534
+ // session filing into the cloud index is exactly the intended shape, not a
535
+ // surprise. Default to "cloud" from the SAME tier verdict every other read
536
+ // in this file already trusts (`resolveReadTier`) — only when the caller
537
+ // passed nothing; an explicit `expect_tier` (including an explicit "disk")
538
+ // is never overridden, so local-reliance behavior is untouched.
539
+ let expectTier = args.expect_tier === 'disk' || args.expect_tier === 'cloud' ? args.expect_tier : null;
540
+ if (!expectTier) {
541
+ const tier = await resolveReadTier();
542
+ if (tier === 'neon') expectTier = 'cloud';
543
+ }
544
+ // WHERE IT LANDED ([[HS-IOJW-5YDH-DIAG]]) — measured, not assumed. A write
545
+ // that reports on the request instead of the result is the defect; this
546
+ // stats the file and names the console it went to.
547
+ const { landed, error: landingError } = await measureFileSpecLanding({
548
+ consoleUrl: result.url,
549
+ consoleBody: result.body,
550
+ relPath,
551
+ vaultRoot,
552
+ expectTier,
553
+ });
554
+ const payload = {
555
+ spec_id: spec?.id ?? null,
556
+ path: relPath,
557
+ landed,
558
+ spec,
559
+ console_response: result.body,
560
+ ...outboxNote,
561
+ };
562
+ if (landingError) {
563
+ return {
564
+ isError: true,
565
+ content: [{ type: 'text', text: JSON.stringify({ ...landingError, ...payload }, null, 2) }],
566
+ };
567
+ }
568
+ // Warn rung of the capture-stage gate: the filing went through, and the reason
569
+ // it would otherwise have been refused rides along on the response.
570
+ if (captureStageVerdict.decision === 'warn') {
571
+ payload.capture_stage_warning = buildCaptureStageRefusal(captureStageVerdict);
572
+ }
573
+ // Non-coercive post-create suggestion ([[HS-TASK-FWPC]] / [[HS-EPIC-MNBD]]):
574
+ // a freshly-filed milestone has no epics yet, so suggest filing the first one.
575
+ // The agent decides whether to act — the MCP layer NEVER auto-executes the
576
+ // follow-on (CLAUDE.md autonomy boundary). Skipping leaves the milestone in
577
+ // `epics: []`, where MCEP/RCNB/VWSE surface it.
578
+ if (args.type === 'milestone' && spec?.id) {
579
+ payload.suggested_next_action = 'file_epic';
580
+ payload.suggested_next_action_args = { milestone_id: spec.id };
581
+ }
582
+ return {
583
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
584
+ };
585
+ }
586
+
587
+ export function handlePlanReview(vaultRoot, args = {}) {
588
+ // Map the snake_case MCP args onto the executor's camelCase contract.
589
+ const result = runPlanReview({
590
+ vaultRoot,
591
+ planPath: typeof args.plan_path === 'string' ? args.plan_path : undefined,
592
+ planContent: typeof args.plan_content === 'string' ? args.plan_content : undefined,
593
+ scope: args.scope === 'breakdown' ? 'breakdown' : 'plan',
594
+ epicId: typeof args.epic_id === 'string' ? args.epic_id : undefined,
595
+ epicName: typeof args.epic_name === 'string' ? args.epic_name : undefined,
596
+ proposedTasks: Array.isArray(args.proposed_tasks) ? args.proposed_tasks : undefined,
597
+ qualityPass: args.quality_pass === true,
598
+ agentQualityFindings: Array.isArray(args.quality_findings) ? args.quality_findings : undefined,
599
+ });
600
+ return wrapDesignResult(result);
601
+ }
602
+
603
+ export function handlePlanQc(vaultRoot, args = {}) {
604
+ const result = runPlanQc({
605
+ vaultRoot,
606
+ planPath: typeof args.plan_path === 'string' ? args.plan_path : undefined,
607
+ planContent: typeof args.plan_content === 'string' ? args.plan_content : undefined,
608
+ findingsSummary: typeof args.findings_summary === 'string' ? args.findings_summary : undefined,
609
+ axisBUserConfirmed: args.axis_b_user_confirmed === true,
610
+ axisBConfirmationNote:
611
+ typeof args.axis_b_confirmation_note === 'string' ? args.axis_b_confirmation_note : undefined,
612
+ });
613
+ return wrapDesignResult(result);
614
+ }
615
+
616
+ /**
617
+ * `heyspex_push_plan`. `deps.writeFocusSelection` is the injection seam (idiom
618
+ * from fleet-queue-tools.mjs) so a test can drive the failed-pin path without a
619
+ * console; cli.mjs calls with two args and gets the real Neon writer.
620
+ */
621
+ export async function handlePushPlan(vaultRoot, args, deps = {}) {
622
+ const { writeFocusSelection = writeFocusSelectionVendor } = deps;
623
+ // Cloud-aware entry ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]
624
+ // sweep): resolves `resolveVaultWriteMode()` ONCE and either runs the
625
+ // untouched synchronous disk path (`local`) or files the epic through
626
+ // `POST /api/specs` (`cloud`). Awaiting is safe in both — the local branch
627
+ // still returns the same plain object it always did.
628
+ const result = await executePushPlanCloudAware(vaultRoot, args);
629
+ if (result.error) {
630
+ return {
631
+ isError: true,
632
+ content: [{ type: 'text', text: JSON.stringify(result) }],
633
+ };
634
+ }
635
+ // executePushPlan no longer writes focus itself (it is sync); do it here so the
636
+ // selection can reach Neon. Neon XOR file via `writeFocusSelectionVendor`.
637
+ //
638
+ // REPORT THE RESULT, NOT THE REQUEST ([[HS-IOJW-5YDH-DIAG]] sweep). The return
639
+ // boolean used to be discarded and `focus_written` came straight off the
640
+ // `on_deck` request flag, so a console that could not be reached — the one case
641
+ // `writeFocusSelectionVendor` is documented to fail in, with NO focus.yaml
642
+ // fallback — produced `success: true, focus_written: true` over a pin that was
643
+ // never stored, and sent the agent to /hs-next against nothing. The next
644
+ // session's entire run hangs off that pin.
645
+ if (result.on_deck && result.epic?.spec_path) {
646
+ const pinned = await writeFocusSelection(
647
+ vaultRoot,
648
+ result.epic.spec_path,
649
+ result.focus_reason,
650
+ );
651
+ result.focus_written = pinned === true;
652
+ if (!result.focus_written) {
653
+ // A failed pin is a LOUD FAILURE, never a field inside a success.
654
+ //
655
+ // It does NOT fail the whole push, and it must not: the epic markdown and
656
+ // the milestone mirror-update are already on disk and are correct. Erroring
657
+ // the push away would strand a filed epic behind a red tool call and invite
658
+ // a re-push that mints a SECOND epic for the same plan. So the filed facts
659
+ // are preserved verbatim in the payload (same shape as the `landingError`
660
+ // merge in handleFileSpec above) while `isError` + `success: false` stop any
661
+ // caller from reading this as a pinned epic. The only thing that failed is
662
+ // recoverable by re-running the pin — which is what `pin_recovery` says.
663
+ return {
664
+ isError: true,
665
+ content: [{
666
+ type: 'text',
667
+ text: JSON.stringify({
668
+ ...result,
669
+ success: false,
670
+ error: 'FOCUS_PIN_NOT_WRITTEN',
671
+ epic_filed: true,
672
+ message:
673
+ `The epic WAS filed (${result.epic.spec_path}) but the on-deck focus pin was NOT written. ` +
674
+ 'Neon is the only focus store (no .heyspex/focus.yaml fallback), so NOTHING is pinned — ' +
675
+ 'do NOT run /hs-next expecting this epic, it will resolve to whatever was pinned before. ' +
676
+ 'Do NOT re-run heyspex_push_plan: that would file a second epic for the same plan.',
677
+ pin_recovery: {
678
+ spec_path: result.epic.spec_path,
679
+ reason: result.focus_reason,
680
+ how: 'Check heyspex_dev_server_status, then re-pin: PUT /api/control/focus-selection ' +
681
+ '{ spec_path, kind: "epic", picked_by: "human", reason } — or re-run heyspex_push_plan ' +
682
+ 'with on_deck once the console is reachable ONLY if the epic above was not filed.',
683
+ },
684
+ }, null, 2),
685
+ }],
686
+ };
687
+ }
688
+ }
689
+ return {
690
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
691
+ };
692
+ }
693
+
694
+ export async function handleGroundPlan(vaultRoot, args) {
695
+ // Resolve the Neon focus selection (layer-2 `spec_path`) at the async boundary
696
+ // and thread it into the sync grounding engine, so /hs-ground's focused-epic
697
+ // conflict scan reads the same Neon selection the console does — not the dormant
698
+ // file. `injectNeonFocusPin` adds `neon_selection_spec_path` (best-effort; an
699
+ // unreachable console / free-tier 402 leaves args untouched → file fallback).
700
+ // [[HS-TASK-FSEL]] residual / [[HS-TASK-FSKP]] session of [[HS-EPIC-FRTR]].
701
+ const resolvedArgs = await injectNeonFocusPin(args);
702
+ // Retrieval is remote and bounded since
703
+ // [[HS-YIR1-8KK5-TASK-planning-grounding-retrieves-through]] — the grounding
704
+ // engine issues `GET /api/search` calls of its own, so it is awaited here.
705
+ const result = await executeGroundPlan(vaultRoot, resolvedArgs);
706
+ if (result.error) {
707
+ return {
708
+ isError: true,
709
+ content: [{ type: 'text', text: JSON.stringify(result) }],
710
+ };
711
+ }
712
+ // Bounded report mode: emit the capped markdown directly so /hs-ground pastes it
713
+ // verbatim — no JSON envelope for the agent to unpack or over-read.
714
+ if (result._report) {
715
+ const footer = `\n\n---\n_grounded ${result.grounded_at} · ~${result.token_estimate}/${result.max_tokens} tok${result.truncated ? ' (truncated to fit)' : ''} · conflicts: ${result.conflict_count}${result.has_blocking_conflicts ? ' (BLOCKING — resolve first)' : ''}_`;
716
+ return {
717
+ content: [{ type: 'text', text: `${result.report_markdown}${footer}` }],
718
+ };
719
+ }
720
+ return {
721
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
722
+ };
723
+ }