heyspex 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/README.md +21 -0
  2. package/bin/hs.mjs +931 -98
  3. package/bin/spexcode.mjs +262 -0
  4. package/dev-factory/engine/worktree.mjs +740 -0
  5. package/dev-factory/lib/code-repo-root.mjs +67 -0
  6. package/dev-factory/lib/context-budget.mjs +84 -0
  7. package/dev-factory/lib/epic-detect.mjs +190 -0
  8. package/dev-factory/lib/fleet-queue-file.mjs +846 -0
  9. package/dev-factory/lib/fleet-queue.mjs +426 -0
  10. package/dev-factory/lib/fleet-state.mjs +14 -18
  11. package/dev-factory/lib/model-catalog.mjs +338 -0
  12. package/dev-factory/lib/seat-context.mjs +199 -0
  13. package/dev-factory/lib/spec-io.mjs +93 -0
  14. package/dev-factory/lib/sufficiency-score.mjs +8 -2
  15. package/dev-factory/lib/worktree-allocator.mjs +744 -0
  16. package/dev-factory/model-catalog.json +222 -0
  17. package/dist/cli/hs-check.mjs +1085 -103
  18. package/dist/cli/hs-claim.mjs +936 -63
  19. package/dist/cli/hs-connect.mjs +9786 -1910
  20. package/dist/cli/hs-defer.mjs +5267 -3053
  21. package/dist/cli/hs-dev.mjs +564 -0
  22. package/dist/cli/hs-env.mjs +385 -299
  23. package/dist/cli/hs-index-rebuild.mjs +7129 -0
  24. package/dist/cli/hs-index-reconcile.mjs +7852 -0
  25. package/dist/cli/hs-init.mjs +777 -138
  26. package/dist/cli/hs-install.mjs +6583 -485
  27. package/dist/cli/hs-join.mjs +10137 -2368
  28. package/dist/cli/hs-vault-pull.mjs +4659 -5882
  29. package/dist/cli/hs-vault.mjs +3793 -1537
  30. package/dist/cli/hs-workspace.mjs +2550 -524
  31. package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
  32. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
  33. package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
  34. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
  35. package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
  36. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
  37. package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
  38. package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
  39. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
  40. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
  41. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
  42. package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
  43. package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
  44. package/dist/cli-templates/harness-commands/claude/hs-ground.md +28 -0
  45. package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
  46. package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
  47. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
  48. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
  49. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
  50. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
  51. package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
  52. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
  53. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
  54. package/dist/cli-templates/harness-commands/claude/hs-view.md +26 -0
  55. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
  56. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
  57. package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
  58. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
  59. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
  60. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
  61. package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
  62. package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
  63. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +31 -0
  64. package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
  65. package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
  66. package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
  67. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
  68. package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
  69. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
  70. package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
  71. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
  72. package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
  73. package/dist/cli-templates/harness-commands/cursor/hs-view.md +30 -0
  74. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
  75. package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
  76. package/docs/mcp-client-config.md +259 -6
  77. package/package.json +231 -124
  78. package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
  79. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
  80. package/plugins/heyspex/.mcp.json +12 -0
  81. package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
  82. package/plugins/heyspex/bin/run-hook.mjs +62 -0
  83. package/plugins/heyspex/hooks/cursor-hooks.json +227 -0
  84. package/plugins/heyspex/hooks/hooks.json +361 -0
  85. package/plugins/heyspex/mcp.json +13 -0
  86. package/scripts/check-mcp-freshness.mjs +17 -9
  87. package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
  88. package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
  89. package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
  90. package/scripts/claude-hooks/_intent-classifier-router.mjs +410 -0
  91. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +351 -0
  92. package/scripts/claude-hooks/_run.mjs +120 -0
  93. package/scripts/claude-hooks/_transcript-text.mjs +60 -0
  94. package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
  95. package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
  96. package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
  97. package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
  98. package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
  99. package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
  100. package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
  101. package/scripts/claude-hooks/claim-liveness-shared.mjs +575 -0
  102. package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
  103. package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
  104. package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
  105. package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
  106. package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
  107. package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
  108. package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
  109. package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
  110. package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
  111. package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
  112. package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
  113. package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
  114. package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
  115. package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
  116. package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
  117. package/scripts/claude-hooks/inject-communication-style.mjs +66 -0
  118. package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
  119. package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
  120. package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
  121. package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
  122. package/scripts/claude-hooks/intent-classifier-trigger.mjs +247 -0
  123. package/scripts/claude-hooks/log-session-activity.mjs +341 -0
  124. package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
  125. package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
  126. package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
  127. package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
  128. package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
  129. package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
  130. package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
  131. package/scripts/claude-hooks/research-method-guard.mjs +398 -0
  132. package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
  133. package/scripts/claude-hooks/session-end.mjs +449 -0
  134. package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
  135. package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
  136. package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
  137. package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
  138. package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
  139. package/scripts/claude-hooks/wip-checkpoint.mjs +298 -0
  140. package/scripts/cursor-hooks/manifest.mjs +235 -0
  141. package/scripts/dev-server-status.mjs +34 -13
  142. package/scripts/dev-server-stop.mjs +313 -0
  143. package/scripts/heyspex-mode.mjs +268 -0
  144. package/scripts/hs-auto-resolve.mjs +240 -0
  145. package/scripts/hs-auto-source.mjs +398 -0
  146. package/scripts/hs-auto.mjs +134 -0
  147. package/scripts/hs-capture-cli.mjs +326 -0
  148. package/scripts/hs-classifier.mjs +136 -0
  149. package/scripts/hs-companion.mjs +88 -0
  150. package/scripts/hs-gateway.mjs +115 -0
  151. package/scripts/hs-mode.mjs +125 -0
  152. package/scripts/hs-next.mjs +441 -0
  153. package/scripts/hs-settings.mjs +338 -0
  154. package/scripts/hs-view.mjs +333 -0
  155. package/scripts/hs-worktree.mjs +201 -0
  156. package/scripts/install-claude-code-hooks.mjs +270 -0
  157. package/scripts/install-cursor-hooks.mjs +471 -0
  158. package/scripts/lib/agent-env-markers.mjs +38 -0
  159. package/scripts/lib/console-url-resolver.mjs +197 -60
  160. package/scripts/lib/customer-kit.mjs +886 -0
  161. package/scripts/lib/dev-mode.mjs +244 -28
  162. package/scripts/lib/heyspex-home.mjs +72 -0
  163. package/scripts/lib/hs-command.mjs +277 -0
  164. package/scripts/lib/hs-companion-settings.mjs +472 -0
  165. package/scripts/lib/hs-source-commit.mjs +59 -5
  166. package/scripts/lib/hs-source-identity.mjs +526 -0
  167. package/scripts/lib/hs-source-log.mjs +11 -21
  168. package/scripts/lib/hs-source-push.mjs +107 -58
  169. package/scripts/lib/hs-worktree-args.mjs +115 -0
  170. package/scripts/lib/shared-path-helpers.mjs +134 -0
  171. package/scripts/lib/standalone-doctor.mjs +540 -3
  172. package/scripts/lib/subagent-git-lock.mjs +109 -0
  173. package/scripts/lib/template-pin.mjs +34 -0
  174. package/scripts/provenance-lib.mjs +91 -0
  175. package/scripts/stamp-provenance-on-edit.mjs +222 -0
  176. package/scripts/wip-ledger-health.mjs +653 -0
  177. package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
  178. package/spexharness/src/adapters/audit.mjs +197 -0
  179. package/spexharness/src/adapters/claude-code.mjs +441 -0
  180. package/spexharness/src/chat/tui/ansi.mjs +448 -0
  181. package/spexharness/src/chat/tui/palette.mjs +22 -0
  182. package/spexharness/src/chat/tui/select.mjs +208 -0
  183. package/spexharness/src/config-root.mjs +54 -0
  184. package/spexharness/src/context-budget.mjs +141 -0
  185. package/spexharness/src/dev-server-control.mjs +389 -0
  186. package/spexharness/src/doctrine/behaviour.mjs +371 -0
  187. package/spexharness/src/doctrine/corpus.mjs +733 -0
  188. package/spexharness/src/doctrine/inject.mjs +338 -0
  189. package/spexharness/src/doctrine/merge.mjs +184 -0
  190. package/spexharness/src/doctrine/sources.mjs +252 -0
  191. package/spexharness/src/doctrine/synthesize.mjs +401 -0
  192. package/spexharness/src/doctrine/vault-source.mjs +574 -0
  193. package/spexharness/src/doctrine/writer.mjs +176 -0
  194. package/spexharness/src/events.mjs +84 -0
  195. package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
  196. package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
  197. package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
  198. package/spexharness/src/gates/deep-research-cost-gate.mjs +274 -0
  199. package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
  200. package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
  201. package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
  202. package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
  203. package/spexharness/src/gates/git-identity-guard.mjs +61 -0
  204. package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
  205. package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
  206. package/spexharness/src/gates/git-stash-gate.mjs +159 -0
  207. package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
  208. package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
  209. package/spexharness/src/gates/inject-vault-context.mjs +239 -0
  210. package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
  211. package/spexharness/src/gates/plan-review-gate.mjs +233 -0
  212. package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
  213. package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
  214. package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
  215. package/spexharness/src/gates/settings-write-gate.mjs +104 -0
  216. package/spexharness/src/git-reflog-audit.mjs +294 -0
  217. package/spexharness/src/intent-classifier.mjs +194 -0
  218. package/spexharness/src/intent-directive.mjs +541 -0
  219. package/spexharness/src/profile/learn.mjs +551 -0
  220. package/spexharness/src/profile/proposals.mjs +357 -0
  221. package/spexharness/src/profile/vault-inclusion.mjs +386 -0
  222. package/spexharness/src/redact.mjs +151 -0
  223. package/spexharness/src/rule-table.mjs +819 -0
  224. package/spexharness/src/seat-context.mjs +122 -0
  225. package/spexharness/src/session-bootstrap.mjs +512 -0
  226. package/spexharness/src/session-record.mjs +815 -0
  227. package/spexharness/src/token-inventory.mjs +207 -0
  228. package/vendor/heyspex-mcp/account-switch.mjs +95 -0
  229. package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
  230. package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
  231. package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
  232. package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
  233. package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
  234. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +46 -5
  235. package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
  236. package/vendor/heyspex-mcp/capture-console-client.mjs +80 -1
  237. package/vendor/heyspex-mcp/capture-focus.mjs +1035 -29
  238. package/vendor/heyspex-mcp/capture-outbox.mjs +5 -1
  239. package/vendor/heyspex-mcp/capture-tools.mjs +1219 -0
  240. package/vendor/heyspex-mcp/claim-tools.mjs +1124 -0
  241. package/vendor/heyspex-mcp/cli-verbs.mjs +2076 -0
  242. package/vendor/heyspex-mcp/cli.mjs +201 -15524
  243. package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
  244. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +412 -0
  245. package/vendor/heyspex-mcp/config-seed.yaml +15 -0
  246. package/vendor/heyspex-mcp/config-settings.mjs +290 -7
  247. package/vendor/heyspex-mcp/console-fetch.mjs +657 -16
  248. package/vendor/heyspex-mcp/console-transport.mjs +166 -0
  249. package/vendor/heyspex-mcp/credentials.mjs +584 -0
  250. package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
  251. package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
  252. package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
  253. package/vendor/heyspex-mcp/design-tools.mjs +4 -1
  254. package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
  255. package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
  256. package/vendor/heyspex-mcp/dev-tools.mjs +124 -0
  257. package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
  258. package/vendor/heyspex-mcp/events-tools.mjs +207 -0
  259. package/vendor/heyspex-mcp/filing-tools.mjs +688 -0
  260. package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
  261. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +69 -14
  262. package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
  263. package/vendor/heyspex-mcp/folder-trust-store.mjs +59 -0
  264. package/vendor/heyspex-mcp/http-transport.mjs +300 -0
  265. package/vendor/heyspex-mcp/list-tools.mjs +257 -0
  266. package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
  267. package/vendor/heyspex-mcp/mcp-config.mjs +94 -0
  268. package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
  269. package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
  270. package/vendor/heyspex-mcp/occupied-keys.mjs +141 -0
  271. package/vendor/heyspex-mcp/on-deck-brief.mjs +3360 -0
  272. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
  273. package/vendor/heyspex-mcp/package-root.mjs +32 -0
  274. package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
  275. package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
  276. package/vendor/heyspex-mcp/plan-push.mjs +906 -21
  277. package/vendor/heyspex-mcp/plan-review.mjs +124 -30
  278. package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
  279. package/vendor/heyspex-mcp/procedure-handlers.mjs +325 -0
  280. package/vendor/heyspex-mcp/procedure-tools.mjs +540 -351
  281. package/vendor/heyspex-mcp/promote-capture.mjs +386 -69
  282. package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
  283. package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
  284. package/vendor/heyspex-mcp/promote-research.mjs +660 -53
  285. package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
  286. package/vendor/heyspex-mcp/prompts.mjs +1460 -0
  287. package/vendor/heyspex-mcp/read-path.mjs +29 -9
  288. package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
  289. package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
  290. package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
  291. package/vendor/heyspex-mcp/register-spec.mjs +266 -54
  292. package/vendor/heyspex-mcp/request-context.mjs +59 -0
  293. package/vendor/heyspex-mcp/research-focus.mjs +207 -4
  294. package/vendor/heyspex-mcp/research-handlers.mjs +623 -0
  295. package/vendor/heyspex-mcp/research-tools.mjs +4 -4
  296. package/vendor/heyspex-mcp/response-budget.mjs +348 -0
  297. package/vendor/heyspex-mcp/review-focus.mjs +25 -1
  298. package/vendor/heyspex-mcp/search-specs.mjs +280 -0
  299. package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
  300. package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
  301. package/vendor/heyspex-mcp/session-owner.mjs +108 -0
  302. package/vendor/heyspex-mcp/session-tools.mjs +152 -0
  303. package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
  304. package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
  305. package/vendor/heyspex-mcp/spec-edit-tools.mjs +446 -0
  306. package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
  307. package/vendor/heyspex-mcp/split-research.mjs +301 -56
  308. package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
  309. package/vendor/heyspex-mcp/submit-for-review.mjs +132 -6
  310. package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
  311. package/vendor/heyspex-mcp/summary-write-tools.mjs +224 -0
  312. package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
  313. package/vendor/heyspex-mcp/test-helpers.mjs +108 -1
  314. package/vendor/heyspex-mcp/tool-catalog.mjs +2813 -0
  315. package/vendor/heyspex-mcp/tool-dispatch.mjs +639 -0
  316. package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
  317. package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
  318. package/vendor/heyspex-mcp/vault-io.mjs +724 -0
  319. package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
  320. package/vendor/heyspex-mcp/vault-resolve.mjs +366 -68
  321. package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
  322. package/vendor/heyspex-mcp/whoami.mjs +91 -0
