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,213 @@
|
|
|
1
|
+
// spexharness/src/gates/naughty-list-gate.mjs
|
|
2
|
+
//
|
|
3
|
+
// Engine port of scripts/claude-hooks/naughty-list-gate.mjs — the Stop hook
|
|
4
|
+
// that enforces the "naughty list" (banned metaphorical jargon,
|
|
5
|
+
// [[HS-PROTO-terminology-governance]]) on the agent's own final message. See
|
|
6
|
+
// ./PORTING.md for the recipe this follows and ./env-vault-guard.mjs for the
|
|
7
|
+
// reference port.
|
|
8
|
+
//
|
|
9
|
+
// This module does NOT reimplement the vocabulary rule itself (AC5's own
|
|
10
|
+
// words: "read their word lists ... from loadRuleTable, not a second copy").
|
|
11
|
+
// The actual decision — does this prose contain a banned term? — comes
|
|
12
|
+
// straight from the shared, already-pure loader:
|
|
13
|
+
// - scripts/claude-hooks/_guardrail-loader.mjs — loadGuardrails,
|
|
14
|
+
// rulesForSurface, isRuleEnabled, vocabularyViolations.
|
|
15
|
+
// vocabularyViolations owns the per-user `.heyspex/terminology.local.json`
|
|
16
|
+
// override merge (resolveVocabulary) AND the prose exemptions (code
|
|
17
|
+
// spans, fenced blocks, [[wikilinks]], URLs, blockquotes via proseOnly)
|
|
18
|
+
// internally — this module never reimplements either.
|
|
19
|
+
// - scripts/claude-hooks/_transcript-text.mjs — lastAssistantText, the same
|
|
20
|
+
// shared transcript-text reader id-display-format-gate.mjs's port also
|
|
21
|
+
// uses (split out of this hook's original sole ownership specifically so
|
|
22
|
+
// a second Stop-hook scanner wouldn't fork a copy).
|
|
23
|
+
// Both are dependency-free / no import hazard, matching enforce-session-
|
|
24
|
+
// sequence.mjs's port.
|
|
25
|
+
//
|
|
26
|
+
// JUDGMENT CALL — the original script does NOT export a pure decision
|
|
27
|
+
// function; its logic is tangled inside main()'s stdin read. Per PORTING.md
|
|
28
|
+
// step 1's "otherwise" branch this would normally move into the original
|
|
29
|
+
// script itself, but this task's Do-nots forbid touching anything under
|
|
30
|
+
// scripts/claude-hooks/. What is re-derived here is NOT the vocabulary rule
|
|
31
|
+
// (that stays imported, per AC5) — it is pure string-formatting glue
|
|
32
|
+
// (`reasonLines`, `blockReason`, the warn-stderr message template) copied
|
|
33
|
+
// verbatim from the original's own (unexported) helpers, plus the
|
|
34
|
+
// interactive-rung resolver (`interactiveRung`, reading
|
|
35
|
+
// HEYSPEX_NAUGHTY_LIST_INTERACTIVE) copied verbatim too.
|
|
36
|
+
//
|
|
37
|
+
// Event mapping: Stop → stop. `stdin.transcript_path` → `evt.transcriptPath`.
|
|
38
|
+
//
|
|
39
|
+
// Decision mapping — TWO rungs (AC4's "including the naughty-list gate's
|
|
40
|
+
// warn-versus-block rung"): CLAUDE_AGENT=true selects the "isAgent" branch,
|
|
41
|
+
// which blocks on any block-severity hit; the interactive default (agent
|
|
42
|
+
// unset, HEYSPEX_NAUGHTY_LIST_INTERACTIVE not raised to "block") maps the
|
|
43
|
+
// SAME hits to a `rewrite` envelope (`{ warn: true, reason, hits }`) instead
|
|
44
|
+
// — the stop is still allowed, matching the original's stderr-only warn path
|
|
45
|
+
// exactly (never silently upgraded to deny, never silently downgraded to a
|
|
46
|
+
// plain allow that would drop the reason text). This gives this gate natural
|
|
47
|
+
// coverage of all three engine decisions (AC3): allow on clean prose or no
|
|
48
|
+
// vocabulary rule compiled, deny on a block-severity hit in the real-block
|
|
49
|
+
// rung, rewrite on the warn rung (or on a warn-severity-only hit in any
|
|
50
|
+
// rung).
|
|
51
|
+
//
|
|
52
|
+
// Not ported HERE: the original's `appendGuardrailEvent(...)` audit-log
|
|
53
|
+
// write on every block-severity hit (fired even in the warn rung, to build
|
|
54
|
+
// the evidence log the rung is raised on) — matching every other port in
|
|
55
|
+
// this set. This `run()` still carries the block-severity hits forward as a
|
|
56
|
+
// top-level `blockHits` field on its `deny` AND `rewrite` results (never on
|
|
57
|
+
// a plain `allow`, and never including warn-severity-only hits — the
|
|
58
|
+
// original's own loop only ever logs block-severity hits, whichever rung
|
|
59
|
+
// the run happens to be on) so a FRONT can rebuild that exact per-hit audit
|
|
60
|
+
// trail without re-deriving hit words/contexts from `reason` text — see
|
|
61
|
+
// spexharness/src/adapters/audit.mjs, called from
|
|
62
|
+
// spexharness/src/adapters/claude-code.mjs's runGateAsHook.
|
|
63
|
+
//
|
|
64
|
+
// Env hatches (unchanged):
|
|
65
|
+
// HEYSPEX_NAUGHTY_LIST_OFF=1 Hard kill switch — silences both rungs.
|
|
66
|
+
// HEYSPEX_NAUGHTY_LIST_INTERACTIVE=block Raise the interactive rung to a real deny.
|
|
67
|
+
// CLAUDE_AGENT=true Unattended session → real deny (unchanged).
|
|
68
|
+
// Also honors the rule's own registry configKey
|
|
69
|
+
// (agent_behaviors.terminology_enforcement.enabled) via isRuleEnabled.
|
|
70
|
+
//
|
|
71
|
+
// `ctx` contract (all optional; every field defaults to a real fs/env read):
|
|
72
|
+
// ctx.cwd - repo root to resolve the guardrail registry + terminology
|
|
73
|
+
// override from. Defaults to CLAUDE_PROJECT_DIR, else this
|
|
74
|
+
// repo's root. When given it is a self-contained fixture root and
|
|
75
|
+
// the rule's config toggle is read from it too.
|
|
76
|
+
// ctx.configRoot - the directory holding `.heyspex/config.yaml` (the rule's
|
|
77
|
+
// configKey toggle); wins over ctx.cwd for that read. With neither,
|
|
78
|
+
// the VAULT root (../config-root.mjs `defaultConfigRoot()`) — never
|
|
79
|
+
// CLAUDE_PROJECT_DIR, which under Claude Code is the app checkout,
|
|
80
|
+
// not where the settings file lives
|
|
81
|
+
// ([[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]]).
|
|
82
|
+
// The terminology override stays on ctx.cwd (a code-repo artifact).
|
|
83
|
+
// ctx.env - env var overrides (tests use this instead of mutating
|
|
84
|
+
// process.env).
|
|
85
|
+
|
|
86
|
+
import path from 'node:path';
|
|
87
|
+
import { fileURLToPath } from 'node:url';
|
|
88
|
+
|
|
89
|
+
import { lastAssistantText } from '../../../scripts/claude-hooks/_transcript-text.mjs';
|
|
90
|
+
import {
|
|
91
|
+
loadGuardrails,
|
|
92
|
+
rulesForSurface,
|
|
93
|
+
isRuleEnabled,
|
|
94
|
+
vocabularyViolations,
|
|
95
|
+
} from '../../../scripts/claude-hooks/_guardrail-loader.mjs';
|
|
96
|
+
import { defaultConfigRoot } from '../config-root.mjs';
|
|
97
|
+
|
|
98
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
99
|
+
const REPO_ROOT = path.resolve(HERE, '..', '..', '..');
|
|
100
|
+
|
|
101
|
+
// Copied verbatim from the original's own (unexported) reasonLines.
|
|
102
|
+
function reasonLines(hits) {
|
|
103
|
+
const seen = new Map();
|
|
104
|
+
for (const h of hits) if (!seen.has(h.word)) seen.set(h.word, h.fix);
|
|
105
|
+
return [...seen.entries()].map(([word, fix]) => ` • "${word}" → ${fix}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Copied verbatim from the original's own (unexported) blockReason.
|
|
109
|
+
function blockReason(hits) {
|
|
110
|
+
return (
|
|
111
|
+
`Naughty-list violation: your last message used banned metaphorical jargon ` +
|
|
112
|
+
`(the HeySpex default list from specs/naughty-list.md, also the owner's preference). ` +
|
|
113
|
+
`Rewrite those words in plain literal language and resend:\n${reasonLines(hits).join('\n')}\n` +
|
|
114
|
+
`(To refer to a banned word as a word, wrap it in backticks. Customize per checkout via ` +
|
|
115
|
+
`.heyspex/terminology.local.json {add,allow,remove}. Break-glass: HEYSPEX_NAUGHTY_LIST_OFF=1.)`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Copied verbatim from the original's own (unexported) warn message template.
|
|
120
|
+
function warnReason(hits) {
|
|
121
|
+
return `[naughty-list] warning — banned terms in your last message (not blocked):\n${reasonLines(hits).join('\n')}\n`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Copied verbatim from the original's own (unexported) interactiveRung.
|
|
125
|
+
function interactiveRung(env) {
|
|
126
|
+
const raw = String(env.HEYSPEX_NAUGHTY_LIST_INTERACTIVE ?? '').trim().toLowerCase();
|
|
127
|
+
return raw === 'block' ? 'block' : 'warn';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The prose this stop event is about. A front that ALREADY holds the reply (`hs chat`, a
|
|
132
|
+
* headless seat, the bench — none of which write a Claude Code transcript file) passes it on
|
|
133
|
+
* the event as `text`, or on the ctx as `reply`; a Claude Code hook shell passes only
|
|
134
|
+
* `transcriptPath` and the text is read from it. Checked in that order, so the same gate runs
|
|
135
|
+
* on every front instead of silently allowing everything on the ones with no transcript —
|
|
136
|
+
* which is exactly what it did before (QC finding, 2026-09-10). The `ctx.reply` name matches
|
|
137
|
+
* enforce-session-sequence.mjs's stop-time corrector, which already took the reply that way.
|
|
138
|
+
*
|
|
139
|
+
* @param {{ text?: string, transcriptPath?: string }} evt
|
|
140
|
+
* @param {{ reply?: string }} ctx
|
|
141
|
+
* @param {(p: string) => string|null} readTranscript
|
|
142
|
+
* @returns {string}
|
|
143
|
+
*/
|
|
144
|
+
function stopEventText(evt, ctx, readTranscript) {
|
|
145
|
+
if (typeof ctx?.reply === 'string' && ctx.reply) return ctx.reply;
|
|
146
|
+
if (typeof evt?.text === 'string' && evt.text) return evt.text;
|
|
147
|
+
const transcriptPath = evt?.transcriptPath;
|
|
148
|
+
if (!transcriptPath) return '';
|
|
149
|
+
return readTranscript(transcriptPath) ?? '';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The naughty-list gate's id — unique across the built-in registry. */
|
|
153
|
+
export const id = 'naughty-list-gate';
|
|
154
|
+
/** Runs on the Stop event, same as its Stop Claude Code hook registration. */
|
|
155
|
+
export const event = 'stop';
|
|
156
|
+
/** Caller-defined severity label; this gate can deny, so it is "block". */
|
|
157
|
+
export const severity = 'block';
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {{ point: string, text?: string, transcriptPath?: string }} evt
|
|
161
|
+
* @param {{ cwd?: string, configRoot?: string, reply?: string, env?: Record<string, string | undefined> }} [ctx]
|
|
162
|
+
* @returns {{ decision: 'allow'|'deny'|'rewrite', reason?: string, rewrite?: object }}
|
|
163
|
+
*/
|
|
164
|
+
export function run(evt, ctx = {}) {
|
|
165
|
+
const env = ctx.env ?? process.env;
|
|
166
|
+
if (String(env.HEYSPEX_NAUGHTY_LIST_OFF ?? '') === '1') {
|
|
167
|
+
return { decision: 'allow' };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const isAgent = env.CLAUDE_AGENT === 'true';
|
|
171
|
+
const warnOnly = !isAgent && interactiveRung(env) !== 'block';
|
|
172
|
+
|
|
173
|
+
const text = stopEventText(evt, ctx, lastAssistantText);
|
|
174
|
+
if (!text) return { decision: 'allow' };
|
|
175
|
+
|
|
176
|
+
const root = ctx.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? REPO_ROOT;
|
|
177
|
+
const configRoot = ctx.configRoot ?? (ctx.cwd ? ctx.cwd : defaultConfigRoot());
|
|
178
|
+
const guardrails = loadGuardrails(root);
|
|
179
|
+
const rules = rulesForSurface(guardrails, 'stop').filter((r) => r && r.kind === 'vocabulary');
|
|
180
|
+
if (!rules.length) return { decision: 'allow' };
|
|
181
|
+
|
|
182
|
+
const blockHits = [];
|
|
183
|
+
const warnHits = [];
|
|
184
|
+
for (const rule of rules) {
|
|
185
|
+
if (!isRuleEnabled(rule, configRoot)) continue;
|
|
186
|
+
const hits = vocabularyViolations(rule, text, root);
|
|
187
|
+
if (!hits.length) continue;
|
|
188
|
+
(rule.severity === 'block' ? blockHits : warnHits).push(...hits);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (blockHits.length && !warnOnly) {
|
|
192
|
+
return { decision: 'deny', reason: blockReason(blockHits), blockHits };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const softHits = warnOnly ? [...blockHits, ...warnHits] : warnHits;
|
|
196
|
+
if (softHits.length) {
|
|
197
|
+
const reason = warnReason(softHits);
|
|
198
|
+
// `blockHits` (block-severity hits ONLY, never the warn-severity ones
|
|
199
|
+
// folded into `softHits`/`rewrite.hits` for display) rides separately so
|
|
200
|
+
// a front's audit log matches the original's own loop, which logs a row
|
|
201
|
+
// per block-severity hit on every rung — never for a warn-severity hit.
|
|
202
|
+
return {
|
|
203
|
+
decision: 'rewrite',
|
|
204
|
+
reason,
|
|
205
|
+
rewrite: { warn: true, reason, hits: softHits },
|
|
206
|
+
...(blockHits.length ? { blockHits } : {}),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { decision: 'allow' };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export default { id, event, severity, run };
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// spexharness/src/gates/plan-review-gate.mjs
|
|
2
|
+
//
|
|
3
|
+
// Engine port of scripts/claude-hooks/plan-review-gate.mjs — the PreToolUse
|
|
4
|
+
// gate over ExitPlanMode that enforces the V1 → quality+canonical check → V2
|
|
5
|
+
// contract (a heyspex_plan_review marker) and, once that passes, the QC
|
|
6
|
+
// gut-check marker (heyspex_plan_qc) — both keyed by sha256(plan content),
|
|
7
|
+
// [[HS-EPIC-PLRV]]. See ./PORTING.md for the recipe this follows and
|
|
8
|
+
// ./env-vault-guard.mjs for the reference port.
|
|
9
|
+
//
|
|
10
|
+
// JUDGMENT CALL — read before touching this file: the original script does
|
|
11
|
+
// NOT export a pure decision function; its logic (resolvePlan, denyReason,
|
|
12
|
+
// denyReasonQc, shouldBlockForReal, fireDeny) is tangled with main()'s stdin
|
|
13
|
+
// read. This task's Do-nots forbid touching anything under
|
|
14
|
+
// scripts/claude-hooks/, so per PORTING.md step 1's "otherwise" branch this
|
|
15
|
+
// module re-derives the plumbing here. What is re-derived is NOT
|
|
16
|
+
// security-relevant regex/rule logic — it is (a) the marker-path
|
|
17
|
+
// construction (a plain sha256(plan content) → `.heyspex/plan-reviews/` /
|
|
18
|
+
// `.heyspex/plan-qc/` join, no judgment call baked in) and (b) the deny-
|
|
19
|
+
// reason string TEMPLATES, copied verbatim from the original's own
|
|
20
|
+
// (unexported) `denyReason`/`denyReasonQc` functions so the text a caller
|
|
21
|
+
// sees is byte-identical to what the original would have written — the same
|
|
22
|
+
// "copy verbatim where the original's own strings are copyable" approach
|
|
23
|
+
// deep-research-cost-gate.mjs's port documents for its own unexported COST
|
|
24
|
+
// table and message templates. If the original script is ever changed to
|
|
25
|
+
// export these, this module's copies should be deleted in favor of the real
|
|
26
|
+
// thing.
|
|
27
|
+
//
|
|
28
|
+
// Event mapping: PreToolUse (ExitPlanMode) → before_tool. The original's own
|
|
29
|
+
// defense-in-depth tool-name check (`tool_name !== 'ExitPlanMode'` → allow)
|
|
30
|
+
// is preserved here.
|
|
31
|
+
//
|
|
32
|
+
// Decision mapping — TWO independent rungs, same two-rung shape as
|
|
33
|
+
// enforce-session-sequence.mjs's port: CLAUDE_AGENT=true or
|
|
34
|
+
// HEYSPEX_PLAN_REVIEW_GATE_INTERACTIVE=block ("forReal") maps a missing
|
|
35
|
+
// marker to a real `deny`; the interactive default maps the SAME missing-
|
|
36
|
+
// marker finding to a `rewrite` envelope (`{ warn: true, reason, rule }`) —
|
|
37
|
+
// the stop is still allowed, matching the original's stderr-only warn path
|
|
38
|
+
// exactly (never silently upgraded to deny, never silently downgraded to a
|
|
39
|
+
// plain allow that would drop the reason text). This gives this gate natural
|
|
40
|
+
// coverage of all three engine decisions (AC3): allow when both markers are
|
|
41
|
+
// present (or the tool isn't ExitPlanMode), deny on the unattended/raised
|
|
42
|
+
// rung, rewrite on the interactive-default rung.
|
|
43
|
+
//
|
|
44
|
+
// Not ported: the original's `appendGuardrailEvent(...)` audit-log write on
|
|
45
|
+
// a fire — matching every other port in this set (a gate module here is a
|
|
46
|
+
// pure decision function with no other observability side effects; a front
|
|
47
|
+
// that wants the same audit trail logs `denials`/`rewrites` itself).
|
|
48
|
+
//
|
|
49
|
+
// Env hatches (unchanged):
|
|
50
|
+
// HEYSPEX_PLAN_REVIEW_GATE_OFF=1 Hard kill switch — silences both rungs and both markers.
|
|
51
|
+
// HEYSPEX_PLAN_REVIEW_GATE_INTERACTIVE=block Raise the interactive rung to a real deny (V1 half only).
|
|
52
|
+
// HEYSPEX_PLAN_QC_GATE_OFF=1 Mute the QC-marker half only (the V1 half still gates).
|
|
53
|
+
// CLAUDE_AGENT=true Unattended session → real deny (unchanged).
|
|
54
|
+
//
|
|
55
|
+
// `ctx` contract (all optional; every field defaults to a real fs/env read):
|
|
56
|
+
// ctx.cwd - repo root to resolve the marker directories
|
|
57
|
+
// from. Defaults to CLAUDE_PROJECT_DIR, else
|
|
58
|
+
// this repo's root.
|
|
59
|
+
// ctx.env - env var overrides (tests use this instead of
|
|
60
|
+
// mutating process.env).
|
|
61
|
+
// ctx.resolvePlanFallback - override for the original's `~/.claude/plans/`
|
|
62
|
+
// most-recently-mtimed-file fallback (used only
|
|
63
|
+
// when the tool input carries no inline
|
|
64
|
+
// `plan`). Defaults to a real fs scan of the
|
|
65
|
+
// real home directory; every fixture here
|
|
66
|
+
// supplies an inline plan, so this fallback is
|
|
67
|
+
// never exercised by the golden/regression
|
|
68
|
+
// tests, only kept for behavioral parity.
|
|
69
|
+
|
|
70
|
+
import fs from 'node:fs';
|
|
71
|
+
import path from 'node:path';
|
|
72
|
+
import os from 'node:os';
|
|
73
|
+
import { createHash } from 'node:crypto';
|
|
74
|
+
import { fileURLToPath } from 'node:url';
|
|
75
|
+
|
|
76
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
77
|
+
const REPO_ROOT = path.resolve(HERE, '..', '..', '..');
|
|
78
|
+
const PROCEDURE_NAME = 'plan-quality-canonical-check';
|
|
79
|
+
|
|
80
|
+
// Copied verbatim from the original's own (unexported) resolvePlan — see the
|
|
81
|
+
// judgment-call note above.
|
|
82
|
+
function defaultResolvePlanFallback() {
|
|
83
|
+
try {
|
|
84
|
+
const plansDir = path.join(os.homedir(), '.claude', 'plans');
|
|
85
|
+
const entries = fs
|
|
86
|
+
.readdirSync(plansDir)
|
|
87
|
+
.map((name) => path.join(plansDir, name))
|
|
88
|
+
.filter((p) => {
|
|
89
|
+
try {
|
|
90
|
+
return fs.statSync(p).isFile();
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
.map((p) => ({ p, mtime: fs.statSync(p).mtimeMs }))
|
|
96
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
97
|
+
if (entries.length > 0) {
|
|
98
|
+
const content = fs.readFileSync(entries[0].p, 'utf8');
|
|
99
|
+
return { content, source: entries[0].p };
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// No plans dir / unreadable — fall through.
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function resolvePlan(toolInput, ctx) {
|
|
108
|
+
const inline = toolInput?.plan;
|
|
109
|
+
if (typeof inline === 'string' && inline.trim()) {
|
|
110
|
+
return { content: inline, source: 'tool_input.plan' };
|
|
111
|
+
}
|
|
112
|
+
const fallback = ctx.resolvePlanFallback ?? defaultResolvePlanFallback;
|
|
113
|
+
try {
|
|
114
|
+
return fallback();
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Copied verbatim from the original's own (unexported) denyReason.
|
|
121
|
+
function denyReason(planSource) {
|
|
122
|
+
const where = planSource ? ` (plan at ${planSource})` : '';
|
|
123
|
+
return (
|
|
124
|
+
`Plan V1 detected${where}. Before leaving plan mode, run ` +
|
|
125
|
+
`heyspex_plan_review({ plan_content: <the plan you are about to submit> }) ` +
|
|
126
|
+
`(or plan_path if the plan is on disk), fold the findings into a V2 revision ` +
|
|
127
|
+
`of the plan, then call ExitPlanMode again with the revised plan. This gate ` +
|
|
128
|
+
`is non-skippable — it enforces the V1 → quality+canonical check → V2 ` +
|
|
129
|
+
`contract; see ${PROCEDURE_NAME}.md. ` +
|
|
130
|
+
`(Break-glass: set HEYSPEX_PLAN_REVIEW_GATE_OFF=1.)`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Copied verbatim from the original's own (unexported) denyReasonQc.
|
|
135
|
+
function denyReasonQc(planSource) {
|
|
136
|
+
const where = planSource ? ` (plan at ${planSource})` : '';
|
|
137
|
+
return (
|
|
138
|
+
`Plan-review passed, but the QC gut-check has not run for this plan ` +
|
|
139
|
+
`version${where}. Before leaving plan mode, run the \`/hs-qc\` gut-check, fold ` +
|
|
140
|
+
`its findings into the plan, then call ` +
|
|
141
|
+
`heyspex_plan_qc({ plan_content: <the plan you are about to submit>, ` +
|
|
142
|
+
`findings_summary: <what the gut-check actually found> }) to record ` +
|
|
143
|
+
`Proof-of-Use, then call ExitPlanMode again. This gate is non-skippable; ` +
|
|
144
|
+
`see ${PROCEDURE_NAME}.md. (Break-glass for the QC half only: set ` +
|
|
145
|
+
`HEYSPEX_PLAN_QC_GATE_OFF=1.)`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function shouldBlockForReal(env) {
|
|
150
|
+
return env.CLAUDE_AGENT === 'true' || env.HEYSPEX_PLAN_REVIEW_GATE_INTERACTIVE === 'block';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Byte-for-byte the original hook's own interactive-warn stderr banner
|
|
154
|
+
// (fireDeny()'s `else` branch) — kept here rather than left to the adapter's
|
|
155
|
+
// generic warn formatter because the second line names THIS gate's own env
|
|
156
|
+
// var, same convention as claim-before-code-gate.mjs's warnReason.
|
|
157
|
+
function warnStderrText(reason) {
|
|
158
|
+
return (
|
|
159
|
+
`[plan-review-gate] warning — ${reason}\n` +
|
|
160
|
+
'(Interactive session: this only warns. Set HEYSPEX_PLAN_REVIEW_GATE_INTERACTIVE=block to enforce it here too.)\n'
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The plan-review gate's id — unique across the built-in registry. */
|
|
165
|
+
export const id = 'plan-review-gate';
|
|
166
|
+
/** Runs pre-tool, same as its PreToolUse Claude Code hook registration. */
|
|
167
|
+
export const event = 'before_tool';
|
|
168
|
+
/** Caller-defined severity label; this gate can deny, so it is "block". */
|
|
169
|
+
export const severity = 'block';
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @param {{ point: string, toolName?: string, toolInput?: Record<string, unknown> }} evt
|
|
173
|
+
* @param {{ cwd?: string, env?: Record<string, string | undefined>,
|
|
174
|
+
* resolvePlanFallback?: () => { content: string, source: string } | null }} [ctx]
|
|
175
|
+
* @returns {{ decision: 'allow'|'deny'|'rewrite', reason?: string, rewrite?: object }}
|
|
176
|
+
*/
|
|
177
|
+
export function run(evt, ctx = {}) {
|
|
178
|
+
const env = ctx.env ?? process.env;
|
|
179
|
+
if (String(env.HEYSPEX_PLAN_REVIEW_GATE_OFF ?? '') === '1') {
|
|
180
|
+
return { decision: 'allow' };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Defense-in-depth: the real settings.json matcher already scopes this to
|
|
184
|
+
// ExitPlanMode, but never gate any other tool even if mis-wired.
|
|
185
|
+
if (evt?.toolName !== 'ExitPlanMode') {
|
|
186
|
+
return { decision: 'allow' };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const plan = resolvePlan(evt?.toolInput ?? {}, ctx);
|
|
190
|
+
if (!plan) {
|
|
191
|
+
// Can't find the plan content to hash → fail open (allow), same as the original.
|
|
192
|
+
return { decision: 'allow' };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const root = ctx.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? REPO_ROOT;
|
|
196
|
+
const hash = createHash('sha256').update(plan.content).digest('hex');
|
|
197
|
+
const reviewMarker = path.join(root, '.heyspex', 'plan-reviews', `${hash}.json`);
|
|
198
|
+
const forReal = shouldBlockForReal(env);
|
|
199
|
+
|
|
200
|
+
if (!fs.existsSync(reviewMarker)) {
|
|
201
|
+
const reason = denyReason(plan.source);
|
|
202
|
+
return forReal
|
|
203
|
+
? { decision: 'deny', reason }
|
|
204
|
+
: {
|
|
205
|
+
decision: 'rewrite',
|
|
206
|
+
reason,
|
|
207
|
+
rewrite: { warn: true, reason, rule: 'plan-review-gate:v1', stderrText: warnStderrText(reason) },
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Plan-review passed. Now require the QC gut-check marker for the SAME
|
|
212
|
+
// plan hash — unless the QC half is muted (the master kill switch above
|
|
213
|
+
// mutes both).
|
|
214
|
+
if (String(env.HEYSPEX_PLAN_QC_GATE_OFF ?? '') === '1') {
|
|
215
|
+
return { decision: 'allow' };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const qcMarker = path.join(root, '.heyspex', 'plan-qc', `${hash}.json`);
|
|
219
|
+
if (fs.existsSync(qcMarker)) {
|
|
220
|
+
return { decision: 'allow' };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const reasonQc = denyReasonQc(plan.source);
|
|
224
|
+
return forReal
|
|
225
|
+
? { decision: 'deny', reason: reasonQc }
|
|
226
|
+
: {
|
|
227
|
+
decision: 'rewrite',
|
|
228
|
+
reason: reasonQc,
|
|
229
|
+
rewrite: { warn: true, reason: reasonQc, rule: 'plan-review-gate:qc', stderrText: warnStderrText(reasonQc) },
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export default { id, event, severity, run };
|