heyspex 0.1.0 → 0.2.1

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 (352) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/README.md +21 -0
  3. package/bin/hs.mjs +1089 -104
  4. package/bin/spexcode.mjs +262 -0
  5. package/dev-factory/engine/worktree.mjs +740 -0
  6. package/dev-factory/lib/code-repo-root.mjs +67 -0
  7. package/dev-factory/lib/context-budget.mjs +84 -0
  8. package/dev-factory/lib/epic-detect.mjs +190 -0
  9. package/dev-factory/lib/fleet-queue-file.mjs +846 -0
  10. package/dev-factory/lib/fleet-queue.mjs +426 -0
  11. package/dev-factory/lib/fleet-state.mjs +14 -18
  12. package/dev-factory/lib/model-catalog.mjs +338 -0
  13. package/dev-factory/lib/seat-context.mjs +199 -0
  14. package/dev-factory/lib/spec-io.mjs +93 -0
  15. package/dev-factory/lib/sufficiency-score.mjs +8 -2
  16. package/dev-factory/lib/worktree-allocator.mjs +744 -0
  17. package/dev-factory/model-catalog.json +222 -0
  18. package/dist/cli/hs-check.mjs +1127 -103
  19. package/dist/cli/hs-claim.mjs +981 -63
  20. package/dist/cli/hs-connect.mjs +10059 -1729
  21. package/dist/cli/hs-defer.mjs +5507 -3072
  22. package/dist/cli/hs-dev.mjs +576 -0
  23. package/dist/cli/hs-env.mjs +416 -302
  24. package/dist/cli/hs-index-rebuild.mjs +7334 -0
  25. package/dist/cli/hs-index-reconcile.mjs +8138 -0
  26. package/dist/cli/hs-init.mjs +2840 -149
  27. package/dist/cli/hs-install.mjs +7214 -594
  28. package/dist/cli/hs-join.mjs +10640 -2421
  29. package/dist/cli/hs-vault-pull.mjs +8157 -6370
  30. package/dist/cli/hs-vault-push.mjs +6433 -0
  31. package/dist/cli/hs-vault.mjs +4029 -1555
  32. package/dist/cli/hs-workspace.mjs +2702 -538
  33. package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
  34. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
  35. package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
  36. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
  37. package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
  38. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
  39. package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
  40. package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
  41. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
  42. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
  43. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
  44. package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
  45. package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
  46. package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -0
  47. package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
  48. package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
  49. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
  50. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
  51. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
  52. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
  53. package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
  54. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
  55. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
  56. package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -0
  57. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
  58. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
  59. package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
  60. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
  61. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
  62. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
  63. package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
  64. package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
  65. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +33 -0
  66. package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
  67. package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
  68. package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
  69. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
  70. package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
  71. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
  72. package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
  73. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
  74. package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
  75. package/dist/cli-templates/harness-commands/cursor/hs-view.md +32 -0
  76. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
  77. package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
  78. package/docs/copilot-plugin-install.md +162 -0
  79. package/docs/driving-heyspex-from-copilot.md +165 -0
  80. package/docs/mcp-client-config.md +259 -6
  81. package/package.json +249 -124
  82. package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
  83. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
  84. package/plugins/heyspex/.mcp.json +12 -0
  85. package/plugins/heyspex/README.md +309 -0
  86. package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
  87. package/plugins/heyspex/bin/run-hook.mjs +80 -0
  88. package/plugins/heyspex/hooks/cursor-hooks.json +308 -0
  89. package/plugins/heyspex/hooks/hooks.json +418 -0
  90. package/plugins/heyspex/mcp.json +13 -0
  91. package/scripts/check-mcp-freshness.mjs +17 -9
  92. package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
  93. package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
  94. package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
  95. package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -0
  96. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +356 -0
  97. package/scripts/claude-hooks/_run.mjs +413 -0
  98. package/scripts/claude-hooks/_transcript-text.mjs +60 -0
  99. package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
  100. package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
  101. package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
  102. package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
  103. package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
  104. package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
  105. package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
  106. package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
  107. package/scripts/claude-hooks/claim-liveness-shared.mjs +577 -0
  108. package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
  109. package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
  110. package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
  111. package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
  112. package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
  113. package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
  114. package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
  115. package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
  116. package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
  117. package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
  118. package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
  119. package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
  120. package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
  121. package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
  122. package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
  123. package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
  124. package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
  125. package/scripts/claude-hooks/inject-communication-style.mjs +86 -0
  126. package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
  127. package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
  128. package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
  129. package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
  130. package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -0
  131. package/scripts/claude-hooks/log-session-activity.mjs +341 -0
  132. package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
  133. package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
  134. package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
  135. package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
  136. package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
  137. package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
  138. package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
  139. package/scripts/claude-hooks/research-method-guard.mjs +398 -0
  140. package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
  141. package/scripts/claude-hooks/session-end.mjs +451 -0
  142. package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
  143. package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
  144. package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
  145. package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
  146. package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
  147. package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
  148. package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
  149. package/scripts/claude-hooks/wip-checkpoint.mjs +302 -0
  150. package/scripts/cursor-hooks/manifest.mjs +281 -0
  151. package/scripts/dev-server-status.mjs +34 -13
  152. package/scripts/dev-server-stop.mjs +313 -0
  153. package/scripts/heyspex-mode.mjs +288 -0
  154. package/scripts/hs-auto-resolve.mjs +240 -0
  155. package/scripts/hs-auto-source.mjs +398 -0
  156. package/scripts/hs-auto.mjs +134 -0
  157. package/scripts/hs-capture-cli.mjs +326 -0
  158. package/scripts/hs-classifier.mjs +136 -0
  159. package/scripts/hs-companion.mjs +88 -0
  160. package/scripts/hs-gateway.mjs +115 -0
  161. package/scripts/hs-mode.mjs +125 -0
  162. package/scripts/hs-next.mjs +441 -0
  163. package/scripts/hs-settings.mjs +338 -0
  164. package/scripts/hs-view.mjs +496 -0
  165. package/scripts/hs-worktree.mjs +201 -0
  166. package/scripts/install-claude-code-hooks.mjs +272 -0
  167. package/scripts/install-cursor-hooks.mjs +471 -0
  168. package/scripts/lib/agent-env-markers.mjs +38 -0
  169. package/scripts/lib/console-url-resolver.mjs +199 -61
  170. package/scripts/lib/customer-kit.mjs +1203 -0
  171. package/scripts/lib/dev-mode.mjs +249 -32
  172. package/scripts/lib/directive-obligations.mjs +349 -0
  173. package/scripts/lib/heyspex-home.mjs +72 -0
  174. package/scripts/lib/hook-dispositions.mjs +210 -0
  175. package/scripts/lib/hook-vault-header.mjs +30 -0
  176. package/scripts/lib/hs-command.mjs +277 -0
  177. package/scripts/lib/hs-companion-settings.mjs +472 -0
  178. package/scripts/lib/hs-source-branch.mjs +196 -0
  179. package/scripts/lib/hs-source-commit.mjs +59 -5
  180. package/scripts/lib/hs-source-identity.mjs +526 -0
  181. package/scripts/lib/hs-source-log.mjs +13 -22
  182. package/scripts/lib/hs-source-preflight.mjs +18 -4
  183. package/scripts/lib/hs-source-push.mjs +386 -307
  184. package/scripts/lib/hs-worktree-args.mjs +115 -0
  185. package/scripts/lib/shared-path-helpers.mjs +134 -0
  186. package/scripts/lib/standalone-doctor.mjs +540 -3
  187. package/scripts/lib/subagent-git-lock.mjs +109 -0
  188. package/scripts/lib/template-pin.mjs +34 -0
  189. package/scripts/lib/window-placement.mjs +109 -0
  190. package/scripts/provenance-lib.mjs +91 -0
  191. package/scripts/stamp-provenance-on-edit.mjs +222 -0
  192. package/scripts/wip-ledger-health.mjs +653 -0
  193. package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
  194. package/spexharness/src/adapters/audit.mjs +197 -0
  195. package/spexharness/src/adapters/claude-code.mjs +449 -0
  196. package/spexharness/src/adapters/copilot.mjs +236 -0
  197. package/spexharness/src/adapters/detect-harness.mjs +34 -0
  198. package/spexharness/src/chat/tui/ansi.mjs +448 -0
  199. package/spexharness/src/chat/tui/palette.mjs +22 -0
  200. package/spexharness/src/chat/tui/select.mjs +208 -0
  201. package/spexharness/src/config-root.mjs +54 -0
  202. package/spexharness/src/context-budget.mjs +141 -0
  203. package/spexharness/src/dev-server-control.mjs +389 -0
  204. package/spexharness/src/doctrine/behaviour.mjs +371 -0
  205. package/spexharness/src/doctrine/corpus.mjs +733 -0
  206. package/spexharness/src/doctrine/inject.mjs +338 -0
  207. package/spexharness/src/doctrine/merge.mjs +184 -0
  208. package/spexharness/src/doctrine/sources.mjs +252 -0
  209. package/spexharness/src/doctrine/synthesize.mjs +401 -0
  210. package/spexharness/src/doctrine/vault-source.mjs +574 -0
  211. package/spexharness/src/doctrine/writer.mjs +176 -0
  212. package/spexharness/src/events.mjs +84 -0
  213. package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
  214. package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
  215. package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
  216. package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
  217. package/spexharness/src/gates/deep-research-cost-gate.mjs +275 -0
  218. package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
  219. package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
  220. package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
  221. package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
  222. package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
  223. package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
  224. package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
  225. package/spexharness/src/gates/git-identity-guard.mjs +61 -0
  226. package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
  227. package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
  228. package/spexharness/src/gates/git-stash-gate.mjs +159 -0
  229. package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
  230. package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
  231. package/spexharness/src/gates/inject-vault-context.mjs +239 -0
  232. package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
  233. package/spexharness/src/gates/plan-review-gate.mjs +233 -0
  234. package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
  235. package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
  236. package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
  237. package/spexharness/src/gates/settings-write-gate.mjs +104 -0
  238. package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
  239. package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
  240. package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
  241. package/spexharness/src/git-reflog-audit.mjs +294 -0
  242. package/spexharness/src/intent-classifier.mjs +196 -0
  243. package/spexharness/src/intent-directive.mjs +626 -0
  244. package/spexharness/src/profile/learn.mjs +551 -0
  245. package/spexharness/src/profile/proposals.mjs +357 -0
  246. package/spexharness/src/profile/vault-inclusion.mjs +386 -0
  247. package/spexharness/src/redact.mjs +151 -0
  248. package/spexharness/src/rule-table.mjs +849 -0
  249. package/spexharness/src/seat-context.mjs +122 -0
  250. package/spexharness/src/session-bootstrap.mjs +512 -0
  251. package/spexharness/src/session-record.mjs +815 -0
  252. package/spexharness/src/token-inventory.mjs +207 -0
  253. package/vendor/heyspex-mcp/account-switch.mjs +96 -0
  254. package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
  255. package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
  256. package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
  257. package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
  258. package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
  259. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +77 -8
  260. package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
  261. package/vendor/heyspex-mcp/capture-console-client.mjs +117 -3
  262. package/vendor/heyspex-mcp/capture-focus.mjs +1061 -31
  263. package/vendor/heyspex-mcp/capture-outbox.mjs +15 -5
  264. package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
  265. package/vendor/heyspex-mcp/capture-tools.mjs +1264 -0
  266. package/vendor/heyspex-mcp/claim-tools.mjs +1147 -0
  267. package/vendor/heyspex-mcp/cli-verbs.mjs +2074 -0
  268. package/vendor/heyspex-mcp/cli.mjs +207 -15524
  269. package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
  270. package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
  271. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +602 -0
  272. package/vendor/heyspex-mcp/config-seed.yaml +15 -0
  273. package/vendor/heyspex-mcp/config-settings.mjs +290 -7
  274. package/vendor/heyspex-mcp/console-fetch.mjs +822 -17
  275. package/vendor/heyspex-mcp/console-transport.mjs +172 -0
  276. package/vendor/heyspex-mcp/credentials.mjs +585 -0
  277. package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
  278. package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
  279. package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
  280. package/vendor/heyspex-mcp/design-tools.mjs +4 -1
  281. package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
  282. package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
  283. package/vendor/heyspex-mcp/dev-tools.mjs +131 -0
  284. package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
  285. package/vendor/heyspex-mcp/events-tools.mjs +207 -0
  286. package/vendor/heyspex-mcp/filing-tools.mjs +723 -0
  287. package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
  288. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +71 -15
  289. package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
  290. package/vendor/heyspex-mcp/folder-trust-store.mjs +60 -0
  291. package/vendor/heyspex-mcp/http-transport.mjs +302 -0
  292. package/vendor/heyspex-mcp/list-tools.mjs +257 -0
  293. package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
  294. package/vendor/heyspex-mcp/mcp-config.mjs +107 -0
  295. package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
  296. package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
  297. package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
  298. package/vendor/heyspex-mcp/occupied-keys.mjs +142 -0
  299. package/vendor/heyspex-mcp/on-deck-brief.mjs +3377 -0
  300. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
  301. package/vendor/heyspex-mcp/package-root.mjs +32 -0
  302. package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
  303. package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
  304. package/vendor/heyspex-mcp/plan-push.mjs +908 -22
  305. package/vendor/heyspex-mcp/plan-review.mjs +124 -30
  306. package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
  307. package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -0
  308. package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -970
  309. package/vendor/heyspex-mcp/promote-capture.mjs +388 -69
  310. package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
  311. package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
  312. package/vendor/heyspex-mcp/promote-research.mjs +666 -53
  313. package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
  314. package/vendor/heyspex-mcp/prompts.mjs +1460 -0
  315. package/vendor/heyspex-mcp/read-path.mjs +191 -15
  316. package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
  317. package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
  318. package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
  319. package/vendor/heyspex-mcp/register-spec.mjs +266 -54
  320. package/vendor/heyspex-mcp/request-context.mjs +83 -0
  321. package/vendor/heyspex-mcp/research-focus.mjs +207 -4
  322. package/vendor/heyspex-mcp/research-handlers.mjs +625 -0
  323. package/vendor/heyspex-mcp/research-tools.mjs +4 -4
  324. package/vendor/heyspex-mcp/response-budget.mjs +348 -0
  325. package/vendor/heyspex-mcp/review-focus.mjs +25 -1
  326. package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
  327. package/vendor/heyspex-mcp/search-specs.mjs +280 -0
  328. package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
  329. package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
  330. package/vendor/heyspex-mcp/session-owner.mjs +109 -0
  331. package/vendor/heyspex-mcp/session-tools.mjs +162 -0
  332. package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
  333. package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
  334. package/vendor/heyspex-mcp/spec-edit-tools.mjs +453 -0
  335. package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
  336. package/vendor/heyspex-mcp/split-research.mjs +304 -56
  337. package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
  338. package/vendor/heyspex-mcp/submit-for-review.mjs +150 -7
  339. package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
  340. package/vendor/heyspex-mcp/summary-write-tools.mjs +228 -0
  341. package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
  342. package/vendor/heyspex-mcp/test-helpers.mjs +123 -1
  343. package/vendor/heyspex-mcp/tool-catalog.mjs +2819 -0
  344. package/vendor/heyspex-mcp/tool-dispatch.mjs +657 -0
  345. package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
  346. package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
  347. package/vendor/heyspex-mcp/vault-io.mjs +720 -0
  348. package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
  349. package/vendor/heyspex-mcp/vault-resolve.mjs +554 -68
  350. package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
  351. package/vendor/heyspex-mcp/whoami.mjs +92 -0
  352. package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