@@ -1,19 +1,182 @@
1
1
  import { createRequire as __hsCreateRequire } from 'node:module';
2
2
  const require = __hsCreateRequire(import.meta.url);
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+
8
+ // vendor/heyspex-mcp/credentials.mjs
9
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
10
+ import os, { homedir } from "node:os";
11
+ import { dirname, join, resolve as resolve2 } from "node:path";
12
+ var init_credentials = __esm({
13
+ "vendor/heyspex-mcp/credentials.mjs"() {
14
+ "use strict";
15
+ }
16
+ });
17
+
18
+ // scripts/lib/dev-mode.mjs
19
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
20
+ import { homedir as homedir2 } from "node:os";
21
+ import { join as join2 } from "node:path";
22
+ function devModeFilePath(homeDir = homedir2()) {
23
+ return join2(homeDir, ".heyspex", DEV_MODE_FILENAME);
24
+ }
25
+ function normalizeMode(raw) {
26
+ const v = String(raw ?? "").trim().toLowerCase();
27
+ if (v === "prod" || v === "production") return { mode: "prod", reliance: "cloud" };
28
+ if (v === "dev") return { mode: "dev", reliance: null };
29
+ if (v === "local") return { mode: "dev", reliance: "local" };
30
+ if (v === "cloud") return { mode: "dev", reliance: "cloud" };
31
+ return null;
32
+ }
33
+ function normalizeReliance(raw) {
34
+ const v = String(raw ?? "").trim().toLowerCase();
35
+ return v === "local" || v === "cloud" ? v : null;
36
+ }
37
+ function deriveSemantics(mode, reliance) {
38
+ const effectiveReliance = mode === "prod" ? "cloud" : reliance ?? "local";
39
+ const local = mode === "dev" && effectiveReliance === "local";
40
+ return {
41
+ reliance: effectiveReliance,
42
+ consoleUrl: local ? LOCAL_CONSOLE_URL : CLOUD_CONSOLE_URL,
43
+ readTier: local ? "disk" : "neon",
44
+ vaultWriteMode: local ? "local" : "cloud"
45
+ };
46
+ }
47
+ function readLocalDevModeRecord({ homeDir = homedir2(), readFile = defaultReadFile } = {}) {
48
+ let raw;
49
+ try {
50
+ raw = readFile(devModeFilePath(homeDir));
51
+ } catch {
52
+ return { record: null, parseError: null };
53
+ }
54
+ try {
55
+ const parsed = JSON.parse(raw);
56
+ const mode = parsed?.mode === "prod" || parsed?.mode === "dev" ? parsed.mode : null;
57
+ const reliance = normalizeReliance(parsed?.reliance);
58
+ if (!mode || mode === "dev" && !reliance) {
59
+ return { record: null, parseError: `invalid dev-mode record: ${JSON.stringify(parsed)}` };
60
+ }
61
+ return {
62
+ record: {
63
+ mode,
64
+ reliance: mode === "prod" ? "cloud" : reliance,
65
+ console_url: typeof parsed.console_url === "string" ? parsed.console_url : null,
66
+ updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : null,
67
+ source: typeof parsed.source === "string" ? parsed.source : null
68
+ },
69
+ parseError: null
70
+ };
71
+ } catch (err) {
72
+ return { record: null, parseError: `unparseable dev-mode.json: ${String(err?.message ?? err)}` };
73
+ }
74
+ }
75
+ function resolveDevMode({
76
+ env = process.env,
77
+ homeDir = homedir2(),
78
+ readFile = defaultReadFile,
79
+ cloudSetting = void 0
80
+ } = {}) {
81
+ const local = readLocalDevModeRecord({ homeDir, readFile });
82
+ const cloudKnown = cloudSetting !== void 0;
83
+ const cloudValue = cloudKnown && cloudSetting ? {
84
+ mode: cloudSetting.mode === "dev" ? "dev" : "prod",
85
+ reliance: normalizeReliance(cloudSetting.reliance) ?? "cloud"
86
+ } : null;
87
+ let mode;
88
+ let reliance;
89
+ let source;
90
+ let relianceSource;
91
+ const envParsed = normalizeMode(env.HEYSPEX_DEV_MODE);
92
+ if (envParsed) {
93
+ mode = envParsed.mode;
94
+ const envReliance = envParsed.reliance ?? normalizeReliance(env.HEYSPEX_DEV_RELIANCE);
95
+ reliance = envReliance ?? "local";
96
+ source = "env";
97
+ relianceSource = envReliance ? "env" : "assumed-local";
98
+ } else if (local.record) {
99
+ mode = local.record.mode;
100
+ reliance = local.record.reliance;
101
+ source = "local-override";
102
+ } else if (cloudValue) {
103
+ mode = cloudValue.mode;
104
+ reliance = cloudValue.reliance;
105
+ source = "cloud-setting";
106
+ } else {
107
+ mode = "prod";
108
+ reliance = "cloud";
109
+ source = "default";
110
+ }
111
+ const semantics = deriveSemantics(mode, reliance);
112
+ const consoleUrl = source === "local-override" && local.record?.console_url ? local.record.console_url.replace(/\/+$/, "") : semantics.consoleUrl;
113
+ const staleAgainstCloud = source === "local-override" && cloudValue !== null && (cloudValue.mode !== mode || mode === "dev" && cloudValue.reliance !== semantics.reliance);
114
+ return {
115
+ mode,
116
+ reliance: semantics.reliance,
117
+ consoleUrl,
118
+ readTier: semantics.readTier,
119
+ vaultWriteMode: semantics.vaultWriteMode,
120
+ sources: {
121
+ mode: source,
122
+ reliance: relianceSource ?? source,
123
+ consoleUrl: source === "local-override" && local.record?.console_url ? "local-override" : "derived",
124
+ readTier: "derived",
125
+ vaultWriteMode: "derived"
126
+ },
127
+ localOverride: {
128
+ present: local.record !== null,
129
+ record: local.record,
130
+ parseError: local.parseError,
131
+ staleAgainstCloud
132
+ },
133
+ cloudSetting: { known: cloudKnown, value: cloudValue }
134
+ };
135
+ }
136
+ var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile;
137
+ var init_dev_mode = __esm({
138
+ "scripts/lib/dev-mode.mjs"() {
139
+ "use strict";
140
+ init_credentials();
141
+ CLOUD_CONSOLE_URL = "https://app.heyspex.com";
142
+ LOCAL_CONSOLE_URL = "http://localhost:3000";
143
+ DEV_MODE_FILENAME = "dev-mode.json";
144
+ defaultReadFile = (p) => readFileSync2(p, "utf8");
145
+ }
146
+ });
3
147
 
