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.
- package/.claude-plugin/marketplace.json +14 -0
- package/README.md +21 -0
- package/bin/hs.mjs +1089 -104
- package/bin/spexcode.mjs +262 -0
- package/dev-factory/engine/worktree.mjs +740 -0
- package/dev-factory/lib/code-repo-root.mjs +67 -0
- package/dev-factory/lib/context-budget.mjs +84 -0
- package/dev-factory/lib/epic-detect.mjs +190 -0
- package/dev-factory/lib/fleet-queue-file.mjs +846 -0
- package/dev-factory/lib/fleet-queue.mjs +426 -0
- package/dev-factory/lib/fleet-state.mjs +14 -18
- package/dev-factory/lib/model-catalog.mjs +338 -0
- package/dev-factory/lib/seat-context.mjs +199 -0
- package/dev-factory/lib/spec-io.mjs +93 -0
- package/dev-factory/lib/sufficiency-score.mjs +8 -2
- package/dev-factory/lib/worktree-allocator.mjs +744 -0
- package/dev-factory/model-catalog.json +222 -0
- package/dist/cli/hs-check.mjs +1127 -103
- package/dist/cli/hs-claim.mjs +981 -63
- package/dist/cli/hs-connect.mjs +10059 -1729
- package/dist/cli/hs-defer.mjs +5507 -3072
- package/dist/cli/hs-dev.mjs +576 -0
- package/dist/cli/hs-env.mjs +416 -302
- package/dist/cli/hs-index-rebuild.mjs +7334 -0
- package/dist/cli/hs-index-reconcile.mjs +8138 -0
- package/dist/cli/hs-init.mjs +2840 -149
- package/dist/cli/hs-install.mjs +7214 -594
- package/dist/cli/hs-join.mjs +10640 -2421
- package/dist/cli/hs-vault-pull.mjs +8157 -6370
- package/dist/cli/hs-vault-push.mjs +6433 -0
- package/dist/cli/hs-vault.mjs +4029 -1555
- package/dist/cli/hs-workspace.mjs +2702 -538
- package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
- package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
- package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
- package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
- package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
- package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
- package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
- package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
- package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
- package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -0
- package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
- package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
- package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
- package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
- package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -0
- package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
- package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
- package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
- package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
- package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
- package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
- package/dist/cli-templates/harness-commands/cursor/hs-ground.md +33 -0
- package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
- package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
- package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
- package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
- package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
- package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-view.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
- package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
- package/docs/copilot-plugin-install.md +162 -0
- package/docs/driving-heyspex-from-copilot.md +165 -0
- package/docs/mcp-client-config.md +259 -6
- package/package.json +249 -124
- package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
- package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
- package/plugins/heyspex/.mcp.json +12 -0
- package/plugins/heyspex/README.md +309 -0
- package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
- package/plugins/heyspex/bin/run-hook.mjs +80 -0
- package/plugins/heyspex/hooks/cursor-hooks.json +308 -0
- package/plugins/heyspex/hooks/hooks.json +418 -0
- package/plugins/heyspex/mcp.json +13 -0
- package/scripts/check-mcp-freshness.mjs +17 -9
- package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
- package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
- package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
- package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -0
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +356 -0
- package/scripts/claude-hooks/_run.mjs +413 -0
- package/scripts/claude-hooks/_transcript-text.mjs +60 -0
- package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
- package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
- package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
- package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
- package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
- package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
- package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
- package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
- package/scripts/claude-hooks/claim-liveness-shared.mjs +577 -0
- package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
- package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
- package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
- package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
- package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
- package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
- package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
- package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
- package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
- package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
- package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
- package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
- package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
- package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
- package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
- package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
- package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
- package/scripts/claude-hooks/inject-communication-style.mjs +86 -0
- package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
- package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
- package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
- package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
- package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -0
- package/scripts/claude-hooks/log-session-activity.mjs +341 -0
- package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
- package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
- package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
- package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
- package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
- package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
- package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
- package/scripts/claude-hooks/research-method-guard.mjs +398 -0
- package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
- package/scripts/claude-hooks/session-end.mjs +451 -0
- package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
- package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
- package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
- package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
- package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
- package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
- package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
- package/scripts/claude-hooks/wip-checkpoint.mjs +302 -0
- package/scripts/cursor-hooks/manifest.mjs +281 -0
- package/scripts/dev-server-status.mjs +34 -13
- package/scripts/dev-server-stop.mjs +313 -0
- package/scripts/heyspex-mode.mjs +288 -0
- package/scripts/hs-auto-resolve.mjs +240 -0
- package/scripts/hs-auto-source.mjs +398 -0
- package/scripts/hs-auto.mjs +134 -0
- package/scripts/hs-capture-cli.mjs +326 -0
- package/scripts/hs-classifier.mjs +136 -0
- package/scripts/hs-companion.mjs +88 -0
- package/scripts/hs-gateway.mjs +115 -0
- package/scripts/hs-mode.mjs +125 -0
- package/scripts/hs-next.mjs +441 -0
- package/scripts/hs-settings.mjs +338 -0
- package/scripts/hs-view.mjs +496 -0
- package/scripts/hs-worktree.mjs +201 -0
- package/scripts/install-claude-code-hooks.mjs +272 -0
- package/scripts/install-cursor-hooks.mjs +471 -0
- package/scripts/lib/agent-env-markers.mjs +38 -0
- package/scripts/lib/console-url-resolver.mjs +199 -61
- package/scripts/lib/customer-kit.mjs +1203 -0
- package/scripts/lib/dev-mode.mjs +249 -32
- package/scripts/lib/directive-obligations.mjs +349 -0
- package/scripts/lib/heyspex-home.mjs +72 -0
- package/scripts/lib/hook-dispositions.mjs +210 -0
- package/scripts/lib/hook-vault-header.mjs +30 -0
- package/scripts/lib/hs-command.mjs +277 -0
- package/scripts/lib/hs-companion-settings.mjs +472 -0
- package/scripts/lib/hs-source-branch.mjs +196 -0
- package/scripts/lib/hs-source-commit.mjs +59 -5
- package/scripts/lib/hs-source-identity.mjs +526 -0
- package/scripts/lib/hs-source-log.mjs +13 -22
- package/scripts/lib/hs-source-preflight.mjs +18 -4
- package/scripts/lib/hs-source-push.mjs +386 -307
- package/scripts/lib/hs-worktree-args.mjs +115 -0
- package/scripts/lib/shared-path-helpers.mjs +134 -0
- package/scripts/lib/standalone-doctor.mjs +540 -3
- package/scripts/lib/subagent-git-lock.mjs +109 -0
- package/scripts/lib/template-pin.mjs +34 -0
- package/scripts/lib/window-placement.mjs +109 -0
- package/scripts/provenance-lib.mjs +91 -0
- package/scripts/stamp-provenance-on-edit.mjs +222 -0
- package/scripts/wip-ledger-health.mjs +653 -0
- package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
- package/spexharness/src/adapters/audit.mjs +197 -0
- package/spexharness/src/adapters/claude-code.mjs +449 -0
- package/spexharness/src/adapters/copilot.mjs +236 -0
- package/spexharness/src/adapters/detect-harness.mjs +34 -0
- package/spexharness/src/chat/tui/ansi.mjs +448 -0
- package/spexharness/src/chat/tui/palette.mjs +22 -0
- package/spexharness/src/chat/tui/select.mjs +208 -0
- package/spexharness/src/config-root.mjs +54 -0
- package/spexharness/src/context-budget.mjs +141 -0
- package/spexharness/src/dev-server-control.mjs +389 -0
- package/spexharness/src/doctrine/behaviour.mjs +371 -0
- package/spexharness/src/doctrine/corpus.mjs +733 -0
- package/spexharness/src/doctrine/inject.mjs +338 -0
- package/spexharness/src/doctrine/merge.mjs +184 -0
- package/spexharness/src/doctrine/sources.mjs +252 -0
- package/spexharness/src/doctrine/synthesize.mjs +401 -0
- package/spexharness/src/doctrine/vault-source.mjs +574 -0
- package/spexharness/src/doctrine/writer.mjs +176 -0
- package/spexharness/src/events.mjs +84 -0
- package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
- package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
- package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
- package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
- package/spexharness/src/gates/deep-research-cost-gate.mjs +275 -0
- package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
- package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
- package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
- package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
- package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
- package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
- package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
- package/spexharness/src/gates/git-identity-guard.mjs +61 -0
- package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
- package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
- package/spexharness/src/gates/git-stash-gate.mjs +159 -0
- package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
- package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
- package/spexharness/src/gates/inject-vault-context.mjs +239 -0
- package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
- package/spexharness/src/gates/plan-review-gate.mjs +233 -0
- package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
- package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
- package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
- package/spexharness/src/gates/settings-write-gate.mjs +104 -0
- package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
- package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
- package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
- package/spexharness/src/git-reflog-audit.mjs +294 -0
- package/spexharness/src/intent-classifier.mjs +196 -0
- package/spexharness/src/intent-directive.mjs +626 -0
- package/spexharness/src/profile/learn.mjs +551 -0
- package/spexharness/src/profile/proposals.mjs +357 -0
- package/spexharness/src/profile/vault-inclusion.mjs +386 -0
- package/spexharness/src/redact.mjs +151 -0
- package/spexharness/src/rule-table.mjs +849 -0
- package/spexharness/src/seat-context.mjs +122 -0
- package/spexharness/src/session-bootstrap.mjs +512 -0
- package/spexharness/src/session-record.mjs +815 -0
- package/spexharness/src/token-inventory.mjs +207 -0
- package/vendor/heyspex-mcp/account-switch.mjs +96 -0
- package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
- package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
- package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
- package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
- package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +77 -8
- package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
- package/vendor/heyspex-mcp/capture-console-client.mjs +117 -3
- package/vendor/heyspex-mcp/capture-focus.mjs +1061 -31
- package/vendor/heyspex-mcp/capture-outbox.mjs +15 -5
- package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
- package/vendor/heyspex-mcp/capture-tools.mjs +1264 -0
- package/vendor/heyspex-mcp/claim-tools.mjs +1147 -0
- package/vendor/heyspex-mcp/cli-verbs.mjs +2074 -0
- package/vendor/heyspex-mcp/cli.mjs +207 -15524
- package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
- package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +602 -0
- package/vendor/heyspex-mcp/config-seed.yaml +15 -0
- package/vendor/heyspex-mcp/config-settings.mjs +290 -7
- package/vendor/heyspex-mcp/console-fetch.mjs +822 -17
- package/vendor/heyspex-mcp/console-transport.mjs +172 -0
- package/vendor/heyspex-mcp/credentials.mjs +585 -0
- package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
- package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
- package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
- package/vendor/heyspex-mcp/design-tools.mjs +4 -1
- package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
- package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
- package/vendor/heyspex-mcp/dev-tools.mjs +131 -0
- package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
- package/vendor/heyspex-mcp/events-tools.mjs +207 -0
- package/vendor/heyspex-mcp/filing-tools.mjs +723 -0
- package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +71 -15
- package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
- package/vendor/heyspex-mcp/folder-trust-store.mjs +60 -0
- package/vendor/heyspex-mcp/http-transport.mjs +302 -0
- package/vendor/heyspex-mcp/list-tools.mjs +257 -0
- package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
- package/vendor/heyspex-mcp/mcp-config.mjs +107 -0
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
- package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
- package/vendor/heyspex-mcp/occupied-keys.mjs +142 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +3377 -0
- package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
- package/vendor/heyspex-mcp/package-root.mjs +32 -0
- package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
- package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
- package/vendor/heyspex-mcp/plan-push.mjs +908 -22
- package/vendor/heyspex-mcp/plan-review.mjs +124 -30
- package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
- package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -970
- package/vendor/heyspex-mcp/promote-capture.mjs +388 -69
- package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
- package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
- package/vendor/heyspex-mcp/promote-research.mjs +666 -53
- package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
- package/vendor/heyspex-mcp/prompts.mjs +1460 -0
- package/vendor/heyspex-mcp/read-path.mjs +191 -15
- package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
- package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
- package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
- package/vendor/heyspex-mcp/register-spec.mjs +266 -54
- package/vendor/heyspex-mcp/request-context.mjs +83 -0
- package/vendor/heyspex-mcp/research-focus.mjs +207 -4
- package/vendor/heyspex-mcp/research-handlers.mjs +625 -0
- package/vendor/heyspex-mcp/research-tools.mjs +4 -4
- package/vendor/heyspex-mcp/response-budget.mjs +348 -0
- package/vendor/heyspex-mcp/review-focus.mjs +25 -1
- package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
- package/vendor/heyspex-mcp/search-specs.mjs +280 -0
- package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
- package/vendor/heyspex-mcp/session-owner.mjs +109 -0
- package/vendor/heyspex-mcp/session-tools.mjs +162 -0
- package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
- package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
- package/vendor/heyspex-mcp/spec-edit-tools.mjs +453 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
- package/vendor/heyspex-mcp/split-research.mjs +304 -56
- package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +150 -7
- package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
- package/vendor/heyspex-mcp/summary-write-tools.mjs +228 -0
- package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +123 -1
- package/vendor/heyspex-mcp/tool-catalog.mjs +2819 -0
- package/vendor/heyspex-mcp/tool-dispatch.mjs +657 -0
- package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
- package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
- package/vendor/heyspex-mcp/vault-io.mjs +720 -0
- package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +554 -68
- package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
- package/vendor/heyspex-mcp/whoami.mjs +92 -0
- package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Code hook → WIP before/after safety net for implementation sessions.
|
|
3
|
+
//
|
|
4
|
+
// Hardens the two ends of the WIP handoff contract that were, until now, pure
|
|
5
|
+
// agent discipline (session-close-protocol says record WIP *after*; the claim
|
|
6
|
+
// contract says read WIP *before*) and — critically — covers the gap NEITHER
|
|
7
|
+
// side covered: context-window rollover *mid*-implementation. The session-close
|
|
8
|
+
// protocol is too heavy for a rollover (it flips status, writes HS-REV, treats
|
|
9
|
+
// work as done) but work is still continuing. This hook injects the right
|
|
10
|
+
// reminder at the right moment instead of relying on the agent remembering.
|
|
11
|
+
//
|
|
12
|
+
// Two events, one script (mirrors emit-claim-status.mjs's multi-event shape):
|
|
13
|
+
//
|
|
14
|
+
// SessionStart (source: startup | resume)
|
|
15
|
+
// → the *before* side. Remind the agent to run heyspex_handoff_load + read
|
|
16
|
+
// the WIP ledger before claiming/continuing, and preview the most recent
|
|
17
|
+
// ledger entry so the resume context is non-empty.
|
|
18
|
+
//
|
|
19
|
+
// SessionStart (source: compact)
|
|
20
|
+
// → the reliable *mid-flight* net. Claude Code re-runs SessionStart right
|
|
21
|
+
// AFTER an auto/manual compaction, so additionalContext here lands in the
|
|
22
|
+
// fresh post-compaction context — the one place a reminder is guaranteed
|
|
23
|
+
// to be seen. Framed as "rollover, not fresh start — work continues."
|
|
24
|
+
//
|
|
25
|
+
// PreCompact
|
|
26
|
+
// → the *before-compaction* warning. Best-effort: fires just before context
|
|
27
|
+
// is compacted. Nudges a heyspex_record_wip + heyspex_handoff_record flush
|
|
28
|
+
// to the self-purging Neon lane while the full context is still present.
|
|
29
|
+
//
|
|
30
|
+
// IMPORTANT — what this hook does NOT do: a shell hook cannot call MCP tools, so
|
|
31
|
+
// it cannot perform the flush/load itself. It injects the reminder (same model
|
|
32
|
+
// as preflight-reminder.mjs). The agent still makes the tool calls.
|
|
33
|
+
//
|
|
34
|
+
// No CLAUDE_AGENT gate ([[HS-DT9L-NLR7-DIAG-nine-hooks-are-switched-off-in]]).
|
|
35
|
+
// This used to exit early in interactive sessions, so the mid-session rollover net
|
|
36
|
+
// never fired where a human is actually doing implementation work. Safe to run
|
|
37
|
+
// everywhere: this hook only ever injects additionalContext, it can never deny
|
|
38
|
+
// anything, so there is no approval-fatigue risk in removing the gate.
|
|
39
|
+
//
|
|
40
|
+
// Env gate:
|
|
41
|
+
// HEYSPEX_WIP_CHECKPOINT_OFF=1 hard kill switch — silent even when opted in.
|
|
42
|
+
//
|
|
43
|
+
// MUST exit 0 on every failure path — a hook that breaks Claude Code is worse
|
|
44
|
+
// than a hook that silently does nothing.
|
|
45
|
+
//
|
|
46
|
+
// Output shape: { hookSpecificOutput: { hookEventName, additionalContext } }.
|
|
47
|
+
|
|
48
|
+
import fs from 'node:fs';
|
|
49
|
+
import path from 'node:path';
|
|
50
|
+
import { fileURLToPath } from 'node:url';
|
|
51
|
+
import { getActiveConfig } from '../../vendor/heyspex-mcp/vault-resolve.mjs';
|
|
52
|
+
import { hookVaultHeaders } from '../lib/hook-vault-header.mjs';
|
|
53
|
+
import { resolveConsoleUrl as resolveConsoleUrlShared } from '../lib/console-url-resolver.mjs';
|
|
54
|
+
import { readCredentialsMcpToken, resolveDevMode } from '../lib/dev-mode.mjs';
|
|
55
|
+
|
|
56
|
+
const NOTES_EXCERPT_MAX = 320;
|
|
57
|
+
const REMAINING_ITEM_MAX = 200;
|
|
58
|
+
const REMAINING_ITEMS_MAX = 3;
|
|
59
|
+
const LEDGER_REL = ['.heyspex', 'wip-state.json'];
|
|
60
|
+
const WIP_FETCH_TIMEOUT_MS = 1500;
|
|
61
|
+
|
|
62
|
+
function repoRoot() {
|
|
63
|
+
// scripts/claude-hooks/ → up two levels.
|
|
64
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Since the repo split (2026-08-31) the WIP ledger lives in the VAULT, not the
|
|
68
|
+
// code checkout. Reading repoRoot() here previewed a frozen pre-split snapshot to
|
|
69
|
+
// every new session — the HS-OR8H repo-split "quiet window OPEN" notice kept
|
|
70
|
+
// re-appearing for days after the window closed, because the live ledger had moved
|
|
71
|
+
// and this copy never advanced. Resolve the vault first; fall back to the code repo
|
|
72
|
+
// so a pre-split or standalone checkout still previews its own ledger.
|
|
73
|
+
function vaultRoot() {
|
|
74
|
+
try {
|
|
75
|
+
return getActiveConfig()?.repoRootAbs ?? null;
|
|
76
|
+
} catch {
|
|
77
|
+
return null; // registry unreadable — the code-repo base still applies
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Ledger roots in precedence order: vault first, code repo second.
|
|
82
|
+
function ledgerRoots() {
|
|
83
|
+
const roots = [];
|
|
84
|
+
const vault = vaultRoot();
|
|
85
|
+
if (vault) roots.push(vault);
|
|
86
|
+
const repo = repoRoot();
|
|
87
|
+
if (!roots.includes(repo)) roots.push(repo);
|
|
88
|
+
return roots;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function readStdinJson() {
|
|
92
|
+
if (process.stdin.isTTY) return null;
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
let buf = '';
|
|
95
|
+
let settled = false;
|
|
96
|
+
const finish = (value) => {
|
|
97
|
+
if (settled) return;
|
|
98
|
+
settled = true;
|
|
99
|
+
resolve(value);
|
|
100
|
+
};
|
|
101
|
+
const timer = setTimeout(() => finish(buf || ''), 200);
|
|
102
|
+
process.stdin.setEncoding('utf8');
|
|
103
|
+
process.stdin.on('data', (chunk) => {
|
|
104
|
+
buf += chunk;
|
|
105
|
+
});
|
|
106
|
+
process.stdin.on('end', () => {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
finish(buf);
|
|
109
|
+
});
|
|
110
|
+
process.stdin.on('error', () => {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
finish(buf);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function emit(eventName, additionalContext) {
|
|
118
|
+
if (!additionalContext) return;
|
|
119
|
+
process.stdout.write(
|
|
120
|
+
JSON.stringify({
|
|
121
|
+
hookSpecificOutput: { hookEventName: eventName, additionalContext },
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function truncate(value, limit) {
|
|
127
|
+
if (typeof value !== 'string') return '';
|
|
128
|
+
const trimmed = value.trim();
|
|
129
|
+
if (!trimmed) return '';
|
|
130
|
+
return trimmed.length > limit ? `${trimmed.slice(0, limit)}…` : trimmed;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Read the file ledger and return the single most-recently-updated entry, or
|
|
134
|
+
// null when the ledger is missing/empty/malformed. The file ledger runs
|
|
135
|
+
// alongside the Neon handoff_context lane until the file→Neon cutover
|
|
136
|
+
// (HS-EPIC-PFMN); reading the file keeps SessionStart robust when console/Neon
|
|
137
|
+
// is offline.
|
|
138
|
+
function mostRecentLedgerEntry() {
|
|
139
|
+
let parsed = null;
|
|
140
|
+
for (const root of ledgerRoots()) {
|
|
141
|
+
try {
|
|
142
|
+
const candidate = JSON.parse(fs.readFileSync(path.join(root, ...LEDGER_REL), 'utf8'));
|
|
143
|
+
if (candidate && typeof candidate === 'object') {
|
|
144
|
+
parsed = candidate;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
// missing or unreadable at this root — try the next one
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
152
|
+
let best = null;
|
|
153
|
+
for (const [epicId, entry] of Object.entries(parsed)) {
|
|
154
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
155
|
+
const updatedAt = typeof entry.updatedAt === 'string' ? entry.updatedAt : '';
|
|
156
|
+
if (!best || updatedAt > best.updatedAt) {
|
|
157
|
+
best = { epicId, updatedAt, entry };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return best;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function ledgerPreview(best, source = 'disk') {
|
|
164
|
+
if (!best) return '';
|
|
165
|
+
const { epicId, entry } = best;
|
|
166
|
+
const lines = [`Most recent WIP ledger entry — ${epicId} (updated ${best.updatedAt || 'unknown'}):`];
|
|
167
|
+
const notes = truncate(entry.notes, NOTES_EXCERPT_MAX);
|
|
168
|
+
if (notes) lines.push(` notes: ${notes}`);
|
|
169
|
+
if (Array.isArray(entry.remaining) && entry.remaining.length) {
|
|
170
|
+
lines.push(' remaining:');
|
|
171
|
+
for (const item of entry.remaining.slice(0, REMAINING_ITEMS_MAX)) {
|
|
172
|
+
const text = truncate(item, REMAINING_ITEM_MAX);
|
|
173
|
+
if (text) lines.push(` • ${text}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
lines.push(
|
|
177
|
+
source === 'cloud'
|
|
178
|
+
? ' (This is a cloud WIP preview from /api/wip — trust it over reconstructing state from scratch.)'
|
|
179
|
+
: ' (This is a disk preview — the vault file on disk is the source of truth; ' +
|
|
180
|
+
'do not reconstruct state by re-reading every child spec + git diff.)',
|
|
181
|
+
);
|
|
182
|
+
return lines.join('\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Cloud-reliance SessionStart preview: `GET {console}/api/wip` (bearer-gated)
|
|
187
|
+
* returns `{ wip_state: { "<SPEC-ID>": { sessionId, agent, updatedAt, notes,
|
|
188
|
+
* remaining, filesTouched, ... } } }` — the same shape `heyspex_record_wip`
|
|
189
|
+
* writes. Only consulted when `resolveDevMode()` reports cloud reliance; falls
|
|
190
|
+
* back to disk on ANY failure (unreachable console, bad token, malformed
|
|
191
|
+
* body) and never throws — this hook must stay fail-open and silent.
|
|
192
|
+
*/
|
|
193
|
+
async function fetchCloudLedgerEntry(root) {
|
|
194
|
+
let mode;
|
|
195
|
+
try {
|
|
196
|
+
mode = resolveDevMode({});
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
if (mode.reliance !== 'cloud') return null;
|
|
201
|
+
|
|
202
|
+
let consoleUrl;
|
|
203
|
+
try {
|
|
204
|
+
consoleUrl = resolveConsoleUrlShared({
|
|
205
|
+
repoRoot: root,
|
|
206
|
+
devMode: { reliance: mode.reliance, consoleUrl: mode.consoleUrl },
|
|
207
|
+
});
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const token = readCredentialsMcpToken();
|
|
213
|
+
const controller = new AbortController();
|
|
214
|
+
const timer = setTimeout(() => controller.abort(), WIP_FETCH_TIMEOUT_MS);
|
|
215
|
+
timer.unref?.();
|
|
216
|
+
try {
|
|
217
|
+
const res = await fetch(`${consoleUrl}/api/wip`, {
|
|
218
|
+
// Repo-bound vault id (P1): without it the console refuses a bearer
|
|
219
|
+
// caller (409 vault-scope-required) and this preview silently fell back
|
|
220
|
+
// to a stale disk ledger (diag HS-5M0G).
|
|
221
|
+
headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), ...hookVaultHeaders() },
|
|
222
|
+
signal: controller.signal,
|
|
223
|
+
});
|
|
224
|
+
if (!res.ok) return null;
|
|
225
|
+
const body = await res.json();
|
|
226
|
+
const wipState = body?.wip_state;
|
|
227
|
+
if (!wipState || typeof wipState !== 'object') return null;
|
|
228
|
+
let best = null;
|
|
229
|
+
for (const [specId, entry] of Object.entries(wipState)) {
|
|
230
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
231
|
+
const updatedAt = typeof entry.updatedAt === 'string' ? entry.updatedAt : '';
|
|
232
|
+
if (!best || updatedAt > best.updatedAt) {
|
|
233
|
+
best = { epicId: specId, updatedAt, entry };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return best;
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
} finally {
|
|
240
|
+
clearTimeout(timer);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const BEFORE_REMINDER =
|
|
245
|
+
'Session start. Before you claim or continue work, load the prior handoff: ' +
|
|
246
|
+
'call `heyspex_handoff_load({})` for your Neon lane context, and read the ' +
|
|
247
|
+
'`/hs-next` WIP block (file ledger at `.heyspex/wip-state.json`). Trust the ' +
|
|
248
|
+
'structured handoff over reconstructing state from scratch.';
|
|
249
|
+
|
|
250
|
+
const COMPACT_REMINDER =
|
|
251
|
+
'Context was just compacted mid-session. This is a ROLLOVER, not a fresh ' +
|
|
252
|
+
'start — work is continuing. Re-orient before your next action: run ' +
|
|
253
|
+
'`heyspex_handoff_load({})` and re-read the WIP ledger below. Do NOT treat ' +
|
|
254
|
+
'this as session close — do not flip status or write an HS-REV; the work is ' +
|
|
255
|
+
'not done.';
|
|
256
|
+
|
|
257
|
+
const PRECOMPACT_REMINDER =
|
|
258
|
+
'⚠️ Context is about to be compacted. CHECKPOINT NOW while the full context ' +
|
|
259
|
+
'is still present, if you are mid-implementation: (1) `heyspex_record_wip` ' +
|
|
260
|
+
'for the worked epic (filesTouched / untrackedArtifacts / specsFiled / ' +
|
|
261
|
+
'verified / remaining / notes), and (2) `heyspex_handoff_record` to the ' +
|
|
262
|
+
'self-purging Neon lane. This is a context-rollover CHECKPOINT, not session ' +
|
|
263
|
+
'close — do NOT flip status or write an HS-REV; work is continuing.';
|
|
264
|
+
|
|
265
|
+
async function main() {
|
|
266
|
+
if (process.env.HEYSPEX_WIP_CHECKPOINT_OFF === '1') return;
|
|
267
|
+
|
|
268
|
+
const raw = await readStdinJson();
|
|
269
|
+
if (!raw) return;
|
|
270
|
+
let payload;
|
|
271
|
+
try {
|
|
272
|
+
payload = JSON.parse(raw);
|
|
273
|
+
} catch {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const event = payload?.hook_event_name;
|
|
278
|
+
if (!event) return;
|
|
279
|
+
|
|
280
|
+
if (event === 'SessionStart') {
|
|
281
|
+
const source = payload?.source;
|
|
282
|
+
let best = await fetchCloudLedgerEntry(repoRoot());
|
|
283
|
+
let ledgerSource = 'cloud';
|
|
284
|
+
if (!best) {
|
|
285
|
+
best = mostRecentLedgerEntry();
|
|
286
|
+
ledgerSource = 'disk';
|
|
287
|
+
}
|
|
288
|
+
const preview = ledgerPreview(best, ledgerSource);
|
|
289
|
+
const head = source === 'compact' ? COMPACT_REMINDER : BEFORE_REMINDER;
|
|
290
|
+
emit(event, preview ? `${head}\n\n${preview}` : head);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (event === 'PreCompact') {
|
|
295
|
+
emit(event, PRECOMPACT_REMINDER);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
main().catch(() => {
|
|
301
|
+
// Never break Claude Code on a hook failure.
|
|
302
|
+
});
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// manifest.mjs — the Cursor registration for every HeySpex lifecycle hook.
|
|
2
|
+
//
|
|
3
|
+
// SSOT for three consumers: install-cursor-hooks.mjs (writes `.cursor/hooks.json`),
|
|
4
|
+
// heyspex-doctor.mjs (the `cursor-hooks` check), and _run.smoke.mjs (asserts the
|
|
5
|
+
// manifest stays in parity with `.claude/settings.json`).
|
|
6
|
+
//
|
|
7
|
+
// HOW THE MAPPING WAS DERIVED
|
|
8
|
+
//
|
|
9
|
+
// Claude Code splits the tool lifecycle by PHASE (PreToolUse / PostToolUse) and
|
|
10
|
+
// selects with a regex over tool names. Cursor splits by CHANNEL — shell, MCP,
|
|
11
|
+
// file read, file edit, subagent each get their own event — and its `matcher`
|
|
12
|
+
// only understands tool KINDS (`Write`, `Shell`, `Read`, `Grep`, `Delete`,
|
|
13
|
+
// `Task`, `MCP:<name>`). So one Claude row can become a row on a different
|
|
14
|
+
// event here, and a Claude matcher like `mcp__heyspex__heyspex_claim_spec` moves
|
|
15
|
+
// into the shim's own `tool` filter.
|
|
16
|
+
//
|
|
17
|
+
// Two rules keep this honest:
|
|
18
|
+
// 1. NO DOUBLE-FIRE. A hook is registered on exactly one channel per phase.
|
|
19
|
+
// Shell-facing gates go on `beforeShellExecution` (the only event that
|
|
20
|
+
// reliably carries the command text) and are therefore NOT also on
|
|
21
|
+
// `preToolUse`, whose registrations are scoped to `Write|Delete`.
|
|
22
|
+
// 2. NO SILENT DOWNGRADE. A Claude hard gate maps to an event where Cursor
|
|
23
|
+
// honors a `permission` verdict, or it is listed in UNPORTABLE with the
|
|
24
|
+
// rule that covers it instead. A gate that can only "advise" in Cursor is
|
|
25
|
+
// not parity, and pretending otherwise is worse than a documented gap.
|
|
26
|
+
//
|
|
27
|
+
// `matcher` is Cursor-side (cheap, evaluated before the process spawns);
|
|
28
|
+
// `tool` becomes `--hs-tool=<regex>` and is evaluated by the shim after it
|
|
29
|
+
// parses the payload. Prefer `matcher`; use `tool` only for what it can't say.
|
|
30
|
+
|
|
31
|
+
export const SHIM = 'scripts/cursor-hooks/_run.mjs';
|
|
32
|
+
export const DEFAULT_TIMEOUT_SECONDS = 5;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {object} HookEntry
|
|
36
|
+
* @property {string} event Cursor hook event name (camelCase).
|
|
37
|
+
* @property {string} hook Hook name resolved under scripts/claude-hooks/,
|
|
38
|
+
* or a repo-relative path for a hook that lives
|
|
39
|
+
* elsewhere.
|
|
40
|
+
* @property {string[]} [args] Extra args forwarded to the hook verbatim.
|
|
41
|
+
* @property {string} [matcher] Cursor-side matcher.
|
|
42
|
+
* @property {string} [tool] Shim-side tool-name regex (`--hs-tool=`).
|
|
43
|
+
* @property {number} [timeout] Seconds; defaults to DEFAULT_TIMEOUT_SECONDS.
|
|
44
|
+
* @property {string} [claudeEvent] Override the Claude event name the hook is
|
|
45
|
+
* told it is running under, when the Cursor event
|
|
46
|
+
* maps ambiguously.
|
|
47
|
+
* @property {string} why One line: what this registration buys.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/** @type {HookEntry[]} */
|
|
51
|
+
export const HOOKS = [
|
|
52
|
+
// ── Session start ───────────────────────────────────────────────────────
|
|
53
|
+
// Cursor's sessionStart honors `additional_context`, so these injections
|
|
54
|
+
// land in context exactly as they do in Claude Code.
|
|
55
|
+
{ event: 'sessionStart', hook: 'wip-checkpoint', why: 'Surface the previous session WIP handoff before work resumes.' },
|
|
56
|
+
{ event: 'sessionStart', hook: 'inject-dev-server-awareness', why: 'Once-per-session reminder that dev-server status is a tool call, not a guess.' },
|
|
57
|
+
{ event: 'sessionStart', hook: 'inject-coo-seat', why: 'Inject the dev-factory COO seat mandate when the COO window is open.' },
|
|
58
|
+
{ event: 'sessionStart', hook: 'inject-doctrine', why: 'Inject the baseline SpexCode Doctrine edition at session start.' },
|
|
59
|
+
{ event: 'sessionStart', hook: 'coo-guard-check', why: 'Warn at session start when a breakdown has not been delegated.' },
|
|
60
|
+
{ event: 'sessionStart', hook: 'check-auto-memory-budget', why: 'Self-heal and budget-check the auto-memory index.' },
|
|
61
|
+
{ event: 'sessionStart', hook: 'check-claude-md-budget', why: 'Advisory budget check on the always-loaded agent contract.' },
|
|
62
|
+
{ event: 'sessionStart', hook: 'check-mcp-freshness-hook', why: 'Warn when the running MCP server is stale versus vendor.' },
|
|
63
|
+
{ event: 'sessionStart', hook: 'git-reflog-audit', why: 'Session-start git audit (stash/reset/rebase witness).' },
|
|
64
|
+
{ event: 'sessionStart', hook: 'vault-mirror-catchup', why: 'Cloud write mode: detached hs pull --apply closes a stale disk mirror (HS-XJFQ).' },
|
|
65
|
+
|
|
66
|
+
// ── Compaction ──────────────────────────────────────────────────────────
|
|
67
|
+
// Claude re-runs SessionStart(source: compact) after compaction; Cursor fires
|
|
68
|
+
// preCompact BEFORE it, so the shim relays this one as SessionStart(compact)
|
|
69
|
+
// to keep wip-checkpoint's "rollover, not fresh start" framing.
|
|
70
|
+
{ event: 'preCompact', hook: 'wip-checkpoint', claudeEvent: 'SessionStart', why: 'Carry the WIP handoff across a context compaction.' },
|
|
71
|
+
|
|
72
|
+
// ── Every user turn ─────────────────────────────────────────────────────
|
|
73
|
+
// beforeSubmitPrompt is Cursor's UserPromptSubmit. It honors `user_message`
|
|
74
|
+
// but not `additional_context`, so these three surface in the transcript
|
|
75
|
+
// rather than as invisible context — the routing still reaches the agent,
|
|
76
|
+
// and the user can see why it fired.
|
|
77
|
+
{ event: 'beforeSubmitPrompt', hook: 'preflight-reminder', why: 'Name the required procedure before a filing/destructive/protocol turn.' },
|
|
78
|
+
{ event: 'beforeSubmitPrompt', hook: 'intent-classifier-trigger', why: 'Route capture / research / settings / defer intents to the right skill.' },
|
|
79
|
+
{ event: 'beforeSubmitPrompt', hook: 'inject-communication-style', why: 'Re-assert the plain-language communication rules each turn.' },
|
|
80
|
+
{ event: 'beforeSubmitPrompt', hook: 'inject-vault-context', timeout: 10, why: 'Lexical vault retrieval for the prompt (surfaced as a user message).' },
|
|
81
|
+
|
|
82
|
+
// ── Before every tool call (any tool) ───────────────────────────────────
|
|
83
|
+
{ event: 'preToolUse', matcher: '.*', hook: 'emit-claim-status', args: ['--event=PreToolUse'], why: 'Claim liveness heartbeat to the console.' },
|
|
84
|
+
{ event: 'preToolUse', matcher: '.*', hook: 'coo-denylist-gate', why: 'Hard deny the COO-window deny list.' },
|
|
85
|
+
{ event: 'preToolUse', matcher: '.*', hook: 'log-session-activity', args: ['--event=PreToolUse'], why: 'Append agent.tool_called rows to the session log.' },
|
|
86
|
+
|
|
87
|
+
// ── Before a file write / delete ─────────────────────────────────────────
|
|
88
|
+
// Cursor's `Write` covers create AND patch (Claude's Write|Edit|MultiEdit);
|
|
89
|
+
// the shim re-derives which one from the input shape. `Delete` is included on
|
|
90
|
+
// the vault guard because removing a profile file is the loss it exists to
|
|
91
|
+
// prevent, and Claude had no separate delete tool to list.
|
|
92
|
+
{ event: 'preToolUse', matcher: 'Write|Delete', hook: 'env-vault-guard', why: 'Block destructive writes to the encrypted env vault (ALWAYS ON).' },
|
|
93
|
+
{ event: 'preToolUse', matcher: 'Write', hook: 'vault-write-tier-guard', why: 'Deny disk writes to vault specs in cloud write mode (HS-FCNH).' },
|
|
94
|
+
{ event: 'preToolUse', matcher: 'Write', hook: 'settings-write-gate', why: 'Require approval before .heyspex/config.yaml changes.' },
|
|
95
|
+
{ event: 'preToolUse', matcher: 'Write', hook: 'enforce-vault-naming', why: 'Block vault filenames that violate the naming registry.' },
|
|
96
|
+
{ event: 'preToolUse', matcher: 'Write', hook: 'guard-auto-memory-writes', why: 'Block new topic files in the auto-memory directory.' },
|
|
97
|
+
{ event: 'preToolUse', matcher: 'Write', hook: 'preflight-reminder', why: 'Preflight reminder on the edit path itself.' },
|
|
98
|
+
{ event: 'preToolUse', matcher: 'Write', hook: 'claim-before-code-gate', why: 'Deny the first code edit after plan exit with no claim.' },
|
|
99
|
+
{ event: 'preToolUse', matcher: 'Write', hook: 'directive-obligation-gate', why: 'Deny product-code writes while a capture_on_idea obligation is unmet, and (claim-before-build) with no claim/capture (HS-20SW).' },
|
|
100
|
+
|
|
101
|
+
// ── Before a shell command ──────────────────────────────────────────────
|
|
102
|
+
// beforeShellExecution, not preToolUse/Shell: it is the only event guaranteed
|
|
103
|
+
// to carry the command string, and Cursor honors allow/deny/ask on it.
|
|
104
|
+
{ event: 'beforeShellExecution', hook: 'env-vault-guard', why: 'Block shell writes/exfiltration targeting the env vault (ALWAYS ON).' },
|
|
105
|
+
{ event: 'beforeShellExecution', hook: 'vault-write-tier-guard', why: 'Deny shell writes to vault specs in cloud write mode (HS-FCNH).' },
|
|
106
|
+
{ event: 'beforeShellExecution', hook: 'settings-write-gate', why: 'Catch shell writes to .heyspex/config.yaml.' },
|
|
107
|
+
{ event: 'beforeShellExecution', hook: 'enforce-vault-naming', why: 'Catch vault files created via the shell.' },
|
|
108
|
+
{ event: 'beforeShellExecution', hook: 'guard-auto-memory-writes', why: 'Catch auto-memory topic files created via the shell.' },
|
|
109
|
+
{ event: 'beforeShellExecution', hook: 'preflight-reminder', why: 'Preflight reminder on rm -rf / force-push shaped commands.' },
|
|
110
|
+
{ event: 'beforeShellExecution', hook: 'dev-server-task-reminder', timeout: 10, why: 'Dev-server start/stop hard gate plus DB-safety reminder.' },
|
|
111
|
+
{ event: 'beforeShellExecution', hook: 'git-stash-gate', timeout: 10, why: 'Always ask before a mutating git stash.' },
|
|
112
|
+
{ event: 'beforeShellExecution', hook: 'git-identity-guard', why: 'Agents never write git identity (owner ruling 2026-09-19, ALWAYS ON).' },
|
|
113
|
+
{ event: 'beforeShellExecution', hook: 'capture-execution-gate', why: 'Block dev-factory research/paid-model/routing shell commands past an ungrounded capture (HS-KNJ8, platform).' },
|
|
114
|
+
{ event: 'beforeShellExecution', hook: 'directive-obligation-gate', why: 'Deny build commands and product-code shell writes while a capture obligation is unmet (HS-20SW).' },
|
|
115
|
+
|
|
116
|
+
// ── Before specific MCP tools ───────────────────────────────────────────
|
|
117
|
+
// Cursor fires preToolUse for MCP tools too (matcher `MCP:<name>`), so these
|
|
118
|
+
// stay on preToolUse rather than beforeMCPExecution — registering both would
|
|
119
|
+
// fire each gate twice. Scoping is the shim's `tool` regex because a Cursor
|
|
120
|
+
// matcher cannot name one MCP tool per entry portably.
|
|
121
|
+
{ event: 'preToolUse', matcher: '.*', tool: 'heyspex_claim_spec$', hook: 'coo-guard-check', why: 'Block a claim whose breakdown was never delegated.' },
|
|
122
|
+
{ event: 'preToolUse', matcher: '.*', tool: 'heyspex_file_spec$', hook: 'capture-workflow-gate', timeout: 8, why: 'Deny filing an implementation spec straight out of an open capture.' },
|
|
123
|
+
{ event: 'preToolUse', matcher: '.*', tool: 'heyspex_settings_set$', hook: 'settings-write-gate', why: 'Require approval for settings changes made through MCP.' },
|
|
124
|
+
{ event: 'preToolUse', matcher: '.*', tool: '[Nn]eon__(prepare_database_migration|complete_database_migration|reset_from_parent|delete_branch)$', hook: 'dev-server-task-reminder', timeout: 10, why: 'DB-safety gate on Neon migration/reset/delete tools.' },
|
|
125
|
+
{ event: 'preToolUse', matcher: '.*', tool: 'heyspex_file_spec$', hook: 'diagnose-capture-reminder', why: 'Warn when a diagnose is filed with no capture holding the owner words (HS-IK2D).' },
|
|
126
|
+
{ event: 'preToolUse', matcher: '.*', tool: '^(Workflow|Skill)$', hook: 'research-method-guard', why: 'Research method guard on Skill/Workflow launches (HS-EN4F, ALWAYS ON).' },
|
|
127
|
+
{ event: 'preToolUse', matcher: '.*', tool: '^(Workflow|Skill)$', hook: 'deep-research-cost-gate', why: 'Cost confirmation before a paid deep-research harness launch.' },
|
|
128
|
+
{ event: 'preToolUse', matcher: '.*', tool: '^(WebSearch|WebFetch|web_search|web_fetch)$|heyspex_save_research$', hook: 'research-protocol-gate', why: 'WebSearch/WebFetch/save_research refused until the research preflight ran (HS-K0ZP, HS-EN4F).' },
|
|
129
|
+
|
|
130
|
+
// ── Before a subagent spawn ─────────────────────────────────────────────
|
|
131
|
+
// subagentStart over preToolUse/Task: it carries subagent_type and the model,
|
|
132
|
+
// which is exactly what the routing gate decides on, and it can deny.
|
|
133
|
+
{ event: 'subagentStart', hook: 'agent-spawn-delegation-gate', why: 'Block a build/breakdown/critic subagent on an off-routing model.' },
|
|
134
|
+
{ event: 'subagentStart', hook: 'capture-execution-gate', why: 'Block a dev-factory subagent spawn past an ungrounded capture (HS-KNJ8, platform).' },
|
|
135
|
+
{ event: 'subagentStart', hook: 'directive-obligation-gate', why: 'Deny a build subagent while a capture obligation is unmet or the session has no claim/capture (HS-20SW).' },
|
|
136
|
+
|
|
137
|
+
// ── After every tool call ───────────────────────────────────────────────
|
|
138
|
+
{ event: 'postToolUse', matcher: '.*', hook: 'emit-claim-status', args: ['--event=PostToolUse'], why: 'Claim liveness heartbeat after the call resolves.' },
|
|
139
|
+
{ event: 'postToolUse', matcher: '.*', hook: 'log-session-activity', args: ['--event=PostToolUse'], why: 'Append the tool result row to the session log.' },
|
|
140
|
+
{ event: 'postToolUse', matcher: '.*', hook: 'directive-obligation-gate', why: 'Discharge the capture obligation on a capture write; track claim acquire/release (HS-20SW).' },
|
|
141
|
+
{ event: 'postToolUse', matcher: '.*', hook: 'close-terminal-on-ship', why: 'Close the Launch terminal once the spec ships.' },
|
|
142
|
+
{ event: 'postToolUse', matcher: 'Write', hook: 'notify-status-flip', why: 'Sound on an in_review status flip.' },
|
|
143
|
+
{ event: 'postToolUse', matcher: 'Write', hook: 'scripts/stamp-provenance-on-edit.mjs', why: 'Stamp assisted_by provenance on direct spec edits.' },
|
|
144
|
+
|
|
145
|
+
// afterFileEdit is a distinct signal, not a duplicate: it reports the edit
|
|
146
|
+
// that actually landed (with old/new strings) even when the write came from a
|
|
147
|
+
// path postToolUse did not describe as an edit.
|
|
148
|
+
{ event: 'afterFileEdit', matcher: '.*', hook: 'close-terminal-on-ship', why: 'Ship detection from the concrete file edit.' },
|
|
149
|
+
|
|
150
|
+
// ── End of the agent turn ───────────────────────────────────────────────
|
|
151
|
+
// Cursor's `stop` honors `followup_message`, which is how a blocking Stop
|
|
152
|
+
// hook re-drives the agent — the same effect as Claude's exit-2 block.
|
|
153
|
+
{ event: 'stop', hook: 'emit-claim-status', args: ['--event=Stop'], why: 'Mark the claim pending-on-response.' },
|
|
154
|
+
{ event: 'stop', hook: 'naughty-list-gate', why: 'Re-drive the turn when the final message uses banned jargon.' },
|
|
155
|
+
{ event: 'stop', hook: 'id-display-format-gate', why: 'Re-drive the turn when a reply breaks the Naming V3 display-id format.' },
|
|
156
|
+
{ event: 'stop', hook: 'epic-milestone-phrasing-gate', why: 'Re-drive the turn when a reply breaks the epic/milestone phrasing rules.' },
|
|
157
|
+
{ event: 'stop', hook: 'git-reflog-audit', why: 'Session-close git audit backstop (stash/reset/rebase witness).' },
|
|
158
|
+
{ event: 'stop', hook: 'enforce-session-sequence', why: 'Re-drive the turn when work shipped without record_wip.' },
|
|
159
|
+
{ event: 'stop', hook: 'directive-obligation-gate', why: 'Re-drive the turn ONCE (followup_message) when a capture obligation is still unmet (HS-20SW).' },
|
|
160
|
+
|
|
161
|
+
// ── Session end ─────────────────────────────────────────────────────────
|
|
162
|
+
{ event: 'sessionEnd', hook: 'session-end', why: 'Release the claim and notify the console.' },
|
|
163
|
+
{ event: 'sessionEnd', hook: 'close-terminal-on-ship', why: 'Final terminal cleanup.' },
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Claude registrations with no Cursor trigger. Kept in code, not in a comment,
|
|
168
|
+
* so the doctor can report the gap and the smoke test can assert the list has
|
|
169
|
+
* not quietly grown.
|
|
170
|
+
*/
|
|
171
|
+
export const UNPORTABLE = [
|
|
172
|
+
{
|
|
173
|
+
hook: 'plan-review-gate',
|
|
174
|
+
claudeEvent: 'PreToolUse/ExitPlanMode',
|
|
175
|
+
reason: 'Cursor exposes no ExitPlanMode tool call to hooks.',
|
|
176
|
+
coveredBy: '.cursor/rules/plan-review-required.mdc (always-on rule)',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
hook: 'plan-exit-ingest-gate',
|
|
180
|
+
claudeEvent: 'PreToolUse/ExitPlanMode',
|
|
181
|
+
reason: 'Same missing trigger; it is also what arms claim-before-code-gate.',
|
|
182
|
+
coveredBy: '.cursor/rules/plan-review-required.mdc + spec-creation-linkage.mdc',
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Hooks that ARE registered above but run weaker than on Claude Code. Kept in code (like UNPORTABLE)
|
|
188
|
+
* so scripts/lib/hook-dispositions.test.mjs can hold the three-host table to it.
|
|
189
|
+
*/
|
|
190
|
+
export const DEGRADED = [
|
|
191
|
+
{
|
|
192
|
+
hook: 'preflight-reminder',
|
|
193
|
+
reason: 'beforeSubmitPrompt honors user_message only: the reminder surfaces in the transcript, not as invisible context.',
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
hook: 'intent-classifier-trigger',
|
|
197
|
+
reason: 'Same channel limit; the routing directive reaches the agent as a visible user message.',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
hook: 'inject-communication-style',
|
|
201
|
+
reason: 'Same channel limit; re-asserted each turn as a visible user message.',
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
hook: 'inject-vault-context',
|
|
205
|
+
reason: 'Same channel limit; retrieved vault context appears as a visible user message.',
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
hook: 'claim-before-code-gate',
|
|
209
|
+
reason: 'Armed by plan-exit-ingest-gate, which has no Cursor trigger (UNPORTABLE), so it stays dormant.',
|
|
210
|
+
},
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The command string Cursor runs for one entry.
|
|
215
|
+
*
|
|
216
|
+
* The tool filter is a REGEX, and its metacharacters are also shell
|
|
217
|
+
* metacharacters — `[Nn]eon__(a|b)$` unquoted is a syntax error in bash, not a
|
|
218
|
+
* pattern. It is therefore always double-quoted. A trailing `$` inside double
|
|
219
|
+
* quotes is literal in both bash and PowerShell, and the patterns contain no
|
|
220
|
+
* whitespace, so quoting is sufficient without any escaping.
|
|
221
|
+
*/
|
|
222
|
+
export function commandFor(entry) {
|
|
223
|
+
const parts = ['node', SHIM, entry.hook];
|
|
224
|
+
if (entry.tool) parts.push(`--hs-tool="${entry.tool}"`);
|
|
225
|
+
if (entry.claudeEvent) parts.push(`--hs-claude-event=${entry.claudeEvent}`);
|
|
226
|
+
for (const arg of entry.args ?? []) parts.push(arg);
|
|
227
|
+
return parts.join(' ');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Same command, addressed from a PLUGIN install.
|
|
232
|
+
*
|
|
233
|
+
* A project hook runs with the repo as its working directory, so a relative
|
|
234
|
+
* path resolves. A plugin hook runs from the plugin's own cache directory,
|
|
235
|
+
* where `scripts/cursor-hooks/_run.mjs` does not exist — the shim and the hook
|
|
236
|
+
* bodies live in the host vault checkout. `$CURSOR_PROJECT_DIR` is how the
|
|
237
|
+
* plugin reaches back into it (the same reason the plugin's MCP entry goes
|
|
238
|
+
* through bin/launch-mcp.mjs instead of naming cli.mjs directly).
|
|
239
|
+
*/
|
|
240
|
+
export function commandForPlugin(entry) {
|
|
241
|
+
return commandFor(entry).replace(`node ${SHIM}`, `node "$CURSOR_PROJECT_DIR/${SHIM}"`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The committed launcher a CUSTOMER repo carries for Cursor, repo-relative.
|
|
246
|
+
* Written by `hs install` / `scripts/install-cursor-hooks.mjs` from
|
|
247
|
+
* `renderCustomerLauncher('cursor')` in scripts/lib/customer-kit.mjs; it finds
|
|
248
|
+
* the `heyspex` package (node_modules/heyspex) and hands off to this shim.
|
|
249
|
+
*/
|
|
250
|
+
export const CUSTOMER_LAUNCHER = '.cursor/heyspex/run-hook.mjs';
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Same command, addressed from a CUSTOMER repo: the committed launcher by
|
|
254
|
+
* `$CURSOR_PROJECT_DIR` — never this machine's path. A customer repo has no
|
|
255
|
+
* `scripts/cursor-hooks/` of its own, so the relative form failed there with
|
|
256
|
+
* MODULE_NOT_FOUND ([[HS-EN4F-F4V2-DIAG-a-repo-set-up-with-hs-install-gets-no]]);
|
|
257
|
+
* the 2026-10-02 fix wrote the platform checkout's ABSOLUTE path, which the
|
|
258
|
+
* customer committed and which existed on no other machine
|
|
259
|
+
* ([[HS-0AF8-WW9R-TASK-portable-hook-and-command-paths-in]]).
|
|
260
|
+
*/
|
|
261
|
+
export function commandForCustomer(entry) {
|
|
262
|
+
return commandFor(entry).replace(`node ${SHIM}`, `node "$CURSOR_PROJECT_DIR/${CUSTOMER_LAUNCHER}"`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Identity of an entry inside `.cursor/hooks.json` — what "already installed"
|
|
266
|
+
* means, and what a repair replaces. Two entries for the same hook on the same
|
|
267
|
+
* event are distinguished by their scoping, so the tool filter is part of it. */
|
|
268
|
+
export function identityFor(entry) {
|
|
269
|
+
return `${entry.hook}::${entry.tool ?? ''}::${(entry.args ?? []).join(' ')}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function timeoutFor(entry) {
|
|
273
|
+
return entry.timeout ?? DEFAULT_TIMEOUT_SECONDS;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Every Cursor event this manifest registers, in a stable order. */
|
|
277
|
+
export function events() {
|
|
278
|
+
const seen = [];
|
|
279
|
+
for (const entry of HOOKS) if (!seen.includes(entry.event)) seen.push(entry.event);
|
|
280
|
+
return seen;
|
|
281
|
+
}
|