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
@@ -0,0 +1,113 @@
1
+ // vendor/heyspex-mcp/seat-block-ledger.mjs
2
+ //
3
+ // Per-session "has the DEV-FACTORY SEAT banner already gone out this session"
4
+ // decision for the two vendor surfaces that used to each keep their OWN flag:
5
+ // - tool-dispatch.mjs handleInitialize() → surface 'mcp_instructions'
6
+ // - cli.mjs maybePrependSeatBanner() → surface 'preflight' (when the
7
+ // first tool called is heyspex_preflight) or 'tool_result' otherwise
8
+ //
9
+ // Before this module, `initialize` always appended the banner to `instructions`
10
+ // AND, independently, the first tools/call response ALSO got the banner
11
+ // prepended (a separate `seatBannerSeenSessions` Set tracked only THAT surface) —
12
+ // so a caller could see "DEV-FACTORY SEAT: ..." twice in one session. That is
13
+ // exactly the bug spexharness/src/bootstrap-injection.mjs's engine contract
14
+ // exists to close for every surface (its own header calls out wiring
15
+ // vendor/heyspex-mcp as the deferred follow-up — HS-8U8X).
16
+ //
17
+ // spexharness/ is dev-only tooling: it is deliberately NOT part of the published
18
+ // `heyspex` npm package (see dev-factory-bridge.mjs for the identical open-core
19
+ // concern with dev-factory/spexcrawl/). scripts/verify-mcp-package.mjs's
20
+ // `closure` check fails the build if the server's STATIC import graph reaches a
21
+ // file the package doesn't ship, so the engine contract is reached the same way
22
+ // spexcrawl is: a memoized `await import()`, never a static import. A
23
+ // published install (no spexharness/ tree on disk) gets the fallback ledger
24
+ // below instead — same "at most one surface, across the whole session" contract,
25
+ // just without a shared spexharness bootstrap ledger to defer to.
26
+
27
+ /** @type {Promise<typeof import('../../spexharness/src/bootstrap-injection.mjs')>|undefined} */
28
+ let enginePromise;
29
+
30
+ function loadEngine() {
31
+ if (!enginePromise) {
32
+ enginePromise = import('../../spexharness/src/bootstrap-injection.mjs').catch(() => null);
33
+ }
34
+ return enginePromise;
35
+ }
36
+
37
+ /**
38
+ * Local, dependency-free ledger with the exact `shouldIncludeSeatBlock` /
39
+ * `markSeatBlockSent` shape bootstrap-injection.mjs's `withSeatBlockLedger`
40
+ * exposes — used when the engine module is not reachable (a published install,
41
+ * or any import failure). "No bootstrap yet" degrades to "emit once, guarded",
42
+ * never to "emit unguarded".
43
+ */
44
+ function createFallbackLedger() {
45
+ const sent = new Set();
46
+ return {
47
+ shouldIncludeSeatBlock(surface) {
48
+ if (sent.has(surface)) return false;
49
+ return sent.size === 0;
50
+ },
51
+ markSeatBlockSent(surface) {
52
+ sent.add(surface);
53
+ },
54
+ };
55
+ }
56
+
57
+ function fallbackSeatBlockForSurface(ledger, surface, seatLine) {
58
+ if (!ledger.shouldIncludeSeatBlock(surface)) return null;
59
+ ledger.markSeatBlockSent(surface);
60
+ return seatLine ?? null;
61
+ }
62
+
63
+ /** One ledger controller per session id — two sessions sharing this process (e.g. two
64
+ * fleet workers spawned back-to-back) never see each other's sends. */
65
+ const controllersBySession = new Map();
66
+
67
+ async function getSeatBlockController(sessionId) {
68
+ const key = sessionId || 'local';
69
+ const existing = controllersBySession.get(key);
70
+ if (existing) return existing;
71
+
72
+ const engine = await loadEngine();
73
+ const ledger = engine?.withSeatBlockLedger ? engine.withSeatBlockLedger({}) : createFallbackLedger();
74
+ const forSurface = engine?.seatBlockForSurface
75
+ ? (surface, seatLine) => engine.seatBlockForSurface(ledger, surface, seatLine)
76
+ : (surface, seatLine) => fallbackSeatBlockForSurface(ledger, surface, seatLine);
77
+
78
+ const controller = { forSurface };
79
+ controllersBySession.set(key, controller);
80
+ return controller;
81
+ }
82
+
83
+ /**
84
+ * Decide whether `surface` should emit the seat banner for `sessionId` this
85
+ * session — sourced from the engine contract's per-session ledger when
86
+ * spexharness/ is reachable (memoized per session id), else a local fallback
87
+ * with identical "exactly once, across every surface" semantics. Returns the
88
+ * banner text when `surface` is cleared to send it, else `null`. Never throws;
89
+ * a falsy `seatLine` (not enabled / unknown caller) short-circuits to `null`
90
+ * WITHOUT touching the ledger, so an ineligible caller never "uses up" the
91
+ * session's one send.
92
+ *
93
+ * @param {string|null|undefined} sessionId
94
+ * @param {string} surface one of bootstrap-injection.mjs's SEAT_BLOCK_SURFACES
95
+ * ('mcp_instructions', 'preflight', 'tool_result', 'bootstrap').
96
+ * @param {string|null|undefined} seatLine
97
+ * @returns {Promise<string|null>}
98
+ */
99
+ export async function seatBlockForSessionSurface(sessionId, surface, seatLine) {
100
+ if (!seatLine) return null;
101
+ try {
102
+ const controller = await getSeatBlockController(sessionId);
103
+ return controller.forSurface(surface, seatLine);
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ /** Test-only: forget every per-session ledger and the memoized engine probe. */
110
+ export function resetSeatBlockLedgerForTests() {
111
+ controllersBySession.clear();
112
+ enginePromise = undefined;
113
+ }
@@ -19,7 +19,7 @@
19
19
 
20
20
  import { readFileSync } from "node:fs";
21
21
  import path from "node:path";
22
- import { fileURLToPath } from "node:url";
22
+ import { PACKAGE_ROOT } from "./package-root.mjs";
23
23
 
24
24
  /**
25
25
  * Read and parse <vaultRoot>/dev-factory/seats.json.
@@ -169,11 +169,7 @@ function selfTest() {
169
169
  };
170
170
 
171
171
  // Repo root is two dirs up from this file (vendor/heyspex-mcp/ → repo root).
172
- const repoRoot = path.resolve(
173
- path.dirname(fileURLToPath(import.meta.url)),
174
- "..",
175
- "..",
176
- );
172
+ const repoRoot = PACKAGE_ROOT;
177
173
 
178
174
  // 1. Real registry smoke-read.
179
175
  const real = readSeatsRegistry(repoRoot);
@@ -0,0 +1,108 @@
1
+ // session-owner.mjs — the ONE place the MCP server learns which account owns
2
+ // the documents it files ([[HS-WE87-WW9R-TASK-thread-the-gated-caller-through-every]],
3
+ // epic [[HS-P1AW-WW9R-EPIC-identity-threading-and-the-end-of-the]] — invariant 1).
4
+ //
5
+ // Until this module the three frontmatter writers (`capture-focus.mjs`,
6
+ // `research-focus.mjs`, `defer-spec.mjs`) each stamped the HeySpex owner's
7
+ // handle as a literal — on a customer machine that put the platform owner's
8
+ // name on the customer's own documents. They now ask here, and when nothing
9
+ // resolves the `owner:` key is OMITTED rather than guessed.
10
+ //
11
+ // Resolution order (no network — this runs inside synchronous writers and
12
+ // under `node --test`):
13
+ // 1. `HS_ACCOUNT_ID` env — the same override the CLI's account reader honours.
14
+ // 2. the credentials store's current account (`credentials.mjs`
15
+ // `readCurrentAccountId`, [[HS-UA9S-WW9R-TASK-store-credentials-per-account-login]]):
16
+ // `current_account_id` of `~/.heyspex/credentials.yaml`, set by `hs login` /
17
+ // `hs mcp pair` from the account GET /api/mcp/session confirmed for the
18
+ // stored bearer and moved by `hs login --switch-account` / `hs logout`.
19
+ // An existing single-account (v1) file has no such key: its account is
20
+ // the `~/.heyspex/config.yaml` `account_id:` the earlier `hs login` wrote
21
+ // ([[HS-630L-WW9R-TASK-one-strict-cli-account-reader-hs-login]]) — read
22
+ // for that case only, never written any more (one account-id store).
23
+ // 3. null — the writer omits `owner:`.
24
+ //
25
+ // This is ALSO the CLI's account reader: `resolveConfiguredAccountId` in
26
+ // spex-console/src/lib/cli/vault-workspace-shared.ts and `readConfiguredAccountId`
27
+ // in spex-console/src/lib/cli/install-identity.ts both delegate to
28
+ // `resolveSessionOwnerUncached`, so one precedence serves `hs vault`, `hs connect`,
29
+ // `hs install` and every MCP frontmatter writer. Its bearer twin is
30
+ // `./credentials.mjs` (HS-HL0D).
31
+ //
32
+ // Cached per process once resolved from the real environment (the MCP server
33
+ // is spawned once per session; pairing changes restart it). Callers inject
34
+ // `env` / `homeDir` / `readFile` for tests, which bypasses the cache.
35
+
36
+ import { readFileSync } from 'node:fs';
37
+ import { homedir } from 'node:os';
38
+ import { join } from 'node:path';
39
+
40
+ import { readCurrentAccountId } from './credentials.mjs';
41
+
42
+ const ACCOUNT_ID_LINE_RE = /^account_id:\s*["']?([^"'\r\n#]+)/m;
43
+
44
+ /**
45
+ * Read the paired account id from `~/.heyspex/config.yaml`, or null.
46
+ * @param {{ homeDir?: string, readFile?: (p: string) => string }} [opts]
47
+ */
48
+ export function readConfigAccountId({ homeDir = homedir(), readFile } = {}) {
49
+ const read = readFile ?? ((p) => readFileSync(p, 'utf8'));
50
+ try {
51
+ const raw = read(join(homeDir, '.heyspex', 'config.yaml'));
52
+ const match = String(raw ?? '').match(ACCOUNT_ID_LINE_RE);
53
+ const value = match?.[1]?.trim() ?? '';
54
+ return value || null;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Resolve the session owner without caching — the pure core.
62
+ * @param {{ env?: NodeJS.ProcessEnv | Record<string, string | undefined>, homeDir?: string, readFile?: (p: string) => string }} [opts]
63
+ * @returns {string | null}
64
+ */
65
+ export function resolveSessionOwnerUncached({ env = process.env, homeDir = homedir(), readFile } = {}) {
66
+ const fromEnv = typeof env?.HS_ACCOUNT_ID === 'string' ? env.HS_ACCOUNT_ID.trim() : '';
67
+ if (fromEnv) return fromEnv;
68
+ return readCurrentAccountId({ homeDir, readFile });
69
+ }
70
+
71
+ let cached = null;
72
+ let cacheFilled = false;
73
+
74
+ /**
75
+ * The account every MCP-authored document is stamped with, or null when the
76
+ * machine has no resolvable account (the writers then OMIT `owner:`).
77
+ *
78
+ * The real-environment answer is memoised per process; any injected option
79
+ * bypasses the cache so tests stay hermetic.
80
+ * @param {{ env?: NodeJS.ProcessEnv | Record<string, string | undefined>, homeDir?: string, readFile?: (p: string) => string }} [opts]
81
+ * @returns {string | null}
82
+ */
83
+ export function resolveSessionOwner(opts) {
84
+ if (opts && Object.keys(opts).length > 0) return resolveSessionOwnerUncached(opts);
85
+ if (!cacheFilled) {
86
+ cached = resolveSessionOwnerUncached();
87
+ cacheFilled = true;
88
+ }
89
+ return cached;
90
+ }
91
+
92
+ /** Test seam: forget the per-process answer. */
93
+ export function __resetSessionOwnerCacheForTests() {
94
+ cached = null;
95
+ cacheFilled = false;
96
+ }
97
+
98
+ /**
99
+ * The `owner:` frontmatter entry for an object-shaped frontmatter — `{ owner }`
100
+ * when an account resolved, `{}` when none did (the key is omitted, never
101
+ * guessed). `explicit` (an `args.owner` the caller supplied) wins.
102
+ * @param {{ explicit?: unknown, resolve?: () => string | null }} [opts]
103
+ */
104
+ export function ownerFrontmatterEntry({ explicit, resolve = resolveSessionOwner } = {}) {
105
+ const chosen =
106
+ typeof explicit === 'string' && explicit.trim() ? explicit.trim() : (resolve() ?? null);
107
+ return chosen ? { owner: chosen } : {};
108
+ }
@@ -0,0 +1,152 @@
1
+ // Session hand-off and agent identity tools: heyspex_handoff_record, handoff_load,
2
+ // import_agent. Extracted from cli.mjs under
3
+ // [[HS-BLNF-6W61-DIAG-cli-mjs-has-grown-into-a-13-665-line]] (no behavior change).
4
+
5
+ import { consoleFetch } from './console-transport.mjs';
6
+ import { consoleErrorCode, consoleErrorMessage } from './console-fetch.mjs';
7
+ import { detectAgentIdentity } from './mcp-config.mjs';
8
+
9
+ /**
10
+ * Import a community agent role via console `POST /api/agents/import` — the MCP
11
+ * face of agent import ([[HS-TASK-Z666]]). Same core as `hs agent import`; no
12
+ * field-mapping logic duplicated in JS.
13
+ */
14
+ export async function handleImportAgent(args = {}) {
15
+ if (!args.collection || typeof args.collection !== 'string') {
16
+ return {
17
+ isError: true,
18
+ content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: 'collection is required' }) }],
19
+ };
20
+ }
21
+ if (!args.role || typeof args.role !== 'string') {
22
+ return {
23
+ isError: true,
24
+ content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: 'role is required' }) }],
25
+ };
26
+ }
27
+ const body = {
28
+ collection: args.collection,
29
+ role: args.role,
30
+ ...(args.dry_run === true || args.dry_run === 'true' ? { dry_run: true } : {}),
31
+ };
32
+ const result = await consoleFetch('/api/agents/import', { method: 'POST', body });
33
+ if (!result.ok) {
34
+ const payload = {
35
+ error: consoleErrorCode(result, 'IMPORT_AGENT_FAILED'),
36
+ message: consoleErrorMessage(result, `POST /api/agents/import returned ${result.status}`),
37
+ status: result.status,
38
+ url: result.url,
39
+ console_response: result.body,
40
+ attempted: body,
41
+ };
42
+ return {
43
+ isError: true,
44
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
45
+ };
46
+ }
47
+ const responseBody = result.body ?? {};
48
+ const payload = {
49
+ spec_id: responseBody.spec_id ?? null,
50
+ path: responseBody.path ?? null,
51
+ warnings: Array.isArray(responseBody.warnings) ? responseBody.warnings : [],
52
+ ...(responseBody.dry_run ? { dry_run: true } : {}),
53
+ console_response: responseBody,
54
+ };
55
+ return {
56
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Upsert the current resume context for this agent/lane into the Neon control
62
+ * plane — [[HS-EPIC-NCPS]] handoff_context / [[HS-EPIC-CLMW]] `hs handoff record`.
63
+ *
64
+ * Self-purging overwrite-in-place (PUT /api/control/handoff-context). The lane
65
+ * defaults to this agent's session id so each session is its own lane. Coordi-
66
+ * nation only — the endpoint rejects durable-knowledge fields.
67
+ */
68
+ export async function handleHandoffRecord(args = {}) {
69
+ const identity = detectAgentIdentity(args.agent, args.session_id);
70
+ const laneKey =
71
+ typeof args.lane === 'string' && args.lane.trim()
72
+ ? args.lane.trim()
73
+ : identity.sessionId;
74
+ const body = {
75
+ lane_key: laneKey,
76
+ agent: identity.agent,
77
+ ...(typeof args.spec_id === 'string' ? { spec_id: args.spec_id } : {}),
78
+ ...(typeof args.phase === 'string' ? { phase: args.phase } : {}),
79
+ ...(typeof args.estimated_time === 'string' ? { estimated_time: args.estimated_time } : {}),
80
+ ...(Array.isArray(args.blockers) ? { blockers: args.blockers.map(String) } : {}),
81
+ ...(Array.isArray(args.links) ? { links: args.links.map(String) } : {}),
82
+ };
83
+ const result = await consoleFetch('/api/control/handoff-context', {
84
+ method: 'PUT',
85
+ body,
86
+ });
87
+ if (!result.ok) {
88
+ const payload = {
89
+ error: consoleErrorCode(result, 'HANDOFF_RECORD_FAILED'),
90
+ message: consoleErrorMessage(result, `Handoff-context endpoint returned ${result.status}`),
91
+ status: result.status,
92
+ url: result.url,
93
+ console_response: result.body,
94
+ attempted: { lane: laneKey },
95
+ };
96
+ return {
97
+ isError: true,
98
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
99
+ };
100
+ }
101
+ return {
102
+ content: [{
103
+ type: 'text',
104
+ text: JSON.stringify({ lane: laneKey, console_response: result.body }, null, 2),
105
+ }],
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Read the current resume context for a lane from the Neon control plane —
111
+ * [[HS-EPIC-NCPS]] handoff_context / [[HS-EPIC-CLMW]] `hs handoff load`.
112
+ *
113
+ * Defaults to this agent's own lane (its session); `all: true` lists every
114
+ * active lane (parallel-agent off-on-deck). Self-purging — no per-session
115
+ * history; expired rows read as absent.
116
+ */
117
+ export async function handleHandoffLoad(args = {}) {
118
+ const identity = detectAgentIdentity(args.agent, args.session_id);
119
+ const wantAll = args.all === true;
120
+ const laneKey =
121
+ typeof args.lane === 'string' && args.lane.trim()
122
+ ? args.lane.trim()
123
+ : identity.sessionId;
124
+ const path = wantAll
125
+ ? '/api/control/handoff-context'
126
+ : `/api/control/handoff-context?lane=${encodeURIComponent(laneKey)}`;
127
+ const result = await consoleFetch(path);
128
+ if (!result.ok) {
129
+ const payload = {
130
+ error: consoleErrorCode(result, 'HANDOFF_LOAD_FAILED'),
131
+ message: consoleErrorMessage(result, `Handoff-context endpoint returned ${result.status}`),
132
+ status: result.status,
133
+ url: result.url,
134
+ console_response: result.body,
135
+ attempted: wantAll ? { all: true } : { lane: laneKey },
136
+ };
137
+ return {
138
+ isError: true,
139
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
140
+ };
141
+ }
142
+ return {
143
+ content: [{
144
+ type: 'text',
145
+ text: JSON.stringify(
146
+ wantAll ? result.body : { lane: laneKey, ...result.body },
147
+ null,
148
+ 2,
149
+ ),
150
+ }],
151
+ };
152
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * HeySpex close-out settings registry — the single declaration of every close-out
3
+ * setting: its dotted key, type, shipped default, description, CLI flag, admin-only
4
+ * marker, enforced marker, and which vault templates ship it.
5
+ *
6
+ * WHY: nothing today says which close-out settings exist, so a shipped default has
7
+ * nowhere to live and a mistyped key fails silently ([[HS-FCF5-15XI-RES-user-editable-close-out-settings-with]]).
8
+ * This registry is the one source every face is meant to read — `hs settings`, the
9
+ * MCP settings tools, and (later) the console — instead of each guessing its own
10
+ * fallback.
11
+ *
12
+ * DECLARATION ONLY (HS-6TDH): this module does not read or write
13
+ * `.heyspex/config.yaml`, does not import `config-settings.mjs`, and does not change
14
+ * any existing settings behavior. A later task in the same epic
15
+ * ([[HS-KW38-15XI-EPIC-settings-registry-core]]) teaches `config-settings.mjs` to
16
+ * resolve reads/writes through this registry (defaults, unknown-key refusal,
17
+ * enforced-entry refusal, type/enum validation).
18
+ *
19
+ * DEPENDENCY-FREE: Node stdlib only, no filesystem access at all — same rationale as
20
+ * the sibling module's "DEPENDENCY-FREE" note.
21
+ *
22
+ * Traceability: [[HS-KW38-15XI-EPIC-settings-registry-core]] ·
23
+ * [[HS-FCF5-15XI-RES-user-editable-close-out-settings-with]] ·
24
+ * [[HS-6TDH-15XI-TASK-declare-every-close-out-setting-in-one]]
25
+ */
26
+
27
+ /**
28
+ * @typedef {'boolean' | 'string' | 'enum'} SettingType
29
+ *
30
+ * @typedef {Object} SettingEntry
31
+ * @property {string} key - Dotted key, e.g. "close_out.update_docs".
32
+ * @property {SettingType} type - The value's declared type.
33
+ * @property {*} default - The shipped default value (absent-key resolution target).
34
+ * @property {string} description - One or two sentences a person can read as help text.
35
+ * @property {string|null} cliFlag - The `hs close` / `hs settings` flag name (e.g.
36
+ * "--update-docs"), or `null` for an enforced entry with no user-facing flag.
37
+ * @property {boolean} adminOnly - True for a HeySpex-admin-only entry; a non-admin
38
+ * vault must treat the key as unknown.
39
+ * @property {boolean} enforced - True for a hard rule that cannot be changed by
40
+ * anyone through any face, including a HeySpex admin.
41
+ * @property {string[]} templates - Which vault templates ship this entry:
42
+ * "heyspex" (the HeySpex platform vault) and/or "customer" (the shipped customer
43
+ * template).
44
+ * @property {string[]} [values] - Allowed values; present only when `type` is "enum".
45
+ */
46
+
47
+ /**
48
+ * The ordered list of every declared close-out setting. Order is the order the
49
+ * close-out runner is meant to evaluate them in (a later task); it is also the
50
+ * order `hs settings list` should render them in.
51
+ *
52
+ * @type {ReadonlyArray<Readonly<SettingEntry>>}
53
+ */
54
+ export const SETTINGS_REGISTRY = Object.freeze(
55
+ [
56
+ {
57
+ key: 'close_out.update_docs',
58
+ type: 'boolean',
59
+ default: true,
60
+ description:
61
+ 'Updates docs/user and docs/admin per the docs-action rule (create/edit/append) at every close-out.',
62
+ cliFlag: '--update-docs',
63
+ adminOnly: false,
64
+ enforced: false,
65
+ templates: ['heyspex', 'customer'],
66
+ },
67
+ {
68
+ key: 'close_out.review_grain',
69
+ type: 'enum',
70
+ values: ['milestone', 'task'],
71
+ default: 'milestone',
72
+ description:
73
+ 'Chooses the close-out review unit: "milestone" flips a completed task straight to closed with no per-task review companion, "task" is the canonical per-task close-out protocol with one HS-REV per task.',
74
+ cliFlag: '--review-grain',
75
+ adminOnly: false,
76
+ enforced: false,
77
+ templates: ['heyspex', 'customer'],
78
+ },
79
+ {
80
+ key: 'close_out.review_backlog_never_blocks',
81
+ type: 'boolean',
82
+ default: true,
83
+ description:
84
+ 'A review backlog sitting at in_review/review_pending never blocks a new claim and never needs owner permission.',
85
+ cliFlag: '--review-backlog-never-blocks',
86
+ adminOnly: false,
87
+ enforced: false,
88
+ templates: ['heyspex', 'customer'],
89
+ },
90
+ {
91
+ key: 'close_out.flip_completed_to_closed',
92
+ type: 'boolean',
93
+ default: true,
94
+ description: 'Flips a completed task straight to closed at close-out.',
95
+ cliFlag: '--flip-completed-to-closed',
96
+ adminOnly: false,
97
+ enforced: false,
98
+ templates: ['heyspex', 'customer'],
99
+ },
100
+ {
101
+ key: 'close_out.append_review_seed',
102
+ type: 'boolean',
103
+ default: true,
104
+ description: 'Appends a one-line review seed to the task change-log at close-out.',
105
+ cliFlag: '--append-review-seed',
106
+ adminOnly: false,
107
+ enforced: false,
108
+ templates: ['heyspex', 'customer'],
109
+ },
110
+ {
111
+ key: 'close_out.rolling_review_doc',
112
+ type: 'boolean',
113
+ default: true,
114
+ description: 'Accumulates one rolling review doc per milestone at close-out.',
115
+ cliFlag: '--rolling-review-doc',
116
+ adminOnly: false,
117
+ enforced: false,
118
+ templates: ['heyspex', 'customer'],
119
+ },
120
+ {
121
+ key: 'close_out.run_command_drift_doctor',
122
+ type: 'boolean',
123
+ default: true,
124
+ description:
125
+ 'Runs the HeySpex command-drift doctor at close-out. HeySpex-admin-only; a customer vault never sees this key.',
126
+ cliFlag: '--run-command-drift-doctor',
127
+ adminOnly: true,
128
+ enforced: false,
129
+ templates: ['heyspex'],
130
+ },
131
+ // Hard rules: enforced entries cannot be changed by anyone through any face,
132
+ // including a HeySpex admin, so they carry no user-facing CLI flag.
133
+ {
134
+ key: 'close_out.alignment',
135
+ type: 'boolean',
136
+ default: true,
137
+ description: 'Hard rule: alignment stays on. No mode may relax it.',
138
+ cliFlag: null,
139
+ adminOnly: false,
140
+ enforced: true,
141
+ templates: ['heyspex', 'customer'],
142
+ },
143
+ {
144
+ key: 'close_out.capture',
145
+ type: 'boolean',
146
+ default: true,
147
+ description: 'Hard rule: capture stays on. No mode may relax it.',
148
+ cliFlag: null,
149
+ adminOnly: false,
150
+ enforced: true,
151
+ templates: ['heyspex', 'customer'],
152
+ },
153
+ {
154
+ key: 'close_out.research',
155
+ type: 'boolean',
156
+ default: true,
157
+ description: 'Hard rule: research stays on. No mode may relax it.',
158
+ cliFlag: null,
159
+ adminOnly: false,
160
+ enforced: true,
161
+ templates: ['heyspex', 'customer'],
162
+ },
163
+ // `close_out.git_commit` moved OUT of the hard-rule set above (owner ruling
164
+ // 2026-09-11, [[HS-PROTO-spexsource-agent-protocol]]): whether an agent
165
+ // ever commits at close-out is now a real, permission-gated toggle instead
166
+ // of an unconditional refusal. Default stays `false` for every vault —
167
+ // heyspex and customer alike — so nobody's out-of-box behavior changes; an
168
+ // owner opts in per vault via the normal settings-write-permission flow
169
+ // (`heyspex_settings_set` / `hs settings set --confirmed-by-user`). When
170
+ // `true`, the agent does not commit unconditionally — it still obeys the
171
+ // existing mode logic: the close-out commit lands at the boundary of the
172
+ // *active review grain* (`close_out.review_grain` — task / epic /
173
+ // milestone), routes only through the sanctioned `hs source preflight` →
174
+ // `hs source commit` path, and stays bound by every entry in
175
+ // spexsource-agent-protocol's always-forbidden list (no `--no-verify`, no
176
+ // force, no push — pushing is the separate `close_out.git_push` key,
177
+ // still enforced below). An explicit owner instruction for the session
178
+ // (e.g. "don't commit this one") overrides the setting for that session;
179
+ // the setting only changes the *default* when the owner said nothing.
180
+ {
181
+ key: 'close_out.git_commit',
182
+ type: 'boolean',
183
+ default: false,
184
+ description:
185
+ 'Whether the agent commits automatically at close-out. Default false (a dirty tree is the expected hand-off; the human reviewer commits on Pass). When true, the agent still obeys the active review grain (close_out.review_grain) for WHEN to commit and the spexsource-agent-protocol always-forbidden list for HOW — never a bypass of those rules, just an opt-in default.',
186
+ cliFlag: '--git-commit',
187
+ adminOnly: false,
188
+ enforced: false,
189
+ templates: ['heyspex', 'customer'],
190
+ },
191
+ {
192
+ key: 'close_out.git_push',
193
+ type: 'boolean',
194
+ default: false,
195
+ description: 'Hard rule: an agent never pushes at close-out.',
196
+ cliFlag: null,
197
+ adminOnly: false,
198
+ enforced: true,
199
+ templates: ['heyspex', 'customer'],
200
+ },
201
+ ].map((entry) => Object.freeze(entry)),
202
+ );
203
+
204
+ /** Fast key → entry lookup, built once from `SETTINGS_REGISTRY`. */
205
+ const BY_KEY = new Map(SETTINGS_REGISTRY.map((entry) => [entry.key, entry]));
206
+
207
+ /**
208
+ * Look up one registry entry by its dotted key.
209
+ * @param {string} key
210
+ * @returns {Readonly<SettingEntry>|null}
211
+ */
212
+ export function getSettingEntry(key) {
213
+ if (typeof key !== 'string') return null;
214
+ return BY_KEY.get(key) ?? null;
215
+ }
216
+
217
+ /** True when `key` names a declared registry entry. */
218
+ export function isKnownSetting(key) {
219
+ return getSettingEntry(key) !== null;
220
+ }
221
+
222
+ /** Every declared key, in registry order. */
223
+ export function listRegistryKeys() {
224
+ return SETTINGS_REGISTRY.map((entry) => entry.key);
225
+ }
226
+
227
+ /** Every declared key the given vault template ships ("heyspex" or "customer"). */
228
+ export function listKeysForTemplate(template) {
229
+ return SETTINGS_REGISTRY.filter((entry) => entry.templates.includes(template)).map(
230
+ (entry) => entry.key,
231
+ );
232
+ }