4
148
  // spex-console/bin/hs-init.ts
5
149
  import {
6
150
  copyFileSync,
7
- existsSync,
8
- mkdirSync,
151
+ existsSync as existsSync4,
152
+ mkdirSync as mkdirSync4,
9
153
  readdirSync,
10
- readFileSync as readFileSync4,
11
- writeFileSync
154
+ readFileSync as readFileSync8,
155
+ statSync,
156
+ writeFileSync as writeFileSync4
12
157
  } from "node:fs";
13
- import { dirname, join as join3, resolve } from "node:path";
14
- import process2 from "node:process";
158
+ import { dirname as dirname5, join as join9, resolve as resolve5 } from "node:path";
159
+ import process3 from "node:process";
15
160
  import { createInterface } from "node:readline/promises";
16
- import { fileURLToPath } from "node:url";
161
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
162
+
163
+ // scripts/lib/shared-path-helpers.mjs
164
+ import { isAbsolute, resolve, sep } from "node:path";
165
+ function resolveCodeRoot({
166
+ explicit,
167
+ packageRoot,
168
+ vaultRoot,
169
+ env = process.env
170
+ } = {}) {
171
+ const fromExplicit = typeof explicit === "string" ? explicit.trim() : "";
172
+ if (fromExplicit) return fromExplicit;
173
+ const fromEnv = typeof env?.HS_PKG_ROOT === "string" ? env.HS_PKG_ROOT.trim() : "";
174
+ if (fromEnv) return fromEnv;
175
+ const fromPackage = typeof packageRoot === "string" ? packageRoot.trim() : "";
176
+ if (fromPackage) return fromPackage;
177
+ const fromVault = typeof vaultRoot === "string" ? vaultRoot.trim() : "";
178
+ return fromVault;
179
+ }
17
180
 
18
181
  // spex-console/src/lib/cli/customer-claude-md.ts
19
182
  function renderCustomerClaudeMd({
@@ -65,15 +228,38 @@ The vault under \`specs/\` is the only persistent queue. File new work via \`hey
65
228
  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
229
  `;
67
230
  }
68
- function customerClaudeMdHook() {
231
+ function writeProjectContract(ctx, options = {}) {
232
+ const content = renderCustomerClaudeMd({
233
+ vaultName: ctx.vaultName,
234
+ templateVersion: ctx.templateVersion
235
+ });
236
+ const results = [];
237
+ for (const file of options.files ?? ["CLAUDE.md"]) {
238
+ const destPath = ctx.deps.joinPath(ctx.targetDir, file);
239
+ if (!ctx.deps.exists(destPath)) {
240
+ ctx.deps.writeFile(destPath, content);
241
+ ctx.recordWrite?.(destPath);
242
+ results.push({ file, action: "created", path: destPath });
243
+ continue;
244
+ }
245
+ if (ctx.deps.readFile?.(destPath) === content) {
246
+ results.push({ file, action: "unchanged" });
247
+ continue;
248
+ }
249
+ const templatePath = ctx.deps.joinPath(ctx.targetDir, `${file}.template`);
250
+ if (ctx.deps.readFile?.(templatePath) !== content) {
251
+ ctx.deps.writeFile(templatePath, content);
252
+ ctx.recordWrite?.(templatePath);
253
+ }
254
+ const hint = `${file} already exists - left untouched. Generated contract written to ${file}.template; compare with: diff ${file} ${file}.template`;
255
+ options.notify?.(hint);
256
+ results.push({ file, action: "template", path: templatePath, hint });
257
+ }
258
+ return results;
259
+ }
260
+ function customerClaudeMdHook(notify) {
69
261
  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);
262
+ writeProjectContract(ctx, { notify });
77
263
  };
78
264
  }
79
265
 
@@ -81,69 +267,182 @@ function customerClaudeMdHook() {
81
267
  import { randomUUID } from "node:crypto";
82
268
 
83
269
  // 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";
270
+ init_dev_mode();
271
+ import { readFileSync as readFileSync3 } from "node:fs";
272
+ import { homedir as homedir3 } from "node:os";
273
+ import { join as join3 } from "node:path";
274
+ var DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
88
275
  function trimTrailingSlash(url) {
89
276
  return url.replace(/\/+$/, "");
90
277
  }
91
- function readCredentialsConsoleUrl({ homeDir = homedir() } = {}) {
278
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
279
+ function isLoopbackConsoleUrl(url) {
280
+ if (typeof url !== "string" || !url) return false;
281
+ try {
282
+ return LOOPBACK_HOSTS.has(new URL(url).hostname.toLowerCase());
283
+ } catch {
284
+ return false;
285
+ }
286
+ }
287
+ var staleLoopbackCredentialsWarned = false;
288
+ function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
289
+ if (staleLoopbackCredentialsWarned) return;
290
+ staleLoopbackCredentialsWarned = true;
291
+ try {
292
+ log(
293
+ `[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).
294
+ `
295
+ );
296
+ } catch {
297
+ }
298
+ }
299
+ function readCredentialsConsoleUrl({ homeDir = homedir3(), readFile = defaultReadFile2 } = {}) {
92
300
  try {
93
- const raw = readFileSync(join(homeDir, ".heyspex", "credentials.yaml"), "utf8");
94
- const match = raw.match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
301
+ const raw = readFile(join3(homeDir, ".heyspex", "credentials.yaml"));
302
+ const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
95
303
  const value = match?.[1]?.trim();
96
304
  return value && value.length > 0 ? trimTrailingSlash(value) : null;
97
305
  } catch {
98
306
  return null;
99
307
  }
100
308
  }
101
- function readDiscoveryFileConsoleUrl(repoRoot) {
309
+ function defaultReadFile2(p) {
310
+ return readFileSync3(p, "utf8");
311
+ }
312
+ function readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery = defaultReadFile2 } = {}) {
102
313
  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
- }
314
+ try {
315
+ const discovery = String(readDiscovery(join3(repoRoot, ".heyspex", "console-url")) ?? "").trim();
316
+ if (discovery.length > 0) return trimTrailingSlash(discovery);
317
+ } catch {
113
318
  }
114
319
  return null;
115
320
  }
