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
@@ -29,10 +29,72 @@
29
29
  import fs from 'node:fs';
30
30
  import path from 'node:path';
31
31
  import crypto from 'node:crypto';
32
+ import { fileURLToPath } from 'node:url';
32
33
 
33
34
  import { yamlScalar } from './yaml-scalar.mjs';
35
+ import { getSettingEntry, SETTINGS_REGISTRY } from './settings-registry.mjs';
36
+ import { listTrustedFolders, readTrustStore, trustStorePath } from './folder-trust-store.mjs';
34
37
 
35
38
  export const CONFIG_REL = path.join('.heyspex', 'config.yaml');
39
+
40
+ /**
41
+ * The seed template a vault's `.heyspex/config.yaml` is created from when it is
42
+ * missing ([[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]] fix 3).
43
+ * Ships in the npm package next to this module (`package.json` `files`), so a
44
+ * published install seeds the same defaults a source checkout does. `hs init`
45
+ * seeds from the same file; the settings core seeds on the FIRST WRITE only —
46
+ * reads (`listConfigLeaves` / `getConfigLeaf`) never create files.
47
+ */
48
+ export const CONFIG_SEED_PATH = path.resolve(
49
+ path.dirname(fileURLToPath(import.meta.url)),
50
+ 'config-seed.yaml',
51
+ );
52
+
53
+ /** The seed template's text. Throws only if the package itself is broken. */
54
+ export function readConfigSeed() {
55
+ return fs.readFileSync(CONFIG_SEED_PATH, 'utf8');
56
+ }
57
+
58
+ /**
59
+ * The one non-scalar, non-`.heyspex/config.yaml` COMPUTED key the settings capability exposes
60
+ * ([[HS-25AJ-8CMW-TASK-folder-trust-revoke-through-the]], agent-settings-protocol.md): the list
61
+ * of `hs chat`/`spexcode` folder-trust decisions, sourced live from the machine-local
62
+ * `~/.heyspex/trusted-folders.json` store (`folder-trust-store.mjs` — the SAME read primitive
63
+ * `spex-console/src/lib/cli/folder-trust.ts` re-exports for `hs trust list`). READ-ONLY: writes
64
+ * are refused in both `requestSettingsWrite` (MCP two-step) and `setConfigLeaf` (the CLI's
65
+ * direct-write path) — see each function below. Never reads or writes config.yaml for this key.
66
+ *
67
+ * Why the core hosts a computed key rather than distorting itself around one: this module's
68
+ * entire model — `listConfigLeaves`/`getConfigLeaf`/`setConfigLeaf` — is a scalar-leaf scan and
69
+ * surgical single-line rewrite of ONE git-tracked YAML file. `trusted_folders` is a LIST of
70
+ * records (path/trusted/decidedAt) sourced from a DIFFERENT, machine-local, never-git-tracked
71
+ * JSON file — hosting a `…trusted_folders.revoke = <path>` WRITE through the two-step confirm
72
+ * store would mean that store's pending-write record and audit row both lie about writing to
73
+ * config.yaml (they don't — a revoke deletes an entry from trusted-folders.json). So the read
74
+ * is computed here (small, honest addition), but the write stays CLI-only (`hs trust revoke
75
+ * <path>`), documented as the task's AC2 explicitly allows.
76
+ */
77
+ export const TRUSTED_FOLDERS_KEY = 'agent_behaviors.spexcode.trusted_folders';
78
+
79
+ function isTrustedFoldersKey(key) {
80
+ return key === TRUSTED_FOLDERS_KEY || key.startsWith(TRUSTED_FOLDERS_KEY + '.');
81
+ }
82
+
83
+ /** Shared refusal for both write entry points (`setConfigLeaf`'s direct/CLI path and
84
+ * `requestSettingsWrite`'s MCP two-step path) — `null` when `key` isn't governed by this
85
+ * computed key, so callers can `return refusal ?? ...normal flow`. */
86
+ function refuseTrustedFoldersWrite(key) {
87
+ if (!isTrustedFoldersKey(key)) return null;
88
+ return {
89
+ ok: false,
90
+ error: 'read_only_computed_key',
91
+ message:
92
+ `"${key}" is a read-only computed key — it is sourced live from the machine-local hs ` +
93
+ `trust store (${trustStorePath()}), never from .heyspex/config.yaml, and settings writes ` +
94
+ 'are not supported here. Run "hs trust revoke <path>" instead — it clears one folder; ' +
95
+ 'omit <path> to clear the current folder.',
96
+ };
97
+ }
36
98
  export const SETTINGS_AUDIT_REL = path.join('.heyspex', 'settings-history.jsonl');
