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,265 @@
|
|
|
1
|
+
// spexharness/src/gates/preflight-reminder-gate.mjs
|
|
2
|
+
//
|
|
3
|
+
// Engine port of scripts/claude-hooks/preflight-reminder.mjs's UserPromptSubmit half — the
|
|
4
|
+
// registry-driven filing-intent prime ([[HS-EPIC-ZGWR]] AC#6: "file a milestone" / "create
|
|
5
|
+
// an epic" → the spec-creation-via-minter rule's inject) and the grounding-before-proposing
|
|
6
|
+
// nudge ([[HS-RES-2QX6]] / [[HS-EPIC-PLRV]]: a design/planning request → "run /hs-ground
|
|
7
|
+
// first"). Task [[HS-N01T-8CMW-TASK-prompt-submit-injects-reach-the-model]], epic
|
|
8
|
+
// [[HS-LS51-8CMW-EPIC-spexcode-plugin-parity-the-prompt]]. See ./PORTING.md for the recipe.
|
|
9
|
+
//
|
|
10
|
+
// The hook's PreToolUse half (protocol-edit, CLAUDE.md-edit and destructive-command
|
|
11
|
+
// reminders) is NOT ported here — it is a `before_tool` concern with its own shape, and
|
|
12
|
+
// this task's scope is the prompt_submit event only.
|
|
13
|
+
//
|
|
14
|
+
// PORTING.md step 1 says "import the pure decision function; never a second copy". That
|
|
15
|
+
// rule bends here for a documented reason: the hook's `detectRegistryPrimeReminder` was
|
|
16
|
+
// written against scripts/claude-hooks/_guardrail-loader.mjs (its file-scanning config
|
|
17
|
+
// helpers and stderr-writing loader), and the engine must not import upward into the
|
|
18
|
+
// plugin's scripts. So the decision function LIVES HERE, against ../rule-table.mjs and the
|
|
19
|
+
// injected config reader ../intent-directive.mjs provides, and the hook script imports it
|
|
20
|
+
// from this module — one implementation, the plugin as the thin caller, which is the same
|
|
21
|
+
// relationship every other ported gate ends up in. The hook's own smoke keeps exercising it
|
|
22
|
+
// through the hook's re-export.
|
|
23
|
+
//
|
|
24
|
+
// Event mapping: UserPromptSubmit → `prompt_submit` (../events.mjs).
|
|
25
|
+
//
|
|
26
|
+
// Decision mapping: the hook's only outcomes are "say nothing" and "emit additionalContext",
|
|
27
|
+
// so: `allow` and `inject` — a PRIME injection, never a deny, never a rewrite.
|
|
28
|
+
//
|
|
29
|
+
// NOT GATED ON CLAUDE_AGENT. The hook exits silently unless `CLAUDE_AGENT=true` (so a human
|
|
30
|
+
// typing in an interactive Claude Code session is not nudged) and keeps that gate in its
|
|
31
|
+
// own `main()`. A SpexCode seat IS the agent, so the engine gate runs unconditionally; the
|
|
32
|
+
// two kill switches port unchanged through `ctx.env`:
|
|
33
|
+
// HEYSPEX_PREFLIGHT_REMINDER_OFF=1 — the whole gate is silent.
|
|
34
|
+
// HEYSPEX_GROUNDING_NUDGE_OFF=1 — the grounding nudge alone is silent.
|
|
35
|
+
//
|
|
36
|
+
// OWNED ELSEWHERE — the one deliberate difference from the hook's sweep. The hook excludes
|
|
37
|
+
// only `research-before-decision` from the userpromptsubmit `prime` rules it will fire,
|
|
38
|
+
// because on the plugin every OTHER intent rule is ALSO fired by the (CLAUDE_AGENT-gated)
|
|
39
|
+
// hook — a latent double-injection that never shows because the gate is unset
|
|
40
|
+
// interactively. Ungated in the engine that would fire capture-on-idea's raw inject (with
|
|
41
|
+
// a literal `{{CONFIRM_MODE}}`) next to ../intent-classifier.mjs's finished directive, and
|
|
42
|
+
// would fire `communication-style-prime` (an unconditional `[\s\S]` rule, 3.7 KB) on every
|
|
43
|
+
// prompt. So the ENGINE default excludes every rule ../intent-classifier.mjs owns on this
|
|
44
|
+
// same event plus communication-style-prime (its own always-on injection is a separate
|
|
45
|
+
// concern — see intent-classifier.mjs's CLASSIFIER_RULE_IDS note). The hook passes its own
|
|
46
|
+
// narrower set explicitly (`PRIME_RULES_OWNED_ELSEWHERE`), so its emitted bytes are
|
|
47
|
+
// unchanged.
|
|
48
|
+
//
|
|
49
|
+
// `ctx` contract (all optional; every field defaults to a real read):
|
|
50
|
+
// ctx.repoRoot / ctx.vaultRoot - the app root holding `.heyspex/guardrails.json`. Defaults
|
|
51
|
+
// to the event's `cwd`, then CLAUDE_PROJECT_DIR, then this repo's root.
|
|
52
|
+
// When given it is a self-contained fixture root and each rule's
|
|
53
|
+
// config toggle is read from it too.
|
|
54
|
+
// ctx.configRoot - the directory holding `.heyspex/config.yaml` (the rules' configKey
|
|
55
|
+
// toggles); wins over ctx.repoRoot/ctx.vaultRoot for that read. With
|
|
56
|
+
// none of the three, the VAULT root (../config-root.mjs
|
|
57
|
+
// `defaultConfigRoot()`) — never the event's cwd or CLAUDE_PROJECT_DIR,
|
|
58
|
+
// which under Claude Code is the app checkout, not where the settings
|
|
59
|
+
// file lives ([[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]]).
|
|
60
|
+
// ctx.ruleTable - an injected ../rule-table.mjs table (tests).
|
|
61
|
+
// ctx.getSetting - an injected settings reader (see ../intent-directive.mjs's header).
|
|
62
|
+
// ctx.env - env var overrides (tests use this instead of mutating process.env).
|
|
63
|
+
// ctx.ownedElsewhere - override the excluded rule-id set (the hook passes its own).
|
|
64
|
+
|
|
65
|
+
import path from 'node:path';
|
|
66
|
+
import { fileURLToPath } from 'node:url';
|
|
67
|
+
|
|
68
|
+
import { loadRuleTable } from '../rule-table.mjs';
|
|
69
|
+
import { CLASSIFIER_RULE_IDS } from '../intent-classifier.mjs';
|
|
70
|
+
import { containsInjectedContent, isRuleEnabled } from '../intent-directive.mjs';
|
|
71
|
+
import { defaultConfigRoot } from '../config-root.mjs';
|
|
72
|
+
|
|
73
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
74
|
+
const REPO_ROOT = path.resolve(HERE, '..', '..', '..');
|
|
75
|
+
|
|
76
|
+
/** This gate's id — unique across the registry. */
|
|
77
|
+
export const GATE_ID = 'preflight-reminder';
|
|
78
|
+
/** Runs at prompt submit, same as its UserPromptSubmit hook registration. */
|
|
79
|
+
export const EVENT = 'prompt_submit';
|
|
80
|
+
/** Caller-defined severity label; a prime injection, never a block. */
|
|
81
|
+
export const SEVERITY = 'prime';
|
|
82
|
+
|
|
83
|
+
/** The whole-gate kill switch (ported unchanged from the hook). */
|
|
84
|
+
export const OFF_ENV = 'HEYSPEX_PREFLIGHT_REMINDER_OFF';
|
|
85
|
+
/** The grounding-nudge-only kill switch (ported unchanged from the hook). */
|
|
86
|
+
export const GROUNDING_OFF_ENV = 'HEYSPEX_GROUNDING_NUDGE_OFF';
|
|
87
|
+
|
|
88
|
+
/** The registry surface the filing-intent prime rules are authored on. */
|
|
89
|
+
export const USERPROMPT_PRIME_SURFACE = 'userpromptsubmit';
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The hook's OWN exclusion set: research-before-decision has its own dedicated dispatch
|
|
93
|
+
* (the unified intent-classifier-trigger.mjs router) that does extra workflow
|
|
94
|
+
* orchestration — exclude it so it never double-injects. The hook passes this set.
|
|
95
|
+
*/
|
|
96
|
+
export const PRIME_RULES_OWNED_ELSEWHERE = new Set(['research-before-decision']);
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The ENGINE's exclusion set (the default here): the hook's set, plus every intent rule
|
|
100
|
+
* ../intent-classifier.mjs fires on this same event, plus the always-on
|
|
101
|
+
* communication-style prime. See the header for why.
|
|
102
|
+
*/
|
|
103
|
+
export const ENGINE_PRIME_RULES_OWNED_ELSEWHERE = new Set([
|
|
104
|
+
...PRIME_RULES_OWNED_ELSEWHERE,
|
|
105
|
+
...CLASSIFIER_RULE_IDS,
|
|
106
|
+
'communication-style-prime',
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
// Grounding-before-proposing nudge ([[HS-RES-2QX6]] / [[HS-EPIC-PLRV]]).
|
|
110
|
+
// Deterministic context injection — the research's PRIMARY lever (F1): surface
|
|
111
|
+
// prior decisions at the moment a design/planning request arrives, BEFORE the
|
|
112
|
+
// agent proposes an approach or runs an AskUserQuestion menu around its own
|
|
113
|
+
// abstraction (the HS-CAP-UIBF "three modes" failure). This is injected context,
|
|
114
|
+
// not a block; it only points the agent at /hs-ground (which now searches
|
|
115
|
+
// captures). Tuned for PRECISION over recall (a noisy nudge is worse than none —
|
|
116
|
+
// F12): fires on imperative design/build/plan requests, NOT on questions about
|
|
117
|
+
// existing work or on filing verbs (covered by the intent reminders above).
|
|
118
|
+
export const DESIGN_INTENT_PATTERNS = [
|
|
119
|
+
// A — design/architecture verb acting directly on a thing (verb + determiner).
|
|
120
|
+
/\b(?:design|architect|re-?design|re-?architect|rework|reimplement|propose|structure|plan|model)\s+(?:a|an|the|our|this|how|out)\b/i,
|
|
121
|
+
// B — build/finish a design-noun, EXCLUDING the filing nouns (epic/task/
|
|
122
|
+
// milestone) the intent reminders already handle.
|
|
123
|
+
/\b(?:build|implement|finish|create|introduce|spec|wire)\b[^.?!\n]{0,40}\b(?:feature|flow|system|capability|workflow|mechanism|surface|component|schema|gate|pipeline|integration|architecture|approach|modes?)\b/i,
|
|
124
|
+
// C — explicit approach-choosing questions.
|
|
125
|
+
/\b(?:how should (?:we|i)|what(?:'s| is) the best way to|which approach|what approach should)\b/i,
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
export const GROUNDING_REMINDER =
|
|
129
|
+
'Detected a design/planning request. Before you propose an approach or run an ' +
|
|
130
|
+
'AskUserQuestion menu to choose between options, run `/hs-ground` (it now ' +
|
|
131
|
+
'searches captures, epics, and research) to surface prior decisions and ' +
|
|
132
|
+
'existing vocabulary — the user is NOT the memory of record. If a related ' +
|
|
133
|
+
'capture/epic exists, reconcile with its terms instead of inventing your own. ' +
|
|
134
|
+
'Skip only for a trivial/mechanical change with no design choice. ' +
|
|
135
|
+
'(Mute: HEYSPEX_GROUNDING_NUDGE_OFF=1.)';
|
|
136
|
+
|
|
137
|
+
/** Compile a rule's string patterns to case-insensitive RegExps; an uncompilable
|
|
138
|
+
* pattern is skipped (fail-safe), never a throw. */
|
|
139
|
+
function compilePatterns(rule) {
|
|
140
|
+
const patterns = rule && Array.isArray(rule.patterns) ? rule.patterns : [];
|
|
141
|
+
const compiled = [];
|
|
142
|
+
for (const p of patterns) {
|
|
143
|
+
try {
|
|
144
|
+
compiled.push(new RegExp(p, 'i'));
|
|
145
|
+
} catch {
|
|
146
|
+
// corrupt pattern — skip it.
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return compiled;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Registry-driven filing-intent prime ([[HS-EPIC-ZGWR]] AC#6). Reads the userpromptsubmit
|
|
154
|
+
* `prime` rules from the engine rule table, matches the prompt against each rule's
|
|
155
|
+
* authored patterns, and returns the matched rules' inject text joined by a blank line.
|
|
156
|
+
* Honors each rule's config_key gate + ignore_injected_content, and skips rules owned by
|
|
157
|
+
* another gate/hook. Fail-safe: any error → null (no reminder, never throws).
|
|
158
|
+
*
|
|
159
|
+
* @param {string} prompt
|
|
160
|
+
* @param {string} [repoRoot] - where `.heyspex/guardrails.json` (the rule table) is read from.
|
|
161
|
+
* @param {{ ruleTable?: object, getSetting?: Function, ownedElsewhere?: Set<string>|string[],
|
|
162
|
+
* configRoot?: string }} [opts] - `configRoot` is where `.heyspex/config.yaml` (each rule's
|
|
163
|
+
* toggle) is read from; defaults to `repoRoot` so a self-contained fixture root — and the
|
|
164
|
+
* hook-parity call that passes neither — keeps working as one unit. `run()` passes the
|
|
165
|
+
* vault root explicitly.
|
|
166
|
+
* @returns {string|null}
|
|
167
|
+
*/
|
|
168
|
+
export function detectRegistryPrimeReminder(prompt, repoRoot = REPO_ROOT, opts = {}) {
|
|
169
|
+
if (typeof prompt !== 'string' || !prompt.trim()) return null;
|
|
170
|
+
const configRoot =
|
|
171
|
+
typeof opts.configRoot === 'string' && opts.configRoot.trim() ? opts.configRoot : repoRoot;
|
|
172
|
+
const owned = opts.ownedElsewhere instanceof Set
|
|
173
|
+
? opts.ownedElsewhere
|
|
174
|
+
: new Set(Array.isArray(opts.ownedElsewhere) ? opts.ownedElsewhere : ENGINE_PRIME_RULES_OWNED_ELSEWHERE);
|
|
175
|
+
let rules;
|
|
176
|
+
try {
|
|
177
|
+
const table = opts.ruleTable ?? loadRuleTable({ vaultRoot: repoRoot });
|
|
178
|
+
rules = (Array.isArray(table?.rules) ? table.rules : []).filter(
|
|
179
|
+
(r) => r && Array.isArray(r.surfaces) && r.surfaces.includes(USERPROMPT_PRIME_SURFACE) && r.severity === 'prime',
|
|
180
|
+
);
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const parts = [];
|
|
185
|
+
for (const rule of rules) {
|
|
186
|
+
if (!rule.inject || owned.has(rule.rule)) continue;
|
|
187
|
+
if (!isRuleEnabled(rule, configRoot, opts)) continue;
|
|
188
|
+
if (rule.ignoreInjectedContent && containsInjectedContent(prompt)) continue;
|
|
189
|
+
const patterns = compilePatterns(rule);
|
|
190
|
+
if (patterns.length && patterns.some((re) => re.test(prompt))) {
|
|
191
|
+
parts.push(rule.inject);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return parts.length ? parts.join('\n\n') : null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The grounding nudge: GROUNDING_REMINDER on a design/planning prompt, else null. */
|
|
198
|
+
export function detectDesignIntent(prompt) {
|
|
199
|
+
if (typeof prompt !== 'string' || !prompt.trim()) return null;
|
|
200
|
+
return DESIGN_INTENT_PATTERNS.some((re) => re.test(prompt)) ? GROUNDING_REMINDER : null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function resolveRoot(evt, ctx) {
|
|
204
|
+
for (const candidate of [ctx.repoRoot, ctx.vaultRoot, evt?.cwd]) {
|
|
205
|
+
if (typeof candidate === 'string' && candidate.trim()) return candidate.trim().replace(/[\\/]+$/, '');
|
|
206
|
+
}
|
|
207
|
+
const env = ctx.env ?? process.env;
|
|
208
|
+
const fromEnv = env.CLAUDE_PROJECT_DIR?.trim();
|
|
209
|
+
if (fromEnv) return fromEnv.replace(/[\\/]+$/, '');
|
|
210
|
+
return REPO_ROOT;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @param {{ point: string, prompt?: string, cwd?: string }} evt
|
|
215
|
+
* @param {{ repoRoot?: string, vaultRoot?: string, configRoot?: string, ruleTable?: object,
|
|
216
|
+
* getSetting?: Function, env?: Record<string, string | undefined>,
|
|
217
|
+
* ownedElsewhere?: Set<string>|string[] }} [ctx]
|
|
218
|
+
* @returns {{ decision: 'allow' }|{ decision: 'inject', reason: string,
|
|
219
|
+
* inject: { additionalContext: string, severity: string } }}
|
|
220
|
+
*/
|
|
221
|
+
export function run(evt, ctx = {}) {
|
|
222
|
+
const env = ctx.env ?? process.env;
|
|
223
|
+
if (String(env[OFF_ENV] ?? '') === '1') return { decision: 'allow' };
|
|
224
|
+
|
|
225
|
+
const prompt = evt?.prompt;
|
|
226
|
+
if (typeof prompt !== 'string' || !prompt.trim()) return { decision: 'allow' };
|
|
227
|
+
|
|
228
|
+
const root = resolveRoot(evt, ctx);
|
|
229
|
+
// Only a ctx-supplied root is a fixture; the event's cwd / CLAUDE_PROJECT_DIR are the live
|
|
230
|
+
// app checkout and never decide where the settings file is read from.
|
|
231
|
+
const fixtureRoot = [ctx.repoRoot, ctx.vaultRoot].some((c) => typeof c === 'string' && c.trim());
|
|
232
|
+
const configRoot = ctx.configRoot ?? (fixtureRoot ? root : defaultConfigRoot());
|
|
233
|
+
|
|
234
|
+
// Combined into ONE injection — the filing-intent reminder and the grounding nudge are
|
|
235
|
+
// independent and can co-fire, exactly as the hook joins them into one emit.
|
|
236
|
+
const parts = [];
|
|
237
|
+
const reasons = [];
|
|
238
|
+
const intentReminder = detectRegistryPrimeReminder(prompt, root, {
|
|
239
|
+
ruleTable: ctx.ruleTable,
|
|
240
|
+
getSetting: ctx.getSetting,
|
|
241
|
+
ownedElsewhere: ctx.ownedElsewhere,
|
|
242
|
+
configRoot,
|
|
243
|
+
});
|
|
244
|
+
if (intentReminder) {
|
|
245
|
+
parts.push(intentReminder);
|
|
246
|
+
reasons.push('filing intent');
|
|
247
|
+
}
|
|
248
|
+
if (String(env[GROUNDING_OFF_ENV] ?? '') !== '1') {
|
|
249
|
+
const grounding = detectDesignIntent(prompt);
|
|
250
|
+
if (grounding) {
|
|
251
|
+
parts.push(grounding);
|
|
252
|
+
reasons.push('design/planning request');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (!parts.length) return { decision: 'allow' };
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
decision: 'inject',
|
|
259
|
+
reason: `preflight-reminder: ${reasons.join(' + ')}`,
|
|
260
|
+
inject: { additionalContext: parts.join('\n\n'), severity: SEVERITY },
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const gate = { id: GATE_ID, event: EVENT, severity: SEVERITY, run };
|
|
265
|
+
export default gate;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// spexharness/src/gates/research-protocol-gate.mjs
|
|
2
|
+
//
|
|
3
|
+
// [[HS-K0ZP]] — the research-capture-protocol proof-of-use gate.
|
|
4
|
+
//
|
|
5
|
+
// THE HOLE THIS CLOSES: research-capture-protocol was enforced as prose. The
|
|
6
|
+
// only machine on the research path was ./deep-research-cost-gate.mjs, which
|
|
7
|
+
// sees Workflow/Skill launches and nothing else — its own protocol text named
|
|
8
|
+
// the bare WebSearch/WebFetch path as "out of reach". On 2026-09-11 an agent
|
|
9
|
+
// asked to verify one claim ran native WebSearch + WebFetch and then filed a
|
|
10
|
+
// full research doc through heyspex_save_research: no method triage (this was
|
|
11
|
+
// a known-target retrieval that should never have entered the research-doc
|
|
12
|
+
// pipeline), no ground-first, no offer-before-save. Nothing refused any of
|
|
13
|
+
// the three calls because nothing was looking at them.
|
|
14
|
+
//
|
|
15
|
+
// WHAT IT DECIDES: a `before_tool` event for WebSearch, WebFetch or
|
|
16
|
+
// heyspex_save_research (bare or MCP-prefixed) is DENIED unless the session
|
|
17
|
+
// holds a current preflight proof-of-use marker for one of
|
|
18
|
+
// RESEARCH_PROTOCOL_INTENTS (`research`, `file_research`, `promote_research`
|
|
19
|
+
// — vendor/heyspex-mcp/preflight-gate.mjs, the same store the MCP server's
|
|
20
|
+
// own PREFLIGHT_NOT_RUN refusals read). heyspex_preflight({ intent:
|
|
21
|
+
// "research" }) writes that marker; so does heyspex_get_procedure on the
|
|
22
|
+
// protocol itself (procedure-handlers.mjs). Every other tool is allowed —
|
|
23
|
+
// this gate never speaks for anything but those three names.
|
|
24
|
+
//
|
|
25
|
+
// WHY A DENY, NOT A PRIME: the owner's ruling on the incident, verbatim —
|
|
26
|
+
// "HeySpex plugin needs to make it impossible for you to break the rule.
|
|
27
|
+
// User permissions is the only way to break the rule." A prime is text the
|
|
28
|
+
// model can read and skip, which is exactly what failed. So: deny by default,
|
|
29
|
+
// the reason names the one call that unlocks it, and the ONLY bypass is the
|
|
30
|
+
// owner setting HEYSPEX_RESEARCH_PROTOCOL_GATE_OFF=1 in their own environment
|
|
31
|
+
// — an env var, never a file (.heyspex/mode.json and config.yaml are
|
|
32
|
+
// agent-writable; scripts/claude-hooks/_run.mjs lists this gate ALWAYS_ON so
|
|
33
|
+
// the mode file cannot silence it either). The agent has no lever here.
|
|
34
|
+
//
|
|
35
|
+
// WHERE IT RUNS: registered in ./index.mjs's PORTED_GATES (every engine front
|
|
36
|
+
// — SpexCode seats, hs chat, overnight runs) and shelled to Claude Code by
|
|
37
|
+
// scripts/claude-hooks/research-protocol-gate.mjs (generated) on the matcher
|
|
38
|
+
// `WebSearch|WebFetch|mcp__heyspex__heyspex_save_research|
|
|
39
|
+
// mcp__plugin_heyspex_heyspex__heyspex_save_research`, and to Cursor through
|
|
40
|
+
// scripts/cursor-hooks/_run.mjs's --hs-tool matcher.
|
|
41
|
+
//
|
|
42
|
+
// MARKER ROOTS: preflight markers live under the VAULT root
|
|
43
|
+
// (`<vault>/.heyspex/preflight/`). A hook process only knows
|
|
44
|
+
// CLAUDE_PROJECT_DIR (the code checkout), so the gate looks in BOTH the
|
|
45
|
+
// project root and the vault the MCP resolver names (vault-resolve.mjs —
|
|
46
|
+
// the same 5-tier resolver the server used when it wrote the marker). On a
|
|
47
|
+
// single-checkout install the two are one directory.
|
|
48
|
+
//
|
|
49
|
+
// `ctx` contract (all optional):
|
|
50
|
+
// ctx.env - env var overrides (falls back to process.env).
|
|
51
|
+
// ctx.cwd - project root (defaults to CLAUDE_PROJECT_DIR, else
|
|
52
|
+
// this repo's root).
|
|
53
|
+
// ctx.vaultRoot - when given, the ONLY marker root searched (tests).
|
|
54
|
+
// ctx.readMarkers - (root, intent, sessionId) => marker[] override
|
|
55
|
+
// (defaults to preflight-gate.mjs's readPreflightMarkers).
|
|
56
|
+
// ctx.now - clock for the session-less TTL fallback (tests).
|
|
57
|
+
|
|
58
|
+
import path from 'node:path';
|
|
59
|
+
import { fileURLToPath } from 'node:url';
|
|
60
|
+
|
|
61
|
+
import {
|
|
62
|
+
RESEARCH_PROTOCOL_GATE_OFF_ENV,
|
|
63
|
+
RESEARCH_PROTOCOL_INTENTS,
|
|
64
|
+
isMarkerCurrent,
|
|
65
|
+
readPreflightMarkers,
|
|
66
|
+
} from '../../../vendor/heyspex-mcp/preflight-gate.mjs';
|
|
67
|
+
import { getActiveConfig } from '../../../vendor/heyspex-mcp/vault-resolve.mjs';
|
|
68
|
+
|
|
69
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
70
|
+
const REPO_ROOT = path.resolve(HERE, '..', '..', '..');
|
|
71
|
+
|
|
72
|
+
/** The gate's id — unique across the registry; also its Claude Code hook name. */
|
|
73
|
+
export const id = 'research-protocol-gate';
|
|
74
|
+
/** Runs pre-tool: a PreToolUse hook in Claude Code, before_tool in the engine. */
|
|
75
|
+
export const event = 'before_tool';
|
|
76
|
+
/** Denies, so "block". */
|
|
77
|
+
export const severity = 'block';
|
|
78
|
+
|
|
79
|
+
/** The exact env var the deny reason names — exported so tests and docs cannot drift from it. */
|
|
80
|
+
export const OFF_ENV = RESEARCH_PROTOCOL_GATE_OFF_ENV;
|
|
81
|
+
|
|
82
|
+
/** The call the deny reason names as the unlock — one string, quoted everywhere it appears. */
|
|
83
|
+
export const UNLOCK_CALL = 'heyspex_preflight({ intent: "research" })';
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Is this tool one of the three the protocol governs? Bare names are what the
|
|
87
|
+
* engine and Claude Code's native tools use; the two MCP prefixes are the
|
|
88
|
+
* project-scoped and plugin-scoped spellings of the same HeySpex tool.
|
|
89
|
+
*
|
|
90
|
+
* @param {unknown} toolName
|
|
91
|
+
* @returns {boolean}
|
|
92
|
+
*/
|
|
93
|
+
export function isResearchTool(toolName) {
|
|
94
|
+
if (typeof toolName !== 'string') return false;
|
|
95
|
+
const name = toolName.trim();
|
|
96
|
+
if (name === 'WebSearch' || name === 'WebFetch') return true;
|
|
97
|
+
return /(^|__)heyspex_save_research$/.test(name);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The deny reason. Kept as a function of the tool name only, so the golden
|
|
102
|
+
* fixtures pin its exact text and a reworded reason is a deliberate edit here.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} toolName
|
|
105
|
+
* @returns {string}
|
|
106
|
+
*/
|
|
107
|
+
export function denyReason(toolName) {
|
|
108
|
+
return (
|
|
109
|
+
`Research blocked: ${toolName} was called before research-capture-protocol was consulted this session. ` +
|
|
110
|
+
`Call ${UNLOCK_CALL} first — it returns the protocol's method triage (known-target-retrieval vs the confidence ladder), ` +
|
|
111
|
+
`the ground-first rule (heyspex_recall_memories + /hs-ground) and the offer-before-save step — read it, then retry this exact call. ` +
|
|
112
|
+
`Reading the protocol directly with heyspex_get_procedure({ name: "research-capture-protocol" }) also counts. ` +
|
|
113
|
+
`This gate is deterministic and is not the agent's to bypass: only the owner can lift it, by setting ${OFF_ENV}=1 in their own environment.`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolvedVaultRoot() {
|
|
118
|
+
try {
|
|
119
|
+
const { repoRootAbs } = getActiveConfig();
|
|
120
|
+
return typeof repoRootAbs === 'string' && repoRootAbs ? repoRootAbs : null;
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The marker roots to search, in order, de-duplicated. `ctx.vaultRoot` pins a
|
|
128
|
+
* single root (tests, and any front that already knows its vault); otherwise
|
|
129
|
+
* the project root first, then the resolver's vault.
|
|
130
|
+
*/
|
|
131
|
+
function markerRoots(ctx) {
|
|
132
|
+
if (typeof ctx.vaultRoot === 'string' && ctx.vaultRoot) return [ctx.vaultRoot];
|
|
133
|
+
const roots = [ctx.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? REPO_ROOT, resolvedVaultRoot()];
|
|
134
|
+
const seen = new Set();
|
|
135
|
+
return roots.filter((r) => {
|
|
136
|
+
if (typeof r !== 'string' || !r) return false;
|
|
137
|
+
const key = path.resolve(r);
|
|
138
|
+
if (seen.has(key)) return false;
|
|
139
|
+
seen.add(key);
|
|
140
|
+
return true;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @param {{ point: string, sessionId?: string, toolName?: string, toolInput?: Record<string, unknown> }} evt
|
|
146
|
+
* @param {{ env?: Record<string, string|undefined>, cwd?: string, vaultRoot?: string,
|
|
147
|
+
* readMarkers?: (root: string, intent: string, sessionId?: string) => object[],
|
|
148
|
+
* now?: number }} [ctx]
|
|
149
|
+
* @returns {{ decision: 'allow'|'deny', reason?: string }}
|
|
150
|
+
*/
|
|
151
|
+
export function run(evt, ctx = {}) {
|
|
152
|
+
const env = ctx.env ?? process.env;
|
|
153
|
+
if (String(env[OFF_ENV] ?? '') === '1') return { decision: 'allow' };
|
|
154
|
+
|
|
155
|
+
const toolName = evt?.toolName;
|
|
156
|
+
if (!isResearchTool(toolName)) return { decision: 'allow' };
|
|
157
|
+
|
|
158
|
+
const read = ctx.readMarkers ?? readPreflightMarkers;
|
|
159
|
+
const now = ctx.now ?? Date.now();
|
|
160
|
+
const sessionId = evt?.sessionId;
|
|
161
|
+
for (const root of markerRoots(ctx)) {
|
|
162
|
+
for (const intent of RESEARCH_PROTOCOL_INTENTS) {
|
|
163
|
+
let markers = [];
|
|
164
|
+
try {
|
|
165
|
+
markers = read(root, intent, sessionId) ?? [];
|
|
166
|
+
} catch {
|
|
167
|
+
markers = []; // an unreadable root proves nothing either way
|
|
168
|
+
}
|
|
169
|
+
if (markers.some((m) => isMarkerCurrent(m, sessionId, now))) return { decision: 'allow' };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { decision: 'deny', reason: denyReason(String(toolName).trim()) };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export default { id, event, severity, run };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// spexharness/src/gates/settings-write-gate.decide.mjs
|
|
2
|
+
//
|
|
3
|
+
// Pure decision logic relocated from scripts/claude-hooks/settings-write-gate.mjs
|
|
4
|
+
// (HS-IQBF, per PORTING.md's "move the pure decision logic into the gate
|
|
5
|
+
// module itself or a sibling pure module"). The original hook's `decide()`
|
|
6
|
+
// had NO isDirectExecution guard around its own module-level `main()`, which
|
|
7
|
+
// is why ./settings-write-gate.mjs (the gate) used to import it lazily with
|
|
8
|
+
// an env-var dance to dodge a stdin hang on static import (see that gate
|
|
9
|
+
// module's git history / PORTING.md for the story). Relocating the pure
|
|
10
|
+
// function here removes the hazard entirely: this file has no main() at all,
|
|
11
|
+
// so a plain static top-level import is always safe. No stdin/stdout/
|
|
12
|
+
// process.exit anywhere in this file.
|
|
13
|
+
|
|
14
|
+
const CONFIG_FILE_RE = /(?:^|[\\/])\.heyspex[\\/]config\.ya?ml$/i;
|
|
15
|
+
|
|
16
|
+
/** Conservative shell write-target scanner (same approach as env-vault-guard.mjs). */
|
|
17
|
+
function extractShellTargets(command) {
|
|
18
|
+
if (!command || typeof command !== 'string') return [];
|
|
19
|
+
const targets = new Set();
|
|
20
|
+
const redirectRe = /(?:>>?|\|\s*tee(?:\s+-a)?)\s+("([^"]+)"|'([^']+)'|(\S+))/g;
|
|
21
|
+
for (const m of command.matchAll(redirectRe)) targets.add(m[2] ?? m[3] ?? m[4]);
|
|
22
|
+
const cmdRe =
|
|
23
|
+
/\b(?:mv|cp|touch|install|rename|tee|rm|del|Remove-Item|Move-Item|Copy-Item|New-Item|Set-Content|Out-File|Add-Content|Clear-Content|sed)\s+([^|;&\n]+)/g;
|
|
24
|
+
for (const m of command.matchAll(cmdRe)) {
|
|
25
|
+
const head = m[0].split(/\s/)[0];
|
|
26
|
+
const args = m[1]
|
|
27
|
+
.trim()
|
|
28
|
+
.split(/\s+/)
|
|
29
|
+
.filter((a) => a && !a.startsWith('-'));
|
|
30
|
+
if (args.length === 0) continue;
|
|
31
|
+
if (/^(?:mv|cp|install|rename|Move-Item|Copy-Item)$/.test(head)) {
|
|
32
|
+
targets.add(args[args.length - 1]);
|
|
33
|
+
} else {
|
|
34
|
+
for (const a of args) targets.add(a);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return [...targets]
|
|
38
|
+
.map((t) => t.replace(/^['"]|['"]$/g, ''))
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.filter((t) => !/[$`(){}]/.test(t));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Pure gate decision. Returns null (pass through) or
|
|
45
|
+
* { ask: true, reason } — the harness then asks the USER for permission.
|
|
46
|
+
*/
|
|
47
|
+
export function decide(toolName, toolInput) {
|
|
48
|
+
// The MCP settings-write tool: gate ONLY the applying call (confirm_token given).
|
|
49
|
+
if (toolName === 'mcp__heyspex__heyspex_settings_set') {
|
|
50
|
+
const token = toolInput?.confirm_token;
|
|
51
|
+
if (typeof token === 'string' && token.trim()) {
|
|
52
|
+
return {
|
|
53
|
+
ask: true,
|
|
54
|
+
reason:
|
|
55
|
+
'Applying a HeySpex settings change (heyspex_settings_set with confirm_token). Settings writes ' +
|
|
56
|
+
'always require explicit user permission (agent-settings-protocol) — approve only if you asked for ' +
|
|
57
|
+
'this exact change.',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return null; // request step writes nothing
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Direct file writes to .heyspex/config.yaml.
|
|
64
|
+
if (toolName === 'Write' || toolName === 'Edit' || toolName === 'NotebookEdit') {
|
|
65
|
+
const filePath = String(toolInput?.file_path ?? toolInput?.notebook_path ?? '');
|
|
66
|
+
if (filePath && CONFIG_FILE_RE.test(filePath)) {
|
|
67
|
+
return {
|
|
68
|
+
ask: true,
|
|
69
|
+
reason:
|
|
70
|
+
`.heyspex/config.yaml is the settings control surface — agent writes require explicit user ` +
|
|
71
|
+
`permission (agent-settings-protocol), regardless of ask_forgiveness_not_permission. Approve only ` +
|
|
72
|
+
`if you asked for this change; the sanctioned agent path is heyspex_settings_set (two-step confirm) ` +
|
|
73
|
+
`or \`hs settings set ... --confirmed-by-user\`.`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Shell writes targeting the config file.
|
|
80
|
+
if (toolName === 'Bash' || toolName === 'PowerShell') {
|
|
81
|
+
for (const target of extractShellTargets(String(toolInput?.command ?? ''))) {
|
|
82
|
+
if (CONFIG_FILE_RE.test(target)) {
|
|
83
|
+
return {
|
|
84
|
+
ask: true,
|
|
85
|
+
reason:
|
|
86
|
+
'This shell command writes to .heyspex/config.yaml — the settings control surface. Settings ' +
|
|
87
|
+
'writes require explicit user permission (agent-settings-protocol). Approve only if you asked ' +
|
|
88
|
+
'for this change; prefer `hs settings set` or heyspex_settings_set.',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// spexharness/src/gates/settings-write-gate.mjs
|
|
2
|
+
//
|
|
3
|
+
// Engine port of scripts/claude-hooks/settings-write-gate.mjs — the
|
|
4
|
+
// PreToolUse gate that asks the user's explicit permission before any agent
|
|
5
|
+
// path writes .heyspex/config.yaml (the settings control surface;
|
|
6
|
+
// agent-settings-protocol). See ./PORTING.md for the recipe this follows and
|
|
7
|
+
// ./env-vault-guard.mjs for the reference port.
|
|
8
|
+
//
|
|
9
|
+
// This module does NOT reimplement the guard's decision logic. It imports
|
|
10
|
+
// `decide(toolName, toolInput)` from ./settings-write-gate.decide.mjs — the
|
|
11
|
+
// pure function relocated out of the original hook script (HS-IQBF; see
|
|
12
|
+
// PORTING.md and that sibling module's own header for why) — and adapts it
|
|
13
|
+
// to the engine's `{ id, event, severity, run(event, ctx) }` gate shape:
|
|
14
|
+
// - `event` (spexharness/src/events.mjs `before_tool`) → the legacy
|
|
15
|
+
// `(toolName, toolInput)` args `decide()` already expects.
|
|
16
|
+
// - the legacy `null | { ask: true, reason }` verdict → the engine's
|
|
17
|
+
// `{ decision: 'allow' | 'rewrite', reason?, rewrite? }` shape.
|
|
18
|
+
//
|
|
19
|
+
// The relocation also retires an old IMPORT HAZARD: the ORIGINAL hook
|
|
20
|
+
// script's `main()` used to run unconditionally at module-evaluation time
|
|
21
|
+
// (no isDirectExecution guard), so a plain static `import` of it hung any
|
|
22
|
+
// process that imported it with an open, not-yet-closed stdin. This module
|
|
23
|
+
// used to work around that with a lazily-imported, env-var-gated dynamic
|
|
24
|
+
// import. Now that the pure `decide()` lives in a sibling module with no
|
|
25
|
+
// main() of its own, that hazard no longer exists — a plain static
|
|
26
|
+
// top-level import is always safe, same as every other gate in this set.
|
|
27
|
+
//
|
|
28
|
+
// Decision mapping (a documented judgment call, the same shape as
|
|
29
|
+
// env-vault-guard.mjs's "ask" → rewrite mapping): the legacy verdict is
|
|
30
|
+
// either `null` (no opinion → allow) or `{ ask: true, reason }` (the harness
|
|
31
|
+
// should show the user a synchronous permission prompt). The engine's
|
|
32
|
+
// decide.mjs has no synchronous-approval primitive, so `ask` is carried
|
|
33
|
+
// forward as a `rewrite` envelope tagging `ask: true` — never silently
|
|
34
|
+
// downgraded to an auto-allow (unsafe: skips the required permission
|
|
35
|
+
// prompt) or upgraded to an auto-deny (a real behavior change; the original
|
|
36
|
+
// never hard-denies a settings write, it only asks). A calling front decides
|
|
37
|
+
// what `rewrite.ask` means for it.
|
|
38
|
+
//
|
|
39
|
+
// This gate never hard-denies (matching the original) — "deny" coverage for
|
|
40
|
+
// AC3 ("allow, deny and rewrite each covered by at least one test per gate",
|
|
41
|
+
// checked as one combined `node --test` run across all three ported gates in
|
|
42
|
+
// this task) comes from the sibling enforce-session-sequence gate, which is
|
|
43
|
+
// the only one of the three that can actually block a turn. See
|
|
44
|
+
// ./enforce-session-sequence.test.mjs.
|
|
45
|
+
//
|
|
46
|
+
// Env hatch (unchanged): HEYSPEX_SETTINGS_GATE_OFF=1.
|
|
47
|
+
// Config toggle (unchanged, full parity with the original's own opt-out, in
|
|
48
|
+
// addition to the env hatch): agent_behaviors.settings_write_guard.enabled
|
|
49
|
+
// (default ON; an explicit `false` in .heyspex/config.yaml silences the
|
|
50
|
+
// gate) — read via the same dependency-free `_guardrail-loader.mjs` the
|
|
51
|
+
// original imports.
|
|
52
|
+
//
|
|
53
|
+
// `ctx` contract (all optional):
|
|
54
|
+
// ctx.cwd - a self-contained fixture root: when given, the config-toggle
|
|
55
|
+
// read resolves from it (tests build a temp root carrying its
|
|
56
|
+
// own .heyspex/config.yaml and pass it here).
|
|
57
|
+
// ctx.configRoot - the directory holding `.heyspex/config.yaml`; wins over
|
|
58
|
+
// ctx.cwd for the toggle read. With neither, the VAULT root
|
|
59
|
+
// (../config-root.mjs `defaultConfigRoot()`) — never
|
|
60
|
+
// CLAUDE_PROJECT_DIR, which under Claude Code is the app
|
|
61
|
+
// checkout, not where the settings file lives
|
|
62
|
+
// ([[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]]).
|
|
63
|
+
// ctx.env - env var overrides (tests use this instead of mutating
|
|
64
|
+
// process.env).
|
|
65
|
+
|
|
66
|
+
import { isConfigKeyEnabled } from '../../../scripts/claude-hooks/_guardrail-loader.mjs';
|
|
67
|
+
import { defaultConfigRoot } from '../config-root.mjs';
|
|
68
|
+
import { decide as legacyDecide } from './settings-write-gate.decide.mjs';
|
|
69
|
+
|
|
70
|
+
const TOGGLE_KEY = 'agent_behaviors.settings_write_guard.enabled';
|
|
71
|
+
const OFF_ENV = 'HEYSPEX_SETTINGS_GATE_OFF';
|
|
72
|
+
|
|
73
|
+
/** The settings-write gate's id — unique across the built-in registry. */
|
|
74
|
+
export const id = 'settings-write-gate';
|
|
75
|
+
/** Runs pre-tool, same as its PreToolUse Claude Code hook registration. */
|
|
76
|
+
export const event = 'before_tool';
|
|
77
|
+
/** Caller-defined severity label; this gate only ever asks (rewrites) or allows. */
|
|
78
|
+
export const severity = 'ask';
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {{ point: string, toolName?: string, toolInput?: Record<string, unknown> }} evt
|
|
82
|
+
* @param {{ cwd?: string, configRoot?: string, env?: Record<string, string | undefined> }} [ctx]
|
|
83
|
+
* @returns {{ decision: 'allow'|'rewrite', reason?: string, rewrite?: object }}
|
|
84
|
+
*/
|
|
85
|
+
export function run(evt, ctx = {}) {
|
|
86
|
+
const env = ctx.env ?? process.env;
|
|
87
|
+
if (String(env[OFF_ENV] ?? '') === '1') return { decision: 'allow' };
|
|
88
|
+
|
|
89
|
+
// The ONLY root this gate reads is the config root — see the ctx contract above.
|
|
90
|
+
const configRoot = ctx.configRoot ?? (ctx.cwd ? ctx.cwd : defaultConfigRoot());
|
|
91
|
+
if (!isConfigKeyEnabled(TOGGLE_KEY, configRoot)) return { decision: 'allow' };
|
|
92
|
+
|
|
93
|
+
const verdict = legacyDecide(evt?.toolName, evt?.toolInput ?? {});
|
|
94
|
+
|
|
95
|
+
if (!verdict || !verdict.ask) return { decision: 'allow' };
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
decision: 'rewrite',
|
|
99
|
+
reason: verdict.reason,
|
|
100
|
+
rewrite: { ask: true, reason: verdict.reason },
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default { id, event, severity, run };
|