116
321
  var ConsoleUrlUnresolvedError = class extends Error {
117
322
  constructor() {
118
323
  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.`
324
+ "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
325
  );
121
326
  this.name = "ConsoleUrlUnresolvedError";
122
327
  }
123
328
  };
329
+ function resolveModeEnvelope({ env, homeDir }) {
330
+ try {
331
+ const resolved = resolveDevMode({ env, homeDir });
332
+ return {
333
+ reliance: resolved.reliance === "cloud" ? "cloud" : "local",
334
+ consoleUrl: resolved.consoleUrl || DEFAULT_CONSOLE_URL
335
+ };
336
+ } catch {
337
+ return { reliance: "local", consoleUrl: DEFAULT_CONSOLE_URL };
338
+ }
339
+ }
124
340
  function resolveConsoleUrl({
125
341
  repoRoot,
126
342
  env = process.env,
127
- homeDir = homedir(),
128
- strict = false
343
+ homeDir = homedir3(),
344
+ strict = false,
345
+ devMode,
346
+ log = (line) => process.stderr.write(line),
347
+ readFile = defaultReadFile2,
348
+ readDiscovery = defaultReadFile2
129
349
  } = {}) {
130
- const envUrl = (env.HEYSPEX_CONSOLE_URL ?? env.HEYSPEX_COCKPIT_URL)?.trim();
350
+ const envUrl = env.HEYSPEX_CONSOLE_URL?.trim();
131
351
  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;
352
+ const mode = devMode ?? resolveModeEnvelope({ env, homeDir });
353
+ const cloud = mode.reliance === "cloud";
354
+ const derivedUrl = trimTrailingSlash(
355
+ cloud && isLoopbackConsoleUrl(mode.consoleUrl) ? CLOUD_CONSOLE_URL : mode.consoleUrl || DEFAULT_CONSOLE_URL
356
+ );
357
+ const credentialsUrl = readCredentialsConsoleUrl({ homeDir, readFile });
358
+ if (credentialsUrl) {
359
+ if (cloud && isLoopbackConsoleUrl(credentialsUrl)) {
360
+ warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log });
361
+ } else {
362
+ return credentialsUrl;
363
+ }
364
+ }
365
+ if (!cloud) {
366
+ const discoveryUrl = readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery });
367
+ if (discoveryUrl) return discoveryUrl;
368
+ if (strict) throw new ConsoleUrlUnresolvedError();
369
+ }
370
+ return derivedUrl;
138
371
  }
139
372
 
140
373
  // 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";
374
+ import path from "node:path";
375
+
376
+ // scripts/lib/hs-command.mjs
377
+ import { existsSync } from "node:fs";
378
+ import { delimiter, dirname as dirname2, join as join4 } from "node:path";
379
+ import { fileURLToPath } from "node:url";
380
+ var HS_BIN_NAME = "hs";
381
+ var HS_DISPATCHER_PATH = (() => {
382
+ try {
383
+ const here = dirname2(fileURLToPath(import.meta.url));
384
+ return join4(dirname2(dirname2(here)), "bin", "hs.mjs");
385
+ } catch {
386
+ return null;
387
+ }
388
+ })();
389
+ function shellQuote(p) {
390
+ return /[\\\s]/.test(p) ? `"${p}"` : p;
391
+ }
392
+ var HS_DISPATCHER_PREFIX = HS_DISPATCHER_PATH ? `node ${shellQuote(HS_DISPATCHER_PATH)}` : HS_BIN_NAME;
393
+
394
+ // vendor/heyspex-mcp/console-fetch.mjs
395
+ init_credentials();
396
+ import { readFileSync as readFileSync6 } from "node:fs";
397
+ import { homedir as homedir6 } from "node:os";
398
+ import { join as join7 } from "node:path";
399
+
400
+ // vendor/heyspex-mcp/session-owner.mjs
401
+ init_credentials();
402
+ import { readFileSync as readFileSync4 } from "node:fs";
403
+ import { homedir as homedir4 } from "node:os";
404
+ import { join as join5 } from "node:path";
405
+
406
+ // vendor/heyspex-mcp/vault-less-endpoints.mjs
407
+ var VAULT_LESS_ENDPOINTS = Object.freeze([
408
+ "* /api/mcp/session",
409
+ "* /api/mcp/pairing-codes",
410
+ "POST /api/vaults",
411
+ "GET /api/vaults",
412
+ "POST /api/workspaces",
413
+ "GET /api/workspaces",
414
+ "* /api/admin/dev-mode"
415
+ ]);
416
+
417
+ // vendor/heyspex-mcp/vault-resolve.mjs
418
+ import { existsSync as existsSync2, readFileSync as readFileSync5 } from "node:fs";
419
+ import { homedir as homedir5, tmpdir } from "node:os";
420
+ import { dirname as dirname3, join as join6, resolve as resolve3 } from "node:path";
421
+
422
+ // vendor/heyspex-mcp/console-fetch.mjs
423
+ var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
424
+
425
+ // spex-console/src/lib/cli/control-shared.ts
426
+ init_credentials();
427
+
428
+ // spex-console/src/lib/vault-registry/registry-file.ts
429
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync7, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "node:fs";
430
+ import { homedir as homedir7 } from "node:os";
431
+ import { dirname as dirname4, join as join8, resolve as resolve4 } from "node:path";
432
+
433
+ // spex-console/src/lib/cli/control-shared.ts
144
434
  var EXIT = {
145
435
  /** Success. */
146
436
  OK: 0,
437
+ /**
438
+ * A read-only CHECK ran correctly and its answer is "no" — `hs connect
439
+ * --check` on an unconnected repo. Distinct from {@link EXIT.USAGE} so a
440
+ * script can tell "you typed the command wrong" from "the command worked,
441
+ * the repo is not connected". Both were `1` until 2026-09-18, which made
442
+ * `--check` unusable in a conditional: every caller had to parse stdout to
443
+ * find out which of the two had happened.
444
+ */
445
+ CHECK_FAILED: 2,
147
446
  /** Usage error — unknown subcommand/flag, or a missing required argument. */
148
447
  USAGE: 1,
149
448
  /** Claim race lost — the spec is already claimed by another agent (409). */
@@ -165,12 +464,63 @@ var EXIT = {
165
464
  * from `USAGE`/`INVALID_ARG` — this is an authorization refusal, not a
166
465
  * malformed command.
167
466
  */
168
- FORBIDDEN: 15
467
+ FORBIDDEN: 15,
468
+ /**
469
+ * A CLOUD write refused because no account id could be resolved (no
470
+ * `HS_ACCOUNT_ID` env var, no `account_id:` in `~/.heyspex/config.yaml`) --
471
+ * `resolveCloudAccountIdOrRefuse` in `vault-workspace-shared.ts`
472
+ * ([[HS-YU1S-UAGY-TASK-cloud-writes-refuse-without-a]]). Distinct from
473
+ * `FORBIDDEN` -- this is "not signed in at all", not "signed in but not
474
+ * authorized for this override".
475
+ */
476
+ SIGN_IN_REQUIRED: 16,
477
+ /**
478
+ * Signed in, but the account a repo would be attached to was not confirmed:
479
+ * a non-interactive install/connect with no matching `--account` / `--yes`,
480
+ * an `account_id` that disagrees with the bearer's user, or the human
481
+ * aborting the identity prompt (`install-identity.ts`,
482
+ * [[HS-HAQI-KODY-DIAG-hs-install-and-hs-connect-bind-a-new]]).
483
+ */
484
+ IDENTITY_UNCONFIRMED: 17,
485
+ /**
486
+ * The repo this verb runs in is not registered on this machine (no
487
+ * `~/.heyspex/registry.json` row for its root, or a legacy slug row with no
488
+ * cloud `vault_id` in its heyspex.yaml), so the call was refused LOCALLY —
489
+ * nothing was sent. Names `hs connect`
490
+ * ([[HS-MVG4-WW9R-TASK-mcp-and-hs-name-account-vault-on-every]]).
491
+ */
492
+ VAULT_NOT_REGISTERED: 18,
493
+ /**
494
+ * The local half of a setup verb (`hs connect`, `hs vault *`, `hs workspace
495
+ * *`) was written but the CLOUD half did not land -- the console refused
496
+ * it or was never reached. The verb's receipt names the failed half and the
497
+ * exact command to re-run
498
+ * ([[HS-STCY-WW9R-TASK-add-a-shared-receipt-that-reads-back]], invariant 6).
499
+ * Distinct from {@link EXIT.WRITE_FAILED} so a script can tell "re-run to
500
+ * sync the cloud" from a generic server failure.
501
+ */
502
+ CLOUD_WRITE_FAILED: 19,
503
+ /**
504
+ * The repo's committed `heyspex.yaml` records `account: <A>` but the machine
505
+ * is signed in as another account, so `hs connect` refused and wrote
506
+ * nothing (marker, registry, cloud). Repair: `hs login --switch-account <A>`
507
+ * ([[HS-VWUD-WW9R-TASK-record-the-connecting-account-in]], invariant 4).
508
+ */
509
+ ACCOUNT_MISMATCH: 20,
510
+ /**
511
+ * Both sides of a setup verb's receipt were read back and still DISAGREE
512
+ * after a short bounded re-read of the cloud side (a briefly stale read is
513
+ * absorbed first). The write may well have landed; what the verb cannot
514
+ * claim is that this machine and the console now say the same thing. The
515
+ * receipt prints the diffs and the command to re-run
516
+ * ([[HS-FKSR-WW9R-TASK-make-one-hs-connect-run-create-a-cloud]], invariant 6).
517
+ * Distinct from {@link EXIT.CLOUD_WRITE_FAILED} (a write that did not land).
518
+ */
519
+ RECEIPT_DISAGREE: 21
169
520
  };
170
521
 
171
522
  // spex-console/src/lib/cli/init.ts
172
523
  var HEYSPEX_YAML_SCHEMA_VERSION = 2;
173
- var DEFAULT_CONSOLE_URL2 = resolveConsoleUrl();
174
524
  function mintVaultId() {
175
525
  return randomUUID();
176
526
  }
@@ -181,6 +531,30 @@ function parseVaultId(content) {
181
531
  const unquoted = raw.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
182
532
  return unquoted.length > 0 ? unquoted : null;
183
533
  }
534
+ function parseAccountId(content) {
535
+ const m = content.match(/^account:[ \t]*(.+?)\s*$/m);
536
+ if (!m) return null;
537
+ const raw = m[1].replace(/\s+#.*$/, "").trim();
538
+ const unquoted = raw.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
539
+ return unquoted.length > 0 ? unquoted : null;
540
+ }
541
+ var MARKER_ACCOUNT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.@+-]*$/;
542
+ function recordMarkerAccount(content, accountId) {
543
+ if (!MARKER_ACCOUNT_ID_RE.test(accountId)) {
544
+ throw new Error(`recordMarkerAccount: "${accountId}" is not a valid account id`);
545
+ }
546
+ const recorded = parseAccountId(content);
547
+ if (recorded === accountId) return { action: "unchanged", content };
548
+ if (recorded !== null) return { action: "conflict", recorded, content };
549
+ const eol = content.includes("\r\n") ? "\r\n" : "\n";
550
+ const lines = content.replace(/(\r?\n)+$/, "").split(/\r?\n/);
551
+ const kept = lines.filter((l) => !/^account:\s*$/.test(l));
552
+ const vaultIdx = kept.findIndex((l) => /^vault_id:/.test(l));
553
+ const line = `account: ${accountId}`;
554
+ if (vaultIdx >= 0) kept.splice(vaultIdx + 1, 0, line);
555
+ else kept.push(line);
556
+ return { action: "added", content: `${kept.join(eol)}${eol}` };
557
+ }
184
558
  function appendVaultIdLine(content, vaultId) {
185
559
  const trimmed = content.replace(/\n+$/, "");
186
560
  return `${trimmed}
@@ -188,21 +562,22 @@ vault_id: ${vaultId}
188
562
  `;
189
563
  }
190
564
  function parseInitArgs(argv) {
191
- let consoleUrl = (process.env.HEYSPEX_CONSOLE_URL ?? process.env.HEYSPEX_COCKPIT_URL)?.trim() || // legacy-cockpit env
192
- DEFAULT_CONSOLE_URL2;
565
+ let consoleUrl = resolveConsoleUrl();
193
566
  let vaultRoot = "specs/";
194
567
  let force = false;
195
568
  let json = false;
196
569
  let customerVaultDir;
197
570
  let principles;
198
571
  let vaultName;
199
- let noGit = false;
572
+ let git = false;
200
573
  for (let i = 0; i < argv.length; i++) {
201
574
  const arg = argv[i];
202
575
  if (arg === "--help" || arg === "-h") return { help: true };
203
576
  else if (arg === "--json") json = true;
204
577
  else if (arg === "--force") force = true;
205
- else if (arg === "--no-git") noGit = true;
578
+ else if (arg === "--git") git = true;
579
+ else if (arg === "--no-git")
580
+ git = false;
206
581
  else if (arg === "--principles" || arg.startsWith("--principles=")) {
207
582
  const v = arg === "--principles" ? argv[++i] : arg.slice("--principles=".length);
208
583
  if (v === void 0 || v.trim() === "") {
@@ -215,8 +590,7 @@ function parseInitArgs(argv) {
215
590
  return { error: { code: EXIT.INVALID_ARG, message: "--vault-name requires a value" } };
216
591
  }
217
592
  vaultName = v.trim();
218
- } else if (arg === "--console-url" || arg.startsWith("--console-url=") || arg === "--cockpit-url" || // legacy-cockpit alias
219
- arg.startsWith("--cockpit-url=")) {
593
+ } else if (arg === "--console-url" || arg.startsWith("--console-url=")) {
220
594
  const eq = arg.indexOf("=");
221
595
  const v = eq === -1 ? argv[++i] : arg.slice(eq + 1);
222
596
  if (v === void 0 || v.trim() === "") {
@@ -247,17 +621,36 @@ function parseInitArgs(argv) {
247
621
  }
248
622
  }
249
623
  return {
250
- args: { consoleUrl, vaultRoot, force, json, customerVaultDir, principles, vaultName, noGit }
624
+ args: { consoleUrl, vaultRoot, force, json, customerVaultDir, principles, vaultName, git }
251
625
  };
252
626
  }
627
+ var HEYSPEX_CONFIG_DIR = ".heyspex";
628
+ var HEYSPEX_CONFIG_FILE = "config.yaml";
629
+ function seedConfigYaml(root, readSeed, fs) {
630
+ const configDir = fs.joinPath(root, HEYSPEX_CONFIG_DIR);
631
+ const configPath = fs.joinPath(configDir, HEYSPEX_CONFIG_FILE);
632
+ if (fs.exists(configPath)) return { status: "kept", path: configPath };
633
+ if (!readSeed) return { status: "no_seed", path: configPath };
634
+ let seed;
635
+ try {
636
+ seed = readSeed();
637
+ } catch {
638
+ seed = null;
639
+ }
640
+ if (seed === null) return { status: "no_seed", path: configPath };
641
+ fs.mkdirRecursive(configDir);
642
+ fs.writeFile(configPath, seed);
643
+ return { status: "seeded", path: configPath };
644
+ }
253
645
  function renderHeyspexYaml(args) {
254
- return [
646
+ const body = [
255
647
  `schema_version: ${HEYSPEX_YAML_SCHEMA_VERSION}`,
256
648
  `console_url: ${args.consoleUrl}`,
257
649
  `vault_root: ${args.vaultRoot}`,
258
650
  `vault_id: ${args.vaultId}`,
259
651
  ""
260
652
  ].join("\n");
653
+ return args.accountId ? recordMarkerAccount(body, args.accountId).content : body;
261
654
  }
262
655
  function readExistingContent(deps, markerPath) {
263
656
  if (!deps.readFile) return null;
@@ -272,17 +665,60 @@ function runInit(args, deps) {
272
665
  const exists = deps.fileExists(markerPath);
273
666
  const existingContent = exists ? readExistingContent(deps, markerPath) : null;
274
667
  const existingVaultId = existingContent ? parseVaultId(existingContent) : null;
668
+ if (deps.mkdirRecursive) {
669
+ const specsPath = deps.joinPath(deps.repoRoot, args.vaultRoot);
670
+ try {
671
+ deps.mkdirRecursive(specsPath);
672
+ } catch (err) {
673
+ const message = err instanceof Error ? err.message : String(err);
674
+ return {
675
+ code: EXIT.WRITE_FAILED,
676
+ message: `failed to create ${specsPath}: ${message}`,
677
+ payload: { ok: false, error: "write_failed", path: specsPath, message },
678
+ isError: true
679
+ };
680
+ }
681
+ }
682
+ let configSeed = null;
683
+ const mkdirRecursive = deps.mkdirRecursive;
684
+ if (mkdirRecursive) {
685
+ const readConfigSeed2 = deps.readConfigSeed;
686
+ try {
687
+ configSeed = seedConfigYaml(
688
+ deps.repoRoot,
689
+ readConfigSeed2 ? () => readConfigSeed2.call(deps) : void 0,
690
+ {
691
+ exists: (p) => deps.fileExists(p),
692
+ mkdirRecursive: (p) => mkdirRecursive.call(deps, p),
693
+ writeFile: (p, c) => deps.writeFile(p, c),
694
+ joinPath: (...s) => deps.joinPath(...s)
695
+ }
696
+ );
697
+ } catch (err) {
698
+ const message = err instanceof Error ? err.message : String(err);
699
+ const configPath = deps.joinPath(deps.repoRoot, HEYSPEX_CONFIG_DIR, HEYSPEX_CONFIG_FILE);
700
+ return {
701
+ code: EXIT.WRITE_FAILED,
702
+ message: `failed to seed ${configPath}: ${message}`,
703
+ payload: { ok: false, error: "write_failed", path: configPath, message },
704
+ isError: true
705
+ };
706
+ }
707
+ }
708
+ const configNote = configSeed?.status === "seeded" ? ` Seeded ${configSeed.path}.` : configSeed?.status === "kept" ? ` Kept existing ${configSeed.path}.` : "";
709
+ const configPayload = configSeed === null || configSeed.status === "no_seed" ? {} : { config_yaml: configSeed.path, config_yaml_seeded: configSeed.status === "seeded" };
275
710
  if (exists && !args.force) {
276
711
  if (existingVaultId) {
277
712
  return {
278
713
  code: EXIT.OK,
279
- message: `heyspex.yaml already exists at ${markerPath} \u2014 vault already initialized (use --force to overwrite).`,
714
+ message: `heyspex.yaml already exists at ${markerPath} \u2014 vault already initialized (use --force to overwrite).${configNote}`,
280
715
  payload: {
281
716
  ok: true,
282
717
  created: false,
283
718
  path: markerPath,
284
719
  reason: "already_initialized",
285
- vault_id: existingVaultId
720
+ vault_id: existingVaultId,
721
+ ...configPayload
286
722
  },
287
723
  isError: false
288
724
  };
@@ -290,8 +726,14 @@ function runInit(args, deps) {
290
726
  if (existingContent === null) {
291
727
  return {
292
728
  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" },
729
+ message: `heyspex.yaml already exists at ${markerPath} \u2014 vault already initialized (use --force to overwrite).${configNote}`,
730
+ payload: {
731
+ ok: true,
732
+ created: false,
733
+ path: markerPath,
734
+ reason: "already_initialized",
735
+ ...configPayload
736
+ },
295
737
  isError: false
296
738
  };
297
739
  }
@@ -310,19 +752,21 @@ function runInit(args, deps) {
310
752
  }
311
753
  return {
312
754
  code: EXIT.OK,
313
- message: `Added vault_id to existing heyspex.yaml at ${markerPath} (schema_version unchanged).`,
755
+ message: `Added vault_id to existing heyspex.yaml at ${markerPath} (schema_version unchanged).${configNote}`,
314
756
  payload: {
315
757
  ok: true,
316
758
  created: false,
317
759
  backfilled: true,
318
760
  path: markerPath,
319
- vault_id: mintedId
761
+ vault_id: mintedId,
762
+ ...configPayload
320
763
  },
321
764
  isError: false
322
765
  };
323
766
  }
324
767
  const vaultId = existingVaultId ?? args.adoptedVaultId ?? mintVaultId();
325
- const content = renderHeyspexYaml({ ...args, vaultId });
768
+ const accountId = args.accountId ?? (existingContent ? parseAccountId(existingContent) : null);
769
+ const content = renderHeyspexYaml({ ...args, vaultId, accountId });
326
770
  try {
327
771
  deps.writeFile(markerPath, content);
328
772
  } catch (err) {
@@ -336,7 +780,7 @@ function runInit(args, deps) {
336
780
  }
337
781
  return {
338
782
  code: EXIT.OK,
339
- message: `${exists ? "Overwrote" : "Wrote"} ${markerPath} (schema_version ${HEYSPEX_YAML_SCHEMA_VERSION}).`,
783
+ message: `${exists ? "Overwrote" : "Wrote"} ${markerPath} (schema_version ${HEYSPEX_YAML_SCHEMA_VERSION}).${configNote}`,
340
784
  payload: {
341
785
  ok: true,
342
786
  created: true,
@@ -345,7 +789,9 @@ function runInit(args, deps) {
345
789
  schema_version: HEYSPEX_YAML_SCHEMA_VERSION,
346
790
  console_url: args.consoleUrl,
347
791
  vault_root: args.vaultRoot,
348
- vault_id: vaultId
792
+ vault_id: vaultId,
793
+ ...accountId ? { account: accountId } : {},
794
+ ...configPayload
349
795
  },
350
796
  isError: false
351
797
  };
@@ -354,27 +800,38 @@ var HELP = [
354
800
  "Usage:",
355
801
  " hs init [--console-url <url>] [--vault-root <path>] [--force] [--json]",
356
802
  " hs init --customer-vault <dir> [--console-url <url>] [--principles <all|none|a,b>]",
357
- " [--vault-name <name>] [--no-git] [--json]",
803
+ " [--vault-name <name>] [--git] [--json]",
358
804
  "",
359
805
  "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",
806
+ "vault_root, vault_id; `hs connect` also records `account:`, the account id the",
807
+ "repo was connected under) \u2014 the committed file the console / web companion read",
361
808
  "to bootstrap a vault. Idempotent: an existing marker is left untouched unless",
362
809
  "--force, EXCEPT a missing vault_id is backfilled in place either way (backward",
363
810
  "compatible \u2014 no schema_version bump). vault_id is reused exactly when already",
364
811
  "present; a fresh one is minted only when absent.",
365
- "Does not modify .heyspex/config.yaml or its templates (ADR-0009).",
812
+ "Also scaffolds the vault_root folder and seeds .heyspex/config.yaml (the",
813
+ "HeySpex workflow settings, ADR-0009) from the shipped template",
814
+ "(vendor/heyspex-mcp/config-seed.yaml) when that file is absent. An existing",
815
+ ".heyspex/config.yaml is never rewritten \u2014 not even with --force; change its",
816
+ "values with `hs settings set <key> <value>`.",
366
817
  "",
367
818
  "--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",
819
+ "specs/ folder tree, heyspex.yaml, the seeded .heyspex/config.yaml, and every",
820
+ "row of the template shipping manifest",
821
+ "(spex-console/src/lib/cli/template-manifest.ts) \u2014 universal docs",
370
822
  "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.",
823
+ "plus a generated CLAUDE.md, the kept-principles index, and the template_version",
824
+ "pin; with --git, one initial `git init` + commit. Rerunnable: a <dir> that already",
825
+ "holds heyspex.yaml and an empty/placeholder-only specs/ is accepted, existing",
826
+ "files are never overwritten (an existing CLAUDE.md gets a CLAUDE.md.template",
827
+ "beside it), and the result lists each manifest row as created or kept. Refuses a",
828
+ "non-empty <dir> without heyspex.yaml, or a specs/ holding managed specs.",
829
+ "See init.customer-vault.ts.",
374
830
  "",
375
831
  "Flags:",
376
- " --console-url <url> console_url value (default: $HEYSPEX_CONSOLE_URL or",
377
- " https://console.heyspex.local).",
832
+ " --console-url <url> console_url value (default: the mode-aware console --",
833
+ " $HEYSPEX_CONSOLE_URL, else the signed-in console, else",
834
+ " https://app.heyspex.com in prod mode).",
378
835
  " --vault-root <path> vault_root value (default: specs/).",
379
836
  " --customer-vault <dir> Scaffold a new customer vault at <dir> (see above).",
380
837
  " --principles <all|none|a,b> Which universal principle docs to ship",
@@ -382,8 +839,9 @@ var HELP = [
382
839
  " keep/skip prompt per principle. Omitted + no TTY: all.",
383
840
  " --vault-name <name> Display name for the scaffolded vault (--customer-vault",
384
841
  " only; default: <dir>'s basename).",
385
- " --no-git Skip git init + the initial commit (--customer-vault only).",
386
- " --force Overwrite an existing heyspex.yaml.",
842
+ " --git Also run git init + one initial commit (--customer-vault only;",
843
+ " default: no git). --no-git is accepted as a no-op.",
844
+ " --force Overwrite an existing heyspex.yaml (never .heyspex/config.yaml).",
387
845
  " --json Emit a machine-readable JSON result.",
388
846
  " --help, -h Show this help.",
389
847
  "",
@@ -407,14 +865,69 @@ var UNIVERSAL_TEMPLATE_ROWS = [
407
865
  {
408
866
  platformSource: "specs/protocols/memory-protocol.md",
409
867
  dest: "specs/protocols/memory-protocol.md"
868
+ },
869
+ {
870
+ // ~455 lines of HS-REV / MVP-grain / guardrail detail that cannot be
871
+ // curated cleanly: ships a template-shape abridgement instead.
872
+ platformSource: "specs/protocols/session-close-protocol.md",
873
+ abridgedSource: "spex-console/templates/abridged/specs/protocols/session-close-protocol.md",
874
+ dest: "specs/protocols/session-close-protocol.md"
875
+ },
876
+ {
877
+ platformSource: "specs/protocols/spec-summary-authoring.md",
878
+ dest: "specs/protocols/spec-summary-authoring.md",
879
+ dropSections: ["Keeping the code snapshot in sync", "Change log"],
880
+ dropLinePatterns: ["vendor/heyspex-mcp/summary-contract\\.mjs"]
881
+ },
882
+ {
883
+ platformSource: "specs/protocols/destructive-action-checklist.md",
884
+ dest: "specs/protocols/destructive-action-checklist.md",
885
+ dropSections: ["Change log"],
886
+ dropLinePatterns: ["spex-console/scripts/validate-destructive-actions\\.mjs"],
887
+ replacements: [
888
+ [
889
+ "The 2026-05-22 idea-discard incident showed that strong UX *principles*\n([[errors-destructive-recovery]], [[reversible-user-actions]]) are not\nenough",
890
+ "A past idea-discard data-loss incident showed that strong UX *principles*\n(recoverable destructive actions, reversible user actions) are not\nenough"
891
+ ],
892
+ [
893
+ "Today: linked from `CLAUDE.md` Console pre-ship checklist; agents must\nwalk it",
894
+ "Today: agents must\nwalk it"
895
+ ],
896
+ [
897
+ '- [ ] 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]].',
898
+ "- [ ] If a *new* destructive surface is introduced, record it in a vault memory."
899
+ ],
900
+ [
901
+ "- [ ] Read [[errors-destructive-recovery]] and\n [[destructive-action-needs-explicit-button-and-undo]] in full.",
902
+ "- [ ] Read any vault memories on destructive-action recovery and Undo."
903
+ ],
904
+ [
905
+ " (Not covered here; see existing dirty-editor\n pattern in `specs-surface.tsx`.)",
906
+ " (Not covered here.)"
907
+ ]
908
+ ]
410
909
  }
411
910
  ];
412
911
 
413
912
  // dist/cli/._template-manifest-sources.cli-shim.mjs
414
913
  var CURATED_TEMPLATE_ROOT = "dist/cli-templates/customer-vault";
415
914
 
915
+ // scripts/lib/template-pin.mjs
916
+ var TEMPLATE_VERSION = "1.1.0";
917
+ var TEMPLATE_CORE_DESTS = Object.freeze([
918
+ "specs/north-star.md",
919
+ "specs/principles-index.md",
920
+ "specs/roadmap.md",
921
+ "specs/entity-model.md",
922
+ "specs/protocols/canonical-frontmatter-templates.md",
923
+ "specs/protocols/spec-hierarchy.md",
924
+ "specs/protocols/memory-protocol.md",
925
+ "specs/protocols/session-close-protocol.md",
926
+ "specs/protocols/spec-summary-authoring.md",
927
+ "specs/protocols/destructive-action-checklist.md"
928
+ ]);
929
+
416
930
  // spex-console/src/lib/cli/template-manifest.ts
417
- var TEMPLATE_VERSION = "1.0.0";
418
931
  var TODO_NORTH_STAR = `---
419
932
  id: HS-L1
420
933
  name: TODO \u2014 your product name
@@ -804,7 +1317,7 @@ var TEMPLATE_MANIFEST = [
804
1317
  ];
805
1318
 
806
1319
  // spex-console/src/lib/cli/init.customer-vault.ts
807
- var DEFAULT_CONSOLE_URL3 = "https://console.heyspex.local";
1320
+ var DEFAULT_CONSOLE_URL2 = "https://console.heyspex.local";
808
1321
  var SCAFFOLD_FOLDERS = [
809
1322
  "tasks",
810
1323
  "epics",
@@ -821,23 +1334,63 @@ var SCAFFOLD_FOLDERS = [
821
1334
  "checks",
822
1335
  "events"
823
1336
  ];
1337
+ var PLACEHOLDER_FILES = /* @__PURE__ */ new Set([".gitkeep"]);
1338
+ function isDir(deps, path2, name) {
1339
+ if (deps.isDirectory) return deps.isDirectory(path2);
1340
+ try {
1341
+ const kids = deps.readdir(path2);
1342
+ return kids.length > 0 || !name.includes(".");
1343
+ } catch {
1344
+ return false;
1345
+ }
1346
+ }
1347
+ function findManagedSpecs(deps, targetDir, manifestDests) {
1348
+ const out = [];
1349
+ const walk = (abs, rel) => {
1350
+ for (const name of deps.readdir(abs)) {
1351
+ const childAbs = deps.joinPath(abs, name);
1352
+ const childRel = `${rel}/${name}`;
1353
+ if (isDir(deps, childAbs, name)) walk(childAbs, childRel);
1354
+ else if (!PLACEHOLDER_FILES.has(name) && name.endsWith(".md") && !manifestDests.has(childRel))
1355
+ out.push(childRel);
1356
+ }
1357
+ };
1358
+ const specsRoot = deps.joinPath(targetDir, "specs");
1359
+ if (deps.exists(specsRoot)) walk(specsRoot, "specs");
1360
+ return out;
1361
+ }
824
1362
  function defaultVaultName(targetDir) {
825
1363
  return basename(targetDir) || targetDir;
826
1364
  }
827
1365
  function runCustomerVaultInit(args, deps, options = {}) {
828
1366
  const { targetDir } = args;
829
1367
  const written = [];
1368
+ const filter = options.manifestFilter ?? (() => true);
1369
+ const rows = TEMPLATE_MANIFEST.filter(filter);
830
1370
  if (deps.exists(targetDir)) {
831
- const entries = deps.readdir(targetDir);
832
- if (entries.length > 0) {
1371
+ const markerPresent = deps.exists(deps.joinPath(targetDir, "heyspex.yaml"));
1372
+ if (!markerPresent && deps.readdir(targetDir).length > 0) {
833
1373
  return {
834
1374
  code: EXIT.INVALID_ARG,
835
- message: `${targetDir} is not empty \u2014 customer-vault init refuses to write into an existing, non-empty directory.`,
1375
+ message: `${targetDir} is not empty \u2014 customer-vault init refuses to write into an existing, non-empty directory that has no heyspex.yaml.`,
836
1376
  payload: { ok: false, error: "target_not_empty", path: targetDir },
837
1377
  isError: true,
838
1378
  written: []
839
1379
  };
840
1380
  }
1381
+ if (markerPresent) {
1382
+ const manifestDests = new Set(TEMPLATE_MANIFEST.map((r) => r.dest));
1383
+ const managed = findManagedSpecs(deps, targetDir, manifestDests);
1384
+ if (managed.length > 0) {
1385
+ return {
1386
+ code: EXIT.INVALID_ARG,
1387
+ 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.`,
1388
+ payload: { ok: false, error: "specs_not_empty", path: targetDir, specs: managed },
1389
+ isError: true,
1390
+ written: []
1391
+ };
1392
+ }
1393
+ }
841
1394
  } else {
842
1395
  deps.mkdirRecursive(targetDir);
843
1396
  }
@@ -846,19 +1399,37 @@ function runCustomerVaultInit(args, deps, options = {}) {
846
1399
  for (const folder of SCAFFOLD_FOLDERS) {
847
1400
  deps.mkdirRecursive(deps.joinPath(specsRoot, folder));
848
1401
  }
849
- const vaultId = mintVaultId();
850
- const yamlContent = renderHeyspexYaml({
851
- consoleUrl: options.consoleUrl ?? DEFAULT_CONSOLE_URL3,
852
- vaultRoot: "specs/",
853
- vaultId
854
- });
855
1402
  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);
1403
+ let vaultId;
1404
+ if (deps.exists(heyspexYamlPath)) {
1405
+ const existingYaml = deps.readFile?.(heyspexYamlPath) ?? null;
1406
+ vaultId = (existingYaml ? parseVaultId(existingYaml) : null) ?? "";
1407
+ } else {
1408
+ vaultId = mintVaultId();
1409
+ deps.writeFile(
1410
+ heyspexYamlPath,
1411
+ renderHeyspexYaml({
1412
+ consoleUrl: options.consoleUrl ?? DEFAULT_CONSOLE_URL2,
1413
+ vaultRoot: "specs/",
1414
+ vaultId
1415
+ })
1416
+ );
1417
+ written.push(heyspexYamlPath);
1418
+ }
1419
+ const readConfigSeed2 = deps.readConfigSeed;
1420
+ const configSeed = seedConfigYaml(
1421
+ targetDir,
1422
+ readConfigSeed2 ? () => readConfigSeed2.call(deps) : void 0,
1423
+ deps
1424
+ );
1425
+ if (configSeed.status === "seeded") written.push(configSeed.path);
1426
+ const rowResults = [];
860
1427
  for (const row of rows) {
861
1428
  const destAbs = deps.joinPath(targetDir, ...row.dest.split("/"));
1429
+ if (deps.exists(destAbs)) {
1430
+ rowResults.push({ dest: row.dest, action: "kept" });
1431
+ continue;
1432
+ }
862
1433
  if (row.classification === "universal-template") {
863
1434
  const srcAbs = deps.joinPath(deps.platformRoot, ...row.source.split("/"));
864
1435
  deps.copyFile(srcAbs, destAbs);
@@ -871,6 +1442,7 @@ function runCustomerVaultInit(args, deps, options = {}) {
871
1442
  deps.writeFile(destAbs, row.content);
872
1443
  }
873
1444
  written.push(destAbs);
1445
+ rowResults.push({ dest: row.dest, action: "created" });
874
1446
  }
875
1447
  if (options.afterScaffold && options.afterScaffold.length > 0) {
876
1448
  const ctx = {
@@ -878,22 +1450,28 @@ function runCustomerVaultInit(args, deps, options = {}) {
878
1450
  vaultName: options.vaultName ?? defaultVaultName(targetDir),
879
1451
  templateVersion: TEMPLATE_VERSION,
880
1452
  manifestRows: rows,
1453
+ rows: rowResults,
881
1454
  deps,
882
- recordWrite: (path) => {
883
- written.push(path);
1455
+ recordWrite: (path2) => {
1456
+ written.push(path2);
884
1457
  }
885
1458
  };
886
1459
  for (const hook of options.afterScaffold) hook(ctx);
887
1460
  }
1461
+ const createdCount = rowResults.filter((r) => r.action === "created").length;
1462
+ const keptCount = rowResults.length - createdCount;
888
1463
  return {
889
1464
  code: EXIT.OK,
890
- message: `Initialized customer vault at ${targetDir} (${written.length} files written, template v${TEMPLATE_VERSION}).`,
1465
+ message: `Initialized customer vault at ${targetDir} (${written.length} files written, ${createdCount} manifest rows created, ${keptCount} kept, template v${TEMPLATE_VERSION}).`,
891
1466
  payload: {
892
1467
  ok: true,
893
1468
  targetDir,
894
1469
  templateVersion: TEMPLATE_VERSION,
895
1470
  vaultId,
896
- written
1471
+ written,
1472
+ rows: rowResults,
1473
+ created: createdCount,
1474
+ kept: keptCount
897
1475
  },
898
1476
  isError: false,
899
1477
  written
@@ -902,7 +1480,6 @@ function runCustomerVaultInit(args, deps, options = {}) {
902
1480
 
903
1481
  // spex-console/src/lib/cli/init-git-finalize.ts
904
1482
  import { spawnSync } from "node:child_process";
905
- import { readFileSync as readFileSync3 } from "node:fs";
906
1483
  var nodeGitRunner = {
907
1484
  run(args, cwd) {
908
1485
  const result = spawnSync("git", args, { cwd, encoding: "utf8" });
@@ -929,14 +1506,19 @@ function ensureTemplateVersionLine(content, version) {
929
1506
  template_version: ${version}
930
1507
  `;
931
1508
  }
1509
+ function templatePinHook() {
1510
+ return (ctx) => {
1511
+ const yamlPath = ctx.deps.joinPath(ctx.targetDir, "heyspex.yaml");
1512
+ const existingYaml = ctx.deps.readFile?.(yamlPath);
1513
+ if (existingYaml === null || existingYaml === void 0) return;
1514
+ if (readTemplateVersion(existingYaml) === ctx.templateVersion) return;
1515
+ ctx.deps.writeFile(yamlPath, ensureTemplateVersionLine(existingYaml, ctx.templateVersion));
1516
+ ctx.recordWrite(yamlPath);
1517
+ };
1518
+ }
932
1519
  function gitFinalizeHook(runner, notify) {
933
1520
  return (ctx) => {
934
1521
  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
1522
  const probe = runner.run(["--version"], target);
941
1523
  if (!probe.ok) {
942
1524
  notify("git not found - vault left uninitialized, run git init yourself");
@@ -1015,6 +1597,7 @@ function principleManifestFilter(selected) {
1015
1597
  }
1016
1598
  function principlesIndexHook(selected, rows) {
1017
1599
  return (ctx) => {
1600
+ if (ctx.rows.some((r) => r.dest === PRINCIPLES_INDEX_DEST && r.action === "kept")) return;
1018
1601
  const indexRow = rows.find((row) => row.dest === PRINCIPLES_INDEX_DEST);
1019
1602
  if (!indexRow || indexRow.content === void 0) {
1020
1603
  throw new Error(
@@ -1031,10 +1614,53 @@ function principlesIndexHook(selected, rows) {
1031
1614
  };
1032
1615
  }
1033
1616
 
1617
+ // spex-console/bin/run-entry.ts
1618
+ import process2 from "node:process";
1619
+ var BinExit = class extends Error {
1620
+ code;
1621
+ constructor(code) {
1622
+ super(`exit ${code}`);
1623
+ this.name = "BinExit";
1624
+ this.code = code;
1625
+ }
1626
+ };
1627
+ function runEntry(name, main2, options = {}) {
1628
+ const { fallbackCode = 1, unhandledLabel = "unhandled" } = options;
1629
+ void (async () => {
1630
+ try {
1631
+ await main2();
1632
+ } catch (err) {
1633
+ if (err instanceof BinExit) {
1634
+ process2.exitCode = err.code;
1635
+ return;
1636
+ }
1637
+ const message = err instanceof Error ? err.message : String(err);
1638
+ process2.stderr.write(`${name}: ${unhandledLabel}: ${message}
1639
+ `);
1640
+ process2.exitCode = fallbackCode;
1641
+ }
1642
+ })();
1643
+ }
1644
+
1034
1645
  // spex-console/bin/hs-init.ts
1035
- var INTERACTIVE = process2.stdin.isTTY === true;
1646
+ var INTERACTIVE = process3.stdin.isTTY === true;
1647
+ var PKG_ROOT = resolve5(
1648
+ resolveCodeRoot({ packageRoot: resolve5(dirname5(fileURLToPath2(import.meta.url)), "..", "..") })
1649
+ );
1650
+ var CONFIG_SEED_PATH = join9(PKG_ROOT, "vendor", "heyspex-mcp", "config-seed.yaml");
1651
+ function readConfigSeed() {
1652
+ try {
1653
+ return readFileSync8(CONFIG_SEED_PATH, "utf8");
1654
+ } catch {
1655
+ process3.stderr.write(
1656
+ `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).
1657
+ `
1658
+ );
1659
+ return null;
1660
+ }
1661
+ }
1036
1662
  async function askYesNo(question) {
1037
- const rl = createInterface({ input: process2.stdin, output: process2.stdout });
1663
+ const rl = createInterface({ input: process3.stdin, output: process3.stdout });
1038
1664
  try {
1039
1665
  const answer = (await rl.question(`${question} `)).trim().toLowerCase();
1040
1666
  return answer === "y" || answer === "yes" || answer === "";
@@ -1047,9 +1673,9 @@ async function resolvePrincipleSelection(principlesFlag) {
1047
1673
  return parsePrinciplesFlag(principlesFlag, TEMPLATE_MANIFEST);
1048
1674
  }
1049
1675
  if (INTERACTIVE) {
1050
- process2.stdout.write("Select which universal principle docs to ship with this vault:\n");
1676
+ process3.stdout.write("Select which universal principle docs to ship with this vault:\n");
1051
1677
  for (const choice of listPrincipleChoices(TEMPLATE_MANIFEST)) {
1052
- process2.stdout.write(` - ${choice.key}: ${choice.summary}
1678
+ process3.stdout.write(` - ${choice.key}: ${choice.summary}
1053
1679
  `);
1054
1680
  }
1055
1681
  const selected = await promptPrincipleSelection(askYesNo, TEMPLATE_MANIFEST);
@@ -1058,47 +1684,60 @@ async function resolvePrincipleSelection(principlesFlag) {
1058
1684
  return parsePrinciplesFlag(void 0, TEMPLATE_MANIFEST);
1059
1685
  }
1060
1686
  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));
1687
+ const repoRoot = process3.env.HS_VAULT_ROOT ? resolve5(process3.env.HS_VAULT_ROOT) : process3.cwd();
1688
+ const parsed = parseInitArgs(process3.argv.slice(3));
1063
1689
  if (parsed.help) {
1064
- process2.stdout.write(`${HELP}
1690
+ process3.stdout.write(`${HELP}
1065
1691
  `);
1066
- process2.exit(EXIT.OK);
1692
+ process3.exitCode = EXIT.OK;
1693
+ return;
1067
1694
  }
1068
1695
  if (parsed.error) {
1069
- process2.stderr.write(`hs init: ${parsed.error.message}
1696
+ process3.stderr.write(`hs init: ${parsed.error.message}
1070
1697
  `);
1071
- process2.exit(parsed.error.code);
1698
+ process3.exitCode = parsed.error.code;
1699
+ return;
1072
1700
  }
1073
1701
  const args = parsed.args;
1074
1702
  if (args.customerVaultDir) {
1075
- const here = dirname(fileURLToPath(import.meta.url));
1076
- const platformRoot = resolve(here, "..", "..");
1077
- const targetDir = resolve(args.customerVaultDir);
1703
+ const here = dirname5(fileURLToPath2(import.meta.url));
1704
+ const platformRoot = resolve5(here, "..", "..");
1705
+ const targetDir = resolve5(args.customerVaultDir);
1078
1706
  const deps = {
1079
1707
  platformRoot,
1080
- exists: (p) => existsSync(p),
1708
+ exists: (p) => existsSync4(p),
1081
1709
  readdir: (p) => readdirSync(p),
1082
1710
  mkdirRecursive: (p) => {
1083
- mkdirSync(p, { recursive: true });
1711
+ mkdirSync4(p, { recursive: true });
1084
1712
  },
1085
- writeFile: (p, c) => writeFileSync(p, c, "utf8"),
1713
+ writeFile: (p, c) => writeFileSync4(p, c, "utf8"),
1086
1714
  copyFile: (src, dest) => copyFileSync(src, dest),
1087
- joinPath: (...s) => join3(...s)
1715
+ joinPath: (...s) => join9(...s),
1716
+ readConfigSeed,
1717
+ readFile: (p) => {
1718
+ try {
1719
+ return readFileSync8(p, "utf8");
1720
+ } catch {
1721
+ return null;
1722
+ }
1723
+ },
1724
+ isDirectory: (p) => statSync(p).isDirectory()
1088
1725
  };
1089
1726
  const principlesResult = await resolvePrincipleSelection(args.principles);
1090
1727
  if (!principlesResult.ok) {
1091
- process2.stderr.write(`hs init: ${principlesResult.message}
1728
+ process3.stderr.write(`hs init: ${principlesResult.message}
1092
1729
  `);
1093
- process2.exit(EXIT.INVALID_ARG);
1730
+ process3.exitCode = EXIT.INVALID_ARG;
1731
+ return;
1094
1732
  }
1095
1733
  const selected = principlesResult.selected;
1096
1734
  const notices = [];
1097
1735
  const afterScaffold = [
1098
- customerClaudeMdHook(),
1099
- principlesIndexHook(selected, TEMPLATE_MANIFEST)
1736
+ customerClaudeMdHook((msg) => notices.push(msg)),
1737
+ principlesIndexHook(selected, TEMPLATE_MANIFEST),
1738
+ templatePinHook()
1100
1739
  ];
1101
- if (!args.noGit) {
1740
+ if (args.git) {
1102
1741
  afterScaffold.push(gitFinalizeHook(nodeGitRunner, (msg) => notices.push(msg)));
1103
1742
  }
1104
1743
  const result2 = runCustomerVaultInit({ targetDir }, deps, {
@@ -1107,7 +1746,7 @@ async function main() {
1107
1746
  manifestFilter: principleManifestFilter(selected),
1108
1747
  afterScaffold
1109
1748
  });
1110
- const sink2 = result2.isError ? process2.stderr : process2.stdout;
1749
+ const sink2 = result2.isError ? process3.stderr : process3.stdout;
1111
1750
  if (args.json) {
1112
1751
  sink2.write(`${JSON.stringify({ ...result2.payload, notices })}
1113
1752
  `);
@@ -1117,29 +1756,29 @@ async function main() {
1117
1756
  for (const notice of notices) sink2.write(`${notice}
1118
1757
  `);
1119
1758
  }
1120
- process2.exit(result2.code);
1759
+ process3.exitCode = result2.code;
1760
+ return;
1121
1761
  }
1122
1762
  const result = runInit(args, {
1123
1763
  repoRoot,
1124
- fileExists: (p) => existsSync(p),
1764
+ fileExists: (p) => existsSync4(p),
1125
1765
  readFile: (p) => {
1126
1766
  try {
1127
- return readFileSync4(p, "utf8");
1767
+ return readFileSync8(p, "utf8");
1128
1768
  } catch {
1129
1769
  return null;
1130
1770
  }
1131
1771
  },
1132
- writeFile: (p, c) => writeFileSync(p, c, "utf8"),
1133
- joinPath: (...s) => join3(...s)
1772
+ writeFile: (p, c) => writeFileSync4(p, c, "utf8"),
1773
+ joinPath: (...s) => join9(...s),
1774
+ mkdirRecursive: (p) => {
1775
+ mkdirSync4(p, { recursive: true });
1776
+ },
1777
+ readConfigSeed
1134
1778
  });
1135
- const sink = result.isError ? process2.stderr : process2.stdout;
1779
+ const sink = result.isError ? process3.stderr : process3.stdout;
1136
1780
  sink.write(`${args.json ? JSON.stringify(result.payload) : result.message}
1137
1781
  `);
1138
- process2.exit(result.code);
1782
+ process3.exitCode = result.code;
1139
1783
  }
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
- });
1784
+ runEntry("hs init", main, { fallbackCode: EXIT.WRITE_FAILED });