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,357 @@
1
+ // spexharness/src/profile/proposals.mjs
2
+ //
3
+ // P6 (epic [[HS-HEBV-8CMW-EPIC-spexcode-profile-v0-learn-the-developer]], task
4
+ // [[HS-B0JT-8CMW-TASK-p6-proposal-flow-learned-observations]]) — the accept/decline
5
+ // gate in front of P1's profile file. A learned observation the P1 learner
6
+ // (spexharness/src/profile/learn.mjs) would otherwise fold straight into the profile
7
+ // instead starts life here as a `pending` proposal; only an explicit `accept()` turns
8
+ // it into a real profile entry P4-style injection would ever read. A `decline()` is
9
+ // remembered by a normalized-text fingerprint, so the same note (or a near-duplicate
10
+ // of it) is never re-queued.
11
+ //
12
+ // Reuse, never duplicate: this module writes through to P1's OWN profile file and
13
+ // incremental-state file via learn.mjs's exported `loadProfileState`/`saveProfileState`/
14
+ // `classifySection`/`renderProfileMarkdown` — it never re-implements the section store or
15
+ // the markdown renderer. It also drives the T15 arrow-key select prompt
16
+ // (spexharness/src/chat/tui/select.mjs) rather than a typed y/n prompt, the same UI every
17
+ // other high-stakes ask in this harness already uses.
18
+ //
19
+ // Ledger: `.heyspex/profile-proposals.jsonl` (machine-local, NEVER written to the vault
20
+ // under `specs/` — same rule as the profile file itself). Append-only event log
21
+ // (`queued`/`accepted`/`declined`), one JSON object per line; current state for any given
22
+ // proposal id is the fold of its own events (latest wins), mirroring the shape DR1's
23
+ // corpus.mjs already uses for an append-only, resumable log.
24
+ //
25
+ // Asking happens only at a turn boundary (turn end / session close) — never mid-stream,
26
+ // never blocking the prompt — via `presentPendingProposal`/`createProposalPrompt`, which a
27
+ // front wires into `spexharness/src/chat/chat-loop.mjs`'s optional `proposalPrompt` hook.
28
+ // `listPending()` itself never conditions a seat's context: it only reads the ledger.
29
+
30
+ import { existsSync, mkdirSync, readFileSync, appendFileSync, writeFileSync } from 'node:fs';
31
+ import { dirname, join } from 'node:path';
32
+
33
+ import { classifySection, loadProfileState, normalizeFingerprint, saveProfileState, renderProfileMarkdown } from './learn.mjs';
34
+ import { select, SELECT_NEEDS_TTY } from '../chat/tui/select.mjs';
35
+
36
+ // ── paths ─────────────────────────────────────────────────────────────────────────
37
+
38
+ /** The proposal ledger's canonical, machine-local location. */
39
+ export function defaultProposalsLedgerPath(repoRoot) {
40
+ return join(repoRoot, '.heyspex', 'profile-proposals.jsonl');
41
+ }
42
+
43
+ // ── fingerprinting (near-duplicate detection for declined proposals) ────────────────
44
+
45
+ /**
46
+ * Normalize `text` into a stable, whitespace/punctuation-insensitive fingerprint — two
47
+ * observations that only differ by casing, punctuation, or extra whitespace normalize to
48
+ * the same string, so a decline blocks a near-duplicate re-queue, not just an exact repeat.
49
+ *
50
+ * Defined in ./learn.mjs and re-exported here (this module's long-standing public name for
51
+ * it). learn.mjs's own `dedupeSection` uses the SAME function, so "already in the profile"
52
+ * and "already proposed" can never drift apart.
53
+ */
54
+ export { normalizeFingerprint };
55
+
56
+ function proposalIdForObservation(observation) {
57
+ if (observation && typeof observation.id === 'string' && observation.id.trim()) return observation.id.trim();
58
+ return `fp:${normalizeFingerprint(observation?.text)}`;
59
+ }
60
+
61
+ // ── ledger I/O ────────────────────────────────────────────────────────────────────
62
+
63
+ /** Read every event from the ledger, oldest first. Missing file -> []. Corrupt lines skipped. */
64
+ export function readLedgerEvents(ledgerPath) {
65
+ if (!existsSync(ledgerPath)) return [];
66
+ const text = readFileSync(ledgerPath, 'utf8');
67
+ const out = [];
68
+ for (const line of text.split('\n')) {
69
+ const trimmed = line.trim();
70
+ if (!trimmed) continue;
71
+ try {
72
+ out.push(JSON.parse(trimmed));
73
+ } catch {
74
+ // corrupt/partial line — skip, don't throw
75
+ }
76
+ }
77
+ return out;
78
+ }
79
+
80
+ function appendLedgerEvent(ledgerPath, event) {
81
+ mkdirSync(dirname(ledgerPath), { recursive: true });
82
+ appendFileSync(ledgerPath, `${JSON.stringify(event)}\n`);
83
+ }
84
+
85
+ /**
86
+ * Fold the append-only ledger into current per-id state (the latest event for a given id
87
+ * wins) plus the full set of fingerprints any `declined` event has ever carried — a
88
+ * near-duplicate is blocked by matching ANY prior decline's fingerprint, not just the one
89
+ * belonging to the same id.
90
+ */
91
+ function foldLedger(events) {
92
+ const byId = new Map();
93
+ const declinedFingerprints = new Set();
94
+ for (const ev of events) {
95
+ if (!ev || typeof ev.id !== 'string') continue;
96
+ const prior = byId.get(ev.id) ?? {};
97
+ byId.set(ev.id, { ...prior, ...ev });
98
+ if (ev.type === 'declined' && typeof ev.fingerprint === 'string') {
99
+ declinedFingerprints.add(ev.fingerprint);
100
+ }
101
+ }
102
+ return { byId, declinedFingerprints };
103
+ }
104
+
105
+ // ── queue / list ──────────────────────────────────────────────────────────────────
106
+
107
+ /**
108
+ * Every proposal currently `pending`, oldest first. Pure read — never mutates the ledger,
109
+ * never touches the profile file or state. This is the ONE thing a seat's context
110
+ * injection must never read (P4-style injection reads only `accepted` profile entries).
111
+ *
112
+ * @param {{ ledgerPath: string }} opts
113
+ * @returns {Array<{ id: string, status: 'pending', fingerprint: string, queuedAt: string, observation: object }>}
114
+ */
115
+ export function listPending({ ledgerPath }) {
116
+ const { byId } = foldLedger(readLedgerEvents(ledgerPath));
117
+ const out = [];
118
+ for (const proposal of byId.values()) {
119
+ if (proposal.status === 'pending') out.push(proposal);
120
+ }
121
+ out.sort((a, b) => String(a.queuedAt ?? '').localeCompare(String(b.queuedAt ?? '')));
122
+ return out;
123
+ }
124
+
125
+ /**
126
+ * Queue a candidate profile entry from the P1 learner as `pending` — it does NOT touch the
127
+ * profile file or condition anything until `accept()` is called. A repeat of the same
128
+ * observation id, or a near-duplicate (by fingerprint) of a previously DECLINED
129
+ * observation, is silently skipped rather than re-queued.
130
+ *
131
+ * @param {{ id?: string, text: string, source?: string, project?: string, session?: string,
132
+ * ts?: string|null, kind?: string, supersedes?: object|null }} observation - the same
133
+ * shape corpus.mjs's `readObservations` yields; `supersedes`, when present (P7's
134
+ * contradiction-detection path), is carried through verbatim and interpreted only by the
135
+ * caller — this module never inspects it.
136
+ * @param {{ ledgerPath: string, now?: () => string }} opts
137
+ * @returns {{ queued: boolean, id: string, reason?: string, fingerprint?: string, queuedAt?: string, status?: string }}
138
+ */
139
+ export function queueProposal(observation, { ledgerPath, now = () => new Date().toISOString() } = {}) {
140
+ if (!ledgerPath) throw new Error('queueProposal: ledgerPath is required');
141
+ if (!observation || typeof observation.text !== 'string' || !observation.text.trim()) {
142
+ return { queued: false, id: '', reason: 'empty-observation' };
143
+ }
144
+
145
+ const id = proposalIdForObservation(observation);
146
+ const fingerprint = normalizeFingerprint(observation.text);
147
+ const { byId, declinedFingerprints } = foldLedger(readLedgerEvents(ledgerPath));
148
+
149
+ const existing = byId.get(id);
150
+ if (existing && (existing.status === 'pending' || existing.status === 'accepted')) {
151
+ return { queued: false, id, reason: 'already-queued', status: existing.status };
152
+ }
153
+ if (existing && existing.status === 'declined') {
154
+ return { queued: false, id, reason: 'declined-duplicate' };
155
+ }
156
+ if (declinedFingerprints.has(fingerprint)) {
157
+ return { queued: false, id, reason: 'declined-near-duplicate', fingerprint };
158
+ }
159
+
160
+ const queuedAt = now();
161
+ appendLedgerEvent(ledgerPath, {
162
+ type: 'queued',
163
+ id,
164
+ status: 'pending',
165
+ fingerprint,
166
+ queuedAt,
167
+ observation: {
168
+ id: observation.id ?? null,
169
+ text: observation.text,
170
+ source: observation.source ?? null,
171
+ project: observation.project ?? null,
172
+ session: observation.session ?? null,
173
+ ts: observation.ts ?? null,
174
+ kind: observation.kind ?? null,
175
+ supersedes: observation.supersedes ?? null,
176
+ },
177
+ });
178
+ return { queued: true, id, fingerprint, queuedAt, status: 'pending' };
179
+ }
180
+
181
+ function requirePendingProposal(ledgerPath, id) {
182
+ const { byId } = foldLedger(readLedgerEvents(ledgerPath));
183
+ const proposal = byId.get(id);
184
+ if (!proposal || proposal.status !== 'pending') return null;
185
+ return proposal;
186
+ }
187
+
188
+ // ── decide ────────────────────────────────────────────────────────────────────────
189
+
190
+ /**
191
+ * Decline a pending proposal. Its fingerprint is remembered so the same observation, or a
192
+ * near-duplicate of it, is never re-queued by a later `queueProposal` call.
193
+ *
194
+ * @param {string} id
195
+ * @param {{ ledgerPath: string, now?: () => string }} opts
196
+ * @returns {{ ok: boolean, id: string, reason?: string, declinedAt?: string }}
197
+ */
198
+ export function decline(id, { ledgerPath, now = () => new Date().toISOString() } = {}) {
199
+ if (!ledgerPath) throw new Error('decline: ledgerPath is required');
200
+ const proposal = requirePendingProposal(ledgerPath, id);
201
+ if (!proposal) return { ok: false, id, reason: 'not-pending' };
202
+
203
+ const declinedAt = now();
204
+ appendLedgerEvent(ledgerPath, {
205
+ type: 'declined',
206
+ id,
207
+ status: 'declined',
208
+ fingerprint: proposal.fingerprint,
209
+ declinedAt,
210
+ });
211
+ return { ok: true, id, declinedAt };
212
+ }
213
+
214
+ /**
215
+ * Accept a pending proposal — the ONLY path that turns a learned observation into a real
216
+ * profile entry. Writes through to P1's own profile file/state (`learn.mjs`'s
217
+ * `loadProfileState`/`saveProfileState`/`renderProfileMarkdown`), classifying the
218
+ * observation into a section exactly like `buildProfile` does, but with provenance
219
+ * (`accepted-at`, the source observation id) folded into the rendered line so an accepted
220
+ * entry is traceable back to the proposal that produced it.
221
+ *
222
+ * @param {string} id
223
+ * @param {{ ledgerPath: string, profilePath: string, profileStatePath: string, now?: () => string }} opts
224
+ * @returns {{ ok: boolean, id: string, reason?: string, acceptedAt?: string, section?: string, profilePath?: string, markdown?: string }}
225
+ */
226
+ export function accept(id, { ledgerPath, profilePath, profileStatePath, now = () => new Date().toISOString() } = {}) {
227
+ if (!ledgerPath) throw new Error('accept: ledgerPath is required');
228
+ if (!profilePath) throw new Error('accept: profilePath is required');
229
+ if (!profileStatePath) throw new Error('accept: profileStatePath is required');
230
+
231
+ const proposal = requirePendingProposal(ledgerPath, id);
232
+ if (!proposal) return { ok: false, id, reason: 'not-pending' };
233
+
234
+ const acceptedAt = now();
235
+ const observation = proposal.observation ?? {};
236
+ const key = classifySection({ kind: observation.kind, text: observation.text });
237
+
238
+ const state = loadProfileState(profileStatePath);
239
+ const sourceObservationId = observation.id ?? id;
240
+ const provenanceText = `${observation.text} _(accepted: ${acceptedAt}, from proposal: ${id}, source observation: ${sourceObservationId})_`;
241
+ state.sections[key].push({
242
+ text: provenanceText,
243
+ source: observation.source ?? 'unknown',
244
+ project: observation.project ?? 'unknown',
245
+ session: observation.session ?? 'unknown',
246
+ ts: observation.ts ?? null,
247
+ });
248
+ state.lastLearnedAt = acceptedAt;
249
+
250
+ const markdown = renderProfileMarkdown(state.sections);
251
+ mkdirSync(dirname(profilePath), { recursive: true });
252
+ writeFileSync(profilePath, markdown);
253
+ saveProfileState(profileStatePath, state);
254
+
255
+ appendLedgerEvent(ledgerPath, {
256
+ type: 'accepted',
257
+ id,
258
+ status: 'accepted',
259
+ fingerprint: proposal.fingerprint,
260
+ acceptedAt,
261
+ sourceObservationId,
262
+ });
263
+
264
+ return { ok: true, id, acceptedAt, section: key, profilePath, markdown };
265
+ }
266
+
267
+ // ── turn-boundary prompt (T15 arrow-key select) ─────────────────────────────────────
268
+
269
+ /**
270
+ * Present the oldest pending proposal (if any) via the T15 arrow-key select prompt and act
271
+ * on the developer's choice. Never called mid-stream — a caller wires this into a turn
272
+ * boundary (turn end / session close) only, same rule as the folder-trust/tool-permission
273
+ * asks already follow. On a non-TTY `stdin` (a script/headless run), this resolves with
274
+ * `{ presented: false, reason: 'no-tty' }` instead of throwing — a proposal prompt must
275
+ * never be the reason a non-interactive run breaks.
276
+ *
277
+ * @param {{ ledgerPath: string, profilePath: string, profileStatePath: string,
278
+ * stdin?: NodeJS.ReadStream, stdout?: { write: Function }, selectFn?: typeof select,
279
+ * now?: () => string }} opts
280
+ * @returns {Promise<{ presented: boolean, reason?: string, decision?: string, id?: string, result?: object }>}
281
+ */
282
+ export async function presentPendingProposal({
283
+ ledgerPath,
284
+ profilePath,
285
+ profileStatePath,
286
+ stdin = process.stdin,
287
+ stdout = process.stdout,
288
+ selectFn = select,
289
+ now,
290
+ } = {}) {
291
+ const pending = listPending({ ledgerPath });
292
+ if (pending.length === 0) return { presented: false, reason: 'no-pending' };
293
+
294
+ const proposal = pending[0];
295
+ const obs = proposal.observation ?? {};
296
+
297
+ let choice;
298
+ try {
299
+ choice = await selectFn({
300
+ title: 'SpexCode noticed a new habit — add it to your SpexCode Profile?',
301
+ lines: [
302
+ obs.text ?? '',
303
+ `_(source: ${obs.source ?? 'unknown'}, project: ${obs.project ?? 'unknown'}, session: ${obs.session ?? 'unknown'})_`,
304
+ ],
305
+ options: [
306
+ { key: 'accept', label: 'Accept — add to my SpexCode Profile' },
307
+ { key: 'decline', label: "Decline — don't ask about this again" },
308
+ { key: 'later', label: 'Ask me later' },
309
+ ],
310
+ footer: '↑/↓ to navigate · enter to select · esc to skip for now',
311
+ stdin,
312
+ stdout,
313
+ });
314
+ } catch (err) {
315
+ if (err && (err.code === SELECT_NEEDS_TTY || err.message === SELECT_NEEDS_TTY)) {
316
+ return { presented: false, reason: 'no-tty' };
317
+ }
318
+ throw err;
319
+ }
320
+
321
+ if (choice === 'accept') {
322
+ const result = accept(proposal.id, { ledgerPath, profilePath, profileStatePath, now });
323
+ return { presented: true, decision: 'accept', id: proposal.id, result };
324
+ }
325
+ if (choice === 'decline') {
326
+ const result = decline(proposal.id, { ledgerPath, now });
327
+ return { presented: true, decision: 'decline', id: proposal.id, result };
328
+ }
329
+ // Escape (null) or 'later' — leave it pending, ask again another turn.
330
+ return { presented: true, decision: choice ?? 'later', id: proposal.id };
331
+ }
332
+
333
+ /**
334
+ * Bind `presentPendingProposal`'s options once and hand back a zero-arg-friendly hook
335
+ * shaped for `spexharness/src/chat/chat-loop.mjs`'s optional `proposalPrompt` — called
336
+ * (awaited) at a turn boundary only.
337
+ *
338
+ * @param {Parameters<typeof presentPendingProposal>[0]} opts
339
+ * @returns {() => Promise<ReturnType<typeof presentPendingProposal>>}
340
+ */
341
+ export function createProposalPrompt(opts = {}) {
342
+ return function proposalPrompt() {
343
+ return presentPendingProposal(opts);
344
+ };
345
+ }
346
+
347
+ export default {
348
+ defaultProposalsLedgerPath,
349
+ normalizeFingerprint,
350
+ readLedgerEvents,
351
+ listPending,
352
+ queueProposal,
353
+ decline,
354
+ accept,
355
+ presentPendingProposal,
356
+ createProposalPrompt,
357
+ };