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,240 @@
1
+ /**
2
+ * Resolve `/hs-auto [filename]` to a dev-factory dispatch target.
3
+ */
4
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
5
+ import { basename, isAbsolute, join, normalize, relative } from 'node:path';
6
+ import { findSpecFile } from '../dev-factory/lib/spec-io.mjs';
7
+ // Epic/parent-shaped detection is the shared source of truth in dev-factory/lib —
8
+ // scripts/ already imports findSpecFile from there, so this import direction is
9
+ // established (2026-07-11 consolidation of the previously-triplicated predicate).
10
+ import { specFacts, isEpicSpec } from '../dev-factory/lib/epic-detect.mjs';
11
+ import {
12
+ fmWikilinkList,
13
+ isPhase1Artifact,
14
+ isSourcePlanningSpec,
15
+ readSpecMetaFull,
16
+ resolvePhase1Spec,
17
+ resolveSourcePlanningSpec,
18
+ } from './hs-auto-source.mjs';
19
+
20
+ const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
21
+ const HS_ID = /^HS-[A-Z0-9]+-(?:TASK|EPIC|MIL|CAP|RES|REV|DIAG)(?:-|$)/i;
22
+
23
+ /**
24
+ * @param {string} repoRoot
25
+ * @param {string} raw
26
+ */
27
+ function toRepoRelative(repoRoot, raw) {
28
+ const abs = isAbsolute(raw) ? normalize(raw) : normalize(join(repoRoot, raw));
29
+ const rel = relative(repoRoot, abs).replace(/\\/g, '/');
30
+ if (rel.startsWith('..')) return raw.replace(/\\/g, '/');
31
+ return rel;
32
+ }
33
+
34
+ /**
35
+ * @param {string} fm
36
+ * @param {string} key
37
+ */
38
+ function fmScalar(fm, key) {
39
+ const m = fm.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
40
+ if (!m) return null;
41
+ return m[1].trim().replace(/^['"]|['"]$/g, '');
42
+ }
43
+
44
+ /**
45
+ * @param {string} filePath
46
+ */
47
+ function readSpecMeta(filePath) {
48
+ const meta = readSpecMetaFull(filePath);
49
+ return {
50
+ id: meta.id,
51
+ type: meta.type,
52
+ status: meta.status,
53
+ target: meta.target,
54
+ spec_path: filePath,
55
+ };
56
+ }
57
+
58
+ /**
59
+ * @param {string} specsDir
60
+ * @param {string} targetNeedle repo-relative, forward slashes
61
+ */
62
+ function findTaskByTargetSync(specsDir, targetNeedle) {
63
+ const tasksDir = join(specsDir, 'tasks');
64
+ if (!existsSync(tasksDir)) return null;
65
+ /** @type {string[]} */
66
+ const files = [];
67
+ const walk = (dir) => {
68
+ for (const entry of readdirSync(dir)) {
69
+ const full = join(dir, entry);
70
+ if (statSync(full).isDirectory()) walk(full);
71
+ else if (entry.endsWith('.md')) files.push(full);
72
+ }
73
+ };
74
+ walk(tasksDir);
75
+
76
+ const norm = targetNeedle.replace(/\\/g, '/');
77
+ for (const file of files) {
78
+ const meta = readSpecMeta(file);
79
+ if (!meta.target) continue;
80
+ const t = meta.target.replace(/\\/g, '/');
81
+ if (t === norm || norm.endsWith(t) || t.endsWith(norm)) {
82
+ return meta;
83
+ }
84
+ }
85
+ return null;
86
+ }
87
+
88
+ /**
89
+ * @param {{ id: string|null, type: string|null, target: string|null }} meta
90
+ * @param {string} rel
91
+ * @param {string} repoRoot
92
+ */
93
+ function resolveExecutableSpec(meta, rel, repoRoot) {
94
+ if (isSourcePlanningSpec(meta.type, meta.id)) {
95
+ const full = readSpecMetaFull(join(repoRoot, rel));
96
+ return resolveSourcePlanningSpec(repoRoot, full, rel);
97
+ }
98
+ // HS-5Q41: a capture (raw or mid-stage) resolves to the Phase-1 runner with a start-at-step hint,
99
+ // instead of dead-ending at MISSING_TARGET. Placed before the epic/target branches — a capture is
100
+ // neither an epic nor a dispatchable task.
101
+ if (isPhase1Artifact(meta.type, meta.id)) {
102
+ const full = readSpecMetaFull(join(repoRoot, rel));
103
+ return resolvePhase1Spec(full, rel);
104
+ }
105
+ // Epic/parent backstop (2026-07-11 idle-fleet incident): an epic is detected by
106
+ // type:, by id — including ids ENDING in "-EPIC", which the old /-EPIC-/ regex
107
+ // missed — by living under specs/epics/, or by carrying a children: frontmatter
108
+ // key at all (parent-shaped). Any of these is STRUCTURALLY blocked from
109
+ // `mode: dispatch`: an epic with children: [] (or with children) can only ever
110
+ // classify as breakdown, even when it carries a target: of its own. The dispatch
111
+ // branch below is unreachable for it by construction, and controller.mjs's
112
+ // dispatch gate (lib/dispatch-gate.mjs) backstops any caller that bypasses this.
113
+ // Detection is the shared source of truth (dev-factory/lib/epic-detect.mjs); this
114
+ // classifier additionally routes ANY parent-shaped spec (a children: key present,
115
+ // even with children) to breakdown.
116
+ let facts = { id: meta.id ?? '', type: (meta.type ?? '').toLowerCase(), path: rel, hasChildrenKey: false, childCount: 0, target: meta.target };
117
+ let childCount = 0;
118
+ try {
119
+ const raw = readFileSync(join(repoRoot, rel), 'utf8');
120
+ facts = specFacts({ raw, specPath: rel, specId: meta.id });
121
+ const fm = raw.match(FRONTMATTER)?.[1] ?? '';
122
+ if (facts.hasChildrenKey) childCount = fmWikilinkList(fm, 'children').length;
123
+ } catch { /* unreadable file → fall through to the target checks below */ }
124
+ const isEpic = isEpicSpec(facts) || facts.hasChildrenKey;
125
+ if (isEpic) {
126
+ return {
127
+ mode: 'breakdown',
128
+ spec_id: meta.id,
129
+ spec_path: rel,
130
+ children_count: childCount,
131
+ dispatch_blocked:
132
+ 'An epic (or a parent-shaped spec with a children: key) is never dispatchable as a single target — breakdown first, then dispatch the FILED child tasks across the fleet.',
133
+ breakdown_command: `node dev-factory/controller.mjs --breakdown-epic ${meta.id}`,
134
+ epic_pipeline_command: `node dev-factory/dispatch-epic.mjs --epic ${meta.id}`,
135
+ filing_helper: 'node dev-factory/lib/breakdown-to-filing.mjs --draft <draft.json> --task-id HS-....-TASK [--target path]',
136
+ coo_contract:
137
+ 'COO orchestration only — run preflight, then breakdown_command (or epic_pipeline_command, which chains breakdown → owner approval → filed-children fan-out via backlog-runner --concurrent). Review draft JSON, run filing_helper per task row, file via heyspex_file_spec with ## Builder brief, re-resolve, dispatch. NEVER controller.mjs --spec <epic> --target — the dispatch gate refuses it. Do NOT implement in Cursor.',
138
+ };
139
+ }
140
+ if (!meta.target) {
141
+ return {
142
+ error: 'MISSING_TARGET',
143
+ message: `${meta.id ?? rel} has no target: frontmatter — add target before dispatch.`,
144
+ spec_id: meta.id ?? undefined,
145
+ spec_path: rel,
146
+ };
147
+ }
148
+ return {
149
+ mode: 'dispatch',
150
+ spec_id: meta.id,
151
+ spec_path: rel,
152
+ target: meta.target.replace(/\\/g, '/'),
153
+ dispatch_command: buildDispatchCommand(rel, meta.target),
154
+ coo_contract:
155
+ 'COO orchestration only — run preflight, then dispatch_command. Do NOT implement in Cursor unless the gate fails after fleet retries.',
156
+ };
157
+ }
158
+
159
+ /**
160
+ * @param {string} repoRoot
161
+ * @param {string | undefined | null} filename
162
+ */
163
+ export function resolveHsAutoEntry(repoRoot, filename) {
164
+ const specsDir = join(repoRoot, 'specs');
165
+ const trimmed = (filename ?? '').trim().replace(/^["']|['"]$/g, '');
166
+ if (!trimmed) {
167
+ return {
168
+ mode: 'discover',
169
+ message: 'No filename — run backlog discovery after pre-flight.',
170
+ coo_contract:
171
+ 'COO orchestration only — run preflight, discover_command, pick a ready task with target:, re-resolve. Do NOT implement in Cursor.',
172
+ };
173
+ }
174
+
175
+ const idToken = trimmed.match(HS_ID)?.[0] ?? (
176
+ basename(trimmed).match(HS_ID)?.[0] ?? null
177
+ );
178
+
179
+ if (idToken) {
180
+ const specPath = findSpecFile(specsDir, idToken);
181
+ if (!specPath) {
182
+ return { error: 'SPEC_NOT_FOUND', message: `No vault spec found for ${idToken}` };
183
+ }
184
+ const meta = readSpecMeta(specPath);
185
+ const rel = relative(repoRoot, specPath).replace(/\\/g, '/');
186
+ return resolveExecutableSpec(meta, rel, repoRoot);
187
+ }
188
+
189
+ const relPath = toRepoRelative(repoRoot, trimmed);
190
+ const absPath = join(repoRoot, relPath);
191
+ if (relPath.endsWith('.md') && existsSync(absPath)) {
192
+ const meta = readSpecMeta(absPath);
193
+ return resolveExecutableSpec(meta, relPath, repoRoot);
194
+ }
195
+
196
+ if (existsSync(absPath)) {
197
+ const byTarget = findTaskByTargetSync(specsDir, relPath);
198
+ if (byTarget?.id && byTarget.target) {
199
+ const specRel = relative(repoRoot, byTarget.spec_path).replace(/\\/g, '/');
200
+ return {
201
+ mode: 'dispatch',
202
+ spec_id: byTarget.id,
203
+ spec_path: specRel,
204
+ target: byTarget.target.replace(/\\/g, '/'),
205
+ matched_via: 'target_frontmatter',
206
+ dispatch_command: buildDispatchCommand(specRel, byTarget.target),
207
+ coo_contract:
208
+ 'COO orchestration only — run preflight, then dispatch_command. Do NOT implement in Cursor unless the gate fails after fleet retries.',
209
+ };
210
+ }
211
+ return {
212
+ error: 'NO_TASK_FOR_FILE',
213
+ message: `No vault task lists target: ${relPath}. File a task with target: or pass a spec path/id.`,
214
+ file: relPath,
215
+ };
216
+ }
217
+
218
+ return {
219
+ error: 'FILE_NOT_FOUND',
220
+ message: `Path not found: ${trimmed}`,
221
+ };
222
+ }
223
+
224
+ /**
225
+ * @param {string} specPath repo-relative
226
+ * @param {string} target
227
+ */
228
+ export function buildDispatchCommand(specPath, target) {
229
+ const specId = basename(specPath, '.md');
230
+ const checkpoint = `dev-factory/state/checkpoint-${specId}.json`;
231
+ return [
232
+ 'node dev-factory/controller.mjs',
233
+ `--spec ${specPath}`,
234
+ `--target ${target.replace(/\\/g, '/')}`,
235
+ '--use-scheduler',
236
+ '--quality-pipeline',
237
+ '--n 3',
238
+ `--checkpoint ${checkpoint}`,
239
+ ].join(' ');
240
+ }
@@ -0,0 +1,398 @@
1
+ /**
2
+ * Source-spec pipeline helpers for `/hs-auto` — diagnose + research are planning
3
+ * inputs (like capture/research in Phase 1). They are NOT dispatchable until a
4
+ * remediation epic + tasks with `target:` exist.
5
+ */
6
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
7
+ import { join, relative, basename } from 'node:path';
8
+
9
+ import { findSpecFile } from '../dev-factory/lib/spec-io.mjs';
10
+
11
+ const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
12
+
13
+ /** @typedef {{ id: string|null, type: string|null, status: string|null, target: string|null, parent: string|null, spec_path: string, body: string }} SpecMeta */
14
+
15
+ /**
16
+ * @param {string} fm
17
+ * @param {string} key
18
+ */
19
+ export function fmScalar(fm, key) {
20
+ const m = fm.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
21
+ if (!m) return null;
22
+ return m[1].trim().replace(/^['"]|['"]$/g, '');
23
+ }
24
+
25
+ /**
26
+ * @param {string} filePath
27
+ * @returns {SpecMeta}
28
+ */
29
+ export function readSpecMetaFull(filePath) {
30
+ const raw = readFileSync(filePath, 'utf8');
31
+ const m = raw.match(FRONTMATTER);
32
+ const fm = m?.[1] ?? '';
33
+ const body = m ? raw.slice(m[0].length) : raw;
34
+ return {
35
+ id: fmScalar(fm, 'id'),
36
+ type: fmScalar(fm, 'type'),
37
+ status: fmScalar(fm, 'status'),
38
+ target: fmScalar(fm, 'target'),
39
+ parent: fmScalar(fm, 'parent'),
40
+ spec_path: filePath,
41
+ body,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * @param {string} fm
47
+ * @param {string} key
48
+ * @returns {string[]}
49
+ */
50
+ export function fmWikilinkList(fm, key) {
51
+ const block = fm.match(new RegExp(`^${key}:\\s*\\n((?:\\s*-\\s+.+\\n?)*)`, 'm'));
52
+ if (!block) {
53
+ const scalar = fmScalar(fm, key);
54
+ if (!scalar) return [];
55
+ const ids = scalar.match(/HS-[A-Z0-9]+-(?:TASK|EPIC|MIL|DIAG|RES|CAP|REV)(?:-[A-Z0-9]+)?/gi);
56
+ return ids ?? [];
57
+ }
58
+ return block[1]
59
+ .split(/\r?\n/)
60
+ .map((line) => line.match(/^\s*-\s+(.+?)\s*$/)?.[1])
61
+ .filter(Boolean)
62
+ .flatMap((entry) => {
63
+ const ids = entry.match(/HS-[A-Z0-9]+-(?:TASK|EPIC|MIL|DIAG|RES|CAP|REV)(?:-[A-Z0-9]+)?/gi);
64
+ return ids ?? [];
65
+ });
66
+ }
67
+
68
+ /**
69
+ * @param {string | null | undefined} type
70
+ * @param {string | null | undefined} id
71
+ */
72
+ export function isSourcePlanningSpec(type, id) {
73
+ const t = (type ?? '').toLowerCase();
74
+ if (t === 'diagnose' || t === 'research') return true;
75
+ const token = id ?? '';
76
+ return /-DIAG(?:-|$)/i.test(token) || /-RES(?:-|$)/i.test(token);
77
+ }
78
+
79
+ // ── Phase-1 resolver (HS-5Q41): detect a capture's Phase-1 stage so runPhase1 can enter at the
80
+ // matching step instead of restarting from scratch. A capture (-CAP / type: capture) is a Phase-1
81
+ // pipeline artifact; DIAG/RES keep their own remediation flow (isSourcePlanningSpec) untouched. ──
82
+
83
+ /** The Phase-1 pipeline stages, ordered raw → plan-draft. */
84
+ export const PHASE1_STAGES = Object.freeze([
85
+ 'raw',
86
+ 'expanded',
87
+ 'research-stub',
88
+ 'research-doc',
89
+ 'plan-draft',
90
+ ]);
91
+
92
+ /**
93
+ * Stage → runPhase1 entry step (name + index; mirrors dev-factory/pipeline/phase1.mjs STEP).
94
+ * A later stage skips the earlier steps; at run time the recorded checkpoint still wins over the
95
+ * hint so a re-pinned artifact never rewinds or restarts.
96
+ */
97
+ export const PHASE1_STAGE_TO_STEP = Object.freeze({
98
+ raw: { step: 'expand', index: 0 },
99
+ expanded: { step: 'classify', index: 2 },
100
+ 'research-stub': { step: 'research_l1', index: 3 },
101
+ // research-doc has its research done but no plan authored yet → enter at the plan-author step
102
+ // (index 6) so the plan is actually authored before filing (HS-KMBT-EPIC). plan-draft already
103
+ // carries a plan → enter at file (now index 7, after PLAN_AUTHOR was inserted at 6).
104
+ 'research-doc': { step: 'plan_author', index: 6 },
105
+ 'plan-draft': { step: 'file', index: 7 },
106
+ });
107
+
108
+ /**
109
+ * A spec is a Phase-1 artifact when it is a capture (type: capture or an -CAP id). DIAG/RES route
110
+ * through isSourcePlanningSpec instead, so this stays orthogonal to the remediation flow.
111
+ * @param {string | null | undefined} type
112
+ * @param {string | null | undefined} id
113
+ */
114
+ export function isPhase1Artifact(type, id) {
115
+ if ((type ?? '').toLowerCase() === 'capture') return true;
116
+ return /-CAP(?:-|$)/i.test(id ?? '');
117
+ }
118
+
119
+ /**
120
+ * Deterministic Phase-1 stage detection for a capture (frontmatter + section presence, never
121
+ * semantic). Precedence: (1) explicit `phase1-stage`/`phase1_stage` frontmatter; (2) `capture_status`
122
+ * (promoted → plan-draft, expanded → at-least-expanded); (3) body section presence. Returns one of
123
+ * PHASE1_STAGES; unknown/empty inputs resolve to 'raw' (the safe full-pipeline entry).
124
+ * @param {SpecMeta} meta
125
+ * @returns {string}
126
+ */
127
+ export function detectPhase1Stage(meta) {
128
+ const raw = readFileSync(meta.spec_path, 'utf8');
129
+ const fmMatch = raw.match(FRONTMATTER);
130
+ const fm = fmMatch?.[1] ?? '';
131
+ const body = meta.body ?? (fmMatch ? raw.slice(fmMatch[0].length) : raw);
132
+
133
+ // 1) An explicit stage field is authoritative — the caller/pipeline stamped it deliberately.
134
+ const explicit = (fmScalar(fm, 'phase1-stage') ?? fmScalar(fm, 'phase1_stage'))?.toLowerCase();
135
+ if (explicit && PHASE1_STAGES.includes(explicit)) return explicit;
136
+
137
+ // 2) Genuine research artifacts override the lifecycle field UPWARD. These headings are never in
138
+ // the raw capture template (which seeds only Core Idea / Notes / Source Material / Summary — a bare
139
+ // `# Core Idea` heading is NOT proof of expansion), so their presence means real added content:
140
+ // - a completed research doc carries BOTH `## Sources` and `## Executive Summary` → research-doc
141
+ // - a research stub carries `## Sources` or a `## Research` / `## Findings` heading → research-stub
142
+ const hasSources = /^##\s+Sources\b/im.test(body);
143
+ const hasExecSummary = /^##\s+Executive Summary\b/im.test(body);
144
+ const hasResearchHeading = /^##\s+(Research|Findings)\b/im.test(body);
145
+ if (hasSources && hasExecSummary) return 'research-doc';
146
+ if (hasSources || hasResearchHeading) return 'research-stub';
147
+
148
+ // 3) `capture_status` is the maintained lifecycle field — authoritative for raw/expanded/promoted.
149
+ // (Heading presence is deliberately NOT used to infer expanded: the capture template seeds a
150
+ // `# Core Idea` heading even for raw captures, so inferring expansion from it misreads raw as
151
+ // expanded — a real defect found driving live captures through the resolver, 2026-07-09.)
152
+ const capStatus = (fmScalar(fm, 'capture_status') ?? '').toLowerCase();
153
+ if (capStatus === 'promoted') return 'plan-draft';
154
+ if (capStatus === 'expanded') return 'expanded';
155
+
156
+ // 4) Default: raw (the safe full-pipeline entry) — including when capture_status is absent/unknown.
157
+ return 'raw';
158
+ }
159
+
160
+ /**
161
+ * Resolve a capture to a `mode: phase1` dispatch plan with the detected stage + start-at-step hint,
162
+ * so `/hs-auto` on a capture (or a mid-stage capture) no longer dead-ends at MISSING_TARGET.
163
+ * @param {SpecMeta} meta full meta (spec_path + body populated)
164
+ * @param {string} relPath repo-relative capture path
165
+ */
166
+ export function resolvePhase1Spec(meta, relPath) {
167
+ const stage = detectPhase1Stage(meta);
168
+ const entry = PHASE1_STAGE_TO_STEP[stage] ?? PHASE1_STAGE_TO_STEP.raw;
169
+ return {
170
+ mode: 'phase1',
171
+ spec_id: meta.id,
172
+ spec_path: relPath,
173
+ stage,
174
+ entry_step: entry.step,
175
+ entry_step_index: entry.index,
176
+ phase1_command: `node dev-factory/pipeline/phase1.mjs --capture ${relPath} --stage ${stage}`,
177
+ coo_contract: [
178
+ 'COO orchestration only — Phase-1 resolver (start from any state).',
179
+ `Run runPhase1 entering at the "${entry.step}" step (stage: ${stage}).`,
180
+ 'Invariant: re-pinning a mid-stage artifact resumes from its recorded checkpoint — never restarts.',
181
+ 'Per-front reach: Handheld advances one checkpoint; Assisted carries to end-of-Phase-1 (milestone + epics filed) then a review gate; Autonomous carries into Phase 2.',
182
+ 'Do NOT implement in Cursor.',
183
+ ].join('\n'),
184
+ };
185
+ }
186
+
187
+ /**
188
+ * @param {SpecMeta} meta
189
+ * @param {string} sourceId
190
+ */
191
+ export function referencesSourceId(meta, sourceId) {
192
+ const raw = readFileSync(meta.spec_path, 'utf8');
193
+ const m = raw.match(FRONTMATTER);
194
+ const fm = m?.[1] ?? '';
195
+ const haystack = [
196
+ fmScalar(fm, 'source'),
197
+ fmScalar(fm, 'remediation_of'),
198
+ fmScalar(fm, 'diagnose'),
199
+ fmScalar(fm, 'research'),
200
+ fmScalar(fm, 'parent'),
201
+ meta.body,
202
+ ]
203
+ .filter(Boolean)
204
+ .join('\n');
205
+ return haystack.includes(sourceId);
206
+ }
207
+
208
+ /**
209
+ * @param {string} specsDir
210
+ */
211
+ function walkMarkdown(specsDir) {
212
+ /** @type {string[]} */
213
+ const files = [];
214
+ const walk = (dir) => {
215
+ if (!existsSync(dir)) return;
216
+ for (const entry of readdirSync(dir)) {
217
+ const full = join(dir, entry);
218
+ if (statSync(full).isDirectory()) walk(full);
219
+ else if (entry.endsWith('.md')) files.push(full);
220
+ }
221
+ };
222
+ walk(specsDir);
223
+ return files;
224
+ }
225
+
226
+ /**
227
+ * @param {string} repoRoot
228
+ * @param {string} sourceId
229
+ * @returns {SpecMeta | null}
230
+ */
231
+ export function findRemediationEpic(repoRoot, sourceId) {
232
+ const epicsDir = join(repoRoot, 'specs', 'epics');
233
+ for (const file of walkMarkdown(epicsDir)) {
234
+ const meta = readSpecMetaFull(file);
235
+ if (referencesSourceId(meta, sourceId)) return meta;
236
+ }
237
+ return null;
238
+ }
239
+
240
+ /**
241
+ * @param {string} repoRoot
242
+ * @param {string} sourceId
243
+ * @param {string | null} epicId
244
+ * @returns {SpecMeta[]}
245
+ */
246
+ export function findLinkedTasks(repoRoot, sourceId, epicId) {
247
+ const tasksDir = join(repoRoot, 'specs', 'tasks');
248
+ /** @type {SpecMeta[]} */
249
+ const out = [];
250
+ for (const file of walkMarkdown(tasksDir)) {
251
+ const meta = readSpecMetaFull(file);
252
+ const linked =
253
+ referencesSourceId(meta, sourceId)
254
+ || (epicId && (meta.parent?.includes(epicId) ?? false));
255
+ if (linked) out.push(meta);
256
+ }
257
+ return out;
258
+ }
259
+
260
+ const DISPATCHABLE_STATUSES = new Set(['open', 'in_progress']);
261
+
262
+ /**
263
+ * @param {SpecMeta[]} tasks
264
+ * @returns {SpecMeta | null}
265
+ */
266
+ export function pickDispatchableTask(tasks) {
267
+ const ready = tasks.filter(
268
+ (t) => t.target && t.status && DISPATCHABLE_STATUSES.has(t.status),
269
+ );
270
+ ready.sort((a, b) => a.spec_path.localeCompare(b.spec_path));
271
+ return ready[0] ?? null;
272
+ }
273
+
274
+ /**
275
+ * @param {string} repoRoot
276
+ * @param {string} epicId
277
+ * @returns {number}
278
+ */
279
+ export function countEpicChildren(repoRoot, epicId) {
280
+ const epicPath = findSpecFile(join(repoRoot, 'specs'), epicId);
281
+ if (!epicPath) return 0;
282
+ const raw = readFileSync(epicPath, 'utf8');
283
+ const fm = raw.match(FRONTMATTER)?.[1] ?? '';
284
+ return fmWikilinkList(fm, 'children').length;
285
+ }
286
+
287
+ /**
288
+ * @param {string} specPath repo-relative
289
+ * @param {string} target
290
+ */
291
+ function buildDispatchCommand(specPath, target) {
292
+ const specId = basename(specPath, '.md');
293
+ const checkpoint = `dev-factory/state/checkpoint-${specId}.json`;
294
+ return [
295
+ 'node dev-factory/controller.mjs',
296
+ `--spec ${specPath}`,
297
+ `--target ${target.replace(/\\/g, '/')}`,
298
+ '--use-scheduler',
299
+ '--n 3',
300
+ `--checkpoint ${checkpoint}`,
301
+ ].join(' ');
302
+ }
303
+
304
+ /**
305
+ * @param {string} repoRoot
306
+ * @param {{ id: string|null, type: string|null, spec_path: string }} sourceMeta
307
+ * @param {string} relPath repo-relative source path
308
+ */
309
+ export function resolveSourcePlanningSpec(repoRoot, sourceMeta, relPath) {
310
+ const sourceId = sourceMeta.id;
311
+ if (!sourceId) {
312
+ return {
313
+ error: 'SOURCE_ID_MISSING',
314
+ message: `${relPath} has no id: frontmatter.`,
315
+ spec_path: relPath,
316
+ };
317
+ }
318
+
319
+ const sourceKind = (sourceMeta.type ?? '').toLowerCase() === 'research' ? 'research' : 'diagnose';
320
+ const epic = findRemediationEpic(repoRoot, sourceId);
321
+ const tasks = findLinkedTasks(repoRoot, sourceId, epic?.id ?? null);
322
+ const dispatchTask = pickDispatchableTask(tasks);
323
+
324
+ if (dispatchTask?.id && dispatchTask.target) {
325
+ const taskRel = relative(repoRoot, dispatchTask.spec_path).replace(/\\/g, '/');
326
+ return {
327
+ mode: 'dispatch',
328
+ spec_id: dispatchTask.id,
329
+ spec_path: taskRel,
330
+ target: dispatchTask.target.replace(/\\/g, '/'),
331
+ source_spec_id: sourceId,
332
+ source_spec_path: relPath,
333
+ source_kind: sourceKind,
334
+ matched_via: 'source_remediation_task',
335
+ dispatch_command: buildDispatchCommand(taskRel, dispatchTask.target),
336
+ coo_contract:
337
+ 'COO orchestration only — run preflight, then dispatch_command. Do NOT implement in Cursor unless the gate fails after fleet retries.',
338
+ };
339
+ }
340
+
341
+ if (epic?.id) {
342
+ const childCount = countEpicChildren(repoRoot, epic.id);
343
+ const epicRel = relative(repoRoot, epic.spec_path).replace(/\\/g, '/');
344
+ if (childCount === 0) {
345
+ return {
346
+ mode: 'breakdown',
347
+ spec_id: epic.id,
348
+ spec_path: epicRel,
349
+ source_spec_id: sourceId,
350
+ source_spec_path: relPath,
351
+ source_kind: sourceKind,
352
+ remediation_epic_id: epic.id,
353
+ breakdown_kind: 'decompose',
354
+ breakdown_command: `node dev-factory/engine/breakdown.mjs --decompose --epic ${epic.id}`,
355
+ coo_contract:
356
+ 'COO orchestration only — run preflight, then breakdown_command. Review the JSON draft, file tasks via heyspex_file_spec (each with target:), register linkage, re-run hs auto on the source spec, then dispatch. Do NOT implement in Cursor.',
357
+ };
358
+ }
359
+ return {
360
+ mode: 'breakdown',
361
+ spec_id: epic.id,
362
+ spec_path: epicRel,
363
+ source_spec_id: sourceId,
364
+ source_spec_path: relPath,
365
+ source_kind: sourceKind,
366
+ remediation_epic_id: epic.id,
367
+ breakdown_kind: 'incremental',
368
+ breakdown_command: `node dev-factory/controller.mjs --breakdown-epic ${epic.id}`,
369
+ linked_tasks_without_target: tasks.filter((t) => !t.target).map((t) => t.id),
370
+ coo_contract:
371
+ 'COO orchestration only — run preflight, then breakdown_command. Review draft, file any missing tasks with target:, re-run hs auto on the source spec, then dispatch. Do NOT implement in Cursor.',
372
+ };
373
+ }
374
+
375
+ return {
376
+ mode: 'source_plan',
377
+ source_kind: sourceKind,
378
+ spec_id: sourceId,
379
+ spec_path: relPath,
380
+ linked_tasks: tasks.map((t) => ({ id: t.id, status: t.status, target: t.target })),
381
+ file_epic_intent: 'file_epic',
382
+ file_task_intent: 'file_task',
383
+ decompose_command: 'node dev-factory/engine/breakdown.mjs --decompose --epic <REMEDIATION_EPIC_ID>',
384
+ re_resolve_command: `node bin/hs.mjs auto ${sourceId} --json`,
385
+ coo_contract: [
386
+ 'COO orchestration only — do NOT implement in Cursor.',
387
+ '1) Run preflight from JSON.',
388
+ '2) heyspex_preflight({ intent: "file_epic" }) → heyspex_file_spec a remediation epic (no milestone required). Body: synthesize Decision, Fix slices, Deferred, and Acceptance from the source spec. Frontmatter source: wikilink to this diagnose/research.',
389
+ '3) heyspex_register_spec(epic_id, parent_id) — parent_id null/omit when no milestone.',
390
+ '4) node dev-factory/engine/breakdown.mjs --decompose --epic <EPIC_ID> — fleet breakdown seat drafts tasks.',
391
+ '5) COO reviews draft → heyspex_file_spec each task with target: + acceptance criteria → heyspex_register_spec per task.',
392
+ '6) Re-run node bin/hs.mjs auto on the source spec → expect mode dispatch.',
393
+ '7) Run dispatch_command; review gate verdict only.',
394
+ ].join('\n'),
395
+ message:
396
+ `No remediation epic linked to ${sourceId}. File epic + tasks from the source spec, then re-resolve for fleet dispatch.`,
397
+ };
398
+ }