37
99
  export const PENDING_WRITE_REL = path.join('.heyspex', 'settings-write-pending.json');
38
100
 
@@ -60,6 +122,82 @@ export function validateSettingKey(key) {
60
122
  return { ok: true };
61
123
  }
62
124
 
125
+ // ── registry-backed write validation (HS-WH06, HS-JXT9) ─────────────────────
126
+ //
127
+ // The registry (`vendor/heyspex-mcp/settings-registry.mjs`) currently declares
128
+ // only the `close_out.*` namespace. Validation below applies ONLY to a key whose
129
+ // top-level segment the registry governs — a key in an ungoverned namespace
130
+ // (`mvp_mode.*`, `agent_behaviors.*`, …) keeps its pre-registry freeform write
131
+ // behavior unchanged. This keeps the epic's scope to "close-out settings" without
132
+ // silently locking down every other setting the registry has not been taught yet
133
+ // ([[HS-KW38-15XI-EPIC-settings-registry-core]] scope).
134
+
135
+ /** Top-level key segments the registry declares at least one entry for. */
136
+ const REGISTRY_GOVERNED_PREFIXES = new Set(
137
+ SETTINGS_REGISTRY.map((entry) => entry.key.split('.')[0]),
138
+ );
139
+
140
+ function isRegistryGoverned(key) {
141
+ return REGISTRY_GOVERNED_PREFIXES.has(key.split('.')[0]);
142
+ }
143
+
144
+ /** Human-readable description of what `entry` accepts, for a refusal message. */
145
+ function describeExpectedSettingValue(entry) {
146
+ if (entry.type === 'enum') return `one of: ${entry.values.join(', ')}`;
147
+ if (entry.type === 'boolean') return 'a boolean (true or false)';
148
+ return 'a string';
149
+ }
150
+
151
+ /** True when `value` matches the type/allowed-values `entry` declares. */
152
+ function settingValueMatchesEntry(entry, value) {
153
+ if (entry.type === 'boolean') return typeof value === 'boolean';
154
+ if (entry.type === 'enum') return typeof value === 'string' && entry.values.includes(value);
155
+ if (entry.type === 'string') return typeof value === 'string';
156
+ return true;
157
+ }
158
+
159
+ /**
160
+ * Validate a proposed write against the settings registry, ahead of any file or
161
+ * audit-log write. Only applies within a registry-governed namespace (see above);
162
+ * a key outside it always passes through with `{ ok: true }`.
163
+ *
164
+ * Refuses, in order:
165
+ * 1. a key the registry does not declare (`unknown_setting`);
166
+ * 2. any change to an entry marked `enforced`, regardless of who the caller is —
167
+ * there is no admin bypass (`enforced_setting`);
168
+ * 3. a value whose type doesn't match the entry, or that falls outside the
169
+ * entry's declared `values` list (`invalid_setting_value`) — names the
170
+ * setting, the value given, and what was expected.
171
+ *
172
+ * Returns `{ ok: true }` or `{ ok: false, error, message }`. Never touches disk.
173
+ */
174
+ export function validateSettingWrite(key, value) {
175
+ if (typeof key !== 'string' || !isRegistryGoverned(key)) return { ok: true };
176
+ const entry = getSettingEntry(key);
177
+ if (!entry) {
178
+ return {
179
+ ok: false,
180
+ error: 'unknown_setting',
181
+ message: `"${key}" is not a setting declared in the settings registry — refusing to write an unknown key.`,
182
+ };
183
+ }
184
+ if (entry.enforced) {
185
+ return {
186
+ ok: false,
187
+ error: 'enforced_setting',
188
+ message: `"${key}" is a hard rule and cannot be changed by anyone, including an administrator (${entry.description})`,
189
+ };
190
+ }
191
+ if (!settingValueMatchesEntry(entry, value)) {
192
+ return {
193
+ ok: false,
194
+ error: 'invalid_setting_value',
195
+ message: `"${key}" was given ${JSON.stringify(value)}, but expected ${describeExpectedSettingValue(entry)}.`,
196
+ };
197
+ }
198
+ return { ok: true };
199
+ }
200
+
63
201
  /**
64
202
  * Parse a raw YAML scalar string into a JS value (boolean/number/null/string).
65
203
  * Quoted strings are unquoted; anything else stays a string.
@@ -92,6 +230,72 @@ function readRaw(repoRoot, options = {}) {
92
230
  }
93
231
  }
94
232
 
233
+ function isExistingDirectory(dir) {
234
+ try {
235
+ return typeof dir === 'string' && dir.length > 0 && fs.statSync(dir).isDirectory();
236
+ } catch {
237
+ return false;
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Make sure `<repoRoot>/.heyspex/` exists ahead of a write into it. Only ever
243
+ * creates the `.heyspex/` directory INSIDE an existing vault root — never the
244
+ * vault root itself (a typo'd `HS_VAULT_ROOT` must not grow a `.heyspex/` tree
245
+ * in a path that does not exist). Returns false when `repoRoot` is not an
246
+ * existing directory or the mkdir fails.
247
+ */
248
+ function ensureHeyspexDir(repoRoot) {
249
+ if (!isExistingDirectory(repoRoot)) return false;
250
+ try {
251
+ fs.mkdirSync(path.join(repoRoot, '.heyspex'), { recursive: true });
252
+ return true;
253
+ } catch {
254
+ return false;
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Seed a MISSING config file from the template ([[HS-4C76-9W86-DIAG]] fix 3).
260
+ * Returns `{ ok: true, raw }` with the seeded text, or `{ ok: false, error,
261
+ * message }` when seeding is not allowed / fails. Refuses (keeps the historic
262
+ * `config_unreadable` contract) when:
263
+ * - `repoRoot` is not an existing directory — never mkdir a vault into a
264
+ * path that does not exist;
265
+ * - the file EXISTS but could not be read (permissions, a directory at that
266
+ * path…) — seeding over it would silently destroy the user's settings.
267
+ */
268
+ function seedMissingConfig(repoRoot, file) {
269
+ if (fs.existsSync(file)) {
270
+ return { ok: false, error: 'config_unreadable', message: `${file} exists but is unreadable` };
271
+ }
272
+ if (!isExistingDirectory(repoRoot)) {
273
+ return {
274
+ ok: false,
275
+ error: 'config_unreadable',
276
+ message: `${file} not found and its vault root ${repoRoot} does not exist — refusing to create a vault there. Run \`hs init\` in the directory that should become the vault.`,
277
+ };
278
+ }
279
+ let seed;
280
+ try {
281
+ seed = readConfigSeed();
282
+ } catch (err) {
283
+ return { ok: false, error: 'seed_unreadable', message: `config seed template ${CONFIG_SEED_PATH} could not be read: ${String(err?.message ?? err)}` };
284
+ }
285
+ try {
286
+ fs.mkdirSync(path.dirname(file), { recursive: true });
287
+ // 'wx' — create only; if a concurrent writer beat us to it, read theirs.
288
+ fs.writeFileSync(file, seed, { encoding: 'utf8', flag: 'wx' });
289
+ } catch (err) {
290
+ if (err?.code === 'EEXIST') {
291
+ const raced = readRaw(repoRoot, { configPath: file });
292
+ if (raced !== null) return { ok: true, raw: raced, seeded: false };
293
+ }
294
+ return { ok: false, error: 'write_failed', message: `could not seed ${file}: ${String(err?.message ?? err)}` };
295
+ }
296
+ return { ok: true, raw: seed, seeded: true };
297
+ }
298
+
95
299
  /**
96
300
  * Walk config.yaml lines maintaining an indent-scoped key path (the same scan the
97
301
  * hook loader uses). Calls `visit({ pathHere, key, indent, rawValue, lineIdx })` for
@@ -177,6 +381,50 @@ export function getConfigLeaf(repoRoot, key, options = {}) {
177
381
  return { key, found: true, raw: hit, value: parseScalar(hit) };
178
382
  }
179
383
 
384
+ /**
385
+ * Resolve the effective value of one setting through the registry-aware read
386
+ * chain ([[HS-FCF5-15XI-RES-user-editable-close-out-settings-with]] §"One-time
387
+ * bypass resolve order"):
388
+ * 1. (future) a one-time command-line flag override — NOT implemented here; a
389
+ * later epic (the `hs close` one-time flag layer) adds it above the file
390
+ * value. This function's `source` field leaves room for a `'flag'` value
391
+ * without changing its shape.
392
+ * 2. the file's value in `.heyspex/config.yaml`, when the key is present.
393
+ * 3. the registry's shipped default (`vendor/heyspex-mcp/settings-registry.mjs`),
394
+ * when the key is a declared registry entry and the file has no value.
395
+ * A key that is neither present in the file nor declared in the registry resolves
396
+ * to `source: 'absent'` with a null value — unchanged from the pre-registry
397
+ * behavior of `getConfigLeaf`.
398
+ *
399
+ * `found` keeps `getConfigLeaf`'s existing meaning (present in the file) so
400
+ * existing callers of that lower-level function are unaffected; this function is
401
+ * the one the CLI/MCP/console faces are meant to resolve reads through instead of
402
+ * each rolling their own fallback (a later task in
403
+ * [[HS-KW38-15XI-EPIC-settings-registry-core]] wires them up).
404
+ *
405
+ * @returns {{ key: string, value: *, found: boolean, source: 'file'|'registry_default'|'absent' }}
406
+ */
407
+ export function getEffectiveSetting(repoRoot, key, options = {}) {
408
+ // Computed key — answered from the machine-local trust store, NEVER from config.yaml (see
409
+ // TRUSTED_FOLDERS_KEY's doc comment above). Only the exact key resolves; a deeper child (e.g.
410
+ // "…trusted_folders.revoke") falls through to the normal absent handling below.
411
+ // `options.trustStorePath` is a test-only override (mirrors `options.configPath` elsewhere in
412
+ // this module) — every real caller omits it and reads the real `~/.heyspex/trusted-folders.json`.
413
+ if (key === TRUSTED_FOLDERS_KEY) {
414
+ const value = listTrustedFolders(readTrustStore(options.trustStorePath));
415
+ return { key, value, found: true, source: 'computed' };
416
+ }
417
+ const fileLeaf = getConfigLeaf(repoRoot, key, options);
418
+ if (fileLeaf.found) {
419
+ return { key, value: fileLeaf.value, found: true, source: 'file' };
420
+ }
421
+ const entry = getSettingEntry(key);
422
+ if (entry) {
423
+ return { key, value: entry.default, found: false, source: 'registry_default' };
424
+ }
425
+ return { key, value: null, found: false, source: 'absent' };
426
+ }
427
+
180
428
  /** Append one row to the settings audit log. Fail-safe: never throws. */
181
429
  export function appendSettingsAudit(repoRoot, row) {
182
430
  try {
@@ -200,21 +448,41 @@ export function appendSettingsAudit(repoRoot, row) {
200
448
  * indent, after the last line of the deepest existing ancestor's block. Preserves
201
449
  * every other line (comments included) byte-for-byte.
202
450
  *
203
- * Returns { ok, key, previous, value, created } or { ok:false, error }.
451
+ * Returns { ok, key, previous, value, created, audited } or { ok:false, error }.
204
452
  * Appends a `settings.write` audit row on success (meta: { actor, face }).
453
+ * `audited` is the MEASURED result of that append — false means the config write
454
+ * landed but the audit trail did not, which callers must report as a partial.
455
+ *
456
+ * SELF-HEALS a missing file ([[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]]
457
+ * fix 3): when `.heyspex/config.yaml` does not exist under an EXISTING vault root,
458
+ * it is created from `config-seed.yaml` first and the write lands on that seeded
459
+ * text; the result and the audit row then carry `seeded: true`. A vault root that
460
+ * does not exist, or a file that exists but is unreadable, still refuses with
461
+ * `config_unreadable` — seeding never creates a vault and never overwrites.
205
462
  */
206
463
  export function setConfigLeaf(repoRoot, key, value, meta = {}, options = {}) {
207
464
  const check = validateSettingKey(key);
208
465
  if (!check.ok) return { ok: false, error: 'invalid_key', message: check.reason };
466
+ const trustedFoldersRefusal = refuseTrustedFoldersWrite(key);
467
+ if (trustedFoldersRefusal) return trustedFoldersRefusal;
468
+ const registryCheck = validateSettingWrite(key, value);
469
+ if (!registryCheck.ok) return registryCheck;
209
470
  if (value !== null && !['string', 'number', 'boolean'].includes(typeof value)) {
210
471
  return { ok: false, error: 'invalid_value', message: 'value must be a scalar (string, number, boolean, or null)' };
211
472
  }
212
473
 
213
474
  const file = configPath(repoRoot, options);
214
- const raw = readRaw(repoRoot, options);
475
+ let raw = readRaw(repoRoot, options);
476
+ let seeded = false;
215
477
  if (raw === null) {
216
- return { ok: false, error: 'config_unreadable', message: `${file} not found or unreadable` };
478
+ const seedResult = seedMissingConfig(repoRoot, file);
479
+ if (!seedResult.ok) return seedResult;
480
+ raw = seedResult.raw;
481
+ seeded = seedResult.seeded === true;
217
482
  }
483
+ // Only present on the row/result when a seed actually happened — an existing
484
+ // file's write result and audit row keep their exact pre-existing shape.
485
+ const seededFields = seeded ? { seeded: true } : {};
218
486
 
219
487
  const eol = raw.includes('\r\n') ? '\r\n' : '\n';
220
488
  const lines = raw.split(/\r?\n/);
@@ -277,14 +545,19 @@ export function setConfigLeaf(repoRoot, key, value, meta = {}, options = {}) {
277
545
  } catch (err) {
278
546
  return { ok: false, error: 'write_failed', message: String(err?.message ?? err) };
279
547
  }
280
- appendSettingsAudit(repoRoot, {
548
+ // `appendSettingsAudit` is fail-safe and returns FALSE on an append that did
549
+ // not land; that boolean used to be discarded, so every face reported the
550
+ // write as "applied and audited" over a missing audit row
551
+ // ([[HS-IOJW-5YDH-DIAG]] sweep: report the result, not the request).
552
+ const audited = appendSettingsAudit(repoRoot, {
281
553
  key,
282
554
  previous,
283
555
  value,
284
556
  actor: meta.actor ?? 'unknown',
285
557
  face: meta.face ?? 'unknown',
558
+ ...seededFields,
286
559
  });
287
- return { ok: true, key, previous, value, created: false };
560
+ return { ok: true, key, previous, value, created: false, audited, ...seededFields };
288
561
  }
289
562
 
290
563
  // Insert. Build the missing chain below the deepest existing ancestor.
@@ -318,14 +591,15 @@ export function setConfigLeaf(repoRoot, key, value, meta = {}, options = {}) {
318
591
  } catch (err) {
319
592
  return { ok: false, error: 'write_failed', message: String(err?.message ?? err) };
320
593
  }
321
- appendSettingsAudit(repoRoot, {
594
+ const audited = appendSettingsAudit(repoRoot, {
322
595
  key,
323
596
  previous: null,
324
597
  value,
325
598
  actor: meta.actor ?? 'unknown',
326
599
  face: meta.face ?? 'unknown',
600
+ ...seededFields,
327
601
  });
328
- return { ok: true, key, previous: null, value, created: true };
602
+ return { ok: true, key, previous: null, value, created: true, audited, ...seededFields };
329
603
  }
330
604
 
331
605
  // ── two-step confirm store (the MCP write-permission handshake) ─────────────
@@ -357,10 +631,19 @@ export function peekPendingWrite(repoRoot) {
357
631
  export function requestSettingsWrite(repoRoot, { key, value, actor, face } = {}, options = {}) {
358
632
  const check = validateSettingKey(key);
359
633
  if (!check.ok) return { ok: false, error: 'invalid_key', message: check.reason };
634
+ const trustedFoldersRefusal = refuseTrustedFoldersWrite(key);
635
+ if (trustedFoldersRefusal) return trustedFoldersRefusal;
636
+ const registryCheck = validateSettingWrite(key, value);
637
+ if (!registryCheck.ok) return registryCheck;
360
638
  if (value !== null && !['string', 'number', 'boolean'].includes(typeof value)) {
361
639
  return { ok: false, error: 'invalid_value', message: 'value must be a scalar (string, number, boolean, or null)' };
362
640
  }
363
641
  const current = getConfigLeaf(repoRoot, key, options);
642
+ // A fresh vault may have no `.heyspex/` directory yet (HS-4C76): the pending
643
+ // record and the request audit row both live there, so create the directory
644
+ // now — inside an EXISTING vault root only (see ensureHeyspexDir). The config
645
+ // file itself is NOT created here; that happens on confirm, via setConfigLeaf.
646
+ ensureHeyspexDir(repoRoot);
364
647
  const token = crypto.randomBytes(6).toString('hex');
365
648
  const record = {
366
649
  token,