@@ -0,0 +1,54 @@
1
+ // spexharness/src/config-root.mjs
2
+ //
3
+ // The ONE answer to "which directory holds `.heyspex/config.yaml`?" for every
4
+ // hook-side reader: the plugin loader (scripts/claude-hooks/_guardrail-loader.mjs),
5
+ // the harness's own leaf reader (./intent-directive.mjs) and the gate-rows
6
+ // severity resolver (./gate-rows.mjs) all default to this.
7
+ //
8
+ // Per ADR-0009 §1 the workflow-settings file lives at the VAULT root — the
9
+ // directory the active-vault resolver (vendor/heyspex-mcp/vault-resolve.mjs)
10
+ // reports as `repoRootAbs`. Since the 2026-08-31 repo split that is a separate
11
+ // checkout from this code repo, and every hook-side reader that still joined
12
+ // `config.yaml` onto the CODE root (DEFAULT_REPO_ROOT / DEFAULT_APP_ROOT) was
13
+ // silently reading a stale copy while the MCP, CLI and console read the vault —
14
+ // [[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]].
15
+ //
16
+ // ONLY `config.yaml` moves. The guardrail registry (`.heyspex/guardrails.json`),
17
+ // the terminology override, the event/history logs, the dev-server lock and the
18
+ // fleet-queue lanes are code-repo artifacts and keep resolving from the code
19
+ // root — the same CODE_ROOT vs vault-root split scripts/lib/resolve-validate-root.mjs
20
+ // makes for the validators (memory lesson [[after-the-repo-split-every-live-specs]]).
21
+ //
22
+ // FAIL-SAFE: resolution is fs/env only (no network) and `getActiveConfig()`
23
+ // memoizes it once per process; if it throws for any reason the code root is
24
+ // returned so a hook never strands the agent. Deliberately NOT memoized a second
25
+ // time here — tests call `resetActiveConfigCache()` between cases and expect the
26
+ // next read to re-resolve.
27
+ //
28
+ // Layering: a harness module importing from vendor/heyspex-mcp, the same
29
+ // direction ./intent-directive.mjs and ./gates/enforce-session-sequence.mjs
30
+ // already take. Never imports from scripts/claude-hooks.
31
+
32
+ import path from 'node:path';
33
+ import { fileURLToPath } from 'node:url';
34
+
35
+ import { getActiveConfig } from '../../vendor/heyspex-mcp/vault-resolve.mjs';
36
+
37
+ /** spexharness/src/ → the code checkout is two levels up. */
38
+ export const CODE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
39
+
40
+ /**
41
+ * The directory `.heyspex/config.yaml` is read from when a caller passes no
42
+ * explicit root: the active vault's repo root, else (resolution threw) the code
43
+ * checkout. Explicit `repoRoot` arguments on the readers always win over this.
44
+ *
45
+ * @returns {string}
46
+ */
47
+ export function defaultConfigRoot() {
48
+ try {
49
+ const root = getActiveConfig()?.repoRootAbs;
50
+ return typeof root === 'string' && root.trim() ? root : CODE_ROOT;
51
+ } catch {
52
+ return CODE_ROOT;
53
+ }
54
+ }
@@ -0,0 +1,141 @@
1
+ // spexharness/src/context-budget.mjs
2
+ //
3
+ // The harness's single context-injection measurement path (HS-7UAQ, epic HS-1YDJ). Every
4
+ // frontend that puts something into a seat's context window — a Claude Code hook shell run
5
+ // through spexharness/src/adapters/claude-code.mjs, the plugin's own SessionStart pieces,
6
+ // a headless seat's bootstrap (session-bootstrap.mjs), an overnight loop run — reports that
7
+ // injection through {@link recordContextInjection}. Nothing else in the harness writes a
8
+ // context-injection line into a session record; this is the one path every frontend goes
9
+ // through, which is what makes the resulting inventory trustworthy across fronts.
10
+ //
11
+ // Sizing is NOT reimplemented here: measuring one piece delegates to
12
+ // spexharness/src/token-inventory.mjs's `measurePiece` (itself built on
13
+ // dev-factory/lib/context-budget.mjs's `estimateTokens`), so this module owns WHERE a
14
+ // measurement is recorded, not HOW big a piece of text is. Recording delegates to
15
+ // spexharness/src/session-record.mjs's `recordEvent`, so the injection lands as an ordinary
16
+ // event on the session record (`point: 'context_injection'`) rather than a bespoke array —
17
+ // a session record inspector that already walks `record.events` sees these for free.
18
+ //
19
+ // Pure: no I/O, no console. `record` is mutated in place (matching recordEvent's and
20
+ // recordToolCall's own contract) and the measured piece is returned so a caller can log or
21
+ // assert on it without re-reading the record.
22
+
23
+ import { measurePiece } from './token-inventory.mjs';
24
+ import { recordEvent } from './session-record.mjs';
25
+
26
+ /** The session-record event point every context injection is recorded under. */
27
+ export const CONTEXT_INJECTION_POINT = 'context_injection';
28
+
29
+ /**
30
+ * @typedef {object} ContextInjectionMeasurement
31
+ * @property {string} source who injected it — a hook id, a gate id, 'seat-context',
32
+ * 'wip', 'memories', 'procedures', etc. Required in spirit (falls back to 'unknown'
33
+ * rather than throwing, matching the rest of the harness's fail-soft posture).
34
+ * @property {string} name a human label for the piece; defaults to `source`.
35
+ * @property {number} tokens the measured (or caller-supplied) token count.
36
+ * @property {number} chars the measured byte/char length of the injected text.
37
+ * @property {boolean} estimated true when `tokens` came from the chars/4 estimate
38
+ * rather than a caller-supplied exact count.
39
+ */
40
+
41
+ /**
42
+ * Measure and record ONE context injection into `record`. This is the single reporting
43
+ * path (AC2): a chat front, a plugin hook shell, and a headless seat all call this same
44
+ * function — none of them measures or records a piece any other way.
45
+ *
46
+ * @param {ReturnType<import('./session-record.mjs').createSessionRecord>} record
47
+ * @param {{ source?: string, name?: string, text?: string, tokens?: number, priority?: number, ts?: number }} [injection]
48
+ * @returns {ContextInjectionMeasurement} the measurement that was recorded.
49
+ */
50
+ export function recordContextInjection(record, injection = {}) {
51
+ const { source, name, text, tokens, priority, ts } = injection;
52
+ const measured = measurePiece({ name: name ?? source, text, tokens, priority }, record?.events?.length ?? 0);
53
+ const decision = {
54
+ source: source ? String(source) : 'unknown',
55
+ name: measured.name,
56
+ tokens: measured.tokens,
57
+ chars: measured.chars,
58
+ estimated: measured.estimated,
59
+ };
60
+ recordEvent(record, { point: CONTEXT_INJECTION_POINT, ts }, decision);
61
+ return decision;
62
+ }
63
+
64
+ /**
65
+ * Every context-injection measurement recorded on `record` so far, in the order they were
66
+ * recorded. This is what "the session record shows bytes per injection with a source"
67
+ * (AC3) means in practice — a caller reads this instead of re-deriving it from raw events.
68
+ *
69
+ * @param {ReturnType<import('./session-record.mjs').createSessionRecord>|null} record
70
+ * @returns {ContextInjectionMeasurement[]}
71
+ */
72
+ export function contextInjectionsOf(record) {
73
+ return (record?.events ?? [])
74
+ .filter((e) => e && e.point === CONTEXT_INJECTION_POINT)
75
+ .map((e) => e.decision)
76
+ .filter(Boolean);
77
+ }
78
+
79
+ /**
80
+ * Roll up every context injection recorded on `record`: total tokens/chars, and a
81
+ * per-source breakdown — the shape a `hs harness context` printer (or a test asserting a
82
+ * budget target) reads from.
83
+ *
84
+ * @param {ReturnType<import('./session-record.mjs').createSessionRecord>|null} record
85
+ * @returns {{
86
+ * injections: ContextInjectionMeasurement[],
87
+ * totalTokens: number,
88
+ * totalChars: number,
89
+ * bySource: Record<string, { tokens: number, chars: number, count: number }>,
90
+ * }}
91
+ */
92
+ export function summarizeContextInjections(record) {
93
+ const injections = contextInjectionsOf(record);
94
+ const bySource = {};
95
+ let totalTokens = 0;
96
+ let totalChars = 0;
97
+ for (const injection of injections) {
98
+ totalTokens += injection.tokens || 0;
99
+ totalChars += injection.chars || 0;
100
+ const bucket = bySource[injection.source] ?? { tokens: 0, chars: 0, count: 0 };
101
+ bucket.tokens += injection.tokens || 0;
102
+ bucket.chars += injection.chars || 0;
103
+ bucket.count += 1;
104
+ bySource[injection.source] = bucket;
105
+ }
106
+ return { injections, totalTokens, totalChars, bySource };
107
+ }
108
+
109
+ /**
110
+ * Context-window-used percentage for a non-Claude-Code session (HS-CUWH, epic HS-8CMW) — the
111
+ * one figure the spexcode status line needs to show a real context number for a seat that
112
+ * isn't Claude Code (which reports its own). Deliberately NOT a second sizing path: the
113
+ * caller resolves `usedTokens` however it already knows tokens (a real usage count from a
114
+ * model response, or this module's own chars/4 `estimateTokens` from token-inventory.mjs when
115
+ * only characters are tracked — see spexharness/src/chat/chat-loop.mjs's `windowTokens` wiring
116
+ * for that fallback in practice) and `windowTokens` from the model's context window; this
117
+ * function only turns the two into a percentage.
118
+ *
119
+ * Returns the explicit marker `null` — never an invented number — when either input is
120
+ * missing, non-numeric, or non-positive, so a caller/renderer can print "unknown" instead of
121
+ * a misleading 0%.
122
+ *
123
+ * @param {{ usedTokens?: number, windowTokens?: number }} [args]
124
+ * @returns {number|null} an integer 0-100 (rounded, clamped at 100), or `null` when unknown.
125
+ */
126
+ export function contextUsedPercent({ usedTokens, windowTokens } = {}) {
127
+ const used = Number(usedTokens);
128
+ const window = Number(windowTokens);
129
+ if (!Number.isFinite(used) || used <= 0) return null;
130
+ if (!Number.isFinite(window) || window <= 0) return null;
131
+ const pct = Math.round((used / window) * 100);
132
+ return Math.min(100, pct);
133
+ }
134
+
135
+ export default {
136
+ recordContextInjection,
137
+ contextInjectionsOf,
138
+ summarizeContextInjections,
139
+ contextUsedPercent,
140
+ CONTEXT_INJECTION_POINT,
141
+ };
@@ -0,0 +1,389 @@
1
+ // spexharness/src/dev-server-control.mjs
2
+ //
3
+ // The SHARED dev-server protection logic — moved here verbatim from
4
+ // scripts/claude-hooks/dev-server-task-reminder.mjs (task HS-SJD3-D0UC,
5
+ // protocol [[protected-shared-actions-gate]]) as wave 6 of [[HS-AZ61-NYPR]]
6
+ // turned that hook into a thin shim over ./gates/dev-server-task-reminder.mjs.
7
+ // There is exactly ONE copy of the start/stop classification, the worktree
8
+ // bypass, the front read, and the fleet-queue claim wrapper in the tree: this
9
+ // one. The gate imports it; the Claude Code shim reaches it through the gate;
10
+ // scripts/claude-hooks/dev-server-task-reminder.smoke.mjs drives it directly.
11
+ //
12
+ // TIER 1 (hard gate): starting or stopping the shared spex-console dev server
13
+ // is a PROTECTED action. In Assisted front it ALWAYS asks (approving IS the
14
+ // permission). In Autonomous front it requires a fleet-queue claim on this
15
+ // checkout's `dev-server-<checkout-key>` lane: a second agent's concurrent
16
+ // attempt is rejected while the claim is held, and can retry after
17
+ // release/TTL expiry. Worktree-isolated agents bypass entirely — each has its
18
+ // own server/port ([[parallel-agent-isolation-guidance]]), so there is no
19
+ // shared resource to gate.
20
+ //
21
+ // TIER 2 (soft reminder): a server-impacting command (migration, Neon branch
22
+ // reset, dependency install, .next delete, a second `next dev`) gets a warning
23
+ // naming the live port, and only when the ground-truth status core says a
24
+ // server is actually up. It NEVER blocks.
25
+ //
26
+ // Pure-ish: the classification/reminder half is pure; the claim, audit, lock
27
+ // and status halves do I/O and every path they touch is a parameter.
28
+
29
+ import fs from 'node:fs';
30
+ import path from 'node:path';
31
+ import { spawnSync } from 'node:child_process';
32
+ import { fileURLToPath, pathToFileURL } from 'node:url';
33
+
34
+ // The runnable form of a HeySpex command on THIS machine — `hs …` when the bin
35
+ // is on PATH, else an absolute-path `node "<install>/bin/hs.mjs" …`. Never an
36
+ // `npx` form ([[HS-I6HT-EKYU-DIAG-every-message-tells-the-user-to-run-hs]]).
37
+ import { hsCommand } from '../../scripts/lib/hs-command.mjs';
38
+ import { enqueue, claim, complete, list } from '../../dev-factory/lib/fleet-queue.mjs';
39
+
40
+ /** spexharness/src/ → repo root is two levels up. */
41
+ const DEFAULT_REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
42
+
43
+ /** The `.heyspex/config.yaml` leaf that can revert tier 1 to legacy warn-only. */
44
+ export const TOGGLE_KEY = 'agent_behaviors.dev_server_control.enabled';
45
+
46
+ /** Env-first repo-root resolution, matching every pre-cutover hook's own helper. */
47
+ export function repoRoot(env = process.env) {
48
+ const fromEnv = env.CLAUDE_PROJECT_DIR?.trim();
49
+ if (fromEnv) return fromEnv.replace(/[\\/]+$/, '');
50
+ return DEFAULT_REPO_ROOT;
51
+ }
52
+
53
+ // ── TIER 2 — warn-only server-impacting detection ───────────────────────────
54
+
55
+ /** Shell-command patterns that can break / collide with a running dev server. */
56
+ export const SERVER_IMPACTING_COMMAND_PATTERNS = [
57
+ /\bdb:(?:migrate|baseline|generate|push)\b/, // npm run db:* (Drizzle migrations)
58
+ /\bdrizzle-kit\b/,
59
+ /\bmigrate\.mjs\b/,
60
+ /\bnpm\s+run\s+dev\b/, // starting a second wrapped dev server
61
+ /\bnext\s+dev\b/, // starting a bare second dev server
62
+ /\b(?:npm|pnpm|yarn)\s+(?:i|install|ci|add)\b/, // dependency changes (need a restart)
63
+ // Deleting the .next build dir out from under a running server.
64
+ /\brm\s+-[a-zA-Z]*r[a-zA-Z]*f?[a-zA-Z]*\s+[^|;&\n]*\.next\b/,
65
+ /\bRemove-Item\b[^|;&\n]*\.next\b/i,
66
+ ];
67
+
68
+ /**
69
+ * Neon MCP tools that mutate the schema / endpoint the running server is bound
70
+ * to. Matched on tool name.
71
+ */
72
+ export const SERVER_IMPACTING_TOOL_NAMES = new Set([
73
+ 'mcp__neon__prepare_database_migration',
74
+ 'mcp__neon__complete_database_migration',
75
+ 'mcp__neon__reset_from_parent',
76
+ 'mcp__neon__delete_branch',
77
+ 'mcp__Neon__prepare_database_migration',
78
+ 'mcp__Neon__complete_database_migration',
79
+ 'mcp__Neon__reset_from_parent',
80
+ 'mcp__Neon__delete_branch',
81
+ ]);
82
+
83
+ /** True when this tool call could break a running dev server. */
84
+ export function isServerImpacting(toolName, toolInput) {
85
+ if (typeof toolName === 'string' && SERVER_IMPACTING_TOOL_NAMES.has(toolName)) return true;
86
+ if (toolName === 'Bash' || toolName === 'PowerShell') {
87
+ const command = toolInput?.command;
88
+ if (typeof command === 'string' && command.trim()) {
89
+ return SERVER_IMPACTING_COMMAND_PATTERNS.some((re) => re.test(command));
90
+ }
91
+ }
92
+ return false;
93
+ }
94
+
95
+ /** Build the soft-reminder text from a status verdict, or null if no server is live. */
96
+ export function buildReminder(status) {
97
+ if (!status || !status.running) return null;
98
+ const live = (status.servers || []).filter((s) => s.probe === 'console');
99
+ const where = live.map((s) => `:${s.port}${s.pid ? ` (listener pid ${s.pid})` : ''}`).join(', ');
100
+ const launcherPid = status.lockfile?.present && status.lockfile?.pid ? status.lockfile.pid : null;
101
+ const stopHint = launcherPid
102
+ ? `Ctrl+C in its terminal, or stop the launcher process (pid ${launcherPid}), or run /kill-ports`
103
+ : 'Ctrl+C in its terminal, or run /kill-ports';
104
+ return (
105
+ `⚠ A spex-console dev server is RUNNING (${where}) and this command can break it. ` +
106
+ `Running a migration / schema change under a live server strands its in-memory ` +
107
+ `schema and forces a restart. Recommended: stop the server first (${stopHint}), run ` +
108
+ `the task, then restart with \`npm run dev\`. Re-check with \`${hsCommand('server status')}\` / ` +
109
+ `heyspex_dev_server_status. (Proceeding anyway is allowed — this is a reminder, not a block. ` +
110
+ `Mute: HEYSPEX_DEV_SERVER_REMINDER_OFF=1.)`
111
+ );
112
+ }
113
+
114
+ /**
115
+ * Ground-truth dev-server status. The answer comes from
116
+ * scripts/dev-server-status.mjs — the SAME core `heyspex_dev_server_status`
117
+ * itself calls (vendor/heyspex-mcp/dev-tools.mjs's `handleDevServerStatus` is a
118
+ * three-line wrapper over it), so an in-process tool-catalog call and this
119
+ * function cannot disagree.
120
+ *
121
+ * @param {string} root
122
+ * @returns {Promise<object>}
123
+ */
124
+ export async function getStatus(root = repoRoot()) {
125
+ const coreUrl = pathToFileURL(path.join(root, 'scripts', 'dev-server-status.mjs')).href;
126
+ const mod = await import(coreUrl);
127
+ return mod.getDevServerStatus({ vaultRoot: root });
128
+ }
129
+
130
+ // ── TIER 1 — hard gate for dev-server START/STOP ────────────────────────────
131
+
132
+ // Narrow start/stop detection — deliberately NOT the broad
133
+ // SERVER_IMPACTING_COMMAND_PATTERNS set above (migration/install detection stays
134
+ // warn-only, unchanged). Per protected-shared-actions-gate.md Gate 1.
135
+ const START_PATTERNS = [/\bnpm\s+run\s+dev\b/, /\bnext\s+dev\b/];
136
+
137
+ /**
138
+ * Generic process-kill commands (Stop-Process / taskkill / bare kill) are
139
+ * ambiguous on their own — an agent kills stray processes for all kinds of
140
+ * reasons. They only count as a server STOP when they target the recorded
141
+ * dev-server lock pid. `/kill-ports` is the one exception: it is the
142
+ * dev-server-aware skill/CLI, already scoped to the recorded lock port.
143
+ */
144
+ export function extractKillTargetPid(command) {
145
+ let m = command.match(/Stop-Process\s+(?:-Id\s+)?(\d+)/i);
146
+ if (m) return Number(m[1]);
147
+ m = command.match(/taskkill\b[^\n]*?\/pid\s+(\d+)/i);
148
+ if (m) return Number(m[1]);
149
+ // `kill <pid>` / `kill -9 <pid>` — word-boundaried so it never matches
150
+ // "skill" or "killed" inside prose/paths.
151
+ m = command.match(/(?:^|[\s;&|(])kill\s+(?:-\d+\s+)?(\d+)\b/i);
152
+ if (m) return Number(m[1]);
153
+ return null;
154
+ }
155
+
156
+ /** The recorded dev-server launcher pid, or null when there is no lockfile. */
157
+ export function readLockPid(repoRootPath) {
158
+ try {
159
+ const raw = JSON.parse(
160
+ fs.readFileSync(path.join(repoRootPath, '.heyspex', 'dev-server.lock.json'), 'utf8'),
161
+ );
162
+ return typeof raw?.pid === 'number' ? raw.pid : null;
163
+ } catch {
164
+ return null;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Classify a tool call as a dev-server 'start', 'stop', or null. `lockPid` (the
170
+ * currently-recorded dev-server launcher pid, or null) scopes the generic kill
171
+ * commands to the actual server — a `kill <unrelated-pid>` never counts.
172
+ */
173
+ export function classifyServerStartStop(toolName, toolInput, { lockPid = null } = {}) {
174
+ if (
175
+ toolName === 'Skill' &&
176
+ typeof toolInput?.skill === 'string' &&
177
+ /kill-ports/i.test(toolInput.skill)
178
+ ) {
179
+ return 'stop';
180
+ }
181
+ if (toolName !== 'Bash' && toolName !== 'PowerShell') return null;
182
+ const command = toolInput?.command;
183
+ if (typeof command !== 'string' || !command.trim()) return null;
184
+ if (START_PATTERNS.some((re) => re.test(command))) return 'start';
185
+ if (/kill-ports/i.test(command)) return 'stop';
186
+ const targetPid = extractKillTargetPid(command);
187
+ if (targetPid != null && lockPid != null && targetPid === lockPid) return 'stop';
188
+ return null;
189
+ }
190
+
191
+ /**
192
+ * Pure comparison: true when `gitDir` and `gitCommonDir` differ, i.e. this
193
+ * checkout is a linked git worktree, not the main checkout.
194
+ */
195
+ export function isWorktreePaths(gitDir, gitCommonDir) {
196
+ if (!gitDir || !gitCommonDir) return false; // can't determine → not isolated (stay gated)
197
+ return path.resolve(gitDir) !== path.resolve(gitCommonDir);
198
+ }
199
+
200
+ /** Shell out to git to answer "is this cwd a linked worktree (not the main checkout)?" */
201
+ export function detectWorktreeIsolation(cwd = process.cwd()) {
202
+ try {
203
+ const dir = spawnSync('git', ['rev-parse', '--git-dir'], { cwd, encoding: 'utf8', timeout: 3000 });
204
+ const common = spawnSync('git', ['rev-parse', '--git-common-dir'], {
205
+ cwd,
206
+ encoding: 'utf8',
207
+ timeout: 3000,
208
+ });
209
+ if (dir.status !== 0 || common.status !== 0 || !dir.stdout?.trim() || !common.stdout?.trim()) {
210
+ return false;
211
+ }
212
+ return isWorktreePaths(
213
+ path.resolve(cwd, dir.stdout.trim()),
214
+ path.resolve(cwd, common.stdout.trim()),
215
+ );
216
+ } catch {
217
+ return false; // undetectable → stay gated (fail toward protection, not bypass)
218
+ }
219
+ }
220
+
221
+ /** The claim-bound session worktree's owner marker, written by
222
+ * dev-factory/lib/worktree-allocator.mjs. The name is mirrored rather than
223
+ * imported so this module stays loadable in a checkout with no dev-factory. */
224
+ export const WORKTREE_OWNER_FILE = '.hsewt-owner.json';
225
+
226
+ /** The env var a SPAWNED worktree session carries (set by the spawner). */
227
+ export const WORKTREE_ENV = 'HEYSPEX_WORKTREE';
228
+
229
+ /** The one stderr line a bypassed Gate 1 emits, so a bypass is never silent. */
230
+ export const WORKTREE_BYPASS_LOG =
231
+ 'protected-actions gate: worktree-isolated session, dev-server gate bypassed';
232
+
233
+ /**
234
+ * Is this checkout a claim-bound SESSION worktree?
235
+ * ([[HS-QDQA]], epic [[HS-03DO]] — the bypass [[protected-shared-actions-gate]]
236
+ * Gate 1 documents ("A worktree-isolated agent bypasses entirely — its own
237
+ * server, its own port") but only ever implemented via git's own linked-worktree
238
+ * probe, which misses a spawned session whose shell never resolved git, and
239
+ * which cannot be exercised without a real worktree on disk.)
240
+ *
241
+ * Two independent signals, either sufficient on its own:
242
+ * - `<cwd>/.hsewt-owner.json` with `kind: "session"` — the allocator's record;
243
+ * - `HEYSPEX_WORKTREE=1` — the spawner's env stamp.
244
+ *
245
+ * A `kind: "build"` marker (dev-factory's per-candidate gate worktree) is NOT a
246
+ * session and gets no bypass. Undetectable → false, i.e. STAY GATED: this
247
+ * function may only ever say "yes, isolated", never guess one.
248
+ *
249
+ * SCOPE: Gate 1 (dev-server start/stop) ONLY. Every destructive GIT verb keeps
250
+ * its own, separate gating in ./gates/git-stash-gate.mjs — a worktree still
251
+ * shares the object store, the reflog and the branch namespace with the primary
252
+ * checkout, so `git stash` / `reset --hard` / `push --force` are exactly as
253
+ * dangerous there as anywhere else. Nothing here is wired into that gate.
254
+ *
255
+ * @param {{ cwd?: string, env?: Record<string, string|undefined>, fs?: object }} [input]
256
+ * @returns {boolean}
257
+ */
258
+ export function isWorktreeIsolated({ cwd = process.cwd(), env = process.env, fs: fsImpl = fs } = {}) {
259
+ try {
260
+ const raw = fsImpl.readFileSync(path.join(String(cwd), WORKTREE_OWNER_FILE), 'utf8');
261
+ const marker = JSON.parse(raw);
262
+ if (marker && typeof marker === 'object' && marker.kind === 'session') return true;
263
+ } catch {
264
+ // No marker / unreadable / not JSON — fall through to the env signal.
265
+ }
266
+ return String(env?.[WORKTREE_ENV] ?? '') === '1';
267
+ }
268
+
269
+ /**
270
+ * Read the live front (`assisted` | `autonomous`) off
271
+ * dev-factory/state/coo-dispatcher-state.json — re-read fresh every call so a
272
+ * mid-session front flip takes effect on the very next gated action.
273
+ */
274
+ export function readFront(repoRootPath) {
275
+ try {
276
+ const p = path.join(repoRootPath, 'dev-factory', 'state', 'coo-dispatcher-state.json');
277
+ const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
278
+ return raw?.front === 'autonomous' ? 'autonomous' : 'assisted';
279
+ } catch {
280
+ return 'assisted';
281
+ }
282
+ }
283
+
284
+ /** Deterministic per-checkout fleet-queue lane name. */
285
+ export function devServerLane(repoRootPath) {
286
+ const key = path.basename(repoRootPath).toLowerCase().replace(/[^a-z0-9]+/g, '-');
287
+ return `dev-server-${key || 'checkout'}`;
288
+ }
289
+
290
+ /** Path of the standing "who holds the dev server" marker for a checkout. */
291
+ export function activeClaimPath(repoRootPath) {
292
+ return path.join(repoRootPath, '.heyspex', 'dev-server-active-claim.json');
293
+ }
294
+
295
+ /**
296
+ * THIN wrapper over dev-factory/lib/fleet-queue.mjs (enqueue/claim/complete/list
297
+ * — unmodified) implementing the one exclusive "who currently owns the shared
298
+ * dev server" resource per lane. All I/O is parameterized.
299
+ */
300
+ export function attemptDevServerClaim({ lane, action, claimant, queuePath, activePath, now = Date.now() }) {
301
+ let active = null;
302
+ try {
303
+ active = JSON.parse(fs.readFileSync(activePath, 'utf8'));
304
+ } catch {
305
+ active = null;
306
+ }
307
+ if (active && active.lane === lane && active.claimant !== claimant) {
308
+ const heldItems = list(lane, 'claimed', { queuePath, now });
309
+ const stillHeld = heldItems.some((i) => i.id === active.id);
310
+ if (stillHeld) {
311
+ return {
312
+ ok: false,
313
+ reason:
314
+ `dev-server resource "${lane}" is already claimed by ${active.claimant} (since ${active.claimedAt}) — ` +
315
+ `back off and retry after release or TTL expiry.`,
316
+ };
317
+ }
318
+ }
319
+
320
+ const enq = enqueue({ lane, payload: { action }, requestedBy: claimant, queuePath, now });
321
+ const res = claim(enq.id, claimant, { queuePath, now });
322
+ if (!res.ok) {
323
+ return { ok: false, reason: `dev-server resource "${lane}" claim rejected: ${res.reason}` };
324
+ }
325
+
326
+ try {
327
+ fs.mkdirSync(path.dirname(activePath), { recursive: true });
328
+ fs.writeFileSync(
329
+ activePath,
330
+ JSON.stringify({ lane, id: enq.id, claimant, action, claimedAt: new Date(now).toISOString() }),
331
+ );
332
+ } catch {
333
+ /* best-effort marker write — the queue-level claim already holds regardless */
334
+ }
335
+
336
+ if (action === 'stop') {
337
+ complete(enq.id, { action }, { queuePath, now });
338
+ try {
339
+ fs.rmSync(activePath, { force: true });
340
+ } catch {
341
+ /* best-effort */
342
+ }
343
+ }
344
+
345
+ return { ok: true, id: enq.id };
346
+ }
347
+
348
+ /** Append one row to `.heyspex/dev-server-history.jsonl`. Never throws. */
349
+ export function auditHistory(repoRootPath, record) {
350
+ try {
351
+ const dir = path.join(repoRootPath, '.heyspex');
352
+ fs.mkdirSync(dir, { recursive: true });
353
+ fs.appendFileSync(
354
+ path.join(dir, 'dev-server-history.jsonl'),
355
+ JSON.stringify({ timestamp: new Date().toISOString(), ...record }) + '\n',
356
+ );
357
+ } catch {
358
+ /* observability must never break the gate */
359
+ }
360
+ }
361
+
362
+ /** The exact ask text the Assisted front shows. Pure; asserted by the goldens. */
363
+ export function askReason(action) {
364
+ return (
365
+ `Starting or stopping the shared spex-console dev server (${action}) is a PROTECTED action ` +
366
+ `(protected-shared-actions-gate) — it always requires explicit permission in Assisted front. ` +
367
+ `Approving this prompt IS the permission.`
368
+ );
369
+ }
370
+
371
+ export default {
372
+ TOGGLE_KEY,
373
+ activeClaimPath,
374
+ askReason,
375
+ attemptDevServerClaim,
376
+ auditHistory,
377
+ buildReminder,
378
+ classifyServerStartStop,
379
+ detectWorktreeIsolation,
380
+ devServerLane,
381
+ extractKillTargetPid,
382
+ getStatus,
383
+ isServerImpacting,
384
+ isWorktreeIsolated,
385
+ isWorktreePaths,
386
+ readFront,
387
+ readLockPid,
388
+ repoRoot,
389
+ };