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,471 @@
1
+ // Install the HeySpex lifecycle hooks into Cursor (`.cursor/hooks.json`), at
2
+ // parity with what `.claude/settings.json` registers for Claude Code.
3
+ //
4
+ // The registration itself lives in scripts/cursor-hooks/manifest.mjs (shared
5
+ // with heyspex-doctor.mjs and the smoke test); this file only knows how to
6
+ // merge it into Cursor's config file without disturbing anything it does not
7
+ // own. Idempotent, and self-heals command-string / timeout drift.
8
+ //
9
+ // Usage:
10
+ // node scripts/install-cursor-hooks.mjs # install / repair
11
+ // node scripts/install-cursor-hooks.mjs --check # exit 1 on drift
12
+ // node scripts/install-cursor-hooks.mjs --uninstall
13
+ // node scripts/install-cursor-hooks.mjs --json # machine-readable report
14
+ //
15
+ // OWNERSHIP. Every entry this script writes runs through the shim
16
+ // (`scripts/cursor-hooks/_run.mjs`), which is also how it recognizes its own
17
+ // work on a re-run. Foreign hooks in the same file are never touched.
18
+ //
19
+ // LEGACY MIGRATION. Earlier versions registered a handful of hooks by pointing
20
+ // Cursor straight at `scripts/claude-hooks/<hook>.mjs`. Those entries are
21
+ // REMOVED on install rather than left alone: the manifest now registers the
22
+ // same hooks through the shim, and leaving both would fire each one twice and
23
+ // bypass the off-switch for the direct copy.
24
+
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ import { fileURLToPath } from 'node:url';
28
+
29
+ import {
30
+ CUSTOMER_LAUNCHER,
31
+ HOOKS,
32
+ SHIM,
33
+ UNPORTABLE,
34
+ commandFor,
35
+ commandForCustomer,
36
+ commandForPlugin,
37
+ identityFor,
38
+ timeoutFor,
39
+ } from './cursor-hooks/manifest.mjs';
40
+ import {
41
+ CUSTOMER_LAUNCHERS,
42
+ hookScope,
43
+ invokedDirectly,
44
+ planCustomerLauncherWrite,
45
+ samePlatformPath,
46
+ } from './lib/customer-kit.mjs';
47
+
48
+ const PLATFORM_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
49
+ const REPO_ROOT = process.env.HS_SETUP_TARGET?.trim()
50
+ ? path.resolve(process.env.HS_SETUP_TARGET.trim())
51
+ : PLATFORM_ROOT;
52
+ const SETTINGS_PATH = path.join(REPO_ROOT, '.cursor', 'hooks.json');
53
+
54
+ // CUSTOMER TARGET (`hs setup cursor` / `hs doctor --fix` with HS_SETUP_TARGET
55
+ // pointing at a repo other than this checkout, or `hs install`): only the
56
+ // customer-scoped hooks of scripts/lib/customer-kit.mjs ship, addressed
57
+ // through a committed LAUNCHER (`.cursor/heyspex/run-hook.mjs`, written here
58
+ // too) that finds the heyspex package from the repo -- never this checkout's
59
+ // absolute path, which exists on no other machine
60
+ // ([[HS-EN4F-F4V2-DIAG-a-repo-set-up-with-hs-install-gets-no]],
61
+ // [[HS-0AF8-WW9R-TASK-portable-hook-and-command-paths-in]]).
62
+
63
+ /**
64
+ * The hook set + command renderer for a target repo. For a customer target
65
+ * `launcher` names the committed launcher file (relative segments + text) the
66
+ * rendered commands point at; the platform checkout has none.
67
+ * @param {string} targetRoot
68
+ * @param {string} [platformRoot]
69
+ */
70
+ export function cursorKitFor(targetRoot, platformRoot = PLATFORM_ROOT) {
71
+ const customer = !samePlatformPath(targetRoot, platformRoot);
72
+ return {
73
+ customer,
74
+ hooks: customer ? HOOKS.filter((h) => hookScope(h.hook) === 'customer') : HOOKS,
75
+ commandFor: customer ? commandForCustomer : commandFor,
76
+ launcher: customer ? { rel: CUSTOMER_LAUNCHERS.cursor.rel, plan: planCustomerLauncherWrite } : null,
77
+ };
78
+ }
79
+
80
+ const KIT = cursorKitFor(REPO_ROOT);
81
+ const ACTIVE_HOOKS = KIT.hooks;
82
+ const LAUNCHER_PATH = path.join(REPO_ROOT, ...CUSTOMER_LAUNCHERS.cursor.rel);
83
+
84
+ const LEGACY_MARKER = 'scripts/claude-hooks/';
85
+
86
+ function readSettings() {
87
+ try {
88
+ const raw = fs.readFileSync(SETTINGS_PATH, 'utf8').trim();
89
+ if (!raw) return { version: 1, hooks: {} };
90
+ const parsed = JSON.parse(raw);
91
+ if (parsed && typeof parsed === 'object') {
92
+ parsed.version ??= 1;
93
+ parsed.hooks ??= {};
94
+ return parsed;
95
+ }
96
+ } catch (err) {
97
+ if (err && err.code === 'ENOENT') return { version: 1, hooks: {} };
98
+ throw err;
99
+ }
100
+ return { version: 1, hooks: {} };
101
+ }
102
+
103
+ function writeSettings(settings) {
104
+ fs.mkdirSync(path.dirname(SETTINGS_PATH), { recursive: true });
105
+ fs.writeFileSync(SETTINGS_PATH, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
106
+ }
107
+
108
+ const commandOf = (entry) => (entry && typeof entry.command === 'string' ? entry.command : '');
109
+
110
+ /** Ours = routed through the shim (any path form) or the committed customer launcher. */
111
+ const isOurRunner = (token) => token.includes(SHIM) || token.includes(CUSTOMER_LAUNCHER);
112
+ const isShimEntry = (raw) => isOurRunner(commandOf(raw));
113
+
114
+ /** A pre-shim registration that the manifest now supersedes. */
115
+ const isLegacyEntry = (raw) => !isShimEntry(raw) && commandOf(raw).includes(LEGACY_MARKER);
116
+
117
+ /**
118
+ * Which manifest entry a shim command corresponds to. Matching on the parsed
119
+ * command (hook name + tool filter + forwarded args) rather than on the whole
120
+ * string is what lets a timeout or flag-order change -- or the 2026-10-02
121
+ * absolute platform path -- be REPAIRED instead of duplicated.
122
+ */
123
+ function identityOfCommand(command) {
124
+ const tokens = command.trim().split(/\s+/);
125
+ const shimAt = tokens.findIndex(isOurRunner);
126
+ if (shimAt === -1) return null;
127
+ const rest = tokens.slice(shimAt + 1);
128
+ const hook = rest[0] ?? '';
129
+ let tool = '';
130
+ const args = [];
131
+ for (const token of rest.slice(1)) {
132
+ // The tool filter is quoted on disk; identityFor() works with the raw
133
+ // regex, so the quotes come off before comparing.
134
+ if (token.startsWith('--hs-tool=')) {
135
+ tool = token.slice('--hs-tool='.length).replace(/^"([\s\S]*)"$/, '$1');
136
+ } else if (token.startsWith('--hs-claude-event=')) continue;
137
+ else args.push(token);
138
+ }
139
+ return `${hook}::${tool}::${args.join(' ')}`;
140
+ }
141
+
142
+ function desiredEntry(entry, render = KIT.commandFor) {
143
+ return {
144
+ command: render(entry),
145
+ timeout: timeoutFor(entry),
146
+ ...(entry.matcher ? { matcher: entry.matcher } : {}),
147
+ };
148
+ }
149
+
150
+ function sameEntry(raw, entry, render = KIT.commandFor) {
151
+ const want = desiredEntry(entry, render);
152
+ return (
153
+ commandOf(raw) === want.command &&
154
+ raw.timeout === want.timeout &&
155
+ (raw.matcher ?? undefined) === (want.matcher ?? undefined)
156
+ );
157
+ }
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // Plugin mirror
161
+ // ---------------------------------------------------------------------------
162
+ // plugins/heyspex ships the same hooks for a PLUGIN install (a consumer repo
163
+ // that installs HeySpex from the marketplace rather than checking the platform
164
+ // out). It is generated from the manifest above, not hand-mirrored — the Claude
165
+ // equivalent (plugins/heyspex/hooks/hooks.json) was a dated hand mirror (now
166
+ // generated from scripts/lib/customer-kit.mjs), and that
167
+ // is exactly the drift this avoids.
168
+ const PLUGIN_HOOKS_PATH = path.join(REPO_ROOT, 'plugins', 'heyspex', 'hooks', 'cursor-hooks.json');
169
+
170
+ function pluginHooksDocument() {
171
+ const hooks = {};
172
+ for (const entry of HOOKS) {
173
+ hooks[entry.event] ??= [];
174
+ hooks[entry.event].push({
175
+ command: commandForPlugin(entry),
176
+ timeout: timeoutFor(entry),
177
+ ...(entry.matcher ? { matcher: entry.matcher } : {}),
178
+ });
179
+ }
180
+ return {
181
+ _comment:
182
+ 'GENERATED by scripts/install-cursor-hooks.mjs from scripts/cursor-hooks/manifest.mjs — ' +
183
+ 'do not edit by hand; run `npm run hooks:install-cursor`. Commands resolve through ' +
184
+ '$CURSOR_PROJECT_DIR because the shim and hook bodies live in the host vault checkout, ' +
185
+ 'not the plugin cache. WARNING: enabling this plugin in a repo whose .cursor/hooks.json ' +
186
+ 'still registers the same hooks runs every hook twice — validate in a fresh clone, or ' +
187
+ 'run `node scripts/install-cursor-hooks.mjs --uninstall` there first.',
188
+ version: 1,
189
+ hooks,
190
+ };
191
+ }
192
+
193
+ function pluginHooksText() {
194
+ return `${JSON.stringify(pluginHooksDocument(), null, 2)}\n`;
195
+ }
196
+
197
+ function syncPluginHooks() {
198
+ const want = pluginHooksText();
199
+ let have = null;
200
+ try {
201
+ have = fs.readFileSync(PLUGIN_HOOKS_PATH, 'utf8');
202
+ } catch {
203
+ have = null;
204
+ }
205
+ if (have === want) return false;
206
+ fs.mkdirSync(path.dirname(PLUGIN_HOOKS_PATH), { recursive: true });
207
+ fs.writeFileSync(PLUGIN_HOOKS_PATH, want, 'utf8');
208
+ return true;
209
+ }
210
+
211
+ /**
212
+ * Merge a hook set into a parsed `.cursor/hooks.json` object, in place. Drops
213
+ * superseded direct registrations, adds/repairs every entry, and retires shim
214
+ * entries the set no longer registers (a renamed hook, or a platform-only one
215
+ * in a customer repo). Foreign entries are never touched.
216
+ * @returns {{ added: string[], repaired: string[], removed: string[] }}
217
+ */
218
+ export function mergeCursorHooks(
219
+ settings,
220
+ { hooks = ACTIVE_HOOKS, commandFor: render = KIT.commandFor } = {},
221
+ ) {
222
+ settings.version = 1;
223
+ settings.hooks ??= {};
224
+
225
+ const added = [];
226
+ const repaired = [];
227
+ const removed = [];
228
+
229
+ // 1. Drop superseded direct registrations.
230
+ for (const [event, entries] of Object.entries(settings.hooks)) {
231
+ if (!Array.isArray(entries)) continue;
232
+ const keep = entries.filter((raw) => {
233
+ if (!isLegacyEntry(raw)) return true;
234
+ removed.push(`${event}: ${commandOf(raw)}`);
235
+ return false;
236
+ });
237
+ settings.hooks[event] = keep;
238
+ }
239
+
240
+ // 2. Add or repair every manifest entry.
241
+ for (const entry of hooks) {
242
+ settings.hooks[entry.event] ??= [];
243
+ const entries = settings.hooks[entry.event];
244
+ const identity = identityFor(entry);
245
+ const existing = entries.filter(
246
+ (raw) => isShimEntry(raw) && identityOfCommand(commandOf(raw)) === identity,
247
+ );
248
+
249
+ if (existing.length === 0) {
250
+ entries.push(desiredEntry(entry, render));
251
+ added.push(`${entry.event}/${entry.hook}`);
252
+ continue;
253
+ }
254
+
255
+ // More than one match means an earlier merge duplicated it; collapse to one.
256
+ for (const extra of existing.slice(1)) {
257
+ entries.splice(entries.indexOf(extra), 1);
258
+ removed.push(`${entry.event}: duplicate ${entry.hook}`);
259
+ }
260
+
261
+ const raw = existing[0];
262
+ if (!sameEntry(raw, entry, render)) {
263
+ Object.assign(raw, desiredEntry(entry, render));
264
+ // A matcher removed from the manifest must also disappear from disk.
265
+ if (!entry.matcher && 'matcher' in raw) delete raw.matcher;
266
+ repaired.push(`${entry.event}/${entry.hook}`);
267
+ }
268
+ }
269
+
270
+ // 3. Retire shim entries we no longer register (a hook renamed or dropped).
271
+ for (const [event, entries] of Object.entries(settings.hooks)) {
272
+ if (!Array.isArray(entries)) continue;
273
+ const wanted = new Set(hooks.filter((h) => h.event === event).map(identityFor));
274
+ settings.hooks[event] = entries.filter((raw) => {
275
+ if (!isShimEntry(raw)) return true;
276
+ if (wanted.has(identityOfCommand(commandOf(raw)))) return true;
277
+ removed.push(`${event}: retired ${commandOf(raw)}`);
278
+ return false;
279
+ });
280
+ if (settings.hooks[event].length === 0) delete settings.hooks[event];
281
+ }
282
+
283
+ return { added, repaired, removed };
284
+ }
285
+
286
+ /** The customer launcher's state on disk, without writing. */
287
+ function launcherPlan() {
288
+ let existing = null;
289
+ try {
290
+ existing = fs.readFileSync(LAUNCHER_PATH, 'utf8');
291
+ } catch {
292
+ existing = null;
293
+ }
294
+ return planCustomerLauncherWrite(existing, 'cursor');
295
+ }
296
+
297
+ function install({ json = false } = {}) {
298
+ const settings = readSettings();
299
+ const { added, repaired, removed } = mergeCursorHooks(settings);
300
+
301
+ const settingsChanged = added.length + repaired.length + removed.length > 0;
302
+ if (settingsChanged) writeSettings(settings);
303
+ // The committed launcher every customer row points at. The platform checkout
304
+ // runs the shim directly and never gets one.
305
+ let launcherWritten = false;
306
+ if (KIT.customer) {
307
+ const plan = launcherPlan();
308
+ if (plan.write) {
309
+ fs.mkdirSync(path.dirname(LAUNCHER_PATH), { recursive: true });
310
+ fs.writeFileSync(LAUNCHER_PATH, plan.text, 'utf8');
311
+ launcherWritten = true;
312
+ }
313
+ }
314
+ // The plugin mirror lives in THIS checkout; a customer target never gets one.
315
+ const pluginChanged = KIT.customer ? false : syncPluginHooks();
316
+
317
+ if (json) {
318
+ console.log(
319
+ JSON.stringify(
320
+ {
321
+ ok: true,
322
+ changed: settingsChanged || pluginChanged || launcherWritten,
323
+ added,
324
+ repaired,
325
+ removed,
326
+ pluginChanged,
327
+ launcherWritten,
328
+ total: ACTIVE_HOOKS.length,
329
+ customer: KIT.customer,
330
+ },
331
+ null,
332
+ 2,
333
+ ),
334
+ );
335
+ return;
336
+ }
337
+
338
+ if (pluginChanged) {
339
+ console.log(`install-cursor-hooks: regenerated ${path.relative(REPO_ROOT, PLUGIN_HOOKS_PATH)}`);
340
+ }
341
+ if (launcherWritten) {
342
+ console.log(`install-cursor-hooks: wrote ${path.relative(REPO_ROOT, LAUNCHER_PATH)} (resolves node_modules/heyspex)`);
343
+ }
344
+
345
+ if (!settingsChanged) {
346
+ console.log(`install-cursor-hooks: already installed (${ACTIVE_HOOKS.length} hooks, no changes).`);
347
+ return;
348
+ }
349
+
350
+ const parts = [];
351
+ if (added.length) parts.push(`added ${added.length}`);
352
+ if (repaired.length) parts.push(`repaired ${repaired.length}`);
353
+ if (removed.length) parts.push(`removed ${removed.length}`);
354
+ console.log(
355
+ `install-cursor-hooks: ${parts.join('; ')} → ${path.relative(REPO_ROOT, SETTINGS_PATH)} ` +
356
+ `(${ACTIVE_HOOKS.length} hooks registered)`,
357
+ );
358
+ for (const line of removed) console.log(` - ${line}`);
359
+ console.log(
360
+ KIT.customer
361
+ ? ` Launcher: ${CUSTOMER_LAUNCHER} → node_modules/heyspex/${SHIM} (HeySpex off-switch + Cursor↔Claude payload translation)`
362
+ : ` Shim: ${SHIM} (HeySpex off-switch + Cursor↔Claude payload translation)`,
363
+ );
364
+ for (const gap of UNPORTABLE) {
365
+ console.log(` Not registerable in Cursor: ${gap.hook} (${gap.reason}) → covered by ${gap.coveredBy}`);
366
+ }
367
+ }
368
+
369
+ function check({ json = false } = {}) {
370
+ const settings = readSettings();
371
+ const issues = [];
372
+ const entriesFor = (event) =>
373
+ Array.isArray(settings?.hooks?.[event]) ? settings.hooks[event] : [];
374
+
375
+ for (const entry of ACTIVE_HOOKS) {
376
+ const identity = identityFor(entry);
377
+ const ours = entriesFor(entry.event).filter(
378
+ (raw) => isShimEntry(raw) && identityOfCommand(commandOf(raw)) === identity,
379
+ );
380
+ if (ours.length === 0) {
381
+ issues.push(`${entry.event}: missing ${entry.hook}`);
382
+ continue;
383
+ }
384
+ if (ours.length > 1) issues.push(`${entry.event}: ${entry.hook} registered ${ours.length}×`);
385
+ for (const raw of ours) {
386
+ if (!sameEntry(raw, entry)) {
387
+ issues.push(
388
+ `${entry.event}/${entry.hook}: drift — expected \`${KIT.commandFor(entry)}\`` +
389
+ ` (timeout ${timeoutFor(entry)}, matcher ${entry.matcher ?? 'none'}), found` +
390
+ ` \`${commandOf(raw)}\` (timeout ${raw.timeout}, matcher ${raw.matcher ?? 'none'})`,
391
+ );
392
+ }
393
+ }
394
+ }
395
+
396
+ for (const [event, entries] of Object.entries(settings.hooks ?? {})) {
397
+ if (!Array.isArray(entries)) continue;
398
+ for (const raw of entries) {
399
+ if (isLegacyEntry(raw)) {
400
+ issues.push(`${event}: superseded direct registration — ${commandOf(raw)}`);
401
+ } else if (
402
+ isShimEntry(raw) &&
403
+ !ACTIVE_HOOKS.some((h) => h.event === event && identityFor(h) === identityOfCommand(commandOf(raw)))
404
+ ) {
405
+ issues.push(`${event}: not part of this repo's hook set — ${commandOf(raw)}`);
406
+ }
407
+ }
408
+ }
409
+
410
+ let pluginOnDisk = null;
411
+ try {
412
+ pluginOnDisk = fs.readFileSync(PLUGIN_HOOKS_PATH, 'utf8');
413
+ } catch {
414
+ pluginOnDisk = null;
415
+ }
416
+ if (!KIT.customer && pluginOnDisk !== pluginHooksText()) {
417
+ issues.push(
418
+ `plugins/heyspex/hooks/cursor-hooks.json: ${pluginOnDisk === null ? 'missing' : 'stale'} — regenerate with \`npm run hooks:install-cursor\``,
419
+ );
420
+ }
421
+ if (KIT.customer) {
422
+ const plan = launcherPlan();
423
+ if (plan.write) {
424
+ issues.push(`${CUSTOMER_LAUNCHER}: ${plan.reason === 'missing' ? 'missing' : 'stale'} — the hook rows point at it`);
425
+ }
426
+ }
427
+
428
+ if (json) {
429
+ console.log(JSON.stringify({ ok: issues.length === 0, issues, total: ACTIVE_HOOKS.length, customer: KIT.customer }, null, 2));
430
+ process.exit(issues.length === 0 ? 0 : 1);
431
+ }
432
+
433
+ if (issues.length === 0) {
434
+ console.log(`install-cursor-hooks --check: OK (${ACTIVE_HOOKS.length} hooks registered)`);
435
+ process.exit(0);
436
+ }
437
+ console.error('install-cursor-hooks --check: drift detected');
438
+ for (const issue of issues) console.error(` - ${issue}`);
439
+ console.error(' Run `node scripts/install-cursor-hooks.mjs` to repair.');
440
+ process.exit(1);
441
+ }
442
+
443
+ function uninstall() {
444
+ const settings = readSettings();
445
+ if (!settings.hooks) {
446
+ console.log('install-cursor-hooks: nothing to uninstall (no hooks block).');
447
+ return;
448
+ }
449
+ let removed = 0;
450
+ for (const [event, entries] of Object.entries(settings.hooks)) {
451
+ if (!Array.isArray(entries)) continue;
452
+ const keep = entries.filter((raw) => !isShimEntry(raw) && !isLegacyEntry(raw));
453
+ removed += entries.length - keep.length;
454
+ if (keep.length === 0) delete settings.hooks[event];
455
+ else settings.hooks[event] = keep;
456
+ }
457
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
458
+ writeSettings(settings);
459
+ console.log(`install-cursor-hooks: uninstalled ${removed} hook entr${removed === 1 ? 'y' : 'ies'}.`);
460
+ }
461
+
462
+ // CLI only when run directly -- `hs install` imports mergeCursorHooks/cursorKitFor,
463
+ // and inside the esbuild CLI bundle a plain `import.meta.url === argv[1]` test
464
+ // is true for EVERY bundled module (see `invokedDirectly` in customer-kit.mjs).
465
+ if (invokedDirectly(import.meta.url, 'install-cursor-hooks.mjs')) {
466
+ const argv = process.argv.slice(2);
467
+ const json = argv.includes('--json');
468
+ if (argv.includes('--uninstall')) uninstall();
469
+ else if (argv.includes('--check')) check({ json });
470
+ else install({ json });
471
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * agent-env-markers.mjs — the ONE list of environment stamps that mean "an AI
3
+ * coding agent is driving this shell".
4
+ *
5
+ * Lifted out of scripts/guard-agent-commit.mjs (2026-09-19) so the git-identity
6
+ * command (`hs source identity set|clear`, scripts/lib/hs-source-identity.mjs)
7
+ * refuses on exactly the same evidence the commit guard refuses on — one list,
8
+ * two consumers, no drift. guard-agent-commit's behaviour is unchanged: it
9
+ * imports this list and filters it the same way it always did.
10
+ *
11
+ * Shipped in the published package (package.json `files`) because
12
+ * hs-source-identity.mjs is a packed `hs source` entrypoint and imports it.
13
+ */
14
+
15
+ /** Environment stamps that mean "an AI coding agent is driving this shell". */
16
+ export const AGENT_ENV_MARKERS = [
17
+ 'CLAUDECODE', // Claude Code
18
+ 'CLAUDE_CODE_ENTRYPOINT',
19
+ 'CLAUDE_CODE_SESSION_ID',
20
+ 'CURSOR_AGENT', // Cursor's agent / cursor-agent CLI — the actor in the 2026-07-12 incident
21
+ 'CURSOR_TRACE_ID',
22
+ 'CURSOR_SESSION_ID',
23
+ 'COMPOSER_NO_INTERACTION',
24
+ 'AIDER_MODEL',
25
+ 'OPENAI_CODEX_SESSION',
26
+ 'DEVIN_SESSION_ID',
27
+ ];
28
+
29
+ /**
30
+ * The markers that are present (set to anything, including an empty string
31
+ * is NOT present — matches guard-agent-commit's `process.env[k]` truthiness).
32
+ *
33
+ * @param {Record<string, string | undefined>} [env]
34
+ * @returns {string[]}
35
+ */
36
+ export function detectAgentMarkers(env = process.env) {
37
+ return AGENT_ENV_MARKERS.filter((k) => env[k]);
38
+ }