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
@@ -1,19 +1,890 @@
1
1
  import { createRequire as __hsCreateRequire } from 'node:module';
2
2
  const require = __hsCreateRequire(import.meta.url);
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
10
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
11
+ }) : x)(function(x) {
12
+ if (typeof require !== "undefined") return require.apply(this, arguments);
13
+ throw Error('Dynamic require of "' + x + '" is not supported');
14
+ });
15
+ var __esm = (fn, res) => function __init() {
16
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
17
+ };
18
+ var __commonJS = (cb, mod) => function __require2() {
19
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
20
+ };
21
+ var __copyProps = (to, from, except, desc) => {
22
+ if (from && typeof from === "object" || typeof from === "function") {
23
+ for (let key of __getOwnPropNames(from))
24
+ if (!__hasOwnProp.call(to, key) && key !== except)
25
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
26
+ }
27
+ return to;
28
+ };
29
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
30
+ // If the importer is in node compatibility mode or this is not an ESM
31
+ // file that has been converted to a CommonJS file using a Babel-
32
+ // compatible transform (i.e. "__esModule" has not been set), then set
33
+ // "default" to the CommonJS "module.exports" for node compatibility.
34
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
35
+ mod
36
+ ));
37
+
38
+ // scripts/lib/heyspex-home.mjs
39
+ import { homedir } from "node:os";
40
+ function heyspexHome(env = process.env) {
41
+ const override = env?.HEYSPEX_HOME;
42
+ if (typeof override === "string" && override.trim().length > 0) {
43
+ return override.trim();
44
+ }
45
+ return homedir();
46
+ }
47
+ var init_heyspex_home = __esm({
48
+ "scripts/lib/heyspex-home.mjs"() {
49
+ "use strict";
50
+ }
51
+ });
52
+
53
+ // vendor/heyspex-mcp/credentials.mjs
54
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
55
+ import os, { homedir as homedir2 } from "node:os";
56
+ import { dirname, join, resolve as resolve2 } from "node:path";
57
+ var init_credentials = __esm({
58
+ "vendor/heyspex-mcp/credentials.mjs"() {
59
+ "use strict";
60
+ init_heyspex_home();
61
+ }
62
+ });
63
+
64
+ // scripts/lib/dev-mode.mjs
65
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
66
+ import { homedir as homedir3 } from "node:os";
67
+ import { join as join2 } from "node:path";
68
+ function devModeFilePath(homeDir = heyspexHome()) {
69
+ return join2(homeDir, ".heyspex", DEV_MODE_FILENAME);
70
+ }
71
+ function normalizeMode(raw) {
72
+ const v = String(raw ?? "").trim().toLowerCase();
73
+ if (v === "prod" || v === "production") return { mode: "prod", reliance: "cloud" };
74
+ if (v === "dev") return { mode: "dev", reliance: null };
75
+ if (v === "local") return { mode: "dev", reliance: "local" };
76
+ if (v === "cloud") return { mode: "dev", reliance: "cloud" };
77
+ return null;
78
+ }
79
+ function normalizeReliance(raw) {
80
+ const v = String(raw ?? "").trim().toLowerCase();
81
+ return v === "local" || v === "cloud" ? v : null;
82
+ }
83
+ function deriveSemantics(mode, reliance) {
84
+ const effectiveReliance = mode === "prod" ? "cloud" : reliance ?? "local";
85
+ const local = mode === "dev" && effectiveReliance === "local";
86
+ return {
87
+ reliance: effectiveReliance,
88
+ consoleUrl: local ? LOCAL_CONSOLE_URL : CLOUD_CONSOLE_URL,
89
+ readTier: local ? "disk" : "neon",
90
+ vaultWriteMode: local ? "local" : "cloud"
91
+ };
92
+ }
93
+ function readLocalDevModeRecord({ homeDir = heyspexHome(), readFile = defaultReadFile } = {}) {
94
+ let raw;
95
+ try {
96
+ raw = readFile(devModeFilePath(homeDir));
97
+ } catch {
98
+ return { record: null, parseError: null };
99
+ }
100
+ try {
101
+ const parsed = JSON.parse(raw);
102
+ const mode = parsed?.mode === "prod" || parsed?.mode === "dev" ? parsed.mode : null;
103
+ const reliance = normalizeReliance(parsed?.reliance);
104
+ if (!mode || mode === "dev" && !reliance) {
105
+ return { record: null, parseError: `invalid dev-mode record: ${JSON.stringify(parsed)}` };
106
+ }
107
+ return {
108
+ record: {
109
+ mode,
110
+ reliance: mode === "prod" ? "cloud" : reliance,
111
+ console_url: typeof parsed.console_url === "string" ? parsed.console_url : null,
112
+ updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : null,
113
+ source: typeof parsed.source === "string" ? parsed.source : null
114
+ },
115
+ parseError: null
116
+ };
117
+ } catch (err) {
118
+ return { record: null, parseError: `unparseable dev-mode.json: ${String(err?.message ?? err)}` };
119
+ }
120
+ }
121
+ function resolveDevMode({
122
+ env = process.env,
123
+ homeDir = heyspexHome(),
124
+ readFile = defaultReadFile,
125
+ cloudSetting = void 0
126
+ } = {}) {
127
+ const local = readLocalDevModeRecord({ homeDir, readFile });
128
+ const cloudKnown = cloudSetting !== void 0;
129
+ const cloudValue = cloudKnown && cloudSetting ? {
130
+ mode: cloudSetting.mode === "dev" ? "dev" : "prod",
131
+ reliance: normalizeReliance(cloudSetting.reliance) ?? "cloud"
132
+ } : null;
133
+ let mode;
134
+ let reliance;
135
+ let source;
136
+ let relianceSource;
137
+ const envParsed = normalizeMode(env.HEYSPEX_DEV_MODE);
138
+ if (envParsed) {
139
+ mode = envParsed.mode;
140
+ const envReliance = envParsed.reliance ?? normalizeReliance(env.HEYSPEX_DEV_RELIANCE);
141
+ reliance = envReliance ?? "local";
142
+ source = "env";
143
+ relianceSource = envReliance ? "env" : "assumed-local";
144
+ } else if (local.record) {
145
+ mode = local.record.mode;
146
+ reliance = local.record.reliance;
147
+ source = "local-override";
148
+ } else if (cloudValue) {
149
+ mode = cloudValue.mode;
150
+ reliance = cloudValue.reliance;
151
+ source = "cloud-setting";
152
+ } else {
153
+ mode = "prod";
154
+ reliance = "cloud";
155
+ source = "default";
156
+ }
157
+ const semantics = deriveSemantics(mode, reliance);
158
+ const consoleUrl = source === "local-override" && local.record?.console_url ? local.record.console_url.replace(/\/+$/, "") : semantics.consoleUrl;
159
+ const staleAgainstCloud = source === "local-override" && cloudValue !== null && (cloudValue.mode !== mode || mode === "dev" && cloudValue.reliance !== semantics.reliance);
160
+ return {
161
+ mode,
162
+ reliance: semantics.reliance,
163
+ consoleUrl,
164
+ readTier: semantics.readTier,
165
+ vaultWriteMode: semantics.vaultWriteMode,
166
+ sources: {
167
+ mode: source,
168
+ reliance: relianceSource ?? source,
169
+ consoleUrl: source === "local-override" && local.record?.console_url ? "local-override" : "derived",
170
+ readTier: "derived",
171
+ vaultWriteMode: "derived"
172
+ },
173
+ localOverride: {
174
+ present: local.record !== null,
175
+ record: local.record,
176
+ parseError: local.parseError,
177
+ staleAgainstCloud
178
+ },
179
+ cloudSetting: { known: cloudKnown, value: cloudValue }
180
+ };
181
+ }
182
+ var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile;
183
+ var init_dev_mode = __esm({
184
+ "scripts/lib/dev-mode.mjs"() {
185
+ "use strict";
186
+ init_heyspex_home();
187
+ init_credentials();
188
+ CLOUD_CONSOLE_URL = "https://app.heyspex.com";
189
+ LOCAL_CONSOLE_URL = "http://localhost:3000";
190
+ DEV_MODE_FILENAME = "dev-mode.json";
191
+ defaultReadFile = (p) => readFileSync2(p, "utf8");
192
+ }
193
+ });
194
+
195
+ // node_modules/kind-of/index.js
196
+ var require_kind_of = __commonJS({
197
+ "node_modules/kind-of/index.js"(exports2, module2) {
198
+ "use strict";
199
+ var toString = Object.prototype.toString;
200
+ module2.exports = function kindOf(val) {
201
+ if (val === void 0) return "undefined";
202
+ if (val === null) return "null";
203
+ var type = typeof val;
204
+ if (type === "boolean") return "boolean";
205
+ if (type === "string") return "string";
206
+ if (type === "number") return "number";
207
+ if (type === "symbol") return "symbol";
208
+ if (type === "function") {
209
+ return isGeneratorFn(val) ? "generatorfunction" : "function";
210
+ }
211
+ if (isArray(val)) return "array";
212
+ if (isBuffer(val)) return "buffer";
213
+ if (isArguments(val)) return "arguments";
214
+ if (isDate(val)) return "date";
215
+ if (isError(val)) return "error";
216
+ if (isRegexp(val)) return "regexp";
217
+ switch (ctorName(val)) {
218
+ case "Symbol":
219
+ return "symbol";
220
+ case "Promise":
221
+ return "promise";
222
+ // Set, Map, WeakSet, WeakMap
223
+ case "WeakMap":
224
+ return "weakmap";
225
+ case "WeakSet":
226
+ return "weakset";
227
+ case "Map":
228
+ return "map";
229
+ case "Set":
230
+ return "set";
231
+ // 8-bit typed arrays
232
+ case "Int8Array":
233
+ return "int8array";
234
+ case "Uint8Array":
235
+ return "uint8array";
236
+ case "Uint8ClampedArray":
237
+ return "uint8clampedarray";
238
+ // 16-bit typed arrays
239
+ case "Int16Array":
240
+ return "int16array";
241
+ case "Uint16Array":
242
+ return "uint16array";
243
+ // 32-bit typed arrays
244
+ case "Int32Array":
245
+ return "int32array";
246
+ case "Uint32Array":
247
+ return "uint32array";
248
+ case "Float32Array":
249
+ return "float32array";
250
+ case "Float64Array":
251
+ return "float64array";
252
+ }
253
+ if (isGeneratorObj(val)) {
254
+ return "generator";
255
+ }
256
+ type = toString.call(val);
257
+ switch (type) {
258
+ case "[object Object]":
259
+ return "object";
260
+ // iterators
261
+ case "[object Map Iterator]":
262
+ return "mapiterator";
263
+ case "[object Set Iterator]":
264
+ return "setiterator";
265
+ case "[object String Iterator]":
266
+ return "stringiterator";
267
+ case "[object Array Iterator]":
268
+ return "arrayiterator";
269
+ }
270
+ return type.slice(8, -1).toLowerCase().replace(/\s/g, "");
271
+ };
272
+ function ctorName(val) {
273
+ return typeof val.constructor === "function" ? val.constructor.name : null;
274
+ }
275
+ function isArray(val) {
276
+ if (Array.isArray) return Array.isArray(val);
277
+ return val instanceof Array;
278
+ }
279
+ function isError(val) {
280
+ return val instanceof Error || typeof val.message === "string" && val.constructor && typeof val.constructor.stackTraceLimit === "number";
281
+ }
282
+ function isDate(val) {
283
+ if (val instanceof Date) return true;
284
+ return typeof val.toDateString === "function" && typeof val.getDate === "function" && typeof val.setDate === "function";
285
+ }
286
+ function isRegexp(val) {
287
+ if (val instanceof RegExp) return true;
288
+ return typeof val.flags === "string" && typeof val.ignoreCase === "boolean" && typeof val.multiline === "boolean" && typeof val.global === "boolean";
289
+ }
290
+ function isGeneratorFn(name, val) {
291
+ return ctorName(name) === "GeneratorFunction";
292
+ }
293
+ function isGeneratorObj(val) {
294
+ return typeof val.throw === "function" && typeof val.return === "function" && typeof val.next === "function";
295
+ }
296
+ function isArguments(val) {
297
+ try {
298
+ if (typeof val.length === "number" && typeof val.callee === "function") {
299
+ return true;
300
+ }
301
+ } catch (err) {
302
+ if (err.message.indexOf("callee") !== -1) {
303
+ return true;
304
+ }
305
+ }
306
+ return false;
307
+ }
308
+ function isBuffer(val) {
309
+ if (val.constructor && typeof val.constructor.isBuffer === "function") {
310
+ return val.constructor.isBuffer(val);
311
+ }
312
+ return false;
313
+ }
314
+ }
315
+ });
316
+
317
+ // node_modules/is-extendable/index.js
318
+ var require_is_extendable = __commonJS({
319
+ "node_modules/is-extendable/index.js"(exports2, module2) {
320
+ "use strict";
321
+ module2.exports = function isExtendable(val) {
322
+ return typeof val !== "undefined" && val !== null && (typeof val === "object" || typeof val === "function");
323
+ };
324
+ }
325
+ });
326
+
327
+ // node_modules/extend-shallow/index.js
328
+ var require_extend_shallow = __commonJS({
329
+ "node_modules/extend-shallow/index.js"(exports2, module2) {
330
+ "use strict";
331
+ var isObject = require_is_extendable();
332
+ module2.exports = function extend(o) {
333
+ if (!isObject(o)) {
334
+ o = {};
335
+ }
336
+ var len = arguments.length;
337
+ for (var i = 1; i < len; i++) {
338
+ var obj = arguments[i];
339
+ if (isObject(obj)) {
340
+ assign(o, obj);
341
+ }
342
+ }
343
+ return o;
344
+ };
345
+ function assign(a, b) {
346
+ for (var key in b) {
347
+ if (hasOwn(b, key)) {
348
+ a[key] = b[key];
349
+ }
350
+ }
351
+ }
352
+ function hasOwn(obj, key) {
353
+ return Object.prototype.hasOwnProperty.call(obj, key);
354
+ }
355
+ }
356
+ });
357
+
358
+ // node_modules/section-matter/index.js
359
+ var require_section_matter = __commonJS({
360
+ "node_modules/section-matter/index.js"(exports2, module2) {
361
+ "use strict";
362
+ var typeOf = require_kind_of();
363
+ var extend = require_extend_shallow();
364
+ module2.exports = function(input, options2) {
365
+ if (typeof options2 === "function") {
366
+ options2 = { parse: options2 };
367
+ }
368
+ var file = toObject(input);
369
+ var defaults = { section_delimiter: "---", parse: identity };
370
+ var opts = extend({}, defaults, options2);
371
+ var delim = opts.section_delimiter;
372
+ var lines = file.content.split(/\r?\n/);
373
+ var sections = null;
374
+ var section = createSection();
375
+ var content = [];
376
+ var stack = [];
377
+ function initSections(val) {
378
+ file.content = val;
379
+ sections = [];
380
+ content = [];
381
+ }
382
+ function closeSection(val) {
383
+ if (stack.length) {
384
+ section.key = getKey(stack[0], delim);
385
+ section.content = val;
386
+ opts.parse(section, sections);
387
+ sections.push(section);
388
+ section = createSection();
389
+ content = [];
390
+ stack = [];
391
+ }
392
+ }
393
+ for (var i = 0; i < lines.length; i++) {
394
+ var line = lines[i];
395
+ var len = stack.length;
396
+ var ln = line.trim();
397
+ if (isDelimiter(ln, delim)) {
398
+ if (ln.length === 3 && i !== 0) {
399
+ if (len === 0 || len === 2) {
400
+ content.push(line);
401
+ continue;
402
+ }
403
+ stack.push(ln);
404
+ section.data = content.join("\n");
405
+ content = [];
406
+ continue;
407
+ }
408
+ if (sections === null) {
409
+ initSections(content.join("\n"));
410
+ }
411
+ if (len === 2) {
412
+ closeSection(content.join("\n"));
413
+ }
414
+ stack.push(ln);
415
+ continue;
416
+ }
417
+ content.push(line);
418
+ }
419
+ if (sections === null) {
420
+ initSections(content.join("\n"));
421
+ } else {
422
+ closeSection(content.join("\n"));
423
+ }
424
+ file.sections = sections;
425
+ return file;
426
+ };
427
+ function isDelimiter(line, delim) {
428
+ if (line.slice(0, delim.length) !== delim) {
429
+ return false;
430
+ }
431
+ if (line.charAt(delim.length + 1) === delim.slice(-1)) {
432
+ return false;
433
+ }
434
+ return true;
435
+ }
436
+ function toObject(input) {
437
+ if (typeOf(input) !== "object") {
438
+ input = { content: input };
439
+ }
440
+ if (typeof input.content !== "string" && !isBuffer(input.content)) {
441
+ throw new TypeError("expected a buffer or string");
442
+ }
443
+ input.content = input.content.toString();
444
+ input.sections = [];
445
+ return input;
446
+ }
447
+ function getKey(val, delim) {
448
+ return val ? val.slice(delim.length).trim() : "";
449
+ }
450
+ function createSection() {
451
+ return { key: "", data: "", content: "" };
452
+ }
453
+ function identity(val) {
454
+ return val;
455
+ }
456
+ function isBuffer(val) {
457
+ if (val && val.constructor && typeof val.constructor.isBuffer === "function") {
458
+ return val.constructor.isBuffer(val);
459
+ }
460
+ return false;
461
+ }
462
+ }
463
+ });
464
+
465
+ // node_modules/gray-matter/lib/engines.js
466
+ var require_engines = __commonJS({
467
+ "node_modules/gray-matter/lib/engines.js"(exports, module) {
468
+ "use strict";
469
+ var yaml = __require("js-yaml");
470
+ var engines = exports = module.exports;
471
+ engines.yaml = {
472
+ parse: yaml.safeLoad.bind(yaml),
473
+ stringify: yaml.safeDump.bind(yaml)
474
+ };
475
+ engines.json = {
476
+ parse: JSON.parse.bind(JSON),
477
+ stringify: function(obj, options2) {
478
+ const opts = Object.assign({ replacer: null, space: 2 }, options2);
479
+ return JSON.stringify(obj, opts.replacer, opts.space);
480
+ }
481
+ };
482
+ engines.javascript = {
483
+ parse: function parse(str, options, wrap) {
484
+ try {
485
+ if (wrap !== false) {
486
+ str = "(function() {\nreturn " + str.trim() + ";\n}());";
487
+ }
488
+ return eval(str) || {};
489
+ } catch (err) {
490
+ if (wrap !== false && /(unexpected|identifier)/i.test(err.message)) {
491
+ return parse(str, options, false);
492
+ }
493
+ throw new SyntaxError(err);
494
+ }
495
+ },
496
+ stringify: function() {
497
+ throw new Error("stringifying JavaScript is not supported");
498
+ }
499
+ };
500
+ }
501
+ });
502
+
503
+ // node_modules/strip-bom-string/index.js
504
+ var require_strip_bom_string = __commonJS({
505
+ "node_modules/strip-bom-string/index.js"(exports2, module2) {
506
+ "use strict";
507
+ module2.exports = function(str2) {
508
+ if (typeof str2 === "string" && str2.charAt(0) === "\uFEFF") {
509
+ return str2.slice(1);
510
+ }
511
+ return str2;
512
+ };
513
+ }
514
+ });
515
+
516
+ // node_modules/gray-matter/lib/utils.js
517
+ var require_utils = __commonJS({
518
+ "node_modules/gray-matter/lib/utils.js"(exports2) {
519
+ "use strict";
520
+ var stripBom = require_strip_bom_string();
521
+ var typeOf = require_kind_of();
522
+ exports2.define = function(obj, key, val) {
523
+ Reflect.defineProperty(obj, key, {
524
+ enumerable: false,
525
+ configurable: true,
526
+ writable: true,
527
+ value: val
528
+ });
529
+ };
530
+ exports2.isBuffer = function(val) {
531
+ return typeOf(val) === "buffer";
532
+ };
533
+ exports2.isObject = function(val) {
534
+ return typeOf(val) === "object";
535
+ };
536
+ exports2.toBuffer = function(input) {
537
+ return typeof input === "string" ? Buffer.from(input) : input;
538
+ };
539
+ exports2.toString = function(input) {
540
+ if (exports2.isBuffer(input)) return stripBom(String(input));
541
+ if (typeof input !== "string") {
542
+ throw new TypeError("expected input to be a string or buffer");
543
+ }
544
+ return stripBom(input);
545
+ };
546
+ exports2.arrayify = function(val) {
547
+ return val ? Array.isArray(val) ? val : [val] : [];
548
+ };
549
+ exports2.startsWith = function(str2, substr, len) {
550
+ if (typeof len !== "number") len = substr.length;
551
+ return str2.slice(0, len) === substr;
552
+ };
553
+ }
554
+ });
555
+
556
+ // node_modules/gray-matter/lib/defaults.js
557
+ var require_defaults = __commonJS({
558
+ "node_modules/gray-matter/lib/defaults.js"(exports2, module2) {
559
+ "use strict";
560
+ var engines2 = require_engines();
561
+ var utils = require_utils();
562
+ module2.exports = function(options2) {
563
+ const opts = Object.assign({}, options2);
564
+ opts.delimiters = utils.arrayify(opts.delims || opts.delimiters || "---");
565
+ if (opts.delimiters.length === 1) {
566
+ opts.delimiters.push(opts.delimiters[0]);
567
+ }
568
+ opts.language = (opts.language || opts.lang || "yaml").toLowerCase();
569
+ opts.engines = Object.assign({}, engines2, opts.parsers, opts.engines);
570
+ return opts;
571
+ };
572
+ }
573
+ });
574
+
575
+ // node_modules/gray-matter/lib/engine.js
576
+ var require_engine = __commonJS({
577
+ "node_modules/gray-matter/lib/engine.js"(exports2, module2) {
578
+ "use strict";
579
+ module2.exports = function(name, options2) {
580
+ let engine = options2.engines[name] || options2.engines[aliase(name)];
581
+ if (typeof engine === "undefined") {
582
+ throw new Error('gray-matter engine "' + name + '" is not registered');
583
+ }
584
+ if (typeof engine === "function") {
585
+ engine = { parse: engine };
586
+ }
587
+ return engine;
588
+ };
589
+ function aliase(name) {
590
+ switch (name.toLowerCase()) {
591
+ case "js":
592
+ case "javascript":
593
+ return "javascript";
594
+ case "coffee":
595
+ case "coffeescript":
596
+ case "cson":
597
+ return "coffee";
598
+ case "yaml":
599
+ case "yml":
600
+ return "yaml";
601
+ default: {
602
+ return name;
603
+ }
604
+ }
605
+ }
606
+ }
607
+ });
608
+
609
+ // node_modules/gray-matter/lib/stringify.js
610
+ var require_stringify = __commonJS({
611
+ "node_modules/gray-matter/lib/stringify.js"(exports2, module2) {
612
+ "use strict";
613
+ var typeOf = require_kind_of();
614
+ var getEngine = require_engine();
615
+ var defaults = require_defaults();
616
+ module2.exports = function(file, data, options2) {
617
+ if (data == null && options2 == null) {
618
+ switch (typeOf(file)) {
619
+ case "object":
620
+ data = file.data;
621
+ options2 = {};
622
+ break;
623
+ case "string":
624
+ return file;
625
+ default: {
626
+ throw new TypeError("expected file to be a string or object");
627
+ }
628
+ }
629
+ }
630
+ const str2 = file.content;
631
+ const opts = defaults(options2);
632
+ if (data == null) {
633
+ if (!opts.data) return file;
634
+ data = opts.data;
635
+ }
636
+ const language = file.language || opts.language;
637
+ const engine = getEngine(language, opts);
638
+ if (typeof engine.stringify !== "function") {
639
+ throw new TypeError('expected "' + language + '.stringify" to be a function');
640
+ }
641
+ data = Object.assign({}, file.data, data);
642
+ const open = opts.delimiters[0];
643
+ const close = opts.delimiters[1];
644
+ const matter2 = engine.stringify(data, options2).trim();
645
+ let buf = "";
646
+ if (matter2 !== "{}") {
647
+ buf = newline(open) + newline(matter2) + newline(close);
648
+ }
649
+ if (typeof file.excerpt === "string" && file.excerpt !== "") {
650
+ if (str2.indexOf(file.excerpt.trim()) === -1) {
651
+ buf += newline(file.excerpt) + newline(close);
652
+ }
653
+ }
654
+ return buf + newline(str2);
655
+ };
656
+ function newline(str2) {
657
+ return str2.slice(-1) !== "\n" ? str2 + "\n" : str2;
658
+ }
659
+ }
660
+ });
661
+
662
+ // node_modules/gray-matter/lib/excerpt.js
663
+ var require_excerpt = __commonJS({
664
+ "node_modules/gray-matter/lib/excerpt.js"(exports2, module2) {
665
+ "use strict";
666
+ var defaults = require_defaults();
667
+ module2.exports = function(file, options2) {
668
+ const opts = defaults(options2);
669
+ if (file.data == null) {
670
+ file.data = {};
671
+ }
672
+ if (typeof opts.excerpt === "function") {
673
+ return opts.excerpt(file, opts);
674
+ }
675
+ const sep2 = file.data.excerpt_separator || opts.excerpt_separator;
676
+ if (sep2 == null && (opts.excerpt === false || opts.excerpt == null)) {
677
+ return file;
678
+ }
679
+ const delimiter2 = typeof opts.excerpt === "string" ? opts.excerpt : sep2 || opts.delimiters[0];
680
+ const idx = file.content.indexOf(delimiter2);
681
+ if (idx !== -1) {
682
+ file.excerpt = file.content.slice(0, idx);
683
+ }
684
+ return file;
685
+ };
686
+ }
687
+ });
688
+
689
+ // node_modules/gray-matter/lib/to-file.js
690
+ var require_to_file = __commonJS({
691
+ "node_modules/gray-matter/lib/to-file.js"(exports2, module2) {
692
+ "use strict";
693
+ var typeOf = require_kind_of();
694
+ var stringify = require_stringify();
695
+ var utils = require_utils();
696
+ module2.exports = function(file) {
697
+ if (typeOf(file) !== "object") {
698
+ file = { content: file };
699
+ }
700
+ if (typeOf(file.data) !== "object") {
701
+ file.data = {};
702
+ }
703
+ if (file.contents && file.content == null) {
704
+ file.content = file.contents;
705
+ }
706
+ utils.define(file, "orig", utils.toBuffer(file.content));
707
+ utils.define(file, "language", file.language || "");
708
+ utils.define(file, "matter", file.matter || "");
709
+ utils.define(file, "stringify", function(data, options2) {
710
+ if (options2 && options2.language) {
711
+ file.language = options2.language;
712
+ }
713
+ return stringify(file, data, options2);
714
+ });
715
+ file.content = utils.toString(file.content);
716
+ file.isEmpty = false;
717
+ file.excerpt = "";
718
+ return file;
719
+ };
720
+ }
721
+ });
722
+
723
+ // node_modules/gray-matter/lib/parse.js
724
+ var require_parse = __commonJS({
725
+ "node_modules/gray-matter/lib/parse.js"(exports2, module2) {
726
+ "use strict";
727
+ var getEngine = require_engine();
728
+ var defaults = require_defaults();
729
+ module2.exports = function(language, str2, options2) {
730
+ const opts = defaults(options2);
731
+ const engine = getEngine(language, opts);
732
+ if (typeof engine.parse !== "function") {
733
+ throw new TypeError('expected "' + language + '.parse" to be a function');
734
+ }
735
+ return engine.parse(str2, opts);
736
+ };
737
+ }
738
+ });
739
+
740
+ // node_modules/gray-matter/index.js
741
+ var require_gray_matter = __commonJS({
742
+ "node_modules/gray-matter/index.js"(exports2, module2) {
743
+ "use strict";
744
+ var fs4 = __require("fs");
745
+ var sections = require_section_matter();
746
+ var defaults = require_defaults();
747
+ var stringify = require_stringify();
748
+ var excerpt = require_excerpt();
749
+ var engines2 = require_engines();
750
+ var toFile = require_to_file();
751
+ var parse2 = require_parse();
752
+ var utils = require_utils();
753
+ function matter2(input, options2) {
754
+ if (input === "") {
755
+ return { data: {}, content: input, excerpt: "", orig: input };
756
+ }
757
+ let file = toFile(input);
758
+ const cached = matter2.cache[file.content];
759
+ if (!options2) {
760
+ if (cached) {
761
+ file = Object.assign({}, cached);
762
+ file.orig = cached.orig;
763
+ return file;
764
+ }
765
+ matter2.cache[file.content] = file;
766
+ }
767
+ return parseMatter(file, options2);
768
+ }
769
+ function parseMatter(file, options2) {
770
+ const opts = defaults(options2);
771
+ const open = opts.delimiters[0];
772
+ const close = "\n" + opts.delimiters[1];
773
+ let str2 = file.content;
774
+ if (opts.language) {
775
+ file.language = opts.language;
776
+ }
777
+ const openLen = open.length;
778
+ if (!utils.startsWith(str2, open, openLen)) {
779
+ excerpt(file, opts);
780
+ return file;
781
+ }
782
+ if (str2.charAt(openLen) === open.slice(-1)) {
783
+ return file;
784
+ }
785
+ str2 = str2.slice(openLen);
786
+ const len = str2.length;
787
+ const language = matter2.language(str2, opts);
788
+ if (language.name) {
789
+ file.language = language.name;
790
+ str2 = str2.slice(language.raw.length);
791
+ }
792
+ let closeIndex = str2.indexOf(close);
793
+ if (closeIndex === -1) {
794
+ closeIndex = len;
795
+ }
796
+ file.matter = str2.slice(0, closeIndex);
797
+ const block = file.matter.replace(/^\s*#[^\n]+/gm, "").trim();
798
+ if (block === "") {
799
+ file.isEmpty = true;
800
+ file.empty = file.content;
801
+ file.data = {};
802
+ } else {
803
+ file.data = parse2(file.language, file.matter, opts);
804
+ }
805
+ if (closeIndex === len) {
806
+ file.content = "";
807
+ } else {
808
+ file.content = str2.slice(closeIndex + close.length);
809
+ if (file.content[0] === "\r") {
810
+ file.content = file.content.slice(1);
811
+ }
812
+ if (file.content[0] === "\n") {
813
+ file.content = file.content.slice(1);
814
+ }
815
+ }
816
+ excerpt(file, opts);
817
+ if (opts.sections === true || typeof opts.section === "function") {
818
+ sections(file, opts.section);
819
+ }
820
+ return file;
821
+ }
822
+ matter2.engines = engines2;
823
+ matter2.stringify = function(file, data, options2) {
824
+ if (typeof file === "string") file = matter2(file, options2);
825
+ return stringify(file, data, options2);
826
+ };
827
+ matter2.read = function(filepath, options2) {
828
+ const str2 = fs4.readFileSync(filepath, "utf8");
829
+ const file = matter2(str2, options2);
830
+ file.path = filepath;
831
+ return file;
832
+ };
833
+ matter2.test = function(str2, options2) {
834
+ return utils.startsWith(str2, defaults(options2).delimiters[0]);
835
+ };
836
+ matter2.language = function(str2, options2) {
837
+ const opts = defaults(options2);
838
+ const open = opts.delimiters[0];
839
+ if (matter2.test(str2)) {
840
+ str2 = str2.slice(open.length);
841
+ }
842
+ const language = str2.slice(0, str2.search(/\r?\n/));
843
+ return {
844
+ raw: language,
845
+ name: language ? language.trim() : ""
846
+ };
847
+ };
848
+ matter2.cache = {};
849
+ matter2.clearCache = function() {
850
+ matter2.cache = {};
851
+ };
852
+ module2.exports = matter2;
853
+ }
854
+ });
3
855
 
4
856
  // spex-console/bin/hs-init.ts
5
857
  import {
6
858
  copyFileSync,
7
- existsSync,
8
- mkdirSync,
9
- readdirSync,
10
- readFileSync as readFileSync4,
11
- writeFileSync
859
+ existsSync as existsSync5,
860
+ mkdirSync as mkdirSync6,
861
+ readdirSync as readdirSync2,
862
+ readFileSync as readFileSync11,
863
+ statSync,
864
+ writeFileSync as writeFileSync6
12
865
  } from "node:fs";
13
- import { dirname, join as join3, resolve } from "node:path";
14
- import process2 from "node:process";
866
+ import { dirname as dirname6, join as join11, resolve as resolve5 } from "node:path";
867
+ import process4 from "node:process";
15
868
  import { createInterface } from "node:readline/promises";
16
- import { fileURLToPath } from "node:url";
869
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
870
+
871
+ // scripts/lib/shared-path-helpers.mjs
872
+ import { isAbsolute, resolve, sep } from "node:path";
873
+ function resolveCodeRoot({
874
+ explicit,
875
+ packageRoot,
876
+ vaultRoot,
877
+ env = process.env
878
+ } = {}) {
879
+ const fromExplicit = typeof explicit === "string" ? explicit.trim() : "";
880
+ if (fromExplicit) return fromExplicit;
881
+ const fromEnv = typeof env?.HS_PKG_ROOT === "string" ? env.HS_PKG_ROOT.trim() : "";
882
+ if (fromEnv) return fromEnv;
883
+ const fromPackage = typeof packageRoot === "string" ? packageRoot.trim() : "";
884
+ if (fromPackage) return fromPackage;
885
+ const fromVault = typeof vaultRoot === "string" ? vaultRoot.trim() : "";
886
+ return fromVault;
887
+ }
17
888
 
18
889
  // spex-console/src/lib/cli/customer-claude-md.ts
19
890
  function renderCustomerClaudeMd({
@@ -65,15 +936,38 @@ The vault under \`specs/\` is the only persistent queue. File new work via \`hey
65
936
  This CLAUDE.md was generated for **${vaultName}** from HeySpex template version **${templateVersion}** \u2014 it is yours to edit. Unlike HeySpex's own platform contract (which carries HeySpex's internal build tooling), everything above applies to *your* vault and *your* agents. Edit it freely as your project's conventions evolve; a later template update will not overwrite your changes.
66
937
  `;
67
938
  }
68
- function customerClaudeMdHook() {
939
+ function writeProjectContract(ctx, options2 = {}) {
940
+ const content = renderCustomerClaudeMd({
941
+ vaultName: ctx.vaultName,
942
+ templateVersion: ctx.templateVersion
943
+ });
944
+ const results = [];
945
+ for (const file of options2.files ?? ["CLAUDE.md"]) {
946
+ const destPath = ctx.deps.joinPath(ctx.targetDir, file);
947
+ if (!ctx.deps.exists(destPath)) {
948
+ ctx.deps.writeFile(destPath, content);
949
+ ctx.recordWrite?.(destPath);
950
+ results.push({ file, action: "created", path: destPath });
951
+ continue;
952
+ }
953
+ if (ctx.deps.readFile?.(destPath) === content) {
954
+ results.push({ file, action: "unchanged" });
955
+ continue;
956
+ }
957
+ const templatePath = ctx.deps.joinPath(ctx.targetDir, `${file}.template`);
958
+ if (ctx.deps.readFile?.(templatePath) !== content) {
959
+ ctx.deps.writeFile(templatePath, content);
960
+ ctx.recordWrite?.(templatePath);
961
+ }
962
+ const hint = `${file} already exists - left untouched. Generated contract written to ${file}.template; compare with: diff ${file} ${file}.template`;
963
+ options2.notify?.(hint);
964
+ results.push({ file, action: "template", path: templatePath, hint });
965
+ }
966
+ return results;
967
+ }
968
+ function customerClaudeMdHook(notify) {
69
969
  return (ctx) => {
70
- const content = renderCustomerClaudeMd({
71
- vaultName: ctx.vaultName,
72
- templateVersion: ctx.templateVersion
73
- });
74
- const destPath = ctx.deps.joinPath(ctx.targetDir, "CLAUDE.md");
75
- ctx.deps.writeFile(destPath, content);
76
- ctx.recordWrite(destPath);
970
+ writeProjectContract(ctx, { notify });
77
971
  };
78
972
  }
79
973
 
@@ -81,69 +975,200 @@ function customerClaudeMdHook() {
81
975
  import { randomUUID } from "node:crypto";
82
976
 
83
977
  // scripts/lib/console-url-resolver.mjs
84
- import { readFileSync } from "node:fs";
85
- import { homedir } from "node:os";
86
- import { join } from "node:path";
87
- var DEFAULT_CONSOLE_URL = "https://app.heyspex.com";
978
+ init_heyspex_home();
979
+ init_dev_mode();
980
+ import { readFileSync as readFileSync3 } from "node:fs";
981
+ import { homedir as homedir4 } from "node:os";
982
+ import { join as join3 } from "node:path";
983
+ var DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
88
984
  function trimTrailingSlash(url) {
89
985
  return url.replace(/\/+$/, "");
90
986
  }
91
- function readCredentialsConsoleUrl({ homeDir = homedir() } = {}) {
987
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
988
+ function isLoopbackConsoleUrl(url) {
989
+ if (typeof url !== "string" || !url) return false;
92
990
  try {
93
- const raw = readFileSync(join(homeDir, ".heyspex", "credentials.yaml"), "utf8");
94
- const match = raw.match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
991
+ return LOOPBACK_HOSTS.has(new URL(url).hostname.toLowerCase());
992
+ } catch {
993
+ return false;
994
+ }
995
+ }
996
+ var staleLoopbackCredentialsWarned = false;
997
+ function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
998
+ if (staleLoopbackCredentialsWarned) return;
999
+ staleLoopbackCredentialsWarned = true;
1000
+ try {
1001
+ log(
1002
+ `[heyspex] ignoring console_url ${credentialsUrl} from ~/.heyspex/credentials.yaml: the resolved reliance is cloud, and a loopback console cannot serve it \u2014 using ${derivedUrl} (set HEYSPEX_CONSOLE_URL to override explicitly, or run \`hs dev mode:local\` to go back to a local console).
1003
+ `
1004
+ );
1005
+ } catch {
1006
+ }
1007
+ }
1008
+ function readCredentialsConsoleUrl({ homeDir = heyspexHome(), readFile = defaultReadFile2 } = {}) {
1009
+ try {
1010
+ const raw = readFile(join3(homeDir, ".heyspex", "credentials.yaml"));
1011
+ const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
95
1012
  const value = match?.[1]?.trim();
96
1013
  return value && value.length > 0 ? trimTrailingSlash(value) : null;
97
1014
  } catch {
98
1015
  return null;
99
1016
  }
100
1017
  }
101
- function readDiscoveryFileConsoleUrl(repoRoot) {
1018
+ function defaultReadFile2(p) {
1019
+ return readFileSync3(p, "utf8");
1020
+ }
1021
+ function readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery = defaultReadFile2 } = {}) {
102
1022
  if (!repoRoot) return null;
103
- for (const name of [
104
- "console-url",
105
- "cockpit-url"
106
- /* legacy-cockpit */
107
- ]) {
108
- try {
109
- const discovery = readFileSync(join(repoRoot, ".heyspex", name), "utf8").trim();
110
- if (discovery.length > 0) return trimTrailingSlash(discovery);
111
- } catch {
112
- }
1023
+ try {
1024
+ const discovery = String(readDiscovery(join3(repoRoot, ".heyspex", "console-url")) ?? "").trim();
1025
+ if (discovery.length > 0) return trimTrailingSlash(discovery);
1026
+ } catch {
113
1027
  }
114
1028
  return null;
115
1029
  }
116
1030
  var ConsoleUrlUnresolvedError = class extends Error {
117
1031
  constructor() {
118
1032
  super(
119
- `resolveConsoleUrl: no HEYSPEX_CONSOLE_URL/HEYSPEX_COCKPIT_URL env, no console_url: in ~/.heyspex/credentials.yaml, and no .heyspex/console-url discovery file \u2014 refusing to silently fall back to the ${DEFAULT_CONSOLE_URL} default in strict mode.`
1033
+ "resolveConsoleUrl: local reliance with no HEYSPEX_CONSOLE_URL env, no console_url: in ~/.heyspex/credentials.yaml, and no .heyspex/console-url discovery file \u2014 refusing to silently fall back to the localhost:3000 default in strict mode."
120
1034
  );
121
1035
  this.name = "ConsoleUrlUnresolvedError";
122
1036
  }
123
1037
  };
1038
+ function resolveModeEnvelope({ env, homeDir }) {
1039
+ try {
1040
+ const resolved = resolveDevMode({ env, homeDir });
1041
+ return {
1042
+ reliance: resolved.reliance === "cloud" ? "cloud" : "local",
1043
+ consoleUrl: resolved.consoleUrl || DEFAULT_CONSOLE_URL
1044
+ };
1045
+ } catch {
1046
+ return { reliance: "local", consoleUrl: DEFAULT_CONSOLE_URL };
1047
+ }
1048
+ }
124
1049
  function resolveConsoleUrl({
125
1050
  repoRoot,
126
1051
  env = process.env,
127
- homeDir = homedir(),
128
- strict = false
1052
+ homeDir = heyspexHome(),
1053
+ strict = false,
1054
+ devMode,
1055
+ log = (line) => process.stderr.write(line),
1056
+ readFile = defaultReadFile2,
1057
+ readDiscovery = defaultReadFile2
129
1058
  } = {}) {
130
- const envUrl = (env.HEYSPEX_CONSOLE_URL ?? env.HEYSPEX_COCKPIT_URL)?.trim();
1059
+ const envUrl = env.HEYSPEX_CONSOLE_URL?.trim();
131
1060
  if (envUrl) return trimTrailingSlash(envUrl);
132
- const credentialsUrl = readCredentialsConsoleUrl({ homeDir });
133
- if (credentialsUrl) return credentialsUrl;
134
- const discoveryUrl = readDiscoveryFileConsoleUrl(repoRoot);
135
- if (discoveryUrl) return discoveryUrl;
136
- if (strict) throw new ConsoleUrlUnresolvedError();
137
- return DEFAULT_CONSOLE_URL;
1061
+ const mode = devMode ?? resolveModeEnvelope({ env, homeDir });
1062
+ const cloud = mode.reliance === "cloud";
1063
+ const derivedUrl = trimTrailingSlash(
1064
+ cloud && isLoopbackConsoleUrl(mode.consoleUrl) ? CLOUD_CONSOLE_URL : mode.consoleUrl || DEFAULT_CONSOLE_URL
1065
+ );
1066
+ const credentialsUrl = readCredentialsConsoleUrl({ homeDir, readFile });
1067
+ if (credentialsUrl) {
1068
+ if (cloud && isLoopbackConsoleUrl(credentialsUrl)) {
1069
+ warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log });
1070
+ } else {
1071
+ return credentialsUrl;
1072
+ }
1073
+ }
1074
+ if (!cloud) {
1075
+ const discoveryUrl = readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery });
1076
+ if (discoveryUrl) return discoveryUrl;
1077
+ if (strict) throw new ConsoleUrlUnresolvedError();
1078
+ }
1079
+ return derivedUrl;
138
1080
  }
