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,436 @@
1
+ // spexharness/src/gates/git-identity-guard.decide.mjs
2
+ //
3
+ // Pure decision logic for ./git-identity-guard.mjs — the always-on PreToolUse
4
+ // guard that keeps an agent from WRITING a git author identity through a
5
+ // Bash / PowerShell tool call. No stdin/stdout/process.exit anywhere in this
6
+ // file; ./git-identity-guard.mjs maps `classify()` onto the engine's decision
7
+ // vocabulary and the generated scripts/claude-hooks/git-identity-guard.mjs
8
+ // shell wires it to Claude Code.
9
+ //
10
+ // THE INCIDENT (2026-09-19): agents that hit "Author identity unknown" ran
11
+ // `git config user.email <the owner's Claude-account address>` in four repos'
12
+ // `.git/config`. Repo-local beats global, so ten days of commits carried a
13
+ // non-team address and Vercel refused two production deploys. Vault lesson:
14
+ // `when-git-says-author-identity-unknown`. Owner ruling: identity is written
15
+ // by a HUMAN through `hs source identity set` — in their own terminal or with
16
+ // Claude Code's `!` prefix — and never by an agent, in any spelling.
17
+ //
18
+ // DENY (any of these in the command text):
19
+ // - `git config` naming user.email / user.name WITH a value, in any scope
20
+ // (bare, --local, --global, --system, --worktree, --file/-f <path>), incl.
21
+ // `git -C <dir> config …`; `--unset`/`--unset-all`/`--replace-all`/`--add`
22
+ // on those keys; `--remove-section user` / `--rename-section user`;
23
+ // `git config --edit` / `-e` (an editor over the file the guard protects);
24
+ // - `-c user.email=…` / `-c user.name=…` on ANY git invocation;
25
+ // - `--author=…` / `--author …` on `git commit` (and am / cherry-pick / rebase);
26
+ // - assignments / exports of GIT_AUTHOR_EMAIL, GIT_AUTHOR_NAME,
27
+ // GIT_COMMITTER_EMAIL, GIT_COMMITTER_NAME and HS_HUMAN_IDENTITY in
28
+ // command position — bash `VAR=… cmd`, `export VAR=`, `env VAR=`,
29
+ // `declare -x VAR=`, cmd `set VAR=` / `setx VAR` — and the PowerShell /
30
+ // .NET forms anywhere: `$env:VAR =`, `${env:VAR} =`, `Set-Item env:VAR`,
31
+ // `[Environment]::SetEnvironmentVariable('VAR'`;
32
+ // - `hs source identity set|clear` in command position, any spelling (`hs`,
33
+ // `npx hs`, `node bin/hs.mjs`, `hs.cmd`, the script path, `--human`);
34
+ // - a write to a git config FILE path (`.git/config`, `config.worktree`,
35
+ // `~/.gitconfig`, `.config/git/config`): redirects, `sed -i`, `perl -i`,
36
+ // `tee`, `Set-Content`, `Add-Content`, `Out-File`, cp/mv/rm and their
37
+ // PowerShell forms, or any leading command not on the read allowlist.
38
+ // ALLOW: `git config user.email` (no value), `--get`, `--get-all`,
39
+ // `--get-regexp`, `--show-origin`, `--show-scope`, `--list` / `-l`,
40
+ // `hs source identity` / `show` / `--json`, `git log --format=%ae`, reads of
41
+ // $GIT_AUTHOR_EMAIL, `cat .git/config`, non-identity `git config` keys, a
42
+ // quoted MENTION of the human form as an argument (`echo "run: hs source
43
+ // identity set …" > notes.txt`), and every non-git command.
44
+ //
45
+ // Obfuscation handling mirrors env-vault-guard.decide.mjs where practical:
46
+ // unquoted `# …` comments are stripped; a de-quoted / de-escaped shadow copy
47
+ // (`"user.email"`, `user.e\mail`, `$'\x2d'c`) is scanned alongside the raw
48
+ // text; `git.exe`, `/usr/bin/git` and `& git` (PowerShell call operator) are
49
+ // recognised; `$(…)` and backtick substitutions are their own command
50
+ // position; a config key with backslashes is compared without them.
51
+
52
+ const IDENTITY_KEY_RE = /^user\.(?:email|name)$/i;
53
+ const IDENTITY_KEY_ASSIGN_RE = /^user\.(?:email|name)=/i;
54
+
55
+ /** A config key never legitimately contains a backslash — `user.e\mail` is an
56
+ * escape-split spelling, so backslashes are dropped before the key test. */
57
+ function keyToken(t) {
58
+ return String(t ?? '').replace(/\\/g, '');
59
+ }
60
+ function isIdentityKey(t) {
61
+ return IDENTITY_KEY_RE.test(keyToken(t));
62
+ }
63
+ function isIdentityKeyAssign(t) {
64
+ return IDENTITY_KEY_ASSIGN_RE.test(keyToken(t));
65
+ }
66
+
67
+ export const IDENTITY_ENV_VARS = [
68
+ 'GIT_AUTHOR_EMAIL',
69
+ 'GIT_AUTHOR_NAME',
70
+ 'GIT_COMMITTER_EMAIL',
71
+ 'GIT_COMMITTER_NAME',
72
+ 'HS_HUMAN_IDENTITY',
73
+ ];
74
+
75
+ /** `git config` flags that read. A key with one of these is never a write. */
76
+ const CONFIG_READ_FLAGS = new Set([
77
+ '--get',
78
+ '--get-all',
79
+ '--get-regexp',
80
+ '--get-urlmatch',
81
+ '--get-color',
82
+ '--get-colorbool',
83
+ '--show-origin',
84
+ '--show-scope',
85
+ '--list',
86
+ '-l',
87
+ ]);
88
+ /** `git config` flags that write even without a positional value. */
89
+ const CONFIG_WRITE_FLAGS = new Set(['--unset', '--unset-all', '--replace-all', '--add']);
90
+ const CONFIG_EDIT_FLAGS = new Set(['--edit', '-e']);
91
+ const CONFIG_SECTION_FLAGS = new Set(['--remove-section', '--rename-section']);
92
+ /** `git config` flags that consume the next token. */
93
+ const CONFIG_FLAGS_WITH_ARG = new Set(['--file', '-f', '--blob', '--type', '--default', '--fixed-value']);
94
+ /** `git` global options that consume the next token (when not `=`-joined). */
95
+ const GIT_GLOBAL_WITH_ARG = new Set(['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--exec-path', '--super-prefix', '--config-env']);
96
+
97
+ /** A git config FILE path mention — the thing the "path write" rule protects. */
98
+ const CONFIG_PATH_RE =
99
+ /(?:^|[\s"'=:(])(?:[^\s"'`;&|<>()]*[\\/])?(?:\.git[\\/]config(?:\.worktree)?|\.gitconfig|\.config[\\/]git[\\/]config)\b/i;
100
+
101
+ /** Leading commands that only READ a file (path rule). `git` is here because the git-config rule owns it. */
102
+ const PATH_READ_COMMANDS = new Set([
103
+ 'cat', 'head', 'tail', 'less', 'more', 'grep', 'rg', 'egrep', 'fgrep', 'wc', 'file', 'stat', 'ls', 'dir', 'type', 'test',
104
+ 'diff', 'cmp', 'cut', 'sort', 'uniq', 'strings', 'md5sum', 'sha1sum', 'sha256sum', 'find', 'realpath', 'readlink',
105
+ 'get-content', 'gc', 'get-item', 'gi', 'get-childitem', 'gci', 'select-string', 'sls', 'test-path', 'get-filehash',
106
+ 'compare-object', 'git', 'echo', 'printf', 'write-output', 'write-host', 'cd', 'pushd', 'set-location', '[',
107
+ ]);
108
+ /** Read-capable commands that become writers with `-i` (in-place). */
109
+ const INPLACE_COMMANDS = new Set(['sed', 'perl', 'awk', 'gawk', 'ruby']);
110
+
111
+ /** Tokens that may precede the real command without changing what it is. */
112
+ const WRAPPERS = new Set([
113
+ '&', '(', '{', 'sudo', 'time', 'nohup', 'exec', 'env', 'node', 'npx', 'npm', 'run', 'cmd', '/c', '/k', 'bash', 'sh',
114
+ 'zsh', 'pwsh', 'powershell', '-c', '-command', '-noprofile', '-nologo', '--', '--yes', '-y', 'call', 'start',
115
+ ]);
116
+
117
+ // ── text normalisation ──────────────────────────────────────────────────────
118
+
119
+ /** Strip unquoted `# …` comments; a `#` inside quotes is left alone. */
120
+ function stripComments(text) {
121
+ const out = [];
122
+ for (const line of String(text ?? '').split('\n')) {
123
+ let quote = null;
124
+ let cut = -1;
125
+ for (let i = 0; i < line.length; i += 1) {
126
+ const c = line[i];
127
+ if (quote) {
128
+ if (c === quote) quote = null;
129
+ continue;
130
+ }
131
+ if (c === '"' || c === "'" || c === '`') {
132
+ quote = c;
133
+ continue;
134
+ }
135
+ if (c === '#' && (i === 0 || /\s/.test(line[i - 1]))) {
136
+ cut = i;
137
+ break;
138
+ }
139
+ }
140
+ out.push(cut === -1 ? line : line.slice(0, cut));
141
+ }
142
+ return out.join('\n');
143
+ }
144
+
145
+ /** De-quoted / de-escaped shadow copy used ONLY for scanning. Backslashes before
146
+ * alphanumerics are preserved (Windows path separators). */
147
+ function deobfuscate(text) {
148
+ return String(text ?? '')
149
+ .replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
150
+ .replace(/\\([^A-Za-z0-9\s])/g, '$1')
151
+ .replace(/["'`]/g, '');
152
+ }
153
+
154
+ function scanVariants(command) {
155
+ const base = stripComments(String(command ?? ''));
156
+ return [...new Set([base, deobfuscate(base)])];
157
+ }
158
+
159
+ /** Split on shell control operators (and `$(` / backtick substitutions) so one
160
+ * segment's flags never leak into the next and a substitution is its own command position. */
161
+ function segments(text) {
162
+ return String(text ?? '')
163
+ .split(/\|\||&&|;|\||\n|\r|\$\(|`/)
164
+ .map((s) => s.trim())
165
+ .filter(Boolean);
166
+ }
167
+
168
+ /** Tokens of a segment, with a `VAR=value` / `env VAR=value` prefix and PowerShell `&` stripped. */
169
+ function tokensOf(segment) {
170
+ const toks = segment.split(/\s+/).filter(Boolean);
171
+ while (toks.length && (toks[0] === '&' || toks[0] === 'env' || toks[0] === 'sudo' || /^[A-Za-z_][A-Za-z0-9_]*=/.test(toks[0]))) {
172
+ toks.shift();
173
+ }
174
+ return toks;
175
+ }
176
+
177
+ function baseCommand(token) {
178
+ return String(token ?? '')
179
+ .replace(/^["'&]+|["']+$/g, '')
180
+ .replace(/^.*[\\/]/, '')
181
+ .replace(/\.(?:exe|cmd|bat|ps1|mjs|js)$/i, '')
182
+ .toLowerCase();
183
+ }
184
+
185
+ // ── rule 1: git invocations ─────────────────────────────────────────────────
186
+
187
+ /** Inspect one `git …` token list (tokens[0] is git). Returns a deny record or null. */
188
+ function inspectGit(tokens) {
189
+ let i = 1;
190
+ // global options
191
+ while (i < tokens.length && tokens[i].startsWith('-')) {
192
+ const t = tokens[i];
193
+ if (t === '-c' || t.startsWith('-c=') || t.startsWith('--config-env')) {
194
+ const kv = t === '-c' || t === '--config-env' ? tokens[i + 1] ?? '' : t.slice(t.indexOf('=') + 1);
195
+ if (isIdentityKeyAssign(kv) || isIdentityKey(kv)) {
196
+ return { kind: 'git -c identity override', matched: `${t} ${kv}`.trim() };
197
+ }
198
+ i += t === '-c' || t === '--config-env' ? 2 : 1;
199
+ continue;
200
+ }
201
+ if (GIT_GLOBAL_WITH_ARG.has(t)) {
202
+ i += 2;
203
+ continue;
204
+ }
205
+ i += 1;
206
+ }
207
+ const sub = (tokens[i] ?? '').toLowerCase();
208
+ const rest = tokens.slice(i + 1);
209
+ if (sub === 'config') return inspectGitConfig(rest);
210
+ if (sub === 'commit' || sub === 'commit-tree' || sub === 'am' || sub === 'cherry-pick' || sub === 'rebase') {
211
+ const author = rest.find((t) => /^--author(?:=|$)/i.test(t));
212
+ if (author) return { kind: `git ${sub} --author`, matched: `git ${sub} ${author}` };
213
+ }
214
+ return null;
215
+ }
216
+
217
+ function inspectGitConfig(args) {
218
+ const flags = [];
219
+ const positionals = [];
220
+ let sectionOp = null;
221
+ for (let i = 0; i < args.length; i += 1) {
222
+ const t = args[i];
223
+ if (t === '--') {
224
+ positionals.push(...args.slice(i + 1));
225
+ break;
226
+ }
227
+ if (t.startsWith('-')) {
228
+ const name = t.includes('=') ? t.slice(0, t.indexOf('=')) : t;
229
+ flags.push(name);
230
+ if (CONFIG_SECTION_FLAGS.has(name)) {
231
+ sectionOp = { flag: name, section: t.includes('=') ? t.slice(name.length + 1) : args[i + 1] ?? '' };
232
+ if (!t.includes('=')) i += 1;
233
+ continue;
234
+ }
235
+ if (CONFIG_FLAGS_WITH_ARG.has(name) && !t.includes('=')) i += 1;
236
+ continue;
237
+ }
238
+ positionals.push(t);
239
+ }
240
+
241
+ if (flags.some((f) => CONFIG_EDIT_FLAGS.has(f))) {
242
+ return { kind: 'git config --edit', matched: 'git config --edit' };
243
+ }
244
+ if (sectionOp && /^user$/i.test(sectionOp.section)) {
245
+ return { kind: `git config ${sectionOp.flag} user`, matched: `git config ${sectionOp.flag} user` };
246
+ }
247
+
248
+ const keyIndex = positionals.findIndex((p) => isIdentityKey(p) || isIdentityKeyAssign(p));
249
+ if (keyIndex === -1) return null;
250
+ const key = positionals[keyIndex];
251
+
252
+ if (isIdentityKeyAssign(key)) return { kind: 'git config identity write', matched: `git config ${key}` };
253
+ if (flags.some((f) => CONFIG_WRITE_FLAGS.has(f))) {
254
+ return { kind: 'git config identity write', matched: `git config ${flags.filter((f) => CONFIG_WRITE_FLAGS.has(f)).join(' ')} ${key}` };
255
+ }
256
+ if (flags.some((f) => CONFIG_READ_FLAGS.has(f))) return null;
257
+ // Bare `git config user.email` reads; a following positional is the value → write.
258
+ if (positionals.length > keyIndex + 1) {
259
+ return { kind: 'git config identity write', matched: `git config ${key} ${positionals[keyIndex + 1]}` };
260
+ }
261
+ return null;
262
+ }
263
+
264
+ // ── rule 2: env var assignment ──────────────────────────────────────────────
265
+
266
+ const ENV_NAMES = IDENTITY_ENV_VARS.join('|');
267
+ const ENV_NAME_RE = new RegExp(`^(?:${ENV_NAMES})$`, 'i');
268
+ const ENV_ASSIGN_TOKEN_RE = new RegExp(`^(?:${ENV_NAMES})=`, 'i');
269
+ const ENV_SETTERS = new Set(['export', 'env', 'set', 'setx', 'declare', 'typeset']);
270
+ /** PowerShell / .NET forms — unambiguous wherever they appear in the text. */
271
+ const ENV_ASSIGN_ANYWHERE_RES = [
272
+ new RegExp(`\\$\\{?env:(?:${ENV_NAMES})\\}?\\s*=`, 'i'),
273
+ new RegExp(`\\b(?:set-item|si|new-item|ni|set-content|sc)\\s+(?:-path\\s+)?["']?env:(?:${ENV_NAMES})\\b`, 'i'),
274
+ new RegExp(`SetEnvironmentVariable\\(\\s*["']?(?:${ENV_NAMES})\\b`, 'i'),
275
+ ];
276
+
277
+ /** Bash-style assignment in COMMAND position: `VAR=… cmd`, `export VAR=…`, `env VAR=… cmd`, `set VAR=…`, `setx VAR …`. */
278
+ function inspectEnvAssignmentSegment(segment) {
279
+ const toks = segment.split(/\s+/).filter(Boolean);
280
+ let i = 0;
281
+ while (i < toks.length && ['(', '{', '&', 'sudo', 'time', 'nohup'].includes(toks[i])) i += 1;
282
+ for (; i < toks.length; i += 1) {
283
+ const t = toks[i].replace(/^[({]+/, '');
284
+ if (ENV_ASSIGN_TOKEN_RE.test(t)) return { kind: 'identity env var assignment', matched: t };
285
+ if (ENV_SETTERS.has(t.toLowerCase())) {
286
+ for (let j = i + 1; j < toks.length; j += 1) {
287
+ const n = toks[j].replace(/^["']|["']$/g, '');
288
+ if (n.startsWith('-')) continue; // declare -x, set -e
289
+ if (ENV_ASSIGN_TOKEN_RE.test(n) || ENV_NAME_RE.test(n)) {
290
+ return { kind: 'identity env var assignment', matched: `${t} ${n}` };
291
+ }
292
+ if (/^[A-Za-z_][A-Za-z0-9_]*(=.*)?$/.test(n)) continue; // another var in the same export
293
+ break;
294
+ }
295
+ continue;
296
+ }
297
+ // a plain `VAR=value` prefix of another var keeps us in prefix position
298
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(t)) continue;
299
+ break; // the real command starts here — anything after is an argument, not an assignment
300
+ }
301
+ return null;
302
+ }
303
+
304
+ function inspectEnvAssignmentAnywhere(text) {
305
+ for (const re of ENV_ASSIGN_ANYWHERE_RES) {
306
+ const m = re.exec(text);
307
+ if (m) return { kind: 'identity env var assignment', matched: m[0].trim() };
308
+ }
309
+ return null;
310
+ }
311
+
312
+ // ── rule 3: hs source identity set|clear ────────────────────────────────────
313
+
314
+ /**
315
+ * `hs [flags] [source] identity set|clear` or `hs-source-identity.mjs [identity] set|clear`,
316
+ * in COMMAND position only (after wrappers like `node`, `npx`, `npm exec … --`), so a
317
+ * quoted mention of the human form inside an `echo` is not a write.
318
+ */
319
+ function inspectHsIdentitySegment(segment) {
320
+ const toks = segment.split(/\s+/).filter(Boolean);
321
+ let i = 0;
322
+ while (i < toks.length && (WRAPPERS.has(toks[i].toLowerCase()) || /^[A-Za-z_][A-Za-z0-9_]*=/.test(toks[i]))) i += 1;
323
+ if (i >= toks.length) return null;
324
+ const head = baseCommand(toks[i]);
325
+ if (head !== 'hs' && head !== 'hs-source-identity') return null;
326
+ const words = [];
327
+ for (let j = i + 1; j < toks.length && words.length < 3; j += 1) {
328
+ const t = toks[j].replace(/^["']|["']$/g, '');
329
+ if (t.startsWith('-')) {
330
+ if ((t === '--vault' || t === '--email' || t === '--name') && !t.includes('=')) j += 1;
331
+ continue;
332
+ }
333
+ words.push(t.toLowerCase());
334
+ }
335
+ const seq = words.join(' ');
336
+ const m = /^(?:source )?(?:identity )?(set|clear)\b/.exec(seq);
337
+ if (!m) return null;
338
+ if (head === 'hs' && !/\bidentity\b/.test(seq)) return null; // `hs source set` is not this command
339
+ return { kind: `hs source identity ${m[1]}`, matched: toks.slice(i, i + 5).join(' ') };
340
+ }
341
+
342
+ // ── rule 4: writes to a git config file path ────────────────────────────────
343
+
344
+ function hasOutputRedirect(segment) {
345
+ const withoutStderrNoise = segment
346
+ .replace(/2\s*>&\s*[12]/g, ' ')
347
+ .replace(/2\s*>>?\s*(?:\/dev\/null|nul|\$null)\b/gi, ' ');
348
+ return /(?:^|[^<])>(?!\s*&)/.test(withoutStderrNoise) || /\|\s*(?:tee|out-file|set-content|add-content|sc|ac)\b/i.test(withoutStderrNoise);
349
+ }
350
+
351
+ function inspectConfigPathWrite(segment) {
352
+ const m = CONFIG_PATH_RE.exec(segment);
353
+ if (!m) return null;
354
+ const pathText = m[0].trim();
355
+ if (hasOutputRedirect(segment)) return { kind: 'git config file write', matched: `${pathText} (redirect)` };
356
+ const toks = tokensOf(segment);
357
+ const lead = baseCommand(toks[0]);
358
+ if (INPLACE_COMMANDS.has(lead)) {
359
+ const inPlace = toks.slice(1).some((t) => /^-[a-zA-Z]*i/.test(t) || /^--in-place/.test(t));
360
+ return inPlace ? { kind: 'git config file write', matched: `${lead} -i ${pathText}` } : null;
361
+ }
362
+ if (PATH_READ_COMMANDS.has(lead)) return null;
363
+ return { kind: 'git config file write', matched: `${lead} … ${pathText}` };
364
+ }
365
+
366
+ // ── the classifier ──────────────────────────────────────────────────────────
367
+
368
+ /**
369
+ * Classify a command string. Returns null when nothing writes identity, else
370
+ * `{ kind, matched }` for the first write found.
371
+ *
372
+ * @param {unknown} command
373
+ * @returns {{ kind: string, matched: string } | null}
374
+ */
375
+ export function classifyIdentityWrite(command) {
376
+ if (typeof command !== 'string' || !command.trim()) return null;
377
+ for (const variant of scanVariants(command)) {
378
+ const env = inspectEnvAssignmentAnywhere(variant);
379
+ if (env) return env;
380
+ for (const seg of segments(variant)) {
381
+ const envSeg = inspectEnvAssignmentSegment(seg);
382
+ if (envSeg) return envSeg;
383
+ const hs = inspectHsIdentitySegment(seg);
384
+ if (hs) return hs;
385
+ const toks = tokensOf(seg);
386
+ // a `git` token may sit after a leading wrapper (`& git`, `cmd /c git`, `bash -c "git …"`)
387
+ for (let i = 0; i < toks.length; i += 1) {
388
+ if (baseCommand(toks[i]) === 'git') {
389
+ const hit = inspectGit(toks.slice(i));
390
+ if (hit) return hit;
391
+ break;
392
+ }
393
+ }
394
+ const pathWrite = inspectConfigPathWrite(seg);
395
+ if (pathWrite) return pathWrite;
396
+ }
397
+ }
398
+ return null;
399
+ }
400
+
401
+ /**
402
+ * The tool-call classifier the gate calls. Only Bash / PowerShell carry a
403
+ * command string; every other tool is not applicable (null).
404
+ *
405
+ * @param {string} toolName
406
+ * @param {Record<string, unknown>} toolInput
407
+ * @returns {{ kind: string, matched: string } | null}
408
+ */
409
+ export function classifyGitIdentity(toolName, toolInput) {
410
+ if (toolName !== 'Bash' && toolName !== 'PowerShell') return null;
411
+ return classifyIdentityWrite(toolInput?.command);
412
+ }
413
+
414
+ export const HUMAN_FORMS = Object.freeze({
415
+ terminal: 'hs source identity set --email you@example.com',
416
+ claudeCode: '!hs source identity set --email you@example.com --human',
417
+ });
418
+
419
+ export const VAULT_LESSON = 'when-git-says-author-identity-unknown';
420
+
421
+ /**
422
+ * The deny reason. A function of the classification so tests pin its text.
423
+ * @param {{ kind: string, matched: string }} hit
424
+ * @returns {string}
425
+ */
426
+ export function denyReason(hit) {
427
+ return (
428
+ `HeySpex git-identity guard: blocked ${hit.kind} (\`${hit.matched}\`). ` +
429
+ 'Git author identity is a HUMAN-authorized action only (owner ruling 2026-09-19, vault lesson ' +
430
+ `${VAULT_LESSON}): agents read identity, never write it — in any scope, any spelling, any env var. ` +
431
+ 'If git says "Author identity unknown", stop and tell the human to run it themselves: ' +
432
+ `in their own terminal \`${HUMAN_FORMS.terminal}\`; inside Claude Code \`${HUMAN_FORMS.claudeCode}\` ` +
433
+ '(the `!` prefix runs outside your tool calls). Read forms stay open: `hs source identity show`, ' +
434
+ '`git config --show-origin --get-all user.email`, `git log --format=%ae`. This gate has no off switch.'
435
+ );
436
+ }
@@ -0,0 +1,61 @@
1
+ // spexharness/src/gates/git-identity-guard.mjs
2
+ //
3
+ // The always-on PreToolUse guard that keeps an agent from WRITING a git
4
+ // author identity through a Bash / PowerShell tool call. Same shape as
5
+ // ./env-vault-guard.mjs (the reference security gate): a pure classifier in
6
+ // ./git-identity-guard.decide.mjs, this thin `{ id, event, severity, run }`
7
+ // adapter over it, a generated Claude Code shell at
8
+ // scripts/claude-hooks/git-identity-guard.mjs, an ALWAYS_ON entry in
9
+ // scripts/claude-hooks/_run.mjs and a PreToolUse row in .claude/settings.json
10
+ // on the matcher `Bash|PowerShell`.
11
+ //
12
+ // WHY (the incident, 2026-09-19): agents that hit git's "Author identity
13
+ // unknown" ran `git config user.email <the owner's Claude-account address>`
14
+ // in four repos' `.git/config`. A repo-local value beats the global identity
15
+ // the owner set later, so every commit from this machine was authored as a
16
+ // non-team address for ten days and Vercel silently refused two production
17
+ // deploys. Vault lesson: `when-git-says-author-identity-unknown`.
18
+ //
19
+ // THE RULING: a repo-local (or global) git identity override is a HUMAN-
20
+ // authorized action only, run through `hs source identity set` by the human
21
+ // themselves — in their own terminal, or in Claude Code with the `!` prefix
22
+ // (which runs outside the agent's tool calls and is therefore invisible to
23
+ // this gate; `--human` exists only because that path inherits the agent's
24
+ // env). Agents may READ identity, never write it. The full deny/allow matrix
25
+ // is the header of ./git-identity-guard.decide.mjs.
26
+ //
27
+ // NO ENV KILL SWITCH — deliberately. env-vault-guard and research-protocol-
28
+ // gate keep an owner-only env hatch; this one does not, because the owner's
29
+ // own path IS the identity command (typed by a human), so there is nothing a
30
+ // hatch would enable that the sanctioned form does not already do. This is a
31
+ // locked owner ruling, not an oversight. It is listed in _run.mjs's ALWAYS_ON
32
+ // so .heyspex/mode.json cannot silence it either.
33
+ //
34
+ // Registered in ./index.mjs's PORTED_GATES (every engine front — SpexCode
35
+ // seats, hs chat, overnight runs), not BUILT_IN_GATES (the frozen twelve).
36
+ //
37
+ // `ctx` is accepted for shape parity with the other gates and ignored: the
38
+ // decision depends on the command text alone.
39
+
40
+ import { classifyGitIdentity, denyReason } from './git-identity-guard.decide.mjs';
41
+
42
+ export { classifyGitIdentity, classifyIdentityWrite, denyReason, HUMAN_FORMS, VAULT_LESSON } from './git-identity-guard.decide.mjs';
43
+
44
+ /** The gate's id — unique across the registry; also its Claude Code hook name. */
45
+ export const id = 'git-identity-guard';
46
+ /** Runs pre-tool, same as its PreToolUse Claude Code hook registration. */
47
+ export const event = 'before_tool';
48
+ /** Denies, so "block". */
49
+ export const severity = 'block';
50
+
51
+ /**
52
+ * @param {{ point: string, toolName?: string, toolInput?: Record<string, unknown> }} evt
53
+ * @returns {{ decision: 'allow'|'deny', reason?: string }}
54
+ */
55
+ export function run(evt) {
56
+ const hit = classifyGitIdentity(evt?.toolName, evt?.toolInput ?? {});
57
+ if (!hit) return { decision: 'allow' };
58
+ return { decision: 'deny', reason: denyReason(hit) };
59
+ }
60
+
61
+ export default { id, event, severity, run };
@@ -0,0 +1,151 @@
1
+ // spexharness/src/gates/git-reflog-audit.mjs
2
+ //
3
+ // Engine port of scripts/claude-hooks/git-reflog-audit.mjs — the session-close
4
+ // git audit BACKSTOP (task HS-610I-P3TJ, epic HS-GG01-P3TJ), wave 6 of
5
+ // [[HS-AZ61-NYPR]]. The rule was Claude-Code-plugin-only, so an overnight
6
+ // DeepSeek or local seat could stash/reset/rebase a shared tree and leave no
7
+ // independent trace. See ./PORTING.md for the recipe.
8
+ //
9
+ // This module does NOT reimplement the reflog parsing. The audit logic moved
10
+ // once, into ../git-reflog-audit.mjs, and there is exactly one copy of it in
11
+ // the tree — this gate, the Claude Code shim, and the original regression suite
12
+ // (scripts/claude-hooks/git-reflog-audit.test.mjs) all drive that module.
13
+ //
14
+ // TWO GATES, one rule. The Claude Code hook was registered on BOTH SessionStart
15
+ // and Stop and branched on `hook_event_name` inside its own `main()`. The
16
+ // engine registry keys a gate on exactly ONE event point (registry.mjs's
17
+ // `assertValidGate`), so the rule ships as a pair:
18
+ // * `git-reflog-audit-snapshot` on `session_start` — writes the baseline.
19
+ // * `git-reflog-audit` on `stop` — diffs and warns.
20
+ // Splitting is the documented deviation from PORTING.md's one-hook-one-gate
21
+ // shape (step 2 asks for the mapping to be written down HERE rather than
22
+ // guessed at): inventing a multi-event gate would change registry.mjs's own
23
+ // contract for one rule, and two ids make `hs harness gates` show both halves
24
+ // of the witness rather than hiding the half that does the recording.
25
+ //
26
+ // Event mapping: SessionStart → `session_start`; Stop → `stop`. (SessionEnd,
27
+ // which the original also accepted, maps to `session_end` in ../events.mjs —
28
+ // `.claude/settings.json` wires only Stop, so the ported pair covers every
29
+ // wiring that exists. A front that wants the audit at session end registers the
30
+ // exported {@link sessionEndGate}.)
31
+ //
32
+ // Decision mapping: this witness NEVER blocks and never asks — "warn only" is
33
+ // its whole scope. Its output channel is `.heyspex/guardrail-history.jsonl`,
34
+ // not stdout, so the honest engine mapping is:
35
+ // * nothing flagged → `allow`.
36
+ // * something flagged → `inject`, carrying the finding as text so a headless
37
+ // seat's own loop can surface it. On the Claude Code Stop hook this is
38
+ // still SILENT (adapters/claude-code.mjs's `stop` branch renders only
39
+ // deny/rewrite-warn), which is exactly what the original hook did — the
40
+ // golden fixtures pin that parity.
41
+ //
42
+ // SIDE EFFECT, deliberately kept in the gate (a documented deviation from the
43
+ // "gates are pure" rule the other twelve follow, PORTING.md step 1): the
44
+ // snapshot write and the guardrail-history append ARE this rule. The other
45
+ // gates could hand their audit row to ../adapters/audit.mjs because that row is
46
+ // a Claude-Code-front observation of a decision; here the row IS the enforcement,
47
+ // and routing it through the Claude Code adapter would mean a DeepSeek seat
48
+ // silently records nothing — the exact gap this port closes. Both writes are
49
+ // injectable through `ctx` so a test never touches the real tree.
50
+ //
51
+ // `ctx` contract (all optional; every field defaults to a real read/write):
52
+ // ctx.repoRoot - the working tree to snapshot/diff. Defaults to
53
+ // CLAUDE_PROJECT_DIR, then this repo's root.
54
+ // ctx.snapshotPath - override the session-scoped snapshot file (tests).
55
+ // ctx.append - override the guardrail-history writer (tests).
56
+ // ctx.env - env var overrides (tests use this instead of mutating
57
+ // process.env).
58
+ //
59
+ // Env hatch (unchanged from the hook): HEYSPEX_GIT_REFLOG_AUDIT_OFF=1.
60
+
61
+ import {
62
+ describeDiff,
63
+ isFlagged,
64
+ repoRoot as defaultRepoRoot,
65
+ runSessionEnd,
66
+ writeSnapshot,
67
+ } from '../git-reflog-audit.mjs';
68
+
69
+ const OFF_ENV = 'HEYSPEX_GIT_REFLOG_AUDIT_OFF';
70
+
71
+ /** Caller-defined severity label shared by both halves; this rule only ever warns. */
72
+ export const severity = 'warn';
73
+
74
+ function isOff(ctx) {
75
+ const env = ctx.env ?? process.env;
76
+ return String(env[OFF_ENV] ?? '') === '1';
77
+ }
78
+
79
+ function resolveRoot(ctx) {
80
+ if (typeof ctx.repoRoot === 'string' && ctx.repoRoot.trim()) return ctx.repoRoot.trim();
81
+ return defaultRepoRoot(ctx.env ?? process.env);
82
+ }
83
+
84
+ /**
85
+ * Session-start half: write the baseline snapshot. Always `allow` — the
86
+ * original hook emits nothing at session start either.
87
+ *
88
+ * @param {{ point: string, sessionId?: string }} evt
89
+ * @param {{ repoRoot?: string, snapshotPath?: string, env?: Record<string, string|undefined> }} [ctx]
90
+ * @returns {{ decision: 'allow', snapshot?: object|null }}
91
+ */
92
+ export function runSnapshot(evt, ctx = {}) {
93
+ if (isOff(ctx)) return { decision: 'allow' };
94
+ const snapshot = writeSnapshot({
95
+ repoRootPath: resolveRoot(ctx),
96
+ sessionId: evt?.sessionId,
97
+ snapshotPath: ctx.snapshotPath,
98
+ });
99
+ return { decision: 'allow', snapshot };
100
+ }
101
+
102
+ /**
103
+ * Stop half: diff against the baseline and, when something destructive
104
+ * happened, record ONE warn row and carry the finding on the decision.
105
+ *
106
+ * @param {{ point: string, sessionId?: string }} evt
107
+ * @param {{ repoRoot?: string, snapshotPath?: string,
108
+ * append?: (row: object, repoRoot: string) => unknown,
109
+ * env?: Record<string, string|undefined> }} [ctx]
110
+ * @returns {{ decision: 'allow'|'inject', inject?: { additionalContext: string, severity: string },
111
+ * diff?: object|null }}
112
+ */
113
+ export function runAudit(evt, ctx = {}) {
114
+ if (isOff(ctx)) return { decision: 'allow' };
115
+ const diff = runSessionEnd({
116
+ repoRootPath: resolveRoot(ctx),
117
+ sessionId: evt?.sessionId,
118
+ snapshotPath: ctx.snapshotPath,
119
+ ...(ctx.append ? { append: ctx.append } : {}),
120
+ });
121
+ if (!isFlagged(diff)) return { decision: 'allow', diff };
122
+
123
+ const text =
124
+ 'Destructive git activity happened during this session and was recorded by the ' +
125
+ `git-reflog-audit witness (warn only, nothing was blocked): ${describeDiff(diff)}. ` +
126
+ 'Say so in the hand-off rather than letting it go unmentioned.';
127
+ return { decision: 'inject', inject: { additionalContext: text, severity: 'warn' }, diff };
128
+ }
129
+
130
+ /** The session-start half of the witness — writes the baseline snapshot. */
131
+ export const snapshotGate = {
132
+ id: 'git-reflog-audit-snapshot',
133
+ event: 'session_start',
134
+ severity,
135
+ run: runSnapshot,
136
+ };
137
+
138
+ /** The session-end half, for a front that closes on `session_end` rather than `stop`. */
139
+ export const sessionEndGate = {
140
+ id: 'git-reflog-audit-session-end',
141
+ event: 'session_end',
142
+ severity,
143
+ run: runAudit,
144
+ };
145
+
146
+ /** The stop half of the witness — the diff, and the only half that can warn. */
147
+ export const id = 'git-reflog-audit';
148
+ export const event = 'stop';
149
+ export const run = runAudit;
150
+
151
+ export default { id, event, severity, run };