139
1081
 
140
1082
  // spex-console/src/lib/cli/control-shared.ts
141
- import { readFileSync as readFileSync2 } from "node:fs";
142
- import { homedir as homedir2 } from "node:os";
143
- import { join as join2 } from "node:path";
1083
+ import path from "node:path";
1084
+
1085
+ // scripts/lib/hs-command.mjs
1086
+ import { existsSync } from "node:fs";
1087
+ import { delimiter, dirname as dirname2, join as join4 } from "node:path";
1088
+ import { fileURLToPath } from "node:url";
1089
+ var HS_BIN_NAME = "hs";
1090
+ var HS_DISPATCHER_PATH = (() => {
1091
+ try {
1092
+ const here = dirname2(fileURLToPath(import.meta.url));
1093
+ return join4(dirname2(dirname2(here)), "bin", "hs.mjs");
1094
+ } catch {
1095
+ return null;
1096
+ }
1097
+ })();
1098
+ function shellQuote(p) {
1099
+ return /[\\\s]/.test(p) ? `"${p}"` : p;
1100
+ }
1101
+ var HS_DISPATCHER_PREFIX = HS_DISPATCHER_PATH ? `node ${shellQuote(HS_DISPATCHER_PATH)}` : HS_BIN_NAME;
1102
+
1103
+ // vendor/heyspex-mcp/console-fetch.mjs
1104
+ init_heyspex_home();
1105
+ init_credentials();
1106
+ import { readFileSync as readFileSync7 } from "node:fs";
1107
+ import { homedir as homedir7 } from "node:os";
1108
+ import { join as join8 } from "node:path";
1109
+
1110
+ // vendor/heyspex-mcp/session-owner.mjs
1111
+ init_heyspex_home();
1112
+ init_credentials();
1113
+ import { readFileSync as readFileSync4 } from "node:fs";
1114
+ import { homedir as homedir5 } from "node:os";
1115
+ import { join as join5 } from "node:path";
1116
+
1117
+ // vendor/heyspex-mcp/vault-less-endpoints.mjs
1118
+ var VAULT_LESS_ENDPOINTS = Object.freeze([
1119
+ "* /api/mcp/session",
1120
+ "* /api/mcp/pairing-codes",
1121
+ "POST /api/vaults",
1122
+ "GET /api/vaults",
1123
+ "POST /api/workspaces",
1124
+ "GET /api/workspaces",
1125
+ "* /api/admin/dev-mode"
1126
+ ]);
1127
+
1128
+ // vendor/heyspex-mcp/write-outbox.mjs
1129
+ import {
1130
+ readdirSync,
1131
+ readFileSync as readFileSync5,
1132
+ writeFileSync as writeFileSync3,
1133
+ mkdirSync as mkdirSync3,
1134
+ existsSync as existsSync2,
1135
+ renameSync as renameSync2,
1136
+ rmSync
1137
+ } from "node:fs";
1138
+ import { basename, join as join6, isAbsolute as isAbsolute2 } from "node:path";
1139
+
1140
+ // vendor/heyspex-mcp/vault-resolve.mjs
1141
+ init_heyspex_home();
1142
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "node:fs";
1143
+ import { homedir as homedir6, tmpdir } from "node:os";
1144
+ import { dirname as dirname3, join as join7, resolve as resolve3 } from "node:path";
1145
+
1146
+ // vendor/heyspex-mcp/console-fetch.mjs
1147
+ var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
1148
+
1149
+ // spex-console/src/lib/cli/control-shared.ts
1150
+ init_credentials();
1151
+
1152
+ // spex-console/src/lib/vault-registry/registry-file.ts
1153
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
1154
+ import { dirname as dirname4, join as join9, resolve as resolve4 } from "node:path";
1155
+
1156
+ // spex-console/src/lib/heyspex-home.ts
1157
+ import { homedir as homedir8 } from "node:os";
1158
+
1159
+ // spex-console/src/lib/cli/control-shared.ts
144
1160
  var EXIT = {
145
1161
  /** Success. */
146
1162
  OK: 0,
1163
+ /**
1164
+ * A read-only CHECK ran correctly and its answer is "no" — `hs connect
1165
+ * --check` on an unconnected repo. Distinct from {@link EXIT.USAGE} so a
1166
+ * script can tell "you typed the command wrong" from "the command worked,
1167
+ * the repo is not connected". Both were `1` until 2026-09-18, which made
1168
+ * `--check` unusable in a conditional: every caller had to parse stdout to
1169
+ * find out which of the two had happened.
1170
+ */
1171
+ CHECK_FAILED: 2,
147
1172
  /** Usage error — unknown subcommand/flag, or a missing required argument. */
148
1173
  USAGE: 1,
149
1174
  /** Claim race lost — the spec is already claimed by another agent (409). */
@@ -165,12 +1190,63 @@ var EXIT = {
165
1190
  * from `USAGE`/`INVALID_ARG` — this is an authorization refusal, not a
166
1191
  * malformed command.
167
1192
  */
168
- FORBIDDEN: 15
1193
+ FORBIDDEN: 15,
1194
+ /**
1195
+ * A CLOUD write refused because no account id could be resolved (no
1196
+ * `HS_ACCOUNT_ID` env var, no `account_id:` in `~/.heyspex/config.yaml`) --
1197
+ * `resolveCloudAccountIdOrRefuse` in `vault-workspace-shared.ts`
1198
+ * ([[HS-YU1S-UAGY-TASK-cloud-writes-refuse-without-a]]). Distinct from
1199
+ * `FORBIDDEN` -- this is "not signed in at all", not "signed in but not
1200
+ * authorized for this override".
1201
+ */
1202
+ SIGN_IN_REQUIRED: 16,
1203
+ /**
1204
+ * Signed in, but the account a repo would be attached to was not confirmed:
1205
+ * a non-interactive install/connect with no matching `--account` / `--yes`,
1206
+ * an `account_id` that disagrees with the bearer's user, or the human
1207
+ * aborting the identity prompt (`install-identity.ts`,
1208
+ * [[HS-HAQI-KODY-DIAG-hs-install-and-hs-connect-bind-a-new]]).
1209
+ */
1210
+ IDENTITY_UNCONFIRMED: 17,
1211
+ /**
1212
+ * The repo this verb runs in is not registered on this machine (no
1213
+ * `~/.heyspex/registry.json` row for its root, or a legacy slug row with no
1214
+ * cloud `vault_id` in its heyspex.yaml), so the call was refused LOCALLY —
1215
+ * nothing was sent. Names `hs connect`
1216
+ * ([[HS-MVG4-WW9R-TASK-mcp-and-hs-name-account-vault-on-every]]).
1217
+ */
1218
+ VAULT_NOT_REGISTERED: 18,
1219
+ /**
1220
+ * The local half of a setup verb (`hs connect`, `hs vault *`, `hs workspace
1221
+ * *`) was written but the CLOUD half did not land -- the console refused
1222
+ * it or was never reached. The verb's receipt names the failed half and the
1223
+ * exact command to re-run
1224
+ * ([[HS-STCY-WW9R-TASK-add-a-shared-receipt-that-reads-back]], invariant 6).
1225
+ * Distinct from {@link EXIT.WRITE_FAILED} so a script can tell "re-run to
1226
+ * sync the cloud" from a generic server failure.
1227
+ */
1228
+ CLOUD_WRITE_FAILED: 19,
1229
+ /**
1230
+ * The repo's committed `heyspex.yaml` records `account: <A>` but the machine
1231
+ * is signed in as another account, so `hs connect` refused and wrote
1232
+ * nothing (marker, registry, cloud). Repair: `hs login --switch-account <A>`
1233
+ * ([[HS-VWUD-WW9R-TASK-record-the-connecting-account-in]], invariant 4).
1234
+ */
1235
+ ACCOUNT_MISMATCH: 20,
1236
+ /**
1237
+ * Both sides of a setup verb's receipt were read back and still DISAGREE
1238
+ * after a short bounded re-read of the cloud side (a briefly stale read is
1239
+ * absorbed first). The write may well have landed; what the verb cannot
1240
+ * claim is that this machine and the console now say the same thing. The
1241
+ * receipt prints the diffs and the command to re-run
1242
+ * ([[HS-FKSR-WW9R-TASK-make-one-hs-connect-run-create-a-cloud]], invariant 6).
1243
+ * Distinct from {@link EXIT.CLOUD_WRITE_FAILED} (a write that did not land).
1244
+ */
1245
+ RECEIPT_DISAGREE: 21
169
1246
  };
170
1247
 
171
1248
  // spex-console/src/lib/cli/init.ts
172
1249
  var HEYSPEX_YAML_SCHEMA_VERSION = 2;
173
- var DEFAULT_CONSOLE_URL2 = resolveConsoleUrl();
174
1250
  function mintVaultId() {
175
1251
  return randomUUID();
176
1252
  }
@@ -181,6 +1257,30 @@ function parseVaultId(content) {
181
1257
  const unquoted = raw.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
182
1258
  return unquoted.length > 0 ? unquoted : null;
183
1259
  }
1260
+ function parseAccountId(content) {
1261
+ const m = content.match(/^account:[ \t]*(.+?)\s*$/m);
1262
+ if (!m) return null;
1263
+ const raw = m[1].replace(/\s+#.*$/, "").trim();
1264
+ const unquoted = raw.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
1265
+ return unquoted.length > 0 ? unquoted : null;
1266
+ }
1267
+ var MARKER_ACCOUNT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.@+-]*$/;
1268
+ function recordMarkerAccount(content, accountId) {
1269
+ if (!MARKER_ACCOUNT_ID_RE.test(accountId)) {
1270
+ throw new Error(`recordMarkerAccount: "${accountId}" is not a valid account id`);
1271
+ }
1272
+ const recorded = parseAccountId(content);
1273
+ if (recorded === accountId) return { action: "unchanged", content };
1274
+ if (recorded !== null) return { action: "conflict", recorded, content };
1275
+ const eol = content.includes("\r\n") ? "\r\n" : "\n";
1276
+ const lines = content.replace(/(\r?\n)+$/, "").split(/\r?\n/);
1277
+ const kept = lines.filter((l) => !/^account:\s*$/.test(l));
1278
+ const vaultIdx = kept.findIndex((l) => /^vault_id:/.test(l));
1279
+ const line = `account: ${accountId}`;
1280
+ if (vaultIdx >= 0) kept.splice(vaultIdx + 1, 0, line);
1281
+ else kept.push(line);
1282
+ return { action: "added", content: `${kept.join(eol)}${eol}` };
1283
+ }
184
1284
  function appendVaultIdLine(content, vaultId) {
185
1285
  const trimmed = content.replace(/\n+$/, "");
186
1286
  return `${trimmed}
@@ -188,21 +1288,22 @@ vault_id: ${vaultId}
188
1288
  `;
189
1289
  }
190
1290
  function parseInitArgs(argv) {
191
- let consoleUrl = (process.env.HEYSPEX_CONSOLE_URL ?? process.env.HEYSPEX_COCKPIT_URL)?.trim() || // legacy-cockpit env
192
- DEFAULT_CONSOLE_URL2;
1291
+ let consoleUrl = resolveConsoleUrl();
193
1292
  let vaultRoot = "specs/";
194
1293
  let force = false;
195
1294
  let json = false;
196
1295
  let customerVaultDir;
197
1296
  let principles;
198
1297
  let vaultName;
199
- let noGit = false;
1298
+ let git = false;
200
1299
  for (let i = 0; i < argv.length; i++) {
201
1300
  const arg = argv[i];
202
1301
  if (arg === "--help" || arg === "-h") return { help: true };
203
1302
  else if (arg === "--json") json = true;
204
1303
  else if (arg === "--force") force = true;
205
- else if (arg === "--no-git") noGit = true;
1304
+ else if (arg === "--git") git = true;
1305
+ else if (arg === "--no-git")
1306
+ git = false;
206
1307
  else if (arg === "--principles" || arg.startsWith("--principles=")) {
207
1308
  const v = arg === "--principles" ? argv[++i] : arg.slice("--principles=".length);
208
1309
  if (v === void 0 || v.trim() === "") {
@@ -215,8 +1316,7 @@ function parseInitArgs(argv) {
215
1316
  return { error: { code: EXIT.INVALID_ARG, message: "--vault-name requires a value" } };
216
1317
  }
217
1318
  vaultName = v.trim();
218
- } else if (arg === "--console-url" || arg.startsWith("--console-url=") || arg === "--cockpit-url" || // legacy-cockpit alias
219
- arg.startsWith("--cockpit-url=")) {
1319
+ } else if (arg === "--console-url" || arg.startsWith("--console-url=")) {
220
1320
  const eq = arg.indexOf("=");
221
1321
  const v = eq === -1 ? argv[++i] : arg.slice(eq + 1);
222
1322
  if (v === void 0 || v.trim() === "") {
@@ -247,17 +1347,36 @@ function parseInitArgs(argv) {
247
1347
  }
248
1348
  }
249
1349
  return {
250
- args: { consoleUrl, vaultRoot, force, json, customerVaultDir, principles, vaultName, noGit }
1350
+ args: { consoleUrl, vaultRoot, force, json, customerVaultDir, principles, vaultName, git }
251
1351
  };
252
1352
  }
1353
+ var HEYSPEX_CONFIG_DIR = ".heyspex";
1354
+ var HEYSPEX_CONFIG_FILE = "config.yaml";
1355
+ function seedConfigYaml(root, readSeed, fs4) {
1356
+ const configDir = fs4.joinPath(root, HEYSPEX_CONFIG_DIR);
1357
+ const configPath = fs4.joinPath(configDir, HEYSPEX_CONFIG_FILE);
1358
+ if (fs4.exists(configPath)) return { status: "kept", path: configPath };
1359
+ if (!readSeed) return { status: "no_seed", path: configPath };
1360
+ let seed;
1361
+ try {
1362
+ seed = readSeed();
1363
+ } catch {
1364
+ seed = null;
1365
+ }
1366
+ if (seed === null) return { status: "no_seed", path: configPath };
1367
+ fs4.mkdirRecursive(configDir);
1368
+ fs4.writeFile(configPath, seed);
1369
+ return { status: "seeded", path: configPath };
1370
+ }
253
1371
  function renderHeyspexYaml(args) {
254
- return [
1372
+ const body = [
255
1373
  `schema_version: ${HEYSPEX_YAML_SCHEMA_VERSION}`,
256
1374
  `console_url: ${args.consoleUrl}`,
257
1375
  `vault_root: ${args.vaultRoot}`,
258
1376
  `vault_id: ${args.vaultId}`,
259
1377
  ""
260
1378
  ].join("\n");
1379
+ return args.accountId ? recordMarkerAccount(body, args.accountId).content : body;
261
1380
  }
262
1381
  function readExistingContent(deps, markerPath) {
263
1382
  if (!deps.readFile) return null;
@@ -272,17 +1391,60 @@ function runInit(args, deps) {
272
1391
  const exists = deps.fileExists(markerPath);
273
1392
  const existingContent = exists ? readExistingContent(deps, markerPath) : null;
274
1393
  const existingVaultId = existingContent ? parseVaultId(existingContent) : null;
1394
+ if (deps.mkdirRecursive) {
1395
+ const specsPath = deps.joinPath(deps.repoRoot, args.vaultRoot);
1396
+ try {
1397
+ deps.mkdirRecursive(specsPath);
1398
+ } catch (err) {
1399
+ const message = err instanceof Error ? err.message : String(err);
1400
+ return {
1401
+ code: EXIT.WRITE_FAILED,
1402
+ message: `failed to create ${specsPath}: ${message}`,
1403
+ payload: { ok: false, error: "write_failed", path: specsPath, message },
1404
+ isError: true
1405
+ };
1406
+ }
1407
+ }
1408
+ let configSeed = null;
1409
+ const mkdirRecursive = deps.mkdirRecursive;
1410
+ if (mkdirRecursive) {
1411
+ const readConfigSeed2 = deps.readConfigSeed;
1412
+ try {
1413
+ configSeed = seedConfigYaml(
1414
+ deps.repoRoot,
1415
+ readConfigSeed2 ? () => readConfigSeed2.call(deps) : void 0,
1416
+ {
1417
+ exists: (p) => deps.fileExists(p),
1418
+ mkdirRecursive: (p) => mkdirRecursive.call(deps, p),
1419
+ writeFile: (p, c) => deps.writeFile(p, c),
1420
+ joinPath: (...s) => deps.joinPath(...s)
1421
+ }
1422
+ );
1423
+ } catch (err) {
1424
+ const message = err instanceof Error ? err.message : String(err);
1425
+ const configPath = deps.joinPath(deps.repoRoot, HEYSPEX_CONFIG_DIR, HEYSPEX_CONFIG_FILE);
1426
+ return {
1427
+ code: EXIT.WRITE_FAILED,
1428
+ message: `failed to seed ${configPath}: ${message}`,
1429
+ payload: { ok: false, error: "write_failed", path: configPath, message },
1430
+ isError: true
1431
+ };
1432
+ }
1433
+ }
1434
+ const configNote = configSeed?.status === "seeded" ? ` Seeded ${configSeed.path}.` : configSeed?.status === "kept" ? ` Kept existing ${configSeed.path}.` : "";
1435
+ const configPayload = configSeed === null || configSeed.status === "no_seed" ? {} : { config_yaml: configSeed.path, config_yaml_seeded: configSeed.status === "seeded" };
275
1436
  if (exists && !args.force) {
276
1437
  if (existingVaultId) {
277
1438
  return {
278
1439
  code: EXIT.OK,
279
- message: `heyspex.yaml already exists at ${markerPath} \u2014 vault already initialized (use --force to overwrite).`,
1440
+ message: `heyspex.yaml already exists at ${markerPath} \u2014 vault already initialized (use --force to overwrite).${configNote}`,
280
1441
  payload: {
281
1442
  ok: true,
282
1443
  created: false,
283
1444
  path: markerPath,
284
1445
  reason: "already_initialized",
285
- vault_id: existingVaultId
1446
+ vault_id: existingVaultId,
1447
+ ...configPayload
286
1448
  },
287
1449
  isError: false
288
1450
  };
@@ -290,8 +1452,14 @@ function runInit(args, deps) {
290
1452
  if (existingContent === null) {
291
1453
  return {
292
1454
  code: EXIT.OK,
293
- message: `heyspex.yaml already exists at ${markerPath} \u2014 vault already initialized (use --force to overwrite).`,
294
- payload: { ok: true, created: false, path: markerPath, reason: "already_initialized" },
1455
+ message: `heyspex.yaml already exists at ${markerPath} \u2014 vault already initialized (use --force to overwrite).${configNote}`,
1456
+ payload: {
1457
+ ok: true,
1458
+ created: false,
1459
+ path: markerPath,
1460
+ reason: "already_initialized",
1461
+ ...configPayload
1462
+ },
295
1463
  isError: false
296
1464
  };
297
1465
  }
@@ -310,19 +1478,21 @@ function runInit(args, deps) {
310
1478
  }
311
1479
  return {
312
1480
  code: EXIT.OK,
313
- message: `Added vault_id to existing heyspex.yaml at ${markerPath} (schema_version unchanged).`,
1481
+ message: `Added vault_id to existing heyspex.yaml at ${markerPath} (schema_version unchanged).${configNote}`,
314
1482
  payload: {
315
1483
  ok: true,
316
1484
  created: false,
317
1485
  backfilled: true,
318
1486
  path: markerPath,
319
- vault_id: mintedId
1487
+ vault_id: mintedId,
1488
+ ...configPayload
320
1489
  },
321
1490
  isError: false
322
1491
  };
323
1492
  }
324
1493
  const vaultId = existingVaultId ?? args.adoptedVaultId ?? mintVaultId();
325
- const content = renderHeyspexYaml({ ...args, vaultId });
1494
+ const accountId = null;
1495
+ const content = renderHeyspexYaml({ ...args, vaultId, accountId });
326
1496
  try {
327
1497
  deps.writeFile(markerPath, content);
328
1498
  } catch (err) {
@@ -336,7 +1506,7 @@ function runInit(args, deps) {
336
1506
  }
337
1507
  return {
338
1508
  code: EXIT.OK,
339
- message: `${exists ? "Overwrote" : "Wrote"} ${markerPath} (schema_version ${HEYSPEX_YAML_SCHEMA_VERSION}).`,
1509
+ message: `${exists ? "Overwrote" : "Wrote"} ${markerPath} (schema_version ${HEYSPEX_YAML_SCHEMA_VERSION}).${configNote}`,
340
1510
  payload: {
341
1511
  ok: true,
342
1512
  created: true,
@@ -345,7 +1515,9 @@ function runInit(args, deps) {
345
1515
  schema_version: HEYSPEX_YAML_SCHEMA_VERSION,
346
1516
  console_url: args.consoleUrl,
347
1517
  vault_root: args.vaultRoot,
348
- vault_id: vaultId
1518
+ vault_id: vaultId,
1519
+ ...accountId ? { account: accountId } : {},
1520
+ ...configPayload
349
1521
  },
350
1522
  isError: false
351
1523
  };
@@ -354,27 +1526,38 @@ var HELP = [
354
1526
  "Usage:",
355
1527
  " hs init [--console-url <url>] [--vault-root <path>] [--force] [--json]",
356
1528
  " hs init --customer-vault <dir> [--console-url <url>] [--principles <all|none|a,b>]",
357
- " [--vault-name <name>] [--no-git] [--json]",
1529
+ " [--vault-name <name>] [--git] [--json]",
358
1530
  "",
359
1531
  "Write the top-level heyspex.yaml vault marker (schema_version, console_url,",
360
- "vault_root, vault_id) \u2014 the committed file the console / web companion read",
1532
+ "vault_root, vault_id; `hs connect` also records `account:`, the account id the",
1533
+ "repo was connected under) \u2014 the committed file the console / web companion read",
361
1534
  "to bootstrap a vault. Idempotent: an existing marker is left untouched unless",
362
1535
  "--force, EXCEPT a missing vault_id is backfilled in place either way (backward",
363
1536
  "compatible \u2014 no schema_version bump). vault_id is reused exactly when already",
364
1537
  "present; a fresh one is minted only when absent.",
365
- "Does not modify .heyspex/config.yaml or its templates (ADR-0009).",
1538
+ "Also scaffolds the vault_root folder and seeds .heyspex/config.yaml (the",
1539
+ "HeySpex workflow settings, ADR-0009) from the shipped template",
1540
+ "(vendor/heyspex-mcp/config-seed.yaml) when that file is absent. An existing",
1541
+ ".heyspex/config.yaml is never rewritten \u2014 not even with --force; change its",
1542
+ "values with `hs settings set <key> <value>`.",
366
1543
  "",
367
1544
  "--customer-vault <dir> scaffolds a REAL customer vault at <dir> instead: the",
368
- "specs/ folder tree, heyspex.yaml, and every row of the template shipping",
369
- "manifest (spex-console/src/lib/cli/template-manifest.ts) \u2014 universal docs",
1545
+ "specs/ folder tree, heyspex.yaml, the seeded .heyspex/config.yaml, and every",
1546
+ "row of the template shipping manifest",
1547
+ "(spex-console/src/lib/cli/template-manifest.ts) \u2014 universal docs",
370
1548
  "copied verbatim, template-shape docs written as author-prompt skeletons \u2014",
371
- "plus a generated CLAUDE.md, the kept-principles index, and (unless --no-git)",
372
- "one initial `git init` + commit pinning the template version. Refuses a",
373
- "non-empty <dir> with a plain error. See init.customer-vault.ts.",
1549
+ "plus a generated CLAUDE.md, the kept-principles index, and the template_version",
1550
+ "pin; with --git, one initial `git init` + commit. Rerunnable: a <dir> that already",
1551
+ "holds heyspex.yaml and an empty/placeholder-only specs/ is accepted, existing",
1552
+ "files are never overwritten (an existing CLAUDE.md gets a CLAUDE.md.template",
1553
+ "beside it), and the result lists each manifest row as created or kept. Refuses a",
1554
+ "non-empty <dir> without heyspex.yaml, or a specs/ holding managed specs.",
1555
+ "See init.customer-vault.ts.",
374
1556
  "",
375
1557
  "Flags:",
376
- " --console-url <url> console_url value (default: $HEYSPEX_CONSOLE_URL or",
377
- " https://console.heyspex.local).",
1558
+ " --console-url <url> console_url value (default: the mode-aware console --",
1559
+ " $HEYSPEX_CONSOLE_URL, else the signed-in console, else",
1560
+ " https://app.heyspex.com in prod mode).",
378
1561
  " --vault-root <path> vault_root value (default: specs/).",
379
1562
  " --customer-vault <dir> Scaffold a new customer vault at <dir> (see above).",
380
1563
  " --principles <all|none|a,b> Which universal principle docs to ship",
@@ -382,8 +1565,9 @@ var HELP = [
382
1565
  " keep/skip prompt per principle. Omitted + no TTY: all.",
383
1566
  " --vault-name <name> Display name for the scaffolded vault (--customer-vault",
384
1567
  " only; default: <dir>'s basename).",
385
- " --no-git Skip git init + the initial commit (--customer-vault only).",
386
- " --force Overwrite an existing heyspex.yaml.",
1568
+ " --git Also run git init + one initial commit (--customer-vault only;",
1569
+ " default: no git). --no-git is accepted as a no-op.",
1570
+ " --force Overwrite an existing heyspex.yaml (never .heyspex/config.yaml).",
387
1571
  " --json Emit a machine-readable JSON result.",
388
1572
  " --help, -h Show this help.",
389
1573
  "",
@@ -391,7 +1575,7 @@ var HELP = [
391
1575
  ].join("\n");
392
1576
 
393
1577
  // spex-console/src/lib/cli/init.customer-vault.ts
394
- import { basename } from "node:path";
1578
+ import { basename as basename2 } from "node:path";
395
1579
 
396
1580
  // spex-console/src/lib/cli/template-manifest.sources.mjs
397
1581
  var UNIVERSAL_TEMPLATE_ROWS = [
@@ -407,14 +1591,69 @@ var UNIVERSAL_TEMPLATE_ROWS = [
407
1591
  {
408
1592
  platformSource: "specs/protocols/memory-protocol.md",
409
1593
  dest: "specs/protocols/memory-protocol.md"
1594
+ },
1595
+ {
1596
+ // ~455 lines of HS-REV / MVP-grain / guardrail detail that cannot be
1597
+ // curated cleanly: ships a template-shape abridgement instead.
1598
+ platformSource: "specs/protocols/session-close-protocol.md",
1599
+ abridgedSource: "spex-console/templates/abridged/specs/protocols/session-close-protocol.md",
1600
+ dest: "specs/protocols/session-close-protocol.md"
1601
+ },
1602
+ {
1603
+ platformSource: "specs/protocols/spec-summary-authoring.md",
1604
+ dest: "specs/protocols/spec-summary-authoring.md",
1605
+ dropSections: ["Keeping the code snapshot in sync", "Change log"],
1606
+ dropLinePatterns: ["vendor/heyspex-mcp/summary-contract\\.mjs"]
1607
+ },
1608
+ {
1609
+ platformSource: "specs/protocols/destructive-action-checklist.md",
1610
+ dest: "specs/protocols/destructive-action-checklist.md",
1611
+ dropSections: ["Change log"],
1612
+ dropLinePatterns: ["spex-console/scripts/validate-destructive-actions\\.mjs"],
1613
+ replacements: [
1614
+ [
1615
+ "The 2026-05-22 idea-discard incident showed that strong UX *principles*\n([[errors-destructive-recovery]], [[reversible-user-actions]]) are not\nenough",
1616
+ "A past idea-discard data-loss incident showed that strong UX *principles*\n(recoverable destructive actions, reversible user actions) are not\nenough"
1617
+ ],
1618
+ [
1619
+ "Today: linked from `CLAUDE.md` Console pre-ship checklist; agents must\nwalk it",
1620
+ "Today: agents must\nwalk it"
1621
+ ],
1622
+ [
1623
+ '- [ ] If a *new* destructive surface is introduced, append it to the\n "Where this is applied" table in\n [[destructive-action-needs-explicit-button-and-undo]].',
1624
+ "- [ ] If a *new* destructive surface is introduced, record it in a vault memory."
1625
+ ],
1626
+ [
1627
+ "- [ ] Read [[errors-destructive-recovery]] and\n [[destructive-action-needs-explicit-button-and-undo]] in full.",
1628
+ "- [ ] Read any vault memories on destructive-action recovery and Undo."
1629
+ ],
1630
+ [
1631
+ " (Not covered here; see existing dirty-editor\n pattern in `specs-surface.tsx`.)",
1632
+ " (Not covered here.)"
1633
+ ]
1634
+ ]
410
1635
  }
411
1636
  ];
412
1637
 
413
1638
  // dist/cli/._template-manifest-sources.cli-shim.mjs
414
1639
  var CURATED_TEMPLATE_ROOT = "dist/cli-templates/customer-vault";
415
1640
 
1641
+ // scripts/lib/template-pin.mjs
1642
+ var TEMPLATE_VERSION = "1.1.0";
1643
+ var TEMPLATE_CORE_DESTS = Object.freeze([
1644
+ "specs/north-star.md",
1645
+ "specs/principles-index.md",
1646
+ "specs/roadmap.md",
1647
+ "specs/entity-model.md",
1648
+ "specs/protocols/canonical-frontmatter-templates.md",
1649
+ "specs/protocols/spec-hierarchy.md",
1650
+ "specs/protocols/memory-protocol.md",
1651
+ "specs/protocols/session-close-protocol.md",
1652
+ "specs/protocols/spec-summary-authoring.md",
1653
+ "specs/protocols/destructive-action-checklist.md"
1654
+ ]);
1655
+
416
1656
  // spex-console/src/lib/cli/template-manifest.ts
417
- var TEMPLATE_VERSION = "1.0.0";
418
1657
  var TODO_NORTH_STAR = `---
419
1658
  id: HS-L1
420
1659
  name: TODO \u2014 your product name
@@ -804,7 +2043,7 @@ var TEMPLATE_MANIFEST = [
804
2043
  ];
805
2044
 
806
2045
  // spex-console/src/lib/cli/init.customer-vault.ts
807
- var DEFAULT_CONSOLE_URL3 = "https://console.heyspex.local";
2046
+ var DEFAULT_CONSOLE_URL2 = "https://console.heyspex.local";
808
2047
  var SCAFFOLD_FOLDERS = [
809
2048
  "tasks",
810
2049
  "epics",
@@ -821,23 +2060,63 @@ var SCAFFOLD_FOLDERS = [
821
2060
  "checks",
822
2061
  "events"
823
2062
  ];
2063
+ var PLACEHOLDER_FILES = /* @__PURE__ */ new Set([".gitkeep"]);
2064
+ function isDir(deps, path5, name) {
2065
+ if (deps.isDirectory) return deps.isDirectory(path5);
2066
+ try {
2067
+ const kids = deps.readdir(path5);
2068
+ return kids.length > 0 || !name.includes(".");
2069
+ } catch {
2070
+ return false;
2071
+ }
2072
+ }
2073
+ function findManagedSpecs(deps, targetDir, manifestDests) {
2074
+ const out = [];
2075
+ const walk = (abs, rel) => {
2076
+ for (const name of deps.readdir(abs)) {
2077
+ const childAbs = deps.joinPath(abs, name);
2078
+ const childRel = `${rel}/${name}`;
2079
+ if (isDir(deps, childAbs, name)) walk(childAbs, childRel);
2080
+ else if (!PLACEHOLDER_FILES.has(name) && name.endsWith(".md") && !manifestDests.has(childRel))
2081
+ out.push(childRel);
2082
+ }
2083
+ };
2084
+ const specsRoot = deps.joinPath(targetDir, "specs");
2085
+ if (deps.exists(specsRoot)) walk(specsRoot, "specs");
2086
+ return out;
2087
+ }
824
2088
  function defaultVaultName(targetDir) {
825
- return basename(targetDir) || targetDir;
2089
+ return basename2(targetDir) || targetDir;
826
2090
  }
827
- function runCustomerVaultInit(args, deps, options = {}) {
2091
+ function runCustomerVaultInit(args, deps, options2 = {}) {
828
2092
  const { targetDir } = args;
829
2093
  const written = [];
2094
+ const filter = options2.manifestFilter ?? (() => true);
2095
+ const rows = TEMPLATE_MANIFEST.filter(filter);
830
2096
  if (deps.exists(targetDir)) {
831
- const entries = deps.readdir(targetDir);
832
- if (entries.length > 0) {
2097
+ const markerPresent = deps.exists(deps.joinPath(targetDir, "heyspex.yaml"));
2098
+ if (!markerPresent && deps.readdir(targetDir).length > 0) {
833
2099
  return {
834
2100
  code: EXIT.INVALID_ARG,
835
- message: `${targetDir} is not empty \u2014 customer-vault init refuses to write into an existing, non-empty directory.`,
2101
+ message: `${targetDir} is not empty \u2014 customer-vault init refuses to write into an existing, non-empty directory that has no heyspex.yaml.`,
836
2102
  payload: { ok: false, error: "target_not_empty", path: targetDir },
837
2103
  isError: true,
838
2104
  written: []
839
2105
  };
840
2106
  }
2107
+ if (markerPresent) {
2108
+ const manifestDests = new Set(TEMPLATE_MANIFEST.map((r) => r.dest));
2109
+ const managed = findManagedSpecs(deps, targetDir, manifestDests);
2110
+ if (managed.length > 0) {
2111
+ return {
2112
+ code: EXIT.INVALID_ARG,
2113
+ message: `${targetDir}/specs already holds managed spec(s) (${managed.slice(0, 3).join(", ")}${managed.length > 3 ? ", ..." : ""}) \u2014 customer-vault init refuses to write over an existing vault.`,
2114
+ payload: { ok: false, error: "specs_not_empty", path: targetDir, specs: managed },
2115
+ isError: true,
2116
+ written: []
2117
+ };
2118
+ }
2119
+ }
841
2120
  } else {
842
2121
  deps.mkdirRecursive(targetDir);
843
2122
  }
@@ -846,19 +2125,37 @@ function runCustomerVaultInit(args, deps, options = {}) {
846
2125
  for (const folder of SCAFFOLD_FOLDERS) {
847
2126
  deps.mkdirRecursive(deps.joinPath(specsRoot, folder));
848
2127
  }
849
- const vaultId = mintVaultId();
850
- const yamlContent = renderHeyspexYaml({
851
- consoleUrl: options.consoleUrl ?? DEFAULT_CONSOLE_URL3,
852
- vaultRoot: "specs/",
853
- vaultId
854
- });
855
2128
  const heyspexYamlPath = deps.joinPath(targetDir, "heyspex.yaml");
856
- deps.writeFile(heyspexYamlPath, yamlContent);
857
- written.push(heyspexYamlPath);
858
- const filter = options.manifestFilter ?? (() => true);
859
- const rows = TEMPLATE_MANIFEST.filter(filter);
2129
+ let vaultId;
2130
+ if (deps.exists(heyspexYamlPath)) {
2131
+ const existingYaml = deps.readFile?.(heyspexYamlPath) ?? null;
2132
+ vaultId = (existingYaml ? parseVaultId(existingYaml) : null) ?? "";
2133
+ } else {
2134
+ vaultId = mintVaultId();
2135
+ deps.writeFile(
2136
+ heyspexYamlPath,
2137
+ renderHeyspexYaml({
2138
+ consoleUrl: options2.consoleUrl ?? DEFAULT_CONSOLE_URL2,
2139
+ vaultRoot: "specs/",
2140
+ vaultId
2141
+ })
2142
+ );
2143
+ written.push(heyspexYamlPath);
2144
+ }
2145
+ const readConfigSeed2 = deps.readConfigSeed;
2146
+ const configSeed = seedConfigYaml(
2147
+ targetDir,
2148
+ readConfigSeed2 ? () => readConfigSeed2.call(deps) : void 0,
2149
+ deps
2150
+ );
2151
+ if (configSeed.status === "seeded") written.push(configSeed.path);
2152
+ const rowResults = [];
860
2153
  for (const row of rows) {
861
2154
  const destAbs = deps.joinPath(targetDir, ...row.dest.split("/"));
2155
+ if (deps.exists(destAbs)) {
2156
+ rowResults.push({ dest: row.dest, action: "kept" });
2157
+ continue;
2158
+ }
862
2159
  if (row.classification === "universal-template") {
863
2160
  const srcAbs = deps.joinPath(deps.platformRoot, ...row.source.split("/"));
864
2161
  deps.copyFile(srcAbs, destAbs);
@@ -871,29 +2168,36 @@ function runCustomerVaultInit(args, deps, options = {}) {
871
2168
  deps.writeFile(destAbs, row.content);
872
2169
  }
873
2170
  written.push(destAbs);
2171
+ rowResults.push({ dest: row.dest, action: "created" });
874
2172
  }
875
- if (options.afterScaffold && options.afterScaffold.length > 0) {
2173
+ if (options2.afterScaffold && options2.afterScaffold.length > 0) {
876
2174
  const ctx = {
877
2175
  targetDir,
878
- vaultName: options.vaultName ?? defaultVaultName(targetDir),
2176
+ vaultName: options2.vaultName ?? defaultVaultName(targetDir),
879
2177
  templateVersion: TEMPLATE_VERSION,
880
2178
  manifestRows: rows,
2179
+ rows: rowResults,
881
2180
  deps,
882
- recordWrite: (path) => {
883
- written.push(path);
2181
+ recordWrite: (path5) => {
2182
+ written.push(path5);
884
2183
  }
885
2184
  };
886
- for (const hook of options.afterScaffold) hook(ctx);
2185
+ for (const hook of options2.afterScaffold) hook(ctx);
887
2186
  }
2187
+ const createdCount = rowResults.filter((r) => r.action === "created").length;
2188
+ const keptCount = rowResults.length - createdCount;
888
2189
  return {
889
2190
  code: EXIT.OK,
890
- message: `Initialized customer vault at ${targetDir} (${written.length} files written, template v${TEMPLATE_VERSION}).`,
2191
+ message: `Initialized customer vault at ${targetDir} (${written.length} files written, ${createdCount} manifest rows created, ${keptCount} kept, template v${TEMPLATE_VERSION}).`,
891
2192
  payload: {
892
2193
  ok: true,
893
2194
  targetDir,
894
2195
  templateVersion: TEMPLATE_VERSION,
895
2196
  vaultId,
896
- written
2197
+ written,
2198
+ rows: rowResults,
2199
+ created: createdCount,
2200
+ kept: keptCount
897
2201
  },
898
2202
  isError: false,
899
2203
  written
@@ -902,7 +2206,6 @@ function runCustomerVaultInit(args, deps, options = {}) {
902
2206
 
903
2207
  // spex-console/src/lib/cli/init-git-finalize.ts
904
2208
  import { spawnSync } from "node:child_process";
905
- import { readFileSync as readFileSync3 } from "node:fs";
906
2209
  var nodeGitRunner = {
907
2210
  run(args, cwd) {
908
2211
  const result = spawnSync("git", args, { cwd, encoding: "utf8" });
@@ -929,14 +2232,19 @@ function ensureTemplateVersionLine(content, version) {
929
2232
  template_version: ${version}
930
2233
  `;
931
2234
  }
2235
+ function templatePinHook() {
2236
+ return (ctx) => {
2237
+ const yamlPath = ctx.deps.joinPath(ctx.targetDir, "heyspex.yaml");
2238
+ const existingYaml = ctx.deps.readFile?.(yamlPath);
2239
+ if (existingYaml === null || existingYaml === void 0) return;
2240
+ if (readTemplateVersion(existingYaml) === ctx.templateVersion) return;
2241
+ ctx.deps.writeFile(yamlPath, ensureTemplateVersionLine(existingYaml, ctx.templateVersion));
2242
+ ctx.recordWrite(yamlPath);
2243
+ };
2244
+ }
932
2245
  function gitFinalizeHook(runner, notify) {
933
2246
  return (ctx) => {
934
2247
  const target = ctx.targetDir;
935
- const yamlPath = ctx.deps.joinPath(target, "heyspex.yaml");
936
- const existingYaml = readFileSync3(yamlPath, "utf8");
937
- const updatedYaml = ensureTemplateVersionLine(existingYaml, ctx.templateVersion);
938
- ctx.deps.writeFile(yamlPath, updatedYaml);
939
- ctx.recordWrite(yamlPath);
940
2248
  const probe = runner.run(["--version"], target);
941
2249
  if (!probe.ok) {
942
2250
  notify("git not found - vault left uninitialized, run git init yourself");
@@ -962,15 +2270,1102 @@ function gitFinalizeHook(runner, notify) {
962
2270
  };
963
2271
  }
964
2272
 
2273
+ // spex-console/src/lib/cli/install.ts
2274
+ import { isAbsolute as isAbsolute3, resolve as resolveNodePath2 } from "node:path";
2275
+
2276
+ // scripts/install-cursor-hooks.mjs
2277
+ import fs2 from "node:fs";
2278
+ import path3 from "node:path";
2279
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
2280
+
2281
+ // scripts/cursor-hooks/manifest.mjs
2282
+ var SHIM = "scripts/cursor-hooks/_run.mjs";
2283
+ var DEFAULT_TIMEOUT_SECONDS = 5;
2284
+ var HOOKS = [
2285
+ // ── Session start ───────────────────────────────────────────────────────
2286
+ // Cursor's sessionStart honors `additional_context`, so these injections
2287
+ // land in context exactly as they do in Claude Code.
2288
+ { event: "sessionStart", hook: "wip-checkpoint", why: "Surface the previous session WIP handoff before work resumes." },
2289
+ { event: "sessionStart", hook: "inject-dev-server-awareness", why: "Once-per-session reminder that dev-server status is a tool call, not a guess." },
2290
+ { event: "sessionStart", hook: "inject-coo-seat", why: "Inject the dev-factory COO seat mandate when the COO window is open." },
2291
+ { event: "sessionStart", hook: "inject-doctrine", why: "Inject the baseline SpexCode Doctrine edition at session start." },
2292
+ { event: "sessionStart", hook: "coo-guard-check", why: "Warn at session start when a breakdown has not been delegated." },
2293
+ { event: "sessionStart", hook: "check-auto-memory-budget", why: "Self-heal and budget-check the auto-memory index." },
2294
+ { event: "sessionStart", hook: "check-claude-md-budget", why: "Advisory budget check on the always-loaded agent contract." },
2295
+ { event: "sessionStart", hook: "check-mcp-freshness-hook", why: "Warn when the running MCP server is stale versus vendor." },
2296
+ { event: "sessionStart", hook: "git-reflog-audit", why: "Session-start git audit (stash/reset/rebase witness)." },
2297
+ { event: "sessionStart", hook: "vault-mirror-catchup", why: "Cloud write mode: detached hs pull --apply closes a stale disk mirror (HS-XJFQ)." },
2298
+ // ── Compaction ──────────────────────────────────────────────────────────
2299
+ // Claude re-runs SessionStart(source: compact) after compaction; Cursor fires
2300
+ // preCompact BEFORE it, so the shim relays this one as SessionStart(compact)
2301
+ // to keep wip-checkpoint's "rollover, not fresh start" framing.
2302
+ { event: "preCompact", hook: "wip-checkpoint", claudeEvent: "SessionStart", why: "Carry the WIP handoff across a context compaction." },
2303
+ // ── Every user turn ─────────────────────────────────────────────────────
2304
+ // beforeSubmitPrompt is Cursor's UserPromptSubmit. It honors `user_message`
2305
+ // but not `additional_context`, so these three surface in the transcript
2306
+ // rather than as invisible context — the routing still reaches the agent,
2307
+ // and the user can see why it fired.
2308
+ { event: "beforeSubmitPrompt", hook: "preflight-reminder", why: "Name the required procedure before a filing/destructive/protocol turn." },
2309
+ { event: "beforeSubmitPrompt", hook: "intent-classifier-trigger", why: "Route capture / research / settings / defer intents to the right skill." },
2310
+ { event: "beforeSubmitPrompt", hook: "inject-communication-style", why: "Re-assert the plain-language communication rules each turn." },
2311
+ { event: "beforeSubmitPrompt", hook: "inject-vault-context", timeout: 10, why: "Lexical vault retrieval for the prompt (surfaced as a user message)." },
2312
+ // ── Before every tool call (any tool) ───────────────────────────────────
2313
+ { event: "preToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PreToolUse"], why: "Claim liveness heartbeat to the console." },
2314
+ { event: "preToolUse", matcher: ".*", hook: "coo-denylist-gate", why: "Hard deny the COO-window deny list." },
2315
+ { event: "preToolUse", matcher: ".*", hook: "log-session-activity", args: ["--event=PreToolUse"], why: "Append agent.tool_called rows to the session log." },
2316
+ // ── Before a file write / delete ─────────────────────────────────────────
2317
+ // Cursor's `Write` covers create AND patch (Claude's Write|Edit|MultiEdit);
2318
+ // the shim re-derives which one from the input shape. `Delete` is included on
2319
+ // the vault guard because removing a profile file is the loss it exists to
2320
+ // prevent, and Claude had no separate delete tool to list.
2321
+ { event: "preToolUse", matcher: "Write|Delete", hook: "env-vault-guard", why: "Block destructive writes to the encrypted env vault (ALWAYS ON)." },
2322
+ { event: "preToolUse", matcher: "Write", hook: "vault-write-tier-guard", why: "Deny disk writes to vault specs in cloud write mode (HS-FCNH)." },
2323
+ { event: "preToolUse", matcher: "Write", hook: "settings-write-gate", why: "Require approval before .heyspex/config.yaml changes." },
2324
+ { event: "preToolUse", matcher: "Write", hook: "enforce-vault-naming", why: "Block vault filenames that violate the naming registry." },
2325
+ { event: "preToolUse", matcher: "Write", hook: "guard-auto-memory-writes", why: "Block new topic files in the auto-memory directory." },
2326
+ { event: "preToolUse", matcher: "Write", hook: "preflight-reminder", why: "Preflight reminder on the edit path itself." },
2327
+ { event: "preToolUse", matcher: "Write", hook: "claim-before-code-gate", why: "Deny the first code edit after plan exit with no claim." },
2328
+ { event: "preToolUse", matcher: "Write", hook: "directive-obligation-gate", why: "Deny product-code writes while a capture_on_idea obligation is unmet, and (claim-before-build) with no claim/capture (HS-20SW)." },
2329
+ // ── Before a shell command ──────────────────────────────────────────────
2330
+ // beforeShellExecution, not preToolUse/Shell: it is the only event guaranteed
2331
+ // to carry the command string, and Cursor honors allow/deny/ask on it.
2332
+ { event: "beforeShellExecution", hook: "env-vault-guard", why: "Block shell writes/exfiltration targeting the env vault (ALWAYS ON)." },
2333
+ { event: "beforeShellExecution", hook: "vault-write-tier-guard", why: "Deny shell writes to vault specs in cloud write mode (HS-FCNH)." },
2334
+ { event: "beforeShellExecution", hook: "settings-write-gate", why: "Catch shell writes to .heyspex/config.yaml." },
2335
+ { event: "beforeShellExecution", hook: "enforce-vault-naming", why: "Catch vault files created via the shell." },
2336
+ { event: "beforeShellExecution", hook: "guard-auto-memory-writes", why: "Catch auto-memory topic files created via the shell." },
2337
+ { event: "beforeShellExecution", hook: "preflight-reminder", why: "Preflight reminder on rm -rf / force-push shaped commands." },
2338
+ { event: "beforeShellExecution", hook: "dev-server-task-reminder", timeout: 10, why: "Dev-server start/stop hard gate plus DB-safety reminder." },
2339
+ { event: "beforeShellExecution", hook: "git-stash-gate", timeout: 10, why: "Always ask before a mutating git stash." },
2340
+ { event: "beforeShellExecution", hook: "git-identity-guard", why: "Agents never write git identity (owner ruling 2026-09-19, ALWAYS ON)." },
2341
+ { event: "beforeShellExecution", hook: "capture-execution-gate", why: "Block dev-factory research/paid-model/routing shell commands past an ungrounded capture (HS-KNJ8, platform)." },
2342
+ { event: "beforeShellExecution", hook: "directive-obligation-gate", why: "Deny build commands and product-code shell writes while a capture obligation is unmet (HS-20SW)." },
2343
+ // ── Before specific MCP tools ───────────────────────────────────────────
2344
+ // Cursor fires preToolUse for MCP tools too (matcher `MCP:<name>`), so these
2345
+ // stay on preToolUse rather than beforeMCPExecution — registering both would
2346
+ // fire each gate twice. Scoping is the shim's `tool` regex because a Cursor
2347
+ // matcher cannot name one MCP tool per entry portably.
2348
+ { event: "preToolUse", matcher: ".*", tool: "heyspex_claim_spec$", hook: "coo-guard-check", why: "Block a claim whose breakdown was never delegated." },
2349
+ { event: "preToolUse", matcher: ".*", tool: "heyspex_file_spec$", hook: "capture-workflow-gate", timeout: 8, why: "Deny filing an implementation spec straight out of an open capture." },
2350
+ { event: "preToolUse", matcher: ".*", tool: "heyspex_settings_set$", hook: "settings-write-gate", why: "Require approval for settings changes made through MCP." },
2351
+ { event: "preToolUse", matcher: ".*", tool: "[Nn]eon__(prepare_database_migration|complete_database_migration|reset_from_parent|delete_branch)$", hook: "dev-server-task-reminder", timeout: 10, why: "DB-safety gate on Neon migration/reset/delete tools." },
2352
+ { event: "preToolUse", matcher: ".*", tool: "heyspex_file_spec$", hook: "diagnose-capture-reminder", why: "Warn when a diagnose is filed with no capture holding the owner words (HS-IK2D)." },
2353
+ { event: "preToolUse", matcher: ".*", tool: "^(Workflow|Skill)$", hook: "research-method-guard", why: "Research method guard on Skill/Workflow launches (HS-EN4F, ALWAYS ON)." },
2354
+ { event: "preToolUse", matcher: ".*", tool: "^(Workflow|Skill)$", hook: "deep-research-cost-gate", why: "Cost confirmation before a paid deep-research harness launch." },
2355
+ { event: "preToolUse", matcher: ".*", tool: "^(WebSearch|WebFetch|web_search|web_fetch)$|heyspex_save_research$", hook: "research-protocol-gate", why: "WebSearch/WebFetch/save_research refused until the research preflight ran (HS-K0ZP, HS-EN4F)." },
2356
+ // ── Before a subagent spawn ─────────────────────────────────────────────
2357
+ // subagentStart over preToolUse/Task: it carries subagent_type and the model,
2358
+ // which is exactly what the routing gate decides on, and it can deny.
2359
+ { event: "subagentStart", hook: "agent-spawn-delegation-gate", why: "Block a build/breakdown/critic subagent on an off-routing model." },
2360
+ { event: "subagentStart", hook: "capture-execution-gate", why: "Block a dev-factory subagent spawn past an ungrounded capture (HS-KNJ8, platform)." },
2361
+ { event: "subagentStart", hook: "directive-obligation-gate", why: "Deny a build subagent while a capture obligation is unmet or the session has no claim/capture (HS-20SW)." },
2362
+ // ── After every tool call ───────────────────────────────────────────────
2363
+ { event: "postToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PostToolUse"], why: "Claim liveness heartbeat after the call resolves." },
2364
+ { event: "postToolUse", matcher: ".*", hook: "log-session-activity", args: ["--event=PostToolUse"], why: "Append the tool result row to the session log." },
2365
+ { event: "postToolUse", matcher: ".*", hook: "directive-obligation-gate", why: "Discharge the capture obligation on a capture write; track claim acquire/release (HS-20SW)." },
2366
+ { event: "postToolUse", matcher: ".*", hook: "close-terminal-on-ship", why: "Close the Launch terminal once the spec ships." },
2367
+ { event: "postToolUse", matcher: "Write", hook: "notify-status-flip", why: "Sound on an in_review status flip." },
2368
+ { event: "postToolUse", matcher: "Write", hook: "scripts/stamp-provenance-on-edit.mjs", why: "Stamp assisted_by provenance on direct spec edits." },
2369
+ // afterFileEdit is a distinct signal, not a duplicate: it reports the edit
2370
+ // that actually landed (with old/new strings) even when the write came from a
2371
+ // path postToolUse did not describe as an edit.
2372
+ { event: "afterFileEdit", matcher: ".*", hook: "close-terminal-on-ship", why: "Ship detection from the concrete file edit." },
2373
+ // ── End of the agent turn ───────────────────────────────────────────────
2374
+ // Cursor's `stop` honors `followup_message`, which is how a blocking Stop
2375
+ // hook re-drives the agent — the same effect as Claude's exit-2 block.
2376
+ { event: "stop", hook: "emit-claim-status", args: ["--event=Stop"], why: "Mark the claim pending-on-response." },
2377
+ { event: "stop", hook: "naughty-list-gate", why: "Re-drive the turn when the final message uses banned jargon." },
2378
+ { event: "stop", hook: "id-display-format-gate", why: "Re-drive the turn when a reply breaks the Naming V3 display-id format." },
2379
+ { event: "stop", hook: "epic-milestone-phrasing-gate", why: "Re-drive the turn when a reply breaks the epic/milestone phrasing rules." },
2380
+ { event: "stop", hook: "git-reflog-audit", why: "Session-close git audit backstop (stash/reset/rebase witness)." },
2381
+ { event: "stop", hook: "enforce-session-sequence", why: "Re-drive the turn when work shipped without record_wip." },
2382
+ { event: "stop", hook: "directive-obligation-gate", why: "Re-drive the turn ONCE (followup_message) when a capture obligation is still unmet (HS-20SW)." },
2383
+ // ── Session end ─────────────────────────────────────────────────────────
2384
+ { event: "sessionEnd", hook: "session-end", why: "Release the claim and notify the console." },
2385
+ { event: "sessionEnd", hook: "close-terminal-on-ship", why: "Final terminal cleanup." }
2386
+ ];
2387
+ var UNPORTABLE = [
2388
+ {
2389
+ hook: "plan-review-gate",
2390
+ claudeEvent: "PreToolUse/ExitPlanMode",
2391
+ reason: "Cursor exposes no ExitPlanMode tool call to hooks.",
2392
+ coveredBy: ".cursor/rules/plan-review-required.mdc (always-on rule)"
2393
+ },
2394
+ {
2395
+ hook: "plan-exit-ingest-gate",
2396
+ claudeEvent: "PreToolUse/ExitPlanMode",
2397
+ reason: "Same missing trigger; it is also what arms claim-before-code-gate.",
2398
+ coveredBy: ".cursor/rules/plan-review-required.mdc + spec-creation-linkage.mdc"
2399
+ }
2400
+ ];
2401
+ function commandFor(entry) {
2402
+ const parts = ["node", SHIM, entry.hook];
2403
+ if (entry.tool) parts.push(`--hs-tool="${entry.tool}"`);
2404
+ if (entry.claudeEvent) parts.push(`--hs-claude-event=${entry.claudeEvent}`);
2405
+ for (const arg of entry.args ?? []) parts.push(arg);
2406
+ return parts.join(" ");
2407
+ }
2408
+ function commandForPlugin(entry) {
2409
+ return commandFor(entry).replace(`node ${SHIM}`, `node "$CURSOR_PROJECT_DIR/${SHIM}"`);
2410
+ }
2411
+ var CUSTOMER_LAUNCHER = ".cursor/heyspex/run-hook.mjs";
2412
+ function commandForCustomer(entry) {
2413
+ return commandFor(entry).replace(`node ${SHIM}`, `node "$CURSOR_PROJECT_DIR/${CUSTOMER_LAUNCHER}"`);
2414
+ }
2415
+ function identityFor(entry) {
2416
+ return `${entry.hook}::${entry.tool ?? ""}::${(entry.args ?? []).join(" ")}`;
2417
+ }
2418
+ function timeoutFor(entry) {
2419
+ return entry.timeout ?? DEFAULT_TIMEOUT_SECONDS;
2420
+ }
2421
+
2422
+ // scripts/lib/customer-kit.mjs
2423
+ import { createHash } from "node:crypto";
2424
+ import fs from "node:fs";
2425
+ import path2 from "node:path";
2426
+ import os2 from "node:os";
2427
+ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
2428
+
2429
+ // scripts/lib/hook-dispositions.mjs
2430
+ var HOSTS = Object.freeze(["claude-code", "cursor", "copilot"]);
2431
+ var DISPOSITIONS = Object.freeze(["mapped", "advisory-degraded", "unportable", "not-shipped"]);
2432
+ var mapped = (note) => Object.freeze({ disposition: "mapped", ...note ? { note } : {} });
2433
+ var degraded = (note) => Object.freeze({ disposition: "advisory-degraded", note });
2434
+ var unportable = (control) => Object.freeze({ disposition: "unportable", control });
2435
+ var notShipped = Object.freeze({ disposition: "not-shipped" });
2436
+ var CC = mapped();
2437
+ var COPILOT_PROMPT_DROPPED = "Copilot drops command-hook output on userPromptSubmitted (no prompt-time context channel); the text moves to sessionStart / .github/copilot-instructions.md.";
2438
+ var CURSOR_PROMPT_USER_MESSAGE = "Cursor beforeSubmitPrompt honors user_message only (no additional_context): the text surfaces in the transcript instead of as invisible context.";
2439
+ var COPILOT_NO_PLAN_TOOL = "MCP preflight refusals (heyspex_claim_spec / heyspex_submit_for_review / heyspex_save_research) + .github/copilot-instructions.md plan rule (state the plan in chat, claim the spec before editing) + HS-20SW directive-obligation-gate claim-before-build deny";
2440
+ var HOOK_DISPOSITIONS = Object.freeze({
2441
+ // ── customer: session start / compaction ──────────────────────────────────
2442
+ "wip-checkpoint": {
2443
+ "claude-code": CC,
2444
+ cursor: mapped("preCompact is relayed as SessionStart(compact)."),
2445
+ copilot: mapped("sessionStart additionalContext is delivered; the preCompact leg writes WIP only (no output channel), re-injected at the next sessionStart.")
2446
+ },
2447
+ "inject-doctrine": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
2448
+ "check-auto-memory-budget": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
2449
+ "check-claude-md-budget": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
2450
+ "git-reflog-audit": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext and agentStop.") },
2451
+ "vault-mirror-catchup": { "claude-code": CC, cursor: mapped(), copilot: mapped("Detached side effect on sessionStart.") },
2452
+ // ── customer: every user turn ─────────────────────────────────────────────
2453
+ "preflight-reminder": {
2454
+ "claude-code": CC,
2455
+ cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
2456
+ copilot: degraded(`${COPILOT_PROMPT_DROPPED} Real enforcement is the MCP preflight refusal.`)
2457
+ },
2458
+ "intent-classifier-trigger": {
2459
+ "claude-code": CC,
2460
+ cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
2461
+ copilot: degraded(
2462
+ `${COPILOT_PROMPT_DROPPED} The hook still runs and arms obligation state; the HS-20SW directive-obligation-gate turns that state into a hard deny.`
2463
+ )
2464
+ },
2465
+ "inject-communication-style": {
2466
+ "claude-code": CC,
2467
+ cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
2468
+ copilot: degraded(COPILOT_PROMPT_DROPPED)
2469
+ },
2470
+ "inject-vault-context": {
2471
+ "claude-code": CC,
2472
+ cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
2473
+ copilot: degraded(`${COPILOT_PROMPT_DROPPED} Per-prompt vault retrieval has no Copilot channel at all.`)
2474
+ },
2475
+ // ── customer: before a tool call (hard gates) ─────────────────────────────
2476
+ "git-identity-guard": { "claude-code": CC, cursor: mapped("beforeShellExecution. Always on."), copilot: mapped("preToolUse on bash/powershell. Always on.") },
2477
+ "vault-write-tier-guard": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/edit/bash/powershell.") },
2478
+ "settings-write-gate": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/edit/bash/powershell and the MCP settings tool.") },
2479
+ "emit-claim-status": { "claude-code": CC, cursor: mapped(), copilot: mapped("Telemetry on preToolUse/postToolUse/agentStop; the cloud agent needs a network allow rule for the console.") },
2480
+ "enforce-vault-naming": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/bash/powershell.") },
2481
+ "guard-auto-memory-writes": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/bash/powershell.") },
2482
+ "directive-obligation-gate": {
2483
+ "claude-code": CC,
2484
+ cursor: mapped(),
2485
+ copilot: mapped("HS-20SW: hard deny on task/create/edit/write-shaped shell while a capture obligation is unmet; the only hard capture-on-idea guarantee on Copilot.")
2486
+ },
2487
+ "claim-before-code-gate": {
2488
+ "claude-code": CC,
2489
+ cursor: degraded("Armed only by plan-exit-ingest-gate, which has no Cursor trigger, so it stays dormant; covered by the HS-20SW claim-before-build rule."),
2490
+ copilot: degraded("Armed only by plan-exit-ingest-gate, which has no Copilot trigger (no ExitPlanMode tool), so it stays dormant; covered by the HS-20SW claim-before-build rule.")
2491
+ },
2492
+ "capture-workflow-gate": {
2493
+ "claude-code": CC,
2494
+ cursor: mapped(),
2495
+ copilot: mapped("preToolUse on the MCP heyspex_file_spec tool; MCP tool naming in hook payloads is unverified live (matched by suffix).")
2496
+ },
2497
+ "diagnose-capture-reminder": {
2498
+ "claude-code": CC,
2499
+ cursor: mapped("preToolUse agent_message delivers the reminder."),
2500
+ copilot: degraded("Copilot preToolUse has no context channel for an inject, so this advisory is silent (lost).")
2501
+ },
2502
+ "git-stash-gate": {
2503
+ "claude-code": CC,
2504
+ cursor: mapped(),
2505
+ copilot: mapped('The Claude "ask" becomes a deny where no user is present (cloud agent, CLI autopilot).')
2506
+ },
2507
+ "research-protocol-gate": { "claude-code": CC, cursor: mapped("Always on."), copilot: mapped("preToolUse on web_fetch/web_search and the MCP save_research tool. Always on; the MCP refusal duplicates it.") },
2508
+ "research-method-guard": {
2509
+ "claude-code": CC,
2510
+ cursor: mapped("preToolUse with a Skill|Workflow tool filter. Always on."),
2511
+ copilot: unportable("MCP preflight refusal on heyspex_save_research (hard floor, hook-independent) + .github/copilot-instructions.md research rule; Copilot has no Skill or Workflow tool to hook.")
2512
+ },
2513
+ "deep-research-cost-gate": {
2514
+ "claude-code": CC,
2515
+ cursor: mapped("preToolUse with a Workflow|Skill tool filter."),
2516
+ copilot: unportable("MCP preflight refusal on heyspex_save_research + .github/copilot-instructions.md cost-confirmation rule; Copilot has no Skill or Workflow tool to hook.")
2517
+ },
2518
+ "plan-review-gate": {
2519
+ "claude-code": CC,
2520
+ cursor: unportable(".cursor/rules/plan-review-required.mdc (always-on rule)"),
2521
+ copilot: unportable(COPILOT_NO_PLAN_TOOL)
2522
+ },
2523
+ "plan-exit-ingest-gate": {
2524
+ "claude-code": CC,
2525
+ cursor: unportable(".cursor/rules/plan-review-required.mdc + spec-creation-linkage.mdc"),
2526
+ copilot: unportable(COPILOT_NO_PLAN_TOOL)
2527
+ },
2528
+ // ── customer: end of turn / session ───────────────────────────────────────
2529
+ "naughty-list-gate": { "claude-code": CC, cursor: mapped("stop followup_message re-drive."), copilot: mapped('agentStop {decision:"block"} re-drive, capped at 8 consecutive blocks.') },
2530
+ "id-display-format-gate": { "claude-code": CC, cursor: mapped("stop followup_message re-drive."), copilot: mapped("agentStop re-drive; reads the transcript (format is not a stable API).") },
2531
+ "epic-milestone-phrasing-gate": { "claude-code": CC, cursor: mapped("stop followup_message re-drive."), copilot: mapped("agentStop re-drive; reads the transcript (format is not a stable API).") },
2532
+ "enforce-session-sequence": { "claude-code": CC, cursor: mapped("stop followup_message re-drive."), copilot: mapped("agentStop re-drive, capped at 8 consecutive blocks.") },
2533
+ "notify-status-flip": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse.") },
2534
+ "close-terminal-on-ship": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse / sessionEnd.") },
2535
+ "scripts/stamp-provenance-on-edit.mjs": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse.") },
2536
+ "session-end": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionEnd.") },
2537
+ // ── platform: heyspex-app development machinery (never installed in a customer repo) ──
2538
+ "inject-dev-server-awareness": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
2539
+ "inject-coo-seat": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
2540
+ "coo-guard-check": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
2541
+ "coo-denylist-gate": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
2542
+ "agent-spawn-delegation-gate": { "claude-code": CC, cursor: mapped("subagentStart."), copilot: notShipped },
2543
+ "capture-execution-gate": { "claude-code": CC, cursor: mapped("beforeShellExecution + subagentStart."), copilot: notShipped },
2544
+ "check-mcp-freshness-hook": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
2545
+ "dev-server-task-reminder": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
2546
+ "env-vault-guard": { "claude-code": CC, cursor: mapped("Always on."), copilot: notShipped },
2547
+ "log-session-activity": { "claude-code": CC, cursor: mapped(), copilot: notShipped }
2548
+ });
2549
+ function dispositionFor(hook, host) {
2550
+ return HOOK_DISPOSITIONS[hook]?.[host] ?? null;
2551
+ }
2552
+
2553
+ // scripts/lib/customer-kit.mjs
2554
+ var HOOK_SCOPES = Object.freeze({
2555
+ // ── customer: vault / workflow / safety gates ─────────────────────────────
2556
+ "wip-checkpoint": { scope: "customer", why: "Previous-session WIP handoff, read from the active vault." },
2557
+ "inject-doctrine": {
2558
+ scope: "customer",
2559
+ why: "Owner ruling 2026-10-03 (HS-A2PO): ships. A customer session gets the baseline edition of SpexCode Doctrine (CUSTOMER_HOOK_DATA), the foundation its own learned layer is built on; never the platform's learned file."
2560
+ },
2561
+ "check-auto-memory-budget": { scope: "customer", why: "Auto-memory index budget for the session project." },
2562
+ "check-claude-md-budget": { scope: "customer", why: "Budget check on the project's always-loaded agent contract." },
2563
+ "git-reflog-audit": { scope: "customer", why: "Session-close git audit backstop (stash/reset/rebase witness)." },
2564
+ "preflight-reminder": { scope: "customer", why: "Names the required procedure before filing/destructive turns." },
2565
+ "intent-classifier-trigger": { scope: "customer", why: "Routes capture/research/settings/defer intents." },
2566
+ "inject-communication-style": { scope: "customer", why: "Plain-language communication rules each turn." },
2567
+ "inject-vault-context": { scope: "customer", why: "Lexical vault retrieval for the prompt." },
2568
+ "git-identity-guard": { scope: "customer", why: "Agents never write git identity (owner ruling 2026-09-19)." },
2569
+ "vault-write-tier-guard": { scope: "customer", why: "Cloud write mode: disk edits to vault specs are a mirror the console never reads back (HS-FCNH)." },
2570
+ "settings-write-gate": { scope: "customer", why: "Approval before the vault's .heyspex/config.yaml changes." },
2571
+ "emit-claim-status": { scope: "customer", why: "Claim liveness heartbeat to the console." },
2572
+ "enforce-vault-naming": { scope: "customer", why: "Naming V3 filename guard (HS-RY2N)." },
2573
+ "guard-auto-memory-writes": { scope: "customer", why: "No new topic files in the auto-memory directory." },
2574
+ "plan-review-gate": { scope: "customer", why: "Plan review + QC markers before ExitPlanMode." },
2575
+ "plan-exit-ingest-gate": { scope: "customer", why: "Plan ingest on ExitPlanMode; arms claim-before-code." },
2576
+ "claim-before-code-gate": { scope: "customer", why: "First code edit after plan exit needs a claim." },
2577
+ "directive-obligation-gate": { scope: "customer", why: "A capture_on_idea directive leaves a durable obligation; an unmet one denies sub-agent spawns, product-code writes and builds, and re-drives Stop once. Also claim-before-build (HS-20SW)." },
2578
+ "capture-workflow-gate": { scope: "customer", why: "No implementation spec straight out of an open capture." },
2579
+ "vault-mirror-catchup": { scope: "customer", why: "Cloud write mode: a SessionStart detached `hs pull --apply` closes a stale disk mirror before the first tool call (HS-XJFQ)." },
2580
+ "diagnose-capture-reminder": { scope: "customer", why: "Warn when a diagnose is filed with no capture holding the owner's words (HS-IK2D)." },
2581
+ "git-stash-gate": { scope: "customer", why: "Always ask before a mutating git stash." },
2582
+ "research-method-guard": { scope: "customer", why: "Research method guard (HS-EN4F)." },
2583
+ "deep-research-cost-gate": { scope: "customer", why: "Cost confirmation before a paid deep-research launch." },
2584
+ "research-protocol-gate": { scope: "customer", why: "WebSearch/WebFetch refused until research preflight ran (HS-K0ZP, HS-EN4F)." },
2585
+ "naughty-list-gate": { scope: "customer", why: "Re-drive a final message that uses banned jargon." },
2586
+ "id-display-format-gate": { scope: "customer", why: "Naming V3 display-id format in replies." },
2587
+ "epic-milestone-phrasing-gate": { scope: "customer", why: "Epic/milestone phrasing rules in replies." },
2588
+ "enforce-session-sequence": { scope: "customer", why: "Re-drive a turn that shipped without record_wip." },
2589
+ "notify-status-flip": { scope: "customer", why: "Sound on an in_review status flip." },
2590
+ "close-terminal-on-ship": { scope: "customer", why: "Close the launch terminal once the spec ships." },
2591
+ "scripts/stamp-provenance-on-edit.mjs": { scope: "customer", why: "Stamp assisted_by provenance on direct spec edits (active vault)." },
2592
+ "session-end": { scope: "customer", why: "Release the claim and export session events." },
2593
+ // ── platform: heyspex-app's own development machinery ─────────────────────
2594
+ "inject-dev-server-awareness": { scope: "platform", why: "About the spex-console dev server in heyspex-app." },
2595
+ "inject-coo-seat": { scope: "platform", why: "dev-factory COO seat mandate." },
2596
+ "coo-guard-check": { scope: "platform", why: "dev-factory breakdown-delegation guard." },
2597
+ "coo-denylist-gate": { scope: "platform", why: "dev-factory COO window deny list." },
2598
+ "agent-spawn-delegation-gate": { scope: "platform", why: "dev-factory seat routing for subagents." },
2599
+ "capture-execution-gate": { scope: "platform", why: "Blocks dev-factory research/paid-model/routing surfaces past an ungrounded capture (HS-KNJ8)." },
2600
+ "check-mcp-freshness-hook": { scope: "platform", why: "Freshness of the vendored MCP build \u2014 a platform-dev concern." },
2601
+ "dev-server-task-reminder": { scope: "platform", why: "spex-console dev server + Neon DB safety." },
2602
+ "env-vault-guard": { scope: "platform", why: "Guards heyspex-app's encrypted env vault paths." },
2603
+ "log-session-activity": { scope: "platform", why: "Resolves .heyspex state from the platform checkout's own location, not the project \u2014 would log customer sessions into heyspex-app." }
2604
+ });
2605
+ var CUSTOMER_HOOK_DATA = Object.freeze({
2606
+ "inject-doctrine": Object.freeze(["spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json"])
2607
+ });
2608
+ var CLAUDE_HOOK_ROWS = Object.freeze([
2609
+ { event: "SessionStart", hook: "wip-checkpoint", timeout: 5 },
2610
+ { event: "SessionStart", hook: "inject-dev-server-awareness", timeout: 5 },
2611
+ { event: "SessionStart", hook: "inject-coo-seat", timeout: 5 },
2612
+ { event: "SessionStart", hook: "inject-doctrine", timeout: 5 },
2613
+ { event: "SessionStart", hook: "coo-guard-check", timeout: 5 },
2614
+ { event: "SessionStart", hook: "check-auto-memory-budget", timeout: 5 },
2615
+ { event: "SessionStart", hook: "check-claude-md-budget", timeout: 5 },
2616
+ { event: "SessionStart", hook: "check-mcp-freshness-hook", timeout: 5 },
2617
+ { event: "SessionStart", hook: "git-reflog-audit", timeout: 5 },
2618
+ { event: "SessionStart", hook: "vault-mirror-catchup", timeout: 5 },
2619
+ { event: "PreCompact", hook: "wip-checkpoint", timeout: 5 },
2620
+ { event: "UserPromptSubmit", hook: "preflight-reminder", timeout: 5 },
2621
+ { event: "UserPromptSubmit", hook: "intent-classifier-trigger", timeout: 5 },
2622
+ { event: "UserPromptSubmit", hook: "inject-communication-style", timeout: 5 },
2623
+ { event: "UserPromptSubmit", hook: "inject-vault-context", timeout: 10 },
2624
+ { event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "env-vault-guard", timeout: 5 },
2625
+ { event: "PreToolUse", matcher: "Bash|PowerShell", hook: "git-identity-guard", timeout: 5 },
2626
+ { event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "vault-write-tier-guard", timeout: 5 },
2627
+ { event: "PreToolUse", matcher: "Write|Edit|NotebookEdit|Bash|PowerShell|mcp__heyspex__heyspex_settings_set", hook: "settings-write-gate", timeout: 5 },
2628
+ { event: "PreToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PreToolUse"], timeout: 5 },
2629
+ { event: "PreToolUse", matcher: ".*", hook: "coo-denylist-gate", timeout: 5 },
2630
+ { event: "PreToolUse", matcher: "Agent|Task", hook: "agent-spawn-delegation-gate", timeout: 5 },
2631
+ { event: "PreToolUse", matcher: "Bash|PowerShell|Agent|Task", hook: "capture-execution-gate", timeout: 5 },
2632
+ { event: "PreToolUse", matcher: "Agent|Task|Edit|Write|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "directive-obligation-gate", timeout: 5 },
2633
+ { event: "PreToolUse", matcher: "mcp__heyspex__heyspex_claim_spec", hook: "coo-guard-check", timeout: 5 },
2634
+ { event: "PreToolUse", matcher: "Write|NotebookEdit|Bash|PowerShell", hook: "enforce-vault-naming", timeout: 5 },
2635
+ { event: "PreToolUse", matcher: "Write|NotebookEdit|Bash|PowerShell", hook: "guard-auto-memory-writes", timeout: 5 },
2636
+ { event: "PreToolUse", matcher: "Edit|MultiEdit|Write|Bash|PowerShell", hook: "preflight-reminder", timeout: 5 },
2637
+ { event: "PreToolUse", matcher: "ExitPlanMode", hook: "plan-review-gate", timeout: 8 },
2638
+ { event: "PreToolUse", matcher: "ExitPlanMode", hook: "plan-exit-ingest-gate", timeout: 5 },
2639
+ { event: "PreToolUse", matcher: "Edit|MultiEdit|Write", hook: "claim-before-code-gate", timeout: 5 },
2640
+ { event: "PreToolUse", matcher: "mcp__heyspex__heyspex_file_spec", hook: "capture-workflow-gate", timeout: 8 },
2641
+ { event: "PreToolUse", matcher: "mcp__heyspex__heyspex_file_spec", hook: "diagnose-capture-reminder", timeout: 5 },
2642
+ { event: "PreToolUse", matcher: "Bash|PowerShell|mcp__neon__prepare_database_migration|mcp__neon__complete_database_migration|mcp__neon__reset_from_parent|mcp__neon__delete_branch|mcp__Neon__prepare_database_migration|mcp__Neon__complete_database_migration|mcp__Neon__reset_from_parent|mcp__Neon__delete_branch", hook: "dev-server-task-reminder", timeout: 10 },
2643
+ { event: "PreToolUse", matcher: "Bash|PowerShell", hook: "git-stash-gate", timeout: 10 },
2644
+ { event: "PreToolUse", matcher: "Skill|Workflow", hook: "research-method-guard", timeout: 5 },
2645
+ { event: "PreToolUse", matcher: "Workflow|Skill", hook: "deep-research-cost-gate", timeout: 5 },
2646
+ { event: "PreToolUse", matcher: "WebSearch|WebFetch|mcp__heyspex__heyspex_save_research|mcp__plugin_heyspex_heyspex__heyspex_save_research", hook: "research-protocol-gate", timeout: 5 },
2647
+ { event: "PreToolUse", matcher: ".*", hook: "log-session-activity", timeout: 5 },
2648
+ { event: "Stop", hook: "emit-claim-status", args: ["--event=Stop"], timeout: 5 },
2649
+ { event: "Stop", hook: "naughty-list-gate", timeout: 5 },
2650
+ { event: "Stop", hook: "id-display-format-gate", timeout: 5 },
2651
+ { event: "Stop", hook: "epic-milestone-phrasing-gate", timeout: 5 },
2652
+ { event: "Stop", hook: "git-reflog-audit", timeout: 5 },
2653
+ { event: "Stop", hook: "enforce-session-sequence", timeout: 5 },
2654
+ { event: "Stop", hook: "directive-obligation-gate", timeout: 5 },
2655
+ { event: "PostToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PostToolUse"], timeout: 5 },
2656
+ { event: "PostToolUse", matcher: "Skill|mcp__heyspex__heyspex_update_capture|mcp__heyspex__heyspex_promote_capture.*|mcp__heyspex__heyspex_file_spec|mcp__heyspex__heyspex_claim_spec|mcp__heyspex__heyspex_release_spec|mcp__heyspex__heyspex_defer_spec", hook: "directive-obligation-gate", timeout: 5 },
2657
+ { event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "notify-status-flip", timeout: 5 },
2658
+ { event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "close-terminal-on-ship", timeout: 5 },
2659
+ { event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "scripts/stamp-provenance-on-edit.mjs", timeout: 5 },
2660
+ { event: "PostToolUse", matcher: ".*", hook: "log-session-activity", timeout: 5 },
2661
+ { event: "SessionEnd", hook: "session-end", timeout: 5 },
2662
+ { event: "SessionEnd", hook: "close-terminal-on-ship", timeout: 5 }
2663
+ ]);
2664
+ var COMMAND_SCOPES = Object.freeze({
2665
+ "hs-backfill.md": { scope: "customer", why: "Backfill completed work into the vault." },
2666
+ "hs-capture.md": { scope: "customer", why: "Capture intake." },
2667
+ "hs-defer.md": { scope: "customer", why: "Split + defer remaining work." },
2668
+ "hs-doctor.md": { scope: "customer", why: "Install-surface repair when heyspex_* tools are missing." },
2669
+ "hs-ground.md": { scope: "customer", why: "Pre-plan grounding report." },
2670
+ "hs-handoff.md": { scope: "customer", why: "Short-code lane handoff." },
2671
+ "hs-next.md": { scope: "customer", why: "On-deck prompt + claim handshake." },
2672
+ "hs-promote.md": { scope: "customer", why: "Capture \u2192 research \u2192 milestone promotion." },
2673
+ "hs-push.md": { scope: "customer", why: "Push an epic plan into the vault." },
2674
+ "hs-qc.md": { scope: "customer", why: "Post-plan QC gate." },
2675
+ "hs-res.md": { scope: "customer", why: "Research method entrypoint (SpexCrawl/DeepSeek)." },
2676
+ "hs-resolve.md": { scope: "customer", why: "Resolution lane." },
2677
+ "hs-review.md": { scope: "customer", why: "Review queue." },
2678
+ "hs-view.md": { scope: "customer", why: "Open a console surface." },
2679
+ "hs-auto.md": { scope: "platform", why: "Routes work into dev-factory (platform build system)." },
2680
+ "hs-dev.md": { scope: "platform", why: "Admin/dev-only HeySpex dev/prod mode switch." },
2681
+ "hs-log.md": { scope: "platform", why: "Admin/dev-only dev-server log watcher." },
2682
+ "kill-ports.md": { scope: "platform", why: "Kills local dev-server ports." },
2683
+ "prototype.md": { scope: "platform", why: "Console design-preview prototypes inside spex-console." }
2684
+ });
2685
+ function hookScope(hook) {
2686
+ return HOOK_SCOPES[hook]?.scope ?? null;
2687
+ }
2688
+ function toPosixPath(p) {
2689
+ return String(p).replace(/\\/g, "/").replace(/\/+$/, "");
2690
+ }
2691
+ function samePlatformPath(a, b) {
2692
+ if (!a || !b) return false;
2693
+ const norm = (p) => {
2694
+ const s = toPosixPath(path2.resolve(p));
2695
+ return process.platform === "win32" ? s.toLowerCase() : s;
2696
+ };
2697
+ return norm(a) === norm(b);
2698
+ }
2699
+ var MCP_PROJECT_PREFIX = "mcp__heyspex__";
2700
+ var MCP_PLUGIN_PREFIX = "mcp__plugin_heyspex_heyspex__";
2701
+ function widenMcpMatcher(matcher) {
2702
+ if (!matcher) return matcher;
2703
+ const parts = matcher.split("|");
2704
+ const out = [];
2705
+ for (const part of parts) {
2706
+ if (!out.includes(part)) out.push(part);
2707
+ if (part.startsWith(MCP_PROJECT_PREFIX)) {
2708
+ const plugin = MCP_PLUGIN_PREFIX + part.slice(MCP_PROJECT_PREFIX.length);
2709
+ if (!parts.includes(plugin) && !out.includes(plugin)) out.push(plugin);
2710
+ }
2711
+ }
2712
+ return out.join("|");
2713
+ }
2714
+ function customerHookRows() {
2715
+ return CLAUDE_HOOK_ROWS.filter((r) => hookScope(r.hook) === "customer");
2716
+ }
2717
+ function hookTail(row) {
2718
+ return [row.hook, ...row.args ?? []].join(" ");
2719
+ }
2720
+ var CLAUDE_HOOK_SHIM = "scripts/claude-hooks/_run.mjs";
2721
+ var CURSOR_HOOK_SHIM = "scripts/cursor-hooks/_run.mjs";
2722
+ var CUSTOMER_LAUNCHERS = Object.freeze({
2723
+ claude: Object.freeze({
2724
+ rel: Object.freeze([".claude", "heyspex", "run-hook.mjs"]),
2725
+ projectDirEnv: "CLAUDE_PROJECT_DIR",
2726
+ shim: CLAUDE_HOOK_SHIM,
2727
+ registry: ".claude/settings.json"
2728
+ }),
2729
+ cursor: Object.freeze({
2730
+ rel: Object.freeze([".cursor", "heyspex", "run-hook.mjs"]),
2731
+ projectDirEnv: "CURSOR_PROJECT_DIR",
2732
+ shim: CURSOR_HOOK_SHIM,
2733
+ registry: ".cursor/hooks.json"
2734
+ })
2735
+ });
2736
+ var BOOTSTRAP_WALK = "var d=process.cwd(),r=e.CLAUDE_PROJECT_DIR;if(!r||!f.existsSync(p.join(r,...n))){r='';for(;;){if(f.existsSync(p.join(d,...n))){r=d;break}var q=p.dirname(d);if(q===d)break;d=q}}";
2737
+ function buildBootstrap({ plugin = false } = {}) {
2738
+ const walk = BOOTSTRAP_WALK;
2739
+ const body = plugin ? "var f=require('fs'),p=require('path'),u=require('url'),e=process.env,n=['scripts','claude-hooks','_run.mjs'],s='',m=e.CLAUDE_PLUGIN_ROOT||e.COPILOT_PLUGIN_ROOT||e.PLUGIN_ROOT;if(m&&f.existsSync(p.join(m,'bin','run-hook.mjs')))s=p.join(m,'bin','run-hook.mjs');if(!s){" + walk + "if(r){e.CLAUDE_PROJECT_DIR=r;s=p.join(r,...n)}}" : "var f=require('fs'),p=require('path'),u=require('url'),e=process.env,n=['scripts','claude-hooks','_run.mjs'],s='';" + walk + "if(r){e.CLAUDE_PROJECT_DIR=r;s=p.join(r,...n)}";
2740
+ return body + "if(!s)process.exit(0);process.argv=[process.argv[0],s].concat(process.argv.slice(1));import(u.pathToFileURL(s).href).catch(function(){process.exit(0)});" + (plugin ? "//bin/run-hook.mjs" : "//scripts/claude-hooks/_run.mjs");
2741
+ }
2742
+ var PLATFORM_RUNNER_PREFIX = 'node -e "' + buildBootstrap() + '"';
2743
+ var PLUGIN_RUNNER_PREFIX = 'node -e "' + buildBootstrap({ plugin: true }) + '"';
2744
+ function buildCustomerBootstrap() {
2745
+ const rel = CUSTOMER_LAUNCHERS.claude.rel.map((s) => "'" + s + "'").join(",");
2746
+ return "var f=require('fs'),p=require('path'),u=require('url'),e=process.env,n=[" + rel + "],s='';" + BOOTSTRAP_WALK + "if(r){e.CLAUDE_PROJECT_DIR=r;s=p.join(r,...n)}if(!s)process.exit(0);process.argv=[process.argv[0],s].concat(process.argv.slice(1));import(u.pathToFileURL(s).href).catch(function(){process.exit(0)});//" + CUSTOMER_LAUNCHERS.claude.rel.join("/");
2747
+ }
2748
+ var CUSTOMER_RUNNER_PREFIX = 'node -e "' + buildCustomerBootstrap() + '"';
2749
+ var LAUNCHER_TEMPLATE = [
2750
+ "#!/usr/bin/env node",
2751
+ "// HeySpex hook launcher -- GENERATED by hs install / hs connect. Commit it with this repo.",
2752
+ "//",
2753
+ "// Every HeySpex hook registered in __REGISTRY__ runs",
2754
+ '// node "$__ENV__/__LAUNCHER__" <hook> [args]',
2755
+ "// This file carries NO machine-specific path. It finds the heyspex package this",
2756
+ "// repo depends on (node_modules/heyspex) and hands off in-process to the",
2757
+ "// package's hook shim (__SHIM__), so the same committed command works on every",
2758
+ "// machine the repo is cloned to.",
2759
+ "//",
2760
+ "// Resolution order (the first root that carries the shim wins):",
2761
+ "// 1. HEYSPEX_PLATFORM_ROOT explicit machine-local override",
2762
+ "// 2. HEYSPEX_MCP_ENTRY -> its package root machine-local override",
2763
+ "// 3. node_modules/heyspex, walking up from this file (the repo-local dependency)",
2764
+ "// 4. node_modules/heyspex up from $__ENV__, $CLAUDE_PROJECT_DIR, $CURSOR_PROJECT_DIR, cwd",
2765
+ "// 5. <npm root -g>/heyspex a global install, last resort",
2766
+ "// Nothing found -> one stderr line and exit 0 (fail-open, like the shim itself).",
2767
+ "import { spawnSync } from 'node:child_process';",
2768
+ "import { existsSync } from 'node:fs';",
2769
+ "import { dirname, resolve } from 'node:path';",
2770
+ "import { fileURLToPath, pathToFileURL } from 'node:url';",
2771
+ "",
2772
+ "const SHIM = '__SHIM__'.split('/');",
2773
+ "const here = dirname(fileURLToPath(import.meta.url));",
2774
+ "const env = process.env;",
2775
+ "const text = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);",
2776
+ "const hasShim = (root) => Boolean(root) && existsSync(resolve(root, ...SHIM));",
2777
+ "",
2778
+ "function packageUpFrom(start) {",
2779
+ " let dir = resolve(start);",
2780
+ " for (;;) {",
2781
+ " const candidate = resolve(dir, 'node_modules', 'heyspex');",
2782
+ " if (hasShim(candidate)) return candidate;",
2783
+ " const parent = dirname(dir);",
2784
+ " if (parent === dir) return null;",
2785
+ " dir = parent;",
2786
+ " }",
2787
+ "}",
2788
+ "",
2789
+ "function globalPackage() {",
2790
+ " try {",
2791
+ " // One command string through the shell: npm is a .cmd on Windows, and a",
2792
+ " // shell + args array is what node deprecates (DEP0190).",
2793
+ " const res = spawnSync('npm root -g', { encoding: 'utf8', timeout: 5000, shell: true });",
2794
+ " const root = String(res.stdout ?? '').trim();",
2795
+ " return root ? resolve(root, 'heyspex') : null;",
2796
+ " } catch {",
2797
+ " return null;",
2798
+ " }",
2799
+ "}",
2800
+ "",
2801
+ "const mcpEntry = text(env.HEYSPEX_MCP_ENTRY);",
2802
+ "const starts = [...new Set([here, text(env.__ENV__), text(env.CLAUDE_PROJECT_DIR), text(env.CURSOR_PROJECT_DIR), process.cwd()].filter(Boolean))];",
2803
+ "let root =",
2804
+ " [text(env.HEYSPEX_PLATFORM_ROOT), mcpEntry && resolve(mcpEntry, '..', '..', '..')].find(hasShim) ??",
2805
+ " starts.map(packageUpFrom).find(Boolean) ??",
2806
+ " null;",
2807
+ "if (!root) {",
2808
+ " const global = globalPackage();",
2809
+ " if (hasShim(global)) root = global;",
2810
+ "}",
2811
+ "",
2812
+ "if (!root) {",
2813
+ " process.stderr.write(",
2814
+ " '[heyspex] hook ' + (process.argv[2] ?? '?') + ' skipped: no heyspex package found ' +",
2815
+ ` '(run "npm install heyspex" in this repo, or set HEYSPEX_PLATFORM_ROOT). Looked for __SHIM__ under ' +`,
2816
+ " 'HEYSPEX_PLATFORM_ROOT, HEYSPEX_MCP_ENTRY, node_modules/heyspex up from ' + starts.join(', ') + ', and npm root -g.\\n',",
2817
+ " );",
2818
+ " process.exit(0);",
2819
+ "}",
2820
+ "",
2821
+ "// argv is already [node, <this file>, <hook>, ...args] -- the shape the shim",
2822
+ '// reads; the shim path replaces this file so a shim guarded by "am I main?"',
2823
+ "// (the Cursor one) runs exactly as if Cursor had invoked it directly.",
2824
+ "const shim = resolve(root, ...SHIM);",
2825
+ "process.argv[1] = shim;",
2826
+ "try {",
2827
+ " await import(pathToFileURL(shim).href);",
2828
+ "} catch (err) {",
2829
+ " process.stderr.write('[heyspex] hook ' + (process.argv[2] ?? '?') + ' skipped: ' + (err && err.message ? err.message : err) + '\\n');",
2830
+ " process.exit(0);",
2831
+ "}",
2832
+ ""
2833
+ ].join("\n");
2834
+ function renderCustomerLauncher(surface = "claude") {
2835
+ const l = CUSTOMER_LAUNCHERS[surface];
2836
+ return LAUNCHER_TEMPLATE.replace(
2837
+ /__(REGISTRY|ENV|LAUNCHER|SHIM)__/g,
2838
+ (_m, token) => token === "REGISTRY" ? l.registry : token === "ENV" ? l.projectDirEnv : token === "LAUNCHER" ? l.rel.join("/") : l.shim
2839
+ );
2840
+ }
2841
+ function planCustomerLauncherWrite(existing, surface = "claude") {
2842
+ const text = renderCustomerLauncher(surface);
2843
+ if (existing === null || existing === void 0) return { write: true, reason: "missing", text };
2844
+ if (String(existing).replace(/\r\n/g, "\n") === text) return { write: false, reason: "unchanged", text };
2845
+ return { write: true, reason: "update", text };
2846
+ }
2847
+ function renderCustomerHooks(runnerPrefix) {
2848
+ return customerHookRows().map((row) => ({
2849
+ event: row.event,
2850
+ ...row.matcher ? { matcher: widenMcpMatcher(row.matcher) } : {},
2851
+ command: `${runnerPrefix} ${hookTail(row)}`,
2852
+ timeout: row.timeout,
2853
+ tail: hookTail(row)
2854
+ }));
2855
+ }
2856
+ function renderPluginHooksDocument() {
2857
+ const hooks = {};
2858
+ for (const d of renderCustomerHooks(PLUGIN_RUNNER_PREFIX)) {
2859
+ (hooks[d.event] ??= []).push({
2860
+ ...d.matcher ? { matcher: d.matcher } : {},
2861
+ hooks: [{ type: "command", command: d.command, timeout: d.timeout }]
2862
+ });
2863
+ }
2864
+ return {
2865
+ hooks
2866
+ };
2867
+ }
2868
+ function renderPluginHooksText() {
2869
+ return `${JSON.stringify(renderPluginHooksDocument(), null, 2)}
2870
+ `;
2871
+ }
2872
+ var COPILOT_EVENT_NAMES = Object.freeze({
2873
+ SessionStart: "sessionStart",
2874
+ SessionEnd: "sessionEnd",
2875
+ UserPromptSubmit: "userPromptSubmitted",
2876
+ PreToolUse: "preToolUse",
2877
+ PostToolUse: "postToolUse",
2878
+ Stop: "agentStop",
2879
+ PreCompact: "preCompact"
2880
+ });
2881
+ var GITHUB_HOOKS_STATE_ARMING = Object.freeze(["intent-classifier-trigger"]);
2882
+ var COPILOT_TOOL_NAMES = Object.freeze({
2883
+ Write: "create",
2884
+ Edit: "edit",
2885
+ MultiEdit: "edit",
2886
+ NotebookEdit: "edit",
2887
+ Bash: "bash",
2888
+ PowerShell: "powershell",
2889
+ Agent: "task",
2890
+ Task: "task",
2891
+ WebFetch: "web_fetch",
2892
+ WebSearch: "web_search"
2893
+ });
2894
+ function toCopilotMatcher(matcher) {
2895
+ if (!matcher) return void 0;
2896
+ const out = [];
2897
+ for (const part of matcher.split("|")) {
2898
+ let t = null;
2899
+ if (part === ".*") t = ".*";
2900
+ else if (COPILOT_TOOL_NAMES[part]) t = COPILOT_TOOL_NAMES[part];
2901
+ else if (part.startsWith(MCP_PROJECT_PREFIX)) t = ".*" + part.slice(MCP_PROJECT_PREFIX.length);
2902
+ else if (part.startsWith(MCP_PLUGIN_PREFIX)) t = ".*" + part.slice(MCP_PLUGIN_PREFIX.length);
2903
+ if (t && !out.includes(t)) out.push(t);
2904
+ }
2905
+ if (out.includes(".*")) return ".*";
2906
+ return out.length ? out.join("|") : null;
2907
+ }
2908
+ function githubHookNames() {
2909
+ const names = /* @__PURE__ */ new Set();
2910
+ for (const r of customerHookRows()) {
2911
+ if (dispositionFor(r.hook, "copilot")?.disposition === "mapped" || GITHUB_HOOKS_STATE_ARMING.includes(r.hook)) names.add(r.hook);
2912
+ }
2913
+ return names;
2914
+ }
2915
+ function githubHookCommand(row) {
2916
+ const tail = hookTail(row);
2917
+ return `R=node_modules/heyspex/scripts/claude-hooks/_run.mjs; [ -f "$R" ] || R=./scripts/claude-hooks/_run.mjs; [ -f "$R" ] || exit 0; exec node "$R" ${tail}`;
2918
+ }
2919
+ function renderGithubHooksDocument() {
2920
+ const allowed = githubHookNames();
2921
+ const hooks = {};
2922
+ for (const row of customerHookRows()) {
2923
+ if (!allowed.has(row.hook)) continue;
2924
+ const event = COPILOT_EVENT_NAMES[row.event];
2925
+ if (!event) continue;
2926
+ const matcher = toCopilotMatcher(row.matcher);
2927
+ if (matcher === null) continue;
2928
+ (hooks[event] ??= []).push({
2929
+ type: "command",
2930
+ ...matcher ? { matcher } : {},
2931
+ bash: githubHookCommand(row),
2932
+ timeoutSec: row.timeout
2933
+ });
2934
+ }
2935
+ return { version: 1, hooks };
2936
+ }
2937
+ function renderGithubHooksText() {
2938
+ return `${JSON.stringify(renderGithubHooksDocument(), null, 2)}
2939
+ `;
2940
+ }
2941
+ var HARNESS_COMMAND_SURFACES = Object.freeze([
2942
+ {
2943
+ installedDir: Object.freeze([".claude", "commands"]),
2944
+ packagedDir: Object.freeze(["dist", "cli-templates", "harness-commands", "claude"]),
2945
+ devSourceDir: Object.freeze([".claude", "commands"])
2946
+ },
2947
+ {
2948
+ installedDir: Object.freeze([".cursor", "commands"]),
2949
+ packagedDir: Object.freeze(["dist", "cli-templates", "harness-commands", "cursor"]),
2950
+ devSourceDir: Object.freeze([".cursor", "commands"])
2951
+ }
2952
+ ]);
2953
+ var NPM_ALIASES = Object.freeze({ doctor: "doctor", setup: "doctor --fix" });
2954
+ var SCRIPT_VERBS = Object.freeze({
2955
+ "scripts/heyspex-doctor.mjs": "doctor",
2956
+ "scripts/hs-view.mjs": "view",
2957
+ "scripts/hs-capture-cli.mjs": "capture",
2958
+ "scripts/hs-next.mjs": "next",
2959
+ "scripts/hs-mode.mjs": "mode",
2960
+ "scripts/hs-settings.mjs": "settings",
2961
+ "scripts/hs-companion.mjs": "companion",
2962
+ "scripts/wip-ledger-health.mjs": "wip",
2963
+ "scripts/dev-server-status.mjs": "server status",
2964
+ "scripts/dev-server-stop.mjs": "server stop"
2965
+ });
2966
+ var PORTABLE_PACKAGE_DIR = "node_modules/heyspex";
2967
+ var fsDeps = {
2968
+ joinPath: (...s) => path2.join(...s),
2969
+ readFile: (p) => {
2970
+ try {
2971
+ return fs.readFileSync(p, "utf8");
2972
+ } catch {
2973
+ return null;
2974
+ }
2975
+ },
2976
+ listDir: (d) => {
2977
+ try {
2978
+ return fs.readdirSync(d);
2979
+ } catch {
2980
+ return [];
2981
+ }
2982
+ }
2983
+ };
2984
+ function invokedDirectly(importMetaUrl, selfBasename, argv1 = process.argv[1]) {
2985
+ try {
2986
+ if (!argv1) return false;
2987
+ const self = fileURLToPath2(importMetaUrl);
2988
+ if (path2.basename(self) !== selfBasename) return false;
2989
+ return pathToFileURL(path2.resolve(argv1)).href === pathToFileURL(self).href;
2990
+ } catch {
2991
+ return false;
2992
+ }
2993
+ }
2994
+ var SELF = fileURLToPath2(import.meta.url);
2995
+ if (invokedDirectly(import.meta.url, "customer-kit.mjs")) {
2996
+ const platformRoot = path2.resolve(path2.dirname(SELF), "..", "..");
2997
+ const target = path2.join(platformRoot, "plugins", "heyspex", "hooks", "hooks.json");
2998
+ const want = renderPluginHooksText();
2999
+ const same = (fsDeps.readFile(target) ?? "").replace(/\r\n/g, "\n") === want;
3000
+ if (process.argv.includes("--write-plugin-hooks")) {
3001
+ if (!same) fs.writeFileSync(target, want, "utf8");
3002
+ console.log(`customer-kit: ${same ? "unchanged" : "wrote"} ${path2.relative(platformRoot, target)}`);
3003
+ } else if (process.argv.includes("--check-plugin-hooks")) {
3004
+ if (!same) {
3005
+ console.error("customer-kit: plugins/heyspex/hooks/hooks.json is stale \u2014 run `node scripts/lib/customer-kit.mjs --write-plugin-hooks`");
3006
+ process.exit(1);
3007
+ }
3008
+ console.log("customer-kit: plugins/heyspex/hooks/hooks.json up to date");
3009
+ } else if (process.argv.includes("--write-github-hooks") || process.argv.includes("--check-github-hooks")) {
3010
+ const ghTarget = path2.join(platformRoot, ".github", "hooks", "heyspex.json");
3011
+ const ghWant = renderGithubHooksText();
3012
+ const ghSame = (fsDeps.readFile(ghTarget) ?? "").replace(/\r\n/g, "\n") === ghWant;
3013
+ if (process.argv.includes("--write-github-hooks")) {
3014
+ if (!ghSame) {
3015
+ fs.mkdirSync(path2.dirname(ghTarget), { recursive: true });
3016
+ fs.writeFileSync(ghTarget, ghWant, "utf8");
3017
+ }
3018
+ console.log(`customer-kit: ${ghSame ? "unchanged" : "wrote"} ${path2.relative(platformRoot, ghTarget).replace(/\\/g, "/")}`);
3019
+ } else if (!ghSame) {
3020
+ console.error("customer-kit: .github/hooks/heyspex.json is stale - run `node scripts/lib/customer-kit.mjs --write-github-hooks`");
3021
+ process.exit(1);
3022
+ } else {
3023
+ console.log("customer-kit: .github/hooks/heyspex.json up to date");
3024
+ }
3025
+ } else {
3026
+ console.log("usage: node scripts/lib/customer-kit.mjs --write-plugin-hooks | --check-plugin-hooks");
3027
+ }
3028
+ }
3029
+
3030
+ // scripts/install-cursor-hooks.mjs
3031
+ var PLATFORM_ROOT = path3.resolve(path3.dirname(fileURLToPath3(import.meta.url)), "..");
3032
+ var REPO_ROOT = process.env.HS_SETUP_TARGET?.trim() ? path3.resolve(process.env.HS_SETUP_TARGET.trim()) : PLATFORM_ROOT;
3033
+ var SETTINGS_PATH = path3.join(REPO_ROOT, ".cursor", "hooks.json");
3034
+ function cursorKitFor(targetRoot, platformRoot = PLATFORM_ROOT) {
3035
+ const customer = !samePlatformPath(targetRoot, platformRoot);
3036
+ return {
3037
+ customer,
3038
+ hooks: customer ? HOOKS.filter((h) => hookScope(h.hook) === "customer") : HOOKS,
3039
+ commandFor: customer ? commandForCustomer : commandFor,
3040
+ launcher: customer ? { rel: CUSTOMER_LAUNCHERS.cursor.rel, plan: planCustomerLauncherWrite } : null
3041
+ };
3042
+ }
3043
+ var KIT = cursorKitFor(REPO_ROOT);
3044
+ var ACTIVE_HOOKS = KIT.hooks;
3045
+ var LAUNCHER_PATH = path3.join(REPO_ROOT, ...CUSTOMER_LAUNCHERS.cursor.rel);
3046
+ var LEGACY_MARKER = "scripts/claude-hooks/";
3047
+ function readSettings() {
3048
+ try {
3049
+ const raw = fs2.readFileSync(SETTINGS_PATH, "utf8").trim();
3050
+ if (!raw) return { version: 1, hooks: {} };
3051
+ const parsed = JSON.parse(raw);
3052
+ if (parsed && typeof parsed === "object") {
3053
+ parsed.version ??= 1;
3054
+ parsed.hooks ??= {};
3055
+ return parsed;
3056
+ }
3057
+ } catch (err) {
3058
+ if (err && err.code === "ENOENT") return { version: 1, hooks: {} };
3059
+ throw err;
3060
+ }
3061
+ return { version: 1, hooks: {} };
3062
+ }
3063
+ function writeSettings(settings) {
3064
+ fs2.mkdirSync(path3.dirname(SETTINGS_PATH), { recursive: true });
3065
+ fs2.writeFileSync(SETTINGS_PATH, `${JSON.stringify(settings, null, 2)}
3066
+ `, "utf8");
3067
+ }
3068
+ var commandOf = (entry) => entry && typeof entry.command === "string" ? entry.command : "";
3069
+ var isOurRunner = (token) => token.includes(SHIM) || token.includes(CUSTOMER_LAUNCHER);
3070
+ var isShimEntry = (raw) => isOurRunner(commandOf(raw));
3071
+ var isLegacyEntry = (raw) => !isShimEntry(raw) && commandOf(raw).includes(LEGACY_MARKER);
3072
+ function identityOfCommand(command) {
3073
+ const tokens = command.trim().split(/\s+/);
3074
+ const shimAt = tokens.findIndex(isOurRunner);
3075
+ if (shimAt === -1) return null;
3076
+ const rest = tokens.slice(shimAt + 1);
3077
+ const hook = rest[0] ?? "";
3078
+ let tool = "";
3079
+ const args = [];
3080
+ for (const token of rest.slice(1)) {
3081
+ if (token.startsWith("--hs-tool=")) {
3082
+ tool = token.slice("--hs-tool=".length).replace(/^"([\s\S]*)"$/, "$1");
3083
+ } else if (token.startsWith("--hs-claude-event=")) continue;
3084
+ else args.push(token);
3085
+ }
3086
+ return `${hook}::${tool}::${args.join(" ")}`;
3087
+ }
3088
+ function desiredEntry(entry, render = KIT.commandFor) {
3089
+ return {
3090
+ command: render(entry),
3091
+ timeout: timeoutFor(entry),
3092
+ ...entry.matcher ? { matcher: entry.matcher } : {}
3093
+ };
3094
+ }
3095
+ function sameEntry(raw, entry, render = KIT.commandFor) {
3096
+ const want = desiredEntry(entry, render);
3097
+ return commandOf(raw) === want.command && raw.timeout === want.timeout && (raw.matcher ?? void 0) === (want.matcher ?? void 0);
3098
+ }
3099
+ var PLUGIN_HOOKS_PATH = path3.join(REPO_ROOT, "plugins", "heyspex", "hooks", "cursor-hooks.json");
3100
+ function pluginHooksDocument() {
3101
+ const hooks = {};
3102
+ for (const entry of HOOKS) {
3103
+ hooks[entry.event] ??= [];
3104
+ hooks[entry.event].push({
3105
+ command: commandForPlugin(entry),
3106
+ timeout: timeoutFor(entry),
3107
+ ...entry.matcher ? { matcher: entry.matcher } : {}
3108
+ });
3109
+ }
3110
+ return {
3111
+ _comment: "GENERATED by scripts/install-cursor-hooks.mjs from scripts/cursor-hooks/manifest.mjs \u2014 do not edit by hand; run `npm run hooks:install-cursor`. Commands resolve through $CURSOR_PROJECT_DIR because the shim and hook bodies live in the host vault checkout, not the plugin cache. WARNING: enabling this plugin in a repo whose .cursor/hooks.json still registers the same hooks runs every hook twice \u2014 validate in a fresh clone, or run `node scripts/install-cursor-hooks.mjs --uninstall` there first.",
3112
+ version: 1,
3113
+ hooks
3114
+ };
3115
+ }
3116
+ function pluginHooksText() {
3117
+ return `${JSON.stringify(pluginHooksDocument(), null, 2)}
3118
+ `;
3119
+ }
3120
+ function syncPluginHooks() {
3121
+ const want = pluginHooksText();
3122
+ let have = null;
3123
+ try {
3124
+ have = fs2.readFileSync(PLUGIN_HOOKS_PATH, "utf8");
3125
+ } catch {
3126
+ have = null;
3127
+ }
3128
+ if (have === want) return false;
3129
+ fs2.mkdirSync(path3.dirname(PLUGIN_HOOKS_PATH), { recursive: true });
3130
+ fs2.writeFileSync(PLUGIN_HOOKS_PATH, want, "utf8");
3131
+ return true;
3132
+ }
3133
+ function mergeCursorHooks(settings, { hooks = ACTIVE_HOOKS, commandFor: render = KIT.commandFor } = {}) {
3134
+ settings.version = 1;
3135
+ settings.hooks ??= {};
3136
+ const added = [];
3137
+ const repaired = [];
3138
+ const removed = [];
3139
+ for (const [event, entries] of Object.entries(settings.hooks)) {
3140
+ if (!Array.isArray(entries)) continue;
3141
+ const keep = entries.filter((raw) => {
3142
+ if (!isLegacyEntry(raw)) return true;
3143
+ removed.push(`${event}: ${commandOf(raw)}`);
3144
+ return false;
3145
+ });
3146
+ settings.hooks[event] = keep;
3147
+ }
3148
+ for (const entry of hooks) {
3149
+ settings.hooks[entry.event] ??= [];
3150
+ const entries = settings.hooks[entry.event];
3151
+ const identity = identityFor(entry);
3152
+ const existing = entries.filter(
3153
+ (raw2) => isShimEntry(raw2) && identityOfCommand(commandOf(raw2)) === identity
3154
+ );
3155
+ if (existing.length === 0) {
3156
+ entries.push(desiredEntry(entry, render));
3157
+ added.push(`${entry.event}/${entry.hook}`);
3158
+ continue;
3159
+ }
3160
+ for (const extra of existing.slice(1)) {
3161
+ entries.splice(entries.indexOf(extra), 1);
3162
+ removed.push(`${entry.event}: duplicate ${entry.hook}`);
3163
+ }
3164
+ const raw = existing[0];
3165
+ if (!sameEntry(raw, entry, render)) {
3166
+ Object.assign(raw, desiredEntry(entry, render));
3167
+ if (!entry.matcher && "matcher" in raw) delete raw.matcher;
3168
+ repaired.push(`${entry.event}/${entry.hook}`);
3169
+ }
3170
+ }
3171
+ for (const [event, entries] of Object.entries(settings.hooks)) {
3172
+ if (!Array.isArray(entries)) continue;
3173
+ const wanted = new Set(hooks.filter((h) => h.event === event).map(identityFor));
3174
+ settings.hooks[event] = entries.filter((raw) => {
3175
+ if (!isShimEntry(raw)) return true;
3176
+ if (wanted.has(identityOfCommand(commandOf(raw)))) return true;
3177
+ removed.push(`${event}: retired ${commandOf(raw)}`);
3178
+ return false;
3179
+ });
3180
+ if (settings.hooks[event].length === 0) delete settings.hooks[event];
3181
+ }
3182
+ return { added, repaired, removed };
3183
+ }
3184
+ function launcherPlan() {
3185
+ let existing = null;
3186
+ try {
3187
+ existing = fs2.readFileSync(LAUNCHER_PATH, "utf8");
3188
+ } catch {
3189
+ existing = null;
3190
+ }
3191
+ return planCustomerLauncherWrite(existing, "cursor");
3192
+ }
3193
+ function install({ json = false } = {}) {
3194
+ const settings = readSettings();
3195
+ const { added, repaired, removed } = mergeCursorHooks(settings);
3196
+ const settingsChanged = added.length + repaired.length + removed.length > 0;
3197
+ if (settingsChanged) writeSettings(settings);
3198
+ let launcherWritten = false;
3199
+ if (KIT.customer) {
3200
+ const plan = launcherPlan();
3201
+ if (plan.write) {
3202
+ fs2.mkdirSync(path3.dirname(LAUNCHER_PATH), { recursive: true });
3203
+ fs2.writeFileSync(LAUNCHER_PATH, plan.text, "utf8");
3204
+ launcherWritten = true;
3205
+ }
3206
+ }
3207
+ const pluginChanged = KIT.customer ? false : syncPluginHooks();
3208
+ if (json) {
3209
+ console.log(
3210
+ JSON.stringify(
3211
+ {
3212
+ ok: true,
3213
+ changed: settingsChanged || pluginChanged || launcherWritten,
3214
+ added,
3215
+ repaired,
3216
+ removed,
3217
+ pluginChanged,
3218
+ launcherWritten,
3219
+ total: ACTIVE_HOOKS.length,
3220
+ customer: KIT.customer
3221
+ },
3222
+ null,
3223
+ 2
3224
+ )
3225
+ );
3226
+ return;
3227
+ }
3228
+ if (pluginChanged) {
3229
+ console.log(`install-cursor-hooks: regenerated ${path3.relative(REPO_ROOT, PLUGIN_HOOKS_PATH)}`);
3230
+ }
3231
+ if (launcherWritten) {
3232
+ console.log(`install-cursor-hooks: wrote ${path3.relative(REPO_ROOT, LAUNCHER_PATH)} (resolves node_modules/heyspex)`);
3233
+ }
3234
+ if (!settingsChanged) {
3235
+ console.log(`install-cursor-hooks: already installed (${ACTIVE_HOOKS.length} hooks, no changes).`);
3236
+ return;
3237
+ }
3238
+ const parts = [];
3239
+ if (added.length) parts.push(`added ${added.length}`);
3240
+ if (repaired.length) parts.push(`repaired ${repaired.length}`);
3241
+ if (removed.length) parts.push(`removed ${removed.length}`);
3242
+ console.log(
3243
+ `install-cursor-hooks: ${parts.join("; ")} \u2192 ${path3.relative(REPO_ROOT, SETTINGS_PATH)} (${ACTIVE_HOOKS.length} hooks registered)`
3244
+ );
3245
+ for (const line of removed) console.log(` - ${line}`);
3246
+ console.log(
3247
+ KIT.customer ? ` Launcher: ${CUSTOMER_LAUNCHER} \u2192 node_modules/heyspex/${SHIM} (HeySpex off-switch + Cursor\u2194Claude payload translation)` : ` Shim: ${SHIM} (HeySpex off-switch + Cursor\u2194Claude payload translation)`
3248
+ );
3249
+ for (const gap of UNPORTABLE) {
3250
+ console.log(` Not registerable in Cursor: ${gap.hook} (${gap.reason}) \u2192 covered by ${gap.coveredBy}`);
3251
+ }
3252
+ }
3253
+ function check({ json = false } = {}) {
3254
+ const settings = readSettings();
3255
+ const issues = [];
3256
+ const entriesFor = (event) => Array.isArray(settings?.hooks?.[event]) ? settings.hooks[event] : [];
3257
+ for (const entry of ACTIVE_HOOKS) {
3258
+ const identity = identityFor(entry);
3259
+ const ours = entriesFor(entry.event).filter(
3260
+ (raw) => isShimEntry(raw) && identityOfCommand(commandOf(raw)) === identity
3261
+ );
3262
+ if (ours.length === 0) {
3263
+ issues.push(`${entry.event}: missing ${entry.hook}`);
3264
+ continue;
3265
+ }
3266
+ if (ours.length > 1) issues.push(`${entry.event}: ${entry.hook} registered ${ours.length}\xD7`);
3267
+ for (const raw of ours) {
3268
+ if (!sameEntry(raw, entry)) {
3269
+ issues.push(
3270
+ `${entry.event}/${entry.hook}: drift \u2014 expected \`${KIT.commandFor(entry)}\` (timeout ${timeoutFor(entry)}, matcher ${entry.matcher ?? "none"}), found \`${commandOf(raw)}\` (timeout ${raw.timeout}, matcher ${raw.matcher ?? "none"})`
3271
+ );
3272
+ }
3273
+ }
3274
+ }
3275
+ for (const [event, entries] of Object.entries(settings.hooks ?? {})) {
3276
+ if (!Array.isArray(entries)) continue;
3277
+ for (const raw of entries) {
3278
+ if (isLegacyEntry(raw)) {
3279
+ issues.push(`${event}: superseded direct registration \u2014 ${commandOf(raw)}`);
3280
+ } else if (isShimEntry(raw) && !ACTIVE_HOOKS.some((h) => h.event === event && identityFor(h) === identityOfCommand(commandOf(raw)))) {
3281
+ issues.push(`${event}: not part of this repo's hook set \u2014 ${commandOf(raw)}`);
3282
+ }
3283
+ }
3284
+ }
3285
+ let pluginOnDisk = null;
3286
+ try {
3287
+ pluginOnDisk = fs2.readFileSync(PLUGIN_HOOKS_PATH, "utf8");
3288
+ } catch {
3289
+ pluginOnDisk = null;
3290
+ }
3291
+ if (!KIT.customer && pluginOnDisk !== pluginHooksText()) {
3292
+ issues.push(
3293
+ `plugins/heyspex/hooks/cursor-hooks.json: ${pluginOnDisk === null ? "missing" : "stale"} \u2014 regenerate with \`npm run hooks:install-cursor\``
3294
+ );
3295
+ }
3296
+ if (KIT.customer) {
3297
+ const plan = launcherPlan();
3298
+ if (plan.write) {
3299
+ issues.push(`${CUSTOMER_LAUNCHER}: ${plan.reason === "missing" ? "missing" : "stale"} \u2014 the hook rows point at it`);
3300
+ }
3301
+ }
3302
+ if (json) {
3303
+ console.log(JSON.stringify({ ok: issues.length === 0, issues, total: ACTIVE_HOOKS.length, customer: KIT.customer }, null, 2));
3304
+ process.exit(issues.length === 0 ? 0 : 1);
3305
+ }
3306
+ if (issues.length === 0) {
3307
+ console.log(`install-cursor-hooks --check: OK (${ACTIVE_HOOKS.length} hooks registered)`);
3308
+ process.exit(0);
3309
+ }
3310
+ console.error("install-cursor-hooks --check: drift detected");
3311
+ for (const issue of issues) console.error(` - ${issue}`);
3312
+ console.error(" Run `node scripts/install-cursor-hooks.mjs` to repair.");
3313
+ process.exit(1);
3314
+ }
3315
+ function uninstall() {
3316
+ const settings = readSettings();
3317
+ if (!settings.hooks) {
3318
+ console.log("install-cursor-hooks: nothing to uninstall (no hooks block).");
3319
+ return;
3320
+ }
3321
+ let removed = 0;
3322
+ for (const [event, entries] of Object.entries(settings.hooks)) {
3323
+ if (!Array.isArray(entries)) continue;
3324
+ const keep = entries.filter((raw) => !isShimEntry(raw) && !isLegacyEntry(raw));
3325
+ removed += entries.length - keep.length;
3326
+ if (keep.length === 0) delete settings.hooks[event];
3327
+ else settings.hooks[event] = keep;
3328
+ }
3329
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
3330
+ writeSettings(settings);
3331
+ console.log(`install-cursor-hooks: uninstalled ${removed} hook entr${removed === 1 ? "y" : "ies"}.`);
3332
+ }
3333
+ if (invokedDirectly(import.meta.url, "install-cursor-hooks.mjs")) {
3334
+ const argv = process.argv.slice(2);
3335
+ const json = argv.includes("--json");
3336
+ if (argv.includes("--uninstall")) uninstall();
3337
+ else if (argv.includes("--check")) check({ json });
3338
+ else install({ json });
3339
+ }
3340
+
3341
+ // spex-console/src/lib/cli/install.ts
3342
+ init_credentials();
3343
+
3344
+ // spex-console/src/lib/cli/connect-checks.ts
3345
+ import { dirname as dirname5 } from "node:path";
3346
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
3347
+
3348
+ // spex-console/src/lib/dev-mode.ts
3349
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
3350
+ import { join as join10 } from "node:path";
3351
+ init_credentials();
3352
+
3353
+ // spex-console/src/lib/cli/connect-checks.ts
3354
+ init_credentials();
3355
+
3356
+ // spex-console/src/lib/cli/satellite.ts
3357
+ import { resolve as resolveNodePath } from "node:path";
3358
+ import process2 from "node:process";
3359
+
965
3360
  // spex-console/src/lib/cli/principle-selection.ts
966
- import { basename as basename2 } from "node:path";
3361
+ import { basename as basename3 } from "node:path";
967
3362
  var PRINCIPLES_INDEX_DEST = "specs/principles-index.md";
968
3363
  function principleKey(row) {
969
- const base = basename2(row.dest, ".md");
3364
+ const base = basename3(row.dest, ".md");
970
3365
  return base.startsWith("principle-") ? base.slice("principle-".length) : base;
971
3366
  }
972
3367
  function principleWikilink(row) {
973
- return basename2(row.dest, ".md");
3368
+ return basename3(row.dest, ".md");
974
3369
  }
975
3370
  function parsePrinciplesFlag(value, rows) {
976
3371
  const principleRows = rows.filter((row) => row.group === "principle");
@@ -1015,6 +3410,7 @@ function principleManifestFilter(selected) {
1015
3410
  }
1016
3411
  function principlesIndexHook(selected, rows) {
1017
3412
  return (ctx) => {
3413
+ if (ctx.rows.some((r) => r.dest === PRINCIPLES_INDEX_DEST && r.action === "kept")) return;
1018
3414
  const indexRow = rows.find((row) => row.dest === PRINCIPLES_INDEX_DEST);
1019
3415
  if (!indexRow || indexRow.content === void 0) {
1020
3416
  throw new Error(
@@ -1031,10 +3427,292 @@ function principlesIndexHook(selected, rows) {
1031
3427
  };
1032
3428
  }
1033
3429
 
3430
+ // spex-console/src/lib/cli/receipt.ts
3431
+ var RECEIPT_AGREEMENT_FIELDS = [
3432
+ "vault_id",
3433
+ "workspace_id",
3434
+ "account_id",
3435
+ "active_vault_id"
3436
+ ];
3437
+ var ALL_FIELDS = [
3438
+ ...RECEIPT_AGREEMENT_FIELDS,
3439
+ "display_name",
3440
+ "archived"
3441
+ ];
3442
+
3443
+ // spex-console/src/lib/cli/vault-workspace-shared.ts
3444
+ import { readFileSync as readFileSync10 } from "node:fs";
3445
+
3446
+ // spex-console/src/lib/indexer/parser.ts
3447
+ var import_gray_matter = __toESM(require_gray_matter(), 1);
3448
+ import { createHash as createHash2 } from "node:crypto";
3449
+ import { promises as fs3 } from "node:fs";
3450
+ import path4 from "node:path";
3451
+
3452
+ // spex-console/src/lib/cli/install.ts
3453
+ var MCP_REL = ["vendor", "heyspex-mcp", "cli.mjs"];
3454
+ var PORTABLE_MCP_CLI = Object.freeze({
3455
+ claude: `\${CLAUDE_PROJECT_DIR:-.}/${PORTABLE_PACKAGE_DIR}/${MCP_REL.join("/")}`,
3456
+ cursor: `\${workspaceFolder}/${PORTABLE_PACKAGE_DIR}/${MCP_REL.join("/")}`
3457
+ });
3458
+ function resolveCallerPath(p, env, fallbackCwd) {
3459
+ const trimmed = p?.trim() ?? "";
3460
+ if (!trimmed) return "";
3461
+ if (isAbsolute3(trimmed)) return trimmed;
3462
+ const base = env.HS_INVOKED_CWD?.trim() || fallbackCwd;
3463
+ return resolveNodePath2(base, trimmed);
3464
+ }
3465
+ var DEFAULT_REMOTE_MCP_URL = "https://app.heyspex.com/api/mcp";
3466
+ var REMOTE_MCP_TOKEN_ENV = "HEYSPEX_MCP_TOKEN";
3467
+ var PLUGIN_MARKETPLACE_NAME = "heyspex-marketplace";
3468
+ var PLUGIN_INSTALL_SPEC = `heyspex@${PLUGIN_MARKETPLACE_NAME}`;
3469
+ var WRITE_TARGET_KIND = {
3470
+ specsGitkeep: "specs_gitkeep",
3471
+ heyspexYaml: "heyspex_yaml",
3472
+ cursorMcp: "cursor_mcp",
3473
+ claudeMcp: "claude_mcp",
3474
+ claudeMd: "claude_md",
3475
+ agentsMd: "agents_md",
3476
+ statuslineVendor: "statusline_vendor",
3477
+ gitignore: "gitignore",
3478
+ claudeMdTemplate: "claude_md_template",
3479
+ agentsMdTemplate: "agents_md_template"
3480
+ };
3481
+ var INSTALL_WRITE_TARGETS = [
3482
+ {
3483
+ kind: WRITE_TARGET_KIND.specsGitkeep,
3484
+ rel: "specs/.gitkeep",
3485
+ path: (targetDir, joinPath) => joinPath(targetDir, "specs", ".gitkeep"),
3486
+ appliesTo: (_args, opts) => !opts.attached
3487
+ },
3488
+ {
3489
+ kind: WRITE_TARGET_KIND.heyspexYaml,
3490
+ rel: "heyspex.yaml",
3491
+ path: (targetDir, joinPath) => joinPath(targetDir, "heyspex.yaml"),
3492
+ appliesTo: (_args, opts) => !opts.attached
3493
+ },
3494
+ {
3495
+ kind: WRITE_TARGET_KIND.cursorMcp,
3496
+ rel: ".cursor/mcp.json",
3497
+ path: (targetDir, joinPath) => joinPath(targetDir, ".cursor", "mcp.json"),
3498
+ appliesTo: (args) => args.cursor
3499
+ },
3500
+ {
3501
+ kind: WRITE_TARGET_KIND.claudeMcp,
3502
+ rel: ".mcp.json",
3503
+ path: (targetDir, joinPath) => joinPath(targetDir, ".mcp.json"),
3504
+ appliesTo: (args) => args.claude
3505
+ },
3506
+ {
3507
+ kind: WRITE_TARGET_KIND.claudeMd,
3508
+ rel: "CLAUDE.md",
3509
+ path: (targetDir, joinPath) => joinPath(targetDir, "CLAUDE.md"),
3510
+ appliesTo: (args, opts) => args.claude && opts.installContract
3511
+ },
3512
+ {
3513
+ kind: WRITE_TARGET_KIND.agentsMd,
3514
+ rel: "AGENTS.md",
3515
+ path: (targetDir, joinPath) => joinPath(targetDir, "AGENTS.md"),
3516
+ appliesTo: (_args, opts) => opts.installContract
3517
+ },
3518
+ // The customer-template step (--register-vault) writes the project contract
3519
+ // beside a hand-authored CLAUDE.md/AGENTS.md as <file>.template; a rerun
3520
+ // rewrites it when the generated text changed.
3521
+ {
3522
+ kind: WRITE_TARGET_KIND.claudeMdTemplate,
3523
+ rel: "CLAUDE.md.template",
3524
+ path: (targetDir, joinPath) => joinPath(targetDir, "CLAUDE.md.template"),
3525
+ appliesTo: (args, opts) => Boolean(args.registerVault) && !opts.attached && args.claude && opts.installContract
3526
+ },
3527
+ {
3528
+ kind: WRITE_TARGET_KIND.agentsMdTemplate,
3529
+ rel: "AGENTS.md.template",
3530
+ path: (targetDir, joinPath) => joinPath(targetDir, "AGENTS.md.template"),
3531
+ appliesTo: (args, opts) => Boolean(args.registerVault) && !opts.attached && opts.installContract
3532
+ },
3533
+ {
3534
+ kind: WRITE_TARGET_KIND.statuslineVendor,
3535
+ rel: "scripts/claude-statusline",
3536
+ path: (targetDir, joinPath) => joinPath(targetDir, "scripts", "claude-statusline"),
3537
+ appliesTo: (args) => (args.statusline ?? true) && args.claude
3538
+ },
3539
+ {
3540
+ kind: WRITE_TARGET_KIND.gitignore,
3541
+ rel: ".gitignore",
3542
+ path: (targetDir, joinPath) => joinPath(targetDir, ".gitignore"),
3543
+ // A satellite owns no vault content and writes no machine-local
3544
+ // manifests of its own (its .mcp.json/.cursor/mcp.json point AT the
3545
+ // attached vault, not a fresh one this repo would need to gitignore) --
3546
+ // only a real vault bootstrap gets one.
3547
+ appliesTo: (_args, opts) => !opts.attached
3548
+ }
3549
+ ];
3550
+ var INSTALL_HELP = [
3551
+ "Usage:",
3552
+ " hs install [--target <dir>] [--platform <dir>] [--force] [--dry-run] [--json]",
3553
+ " [--no-cursor] [--no-claude] [--claude-apply]",
3554
+ " [--plugin | --plugin-apply] [--plugin-scope user|project|local]",
3555
+ " [--copilot-hooks | --copilot-plugin | --copilot-plugin-apply]",
3556
+ " [--no-statusline]",
3557
+ " [--cc | --command-code] [--register-vault [--workspace <id>]]",
3558
+ " [--remote[=<url>]]",
3559
+ " hs install --attach-vault <vault-id|vault-path> [--target <dir>] [...]",
3560
+ "",
3561
+ "Bootstrap a project vault for HeySpex agent tooling:",
3562
+ " \u2022 specs/ + heyspex.yaml (via hs init)",
3563
+ " \u2022 .cursor/mcp.json \u2014 Cursor MCP (gitignored, machine-local path to platform MCP)",
3564
+ " \u2022 .mcp.json \u2014 Claude Code project MCP (absolute platform path)",
3565
+ " \u2022 CLAUDE.md / AGENTS.md \u2014 thin agent contract pointing at the HeySpex MCP server",
3566
+ " \u2022 agent kit (customer repos) \u2014 the customer-scoped HeySpex hooks in",
3567
+ " .claude/settings.json + .cursor/hooks.json and the /hs-* commands in",
3568
+ " .claude/commands + .cursor/commands, every platform path absolute.",
3569
+ " Scope table: scripts/lib/customer-kit.mjs. Re-runs are no-ops; a command",
3570
+ " file you edited is left alone (--force overwrites it). `hs doctor` flags",
3571
+ " a missing or stale kit.",
3572
+ "",
3573
+ "Satellite repo (--attach-vault): wire a CODE repo to an EXISTING vault instead",
3574
+ "of making it a vault. Writes the MCP manifests + agent contract only, with",
3575
+ "HEYSPEX_VAULT_ROOT pinned to the attached vault's absolute root -- no specs/,",
3576
+ "no heyspex.yaml, no new registry row. The target is the repo you are standing",
3577
+ "in (or --target), never the vault being attached.",
3578
+ "",
3579
+ "That pin IS the attachment, so it must land somewhere: --attach-vault refuses a",
3580
+ "flag combination that writes no stdio manifest (--no-cursor --no-claude), and",
3581
+ "refuses to re-pin over manifests that name a different vault unless you pass",
3582
+ "--force. Re-attaching to the SAME vault is a no-op and says so.",
3583
+ "",
3584
+ '--attach-vault --remote: the stdio pin under "heyspex" in .mcp.json is preserved',
3585
+ "(never replaced by the HTTP form, even with --force) and the cloud form is",
3586
+ 'written under a second key, "heyspex-cloud" -- Claude Code registers both the',
3587
+ "attached-vault local server and the cloud server. Without --force, a pin that",
3588
+ "already names a DIFFERENT vault still refuses and names the conflicting key,",
3589
+ "same as the non-remote case.",
3590
+ "",
3591
+ "Verify with `hs connect --check` from the satellite -- it is the command that",
3592
+ "audits THIS repo's rows and reports the attachment. `hs doctor` resolves",
3593
+ "THROUGH the pin and audits the ATTACHED VAULT repo, so it reports that vault's",
3594
+ "connection state, not the satellite's.",
3595
+ "",
3596
+ "Conflict scan: every install checks the target repo against the known-conflict",
3597
+ "registry (beads, workflow-pm, bd hooks, \u2026). See:",
3598
+ " specs/protocols/customer-vault-local-install-protocol.md",
3599
+ "",
3600
+ "Run from the HeySpex platform repo, or pass --platform to its checkout.",
3601
+ "Target defaults to the current directory.",
3602
+ "",
3603
+ "Flags:",
3604
+ " --target <dir> Project repo to bootstrap (default: cwd).",
3605
+ " --platform <dir> HeySpex platform checkout (default: HS_INSTALL_PLATFORM or dispatcher root).",
3606
+ " --claude-apply Run `claude mcp add --scope project` after writing files.",
3607
+ " --plugin Print the Claude Code plugin registration block (self-hosted",
3608
+ " marketplace at the platform root \u2014 HS-QTBK prototype).",
3609
+ " --plugin-apply Run `claude plugin marketplace add` + `claude plugin install`",
3610
+ " after writing files. Marketplace + plugin names are read from",
3611
+ ` the platform's .claude-plugin/marketplace.json (fallback: ${PLUGIN_INSTALL_SPEC}).`,
3612
+ " --plugin-scope <s> Plugin install scope: user, project (default), or local.",
3613
+ " Implies --plugin.",
3614
+ " --copilot-hooks Write .github/hooks/heyspex.json (GitHub Copilot hooks) into the",
3615
+ " target. Commands resolve node_modules/heyspex first, so an npm-only",
3616
+ " install (no heyspex repo checkout) works. Pick this OR the plugin.",
3617
+ " --copilot-plugin Print `copilot plugin marketplace add '<platform>'` +",
3618
+ " `copilot plugin install <plugin>@<marketplace>` (the platform root is",
3619
+ " node_modules/heyspex for a published install).",
3620
+ " --copilot-plugin-apply Run those two commands when `copilot` is on PATH.",
3621
+ " Requesting the Copilot plugin AND --copilot-hooks warns and skips the",
3622
+ " hooks file (both would fire every hook twice).",
3623
+ " --remote[=<url>] Write .mcp.json for the REMOTE (cloud) MCP endpoint instead of",
3624
+ ` the local stdio server \u2014 the Streamable-HTTP form against`,
3625
+ ` ${DEFAULT_REMOTE_MCP_URL} (or <url>), authenticated by the`,
3626
+ ` env reference \`\${${REMOTE_MCP_TOKEN_ENV}}\` (never the literal token).`,
3627
+ ' With --attach-vault, the stdio pin survives under "heyspex" and',
3628
+ ' this is written under "heyspex-cloud" instead (see above). If',
3629
+ " this machine already ran `hs login` / `hs mcp pair <code>`, the",
3630
+ " printed guidance resolves the bearer from",
3631
+ " ~/.heyspex/credentials.yaml automatically -- no manual export",
3632
+ " needed on that machine.",
3633
+ " --no-statusline Skip the branded statusline (statusLine entry in the target's",
3634
+ " .claude/settings.json + machine-level glyph install).",
3635
+ " --cc, --command-code Print `cmdc mcp add` PowerShell one-liner for Command Code.",
3636
+ " --attach-vault <v> SATELLITE mode: attach this repo to the EXISTING vault named",
3637
+ " by a registry id (`hs vault list`) or the vault repo's path,",
3638
+ " instead of scaffolding specs/ + heyspex.yaml here. The vault",
3639
+ " must already be registered on this machine. Mutually exclusive",
3640
+ " with --register-vault and --workspace, and with any flag",
3641
+ " combination that would leave no stdio manifest to pin.",
3642
+ " Combines with --remote (see --remote above): the pin and the",
3643
+ " cloud endpoint coexist in .mcp.json under separate keys.",
3644
+ " --register-vault Register this repo's vault into ~/.heyspex/registry.json during install,",
3645
+ " and -- when this machine is signed in -- on the console too; both",
3646
+ " sides are read back (GET /api/vaults) and printed as a receipt.",
3647
+ " Re-running changes nothing and sends no cloud write.",
3648
+ " --workspace <id> With --register-vault: ensure this workspace exists, make it active,",
3649
+ " and associate the vault with it (default: the vault's current",
3650
+ " workspace, else your per-account default ws-<account>).",
3651
+ " --no-cursor Skip .cursor/mcp.json.",
3652
+ " --no-claude Skip .mcp.json and Claude instructions.",
3653
+ " --force Overwrite existing MCP manifests and heyspex.yaml.",
3654
+ " --dry-run Print the plan without writing.",
3655
+ " --skip-conflict-check Skip the known-conflict registry scan.",
3656
+ " --json Machine-readable result.",
3657
+ " --account <email|id> Confirm the account this repo is attached to. An interactive",
3658
+ " install asks (continue / sign in as someone else / create an",
3659
+ " account / abort); a non-interactive one with --register-vault or",
3660
+ " --remote refuses without this or --yes. See `hs whoami`.",
3661
+ " --yes Confirm the signed-in account shown, without typing it.",
3662
+ " --help, -h Show this help.",
3663
+ "",
3664
+ "Exit codes: 0 ok \xB7 1 usage \xB7 11 bad-arg \xB7 13 write-failed \xB7 16 sign-in required \xB7",
3665
+ "12 console unreachable (account check) \xB7 17 account not confirmed \xB7",
3666
+ "19 the cloud half did not land (the receipt names the retry) \xB7 21 both sides read back and disagree"
3667
+ ].join("\n");
3668
+
3669
+ // spex-console/bin/run-entry.ts
3670
+ import process3 from "node:process";
3671
+ var BinExit = class extends Error {
3672
+ code;
3673
+ constructor(code) {
3674
+ super(`exit ${code}`);
3675
+ this.name = "BinExit";
3676
+ this.code = code;
3677
+ }
3678
+ };
3679
+ function runEntry(name, main2, options2 = {}) {
3680
+ const { fallbackCode = 1, unhandledLabel = "unhandled" } = options2;
3681
+ void (async () => {
3682
+ try {
3683
+ await main2();
3684
+ } catch (err) {
3685
+ if (err instanceof BinExit) {
3686
+ process3.exitCode = err.code;
3687
+ return;
3688
+ }
3689
+ const message = err instanceof Error ? err.message : String(err);
3690
+ process3.stderr.write(`${name}: ${unhandledLabel}: ${message}
3691
+ `);
3692
+ process3.exitCode = fallbackCode;
3693
+ }
3694
+ })();
3695
+ }
3696
+
1034
3697
  // spex-console/bin/hs-init.ts
1035
- var INTERACTIVE = process2.stdin.isTTY === true;
3698
+ var INTERACTIVE = process4.stdin.isTTY === true;
3699
+ var PKG_ROOT = resolve5(
3700
+ resolveCodeRoot({ packageRoot: resolve5(dirname6(fileURLToPath4(import.meta.url)), "..", "..") })
3701
+ );
3702
+ var CONFIG_SEED_PATH = join11(PKG_ROOT, "vendor", "heyspex-mcp", "config-seed.yaml");
3703
+ function readConfigSeed() {
3704
+ try {
3705
+ return readFileSync11(CONFIG_SEED_PATH, "utf8");
3706
+ } catch {
3707
+ process4.stderr.write(
3708
+ `hs init: warning \u2014 config seed template not found at ${CONFIG_SEED_PATH}; .heyspex/config.yaml was not seeded (run \`hs settings set <key> <value>\` to create it).
3709
+ `
3710
+ );
3711
+ return null;
3712
+ }
3713
+ }
1036
3714
  async function askYesNo(question) {
1037
- const rl = createInterface({ input: process2.stdin, output: process2.stdout });
3715
+ const rl = createInterface({ input: process4.stdin, output: process4.stdout });
1038
3716
  try {
1039
3717
  const answer = (await rl.question(`${question} `)).trim().toLowerCase();
1040
3718
  return answer === "y" || answer === "yes" || answer === "";
@@ -1047,9 +3725,9 @@ async function resolvePrincipleSelection(principlesFlag) {
1047
3725
  return parsePrinciplesFlag(principlesFlag, TEMPLATE_MANIFEST);
1048
3726
  }
1049
3727
  if (INTERACTIVE) {
1050
- process2.stdout.write("Select which universal principle docs to ship with this vault:\n");
3728
+ process4.stdout.write("Select which universal principle docs to ship with this vault:\n");
1051
3729
  for (const choice of listPrincipleChoices(TEMPLATE_MANIFEST)) {
1052
- process2.stdout.write(` - ${choice.key}: ${choice.summary}
3730
+ process4.stdout.write(` - ${choice.key}: ${choice.summary}
1053
3731
  `);
1054
3732
  }
1055
3733
  const selected = await promptPrincipleSelection(askYesNo, TEMPLATE_MANIFEST);
@@ -1058,47 +3736,60 @@ async function resolvePrincipleSelection(principlesFlag) {
1058
3736
  return parsePrinciplesFlag(void 0, TEMPLATE_MANIFEST);
1059
3737
  }
1060
3738
  async function main() {
1061
- const repoRoot = process2.env.HS_VAULT_ROOT ? resolve(process2.env.HS_VAULT_ROOT) : process2.cwd();
1062
- const parsed = parseInitArgs(process2.argv.slice(3));
3739
+ const repoRoot = process4.env.HS_VAULT_ROOT ? resolve5(process4.env.HS_VAULT_ROOT) : process4.cwd();
3740
+ const parsed = parseInitArgs(process4.argv.slice(3));
1063
3741
  if (parsed.help) {
1064
- process2.stdout.write(`${HELP}
3742
+ process4.stdout.write(`${HELP}
1065
3743
  `);
1066
- process2.exit(EXIT.OK);
3744
+ process4.exitCode = EXIT.OK;
3745
+ return;
1067
3746
  }
1068
3747
  if (parsed.error) {
1069
- process2.stderr.write(`hs init: ${parsed.error.message}
3748
+ process4.stderr.write(`hs init: ${parsed.error.message}
1070
3749
  `);
1071
- process2.exit(parsed.error.code);
3750
+ process4.exitCode = parsed.error.code;
3751
+ return;
1072
3752
  }
1073
3753
  const args = parsed.args;
1074
3754
  if (args.customerVaultDir) {
1075
- const here = dirname(fileURLToPath(import.meta.url));
1076
- const platformRoot = resolve(here, "..", "..");
1077
- const targetDir = resolve(args.customerVaultDir);
3755
+ const here = dirname6(fileURLToPath4(import.meta.url));
3756
+ const platformRoot = resolve5(here, "..", "..");
3757
+ const targetDir = resolve5(resolveCallerPath(args.customerVaultDir, process4.env, process4.cwd()));
1078
3758
  const deps = {
1079
3759
  platformRoot,
1080
- exists: (p) => existsSync(p),
1081
- readdir: (p) => readdirSync(p),
3760
+ exists: (p) => existsSync5(p),
3761
+ readdir: (p) => readdirSync2(p),
1082
3762
  mkdirRecursive: (p) => {
1083
- mkdirSync(p, { recursive: true });
3763
+ mkdirSync6(p, { recursive: true });
1084
3764
  },
1085
- writeFile: (p, c) => writeFileSync(p, c, "utf8"),
3765
+ writeFile: (p, c) => writeFileSync6(p, c, "utf8"),
1086
3766
  copyFile: (src, dest) => copyFileSync(src, dest),
1087
- joinPath: (...s) => join3(...s)
3767
+ joinPath: (...s) => join11(...s),
3768
+ readConfigSeed,
3769
+ readFile: (p) => {
3770
+ try {
3771
+ return readFileSync11(p, "utf8");
3772
+ } catch {
3773
+ return null;
3774
+ }
3775
+ },
3776
+ isDirectory: (p) => statSync(p).isDirectory()
1088
3777
  };
1089
3778
  const principlesResult = await resolvePrincipleSelection(args.principles);
1090
3779
  if (!principlesResult.ok) {
1091
- process2.stderr.write(`hs init: ${principlesResult.message}
3780
+ process4.stderr.write(`hs init: ${principlesResult.message}
1092
3781
  `);
1093
- process2.exit(EXIT.INVALID_ARG);
3782
+ process4.exitCode = EXIT.INVALID_ARG;
3783
+ return;
1094
3784
  }
1095
3785
  const selected = principlesResult.selected;
1096
3786
  const notices = [];
1097
3787
  const afterScaffold = [
1098
- customerClaudeMdHook(),
1099
- principlesIndexHook(selected, TEMPLATE_MANIFEST)
3788
+ customerClaudeMdHook((msg) => notices.push(msg)),
3789
+ principlesIndexHook(selected, TEMPLATE_MANIFEST),
3790
+ templatePinHook()
1100
3791
  ];
1101
- if (!args.noGit) {
3792
+ if (args.git) {
1102
3793
  afterScaffold.push(gitFinalizeHook(nodeGitRunner, (msg) => notices.push(msg)));
1103
3794
  }
1104
3795
  const result2 = runCustomerVaultInit({ targetDir }, deps, {
@@ -1107,7 +3798,7 @@ async function main() {
1107
3798
  manifestFilter: principleManifestFilter(selected),
1108
3799
  afterScaffold
1109
3800
  });
1110
- const sink2 = result2.isError ? process2.stderr : process2.stdout;
3801
+ const sink2 = result2.isError ? process4.stderr : process4.stdout;
1111
3802
  if (args.json) {
1112
3803
  sink2.write(`${JSON.stringify({ ...result2.payload, notices })}
1113
3804
  `);
@@ -1117,29 +3808,29 @@ async function main() {
1117
3808
  for (const notice of notices) sink2.write(`${notice}
1118
3809
  `);
1119
3810
  }
1120
- process2.exit(result2.code);
3811
+ process4.exitCode = result2.code;
3812
+ return;
1121
3813
  }
1122
3814
  const result = runInit(args, {
1123
3815
  repoRoot,
1124
- fileExists: (p) => existsSync(p),
3816
+ fileExists: (p) => existsSync5(p),
1125
3817
  readFile: (p) => {
1126
3818
  try {
1127
- return readFileSync4(p, "utf8");
3819
+ return readFileSync11(p, "utf8");
1128
3820
  } catch {
1129
3821
  return null;
1130
3822
  }
1131
3823
  },
1132
- writeFile: (p, c) => writeFileSync(p, c, "utf8"),
1133
- joinPath: (...s) => join3(...s)
3824
+ writeFile: (p, c) => writeFileSync6(p, c, "utf8"),
3825
+ joinPath: (...s) => join11(...s),
3826
+ mkdirRecursive: (p) => {
3827
+ mkdirSync6(p, { recursive: true });
3828
+ },
3829
+ readConfigSeed
1134
3830
  });
1135
- const sink = result.isError ? process2.stderr : process2.stdout;
3831
+ const sink = result.isError ? process4.stderr : process4.stdout;
1136
3832
  sink.write(`${args.json ? JSON.stringify(result.payload) : result.message}
1137
3833
  `);
1138
- process2.exit(result.code);
3834
+ process4.exitCode = result.code;
1139
3835
  }
1140
- main().catch((err) => {
1141
- const message = err instanceof Error ? err.message : String(err);
1142
- process2.stderr.write(`hs init: unhandled: ${message}
1143
- `);
1144
- process2.exit(EXIT.WRITE_FAILED);
1145
- });
3836
+ runEntry("hs init", main, { fallbackCode: EXIT.WRITE_FAILED });