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,413 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// _run.mjs — the single chokepoint every HeySpex Claude Code hook runs through.
|
|
3
|
+
//
|
|
4
|
+
// `.claude/settings.json` never names a hook script directly. It names this
|
|
5
|
+
// shim plus the hook to run:
|
|
6
|
+
//
|
|
7
|
+
// node "$CLAUDE_PROJECT_DIR/scripts/claude-hooks/_run.mjs" wip-checkpoint
|
|
8
|
+
// node "$CLAUDE_PROJECT_DIR/scripts/claude-hooks/_run.mjs" emit-claim-status --event=Stop
|
|
9
|
+
//
|
|
10
|
+
// The shim asks one question — is HeySpex on? — and either exits 0 immediately
|
|
11
|
+
// or hands off to the real hook. One place to reason about, and a hook added
|
|
12
|
+
// tomorrow inherits the off-switch for free.
|
|
13
|
+
//
|
|
14
|
+
// Delegation is a dynamic `import()` in THIS process, not a subprocess, so the
|
|
15
|
+
// hook still owns stdin (Claude Code pipes the event payload there) and still
|
|
16
|
+
// sets this process's exit code. `process.argv` is rewritten first so a hook
|
|
17
|
+
// reading `--event=Stop` sees exactly what it saw before the shim existed.
|
|
18
|
+
//
|
|
19
|
+
// Everything here is fail-open. A shim that throws must never take down a
|
|
20
|
+
// Claude Code session, and it must never be the reason a guardrail silently
|
|
21
|
+
// stops firing — so an unresolvable mode reads as ON, and a genuinely broken
|
|
22
|
+
// hook path reports on stderr and exits 0.
|
|
23
|
+
|
|
24
|
+
import crypto from 'node:crypto';
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import os from 'node:os';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
29
|
+
|
|
30
|
+
const STATE_DIR = '.' + 'heyspex';
|
|
31
|
+
const HOOK_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const REPO_ROOT = path.resolve(HOOK_DIR, '..', '..');
|
|
33
|
+
|
|
34
|
+
const [target, ...rest] = process.argv.slice(2);
|
|
35
|
+
|
|
36
|
+
if (!target) {
|
|
37
|
+
process.stderr.write('[heyspex] _run.mjs: no hook name given\n');
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the hook to run.
|
|
43
|
+
* `wip-checkpoint` → scripts/claude-hooks/wip-checkpoint.mjs
|
|
44
|
+
* `scripts/stamp-provenance.mjs` → <repo>/scripts/stamp-provenance.mjs
|
|
45
|
+
*
|
|
46
|
+
* A bare name is constrained to a safe charset and must land inside the hook
|
|
47
|
+
* directory; a path form is resolved from the repo root and must stay inside
|
|
48
|
+
* it. Both guards exist so a mangled settings.json can't turn the shim into an
|
|
49
|
+
* arbitrary-script runner.
|
|
50
|
+
*/
|
|
51
|
+
function resolveHook(name) {
|
|
52
|
+
const abs = name.includes('/') || name.includes('\\')
|
|
53
|
+
? path.resolve(REPO_ROOT, name)
|
|
54
|
+
: /^[A-Za-z0-9._-]+$/.test(name)
|
|
55
|
+
? path.join(HOOK_DIR, name.endsWith('.mjs') ? name : `${name}.mjs`)
|
|
56
|
+
: null;
|
|
57
|
+
if (!abs) return null;
|
|
58
|
+
const inRepo = path.relative(REPO_ROOT, abs);
|
|
59
|
+
if (inRepo.startsWith('..') || path.isAbsolute(inRepo)) return null;
|
|
60
|
+
return abs;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- the off-switch: see modeOffFromProject() below (project-scoped; must agree with heyspex-mode.mjs resolution order:
|
|
64
|
+
// HEYSPEX_MODE env first, then <project>/.heyspex/mode.json).
|
|
65
|
+
|
|
66
|
+
// --- hooks the off-switch may NOT silence ---------------------------------
|
|
67
|
+
// The productivity hooks (reminders, injections, checkpoints) exist to help,
|
|
68
|
+
// so a global "HeySpex is off" is the right answer for them. A SECURITY
|
|
69
|
+
// control is different: the mode is decided by a repo file (.heyspex/mode.json)
|
|
70
|
+
// that any agent can write, and nothing guards that file — so sharing the
|
|
71
|
+
// off-switch would mean "write one JSON file, disarm the guard forever"
|
|
72
|
+
// (the disarm chain, C8). Anything named here runs regardless of mode.
|
|
73
|
+
// Each always-on hook keeps its OWN documented escape hatch, and it must be
|
|
74
|
+
// an ENV VAR (HEYSPEX_ENV_VAULT_GUARD_OFF=1 for env-vault-guard;
|
|
75
|
+
// HEYSPEX_RESEARCH_GUARD_OFF=1 for research-method-guard) — a human setting a
|
|
76
|
+
// variable for their session, not a file an agent can create.
|
|
77
|
+
// research-protocol-gate ([[HS-K0ZP]]) joins the set for the same reason as
|
|
78
|
+
// research-method-guard: its whole point is that the agent cannot switch it
|
|
79
|
+
// off. Escape hatch: HEYSPEX_RESEARCH_PROTOCOL_GATE_OFF=1, owner-set, env only.
|
|
80
|
+
// git-identity-guard (owner ruling 2026-09-19, vault lesson
|
|
81
|
+
// when-git-says-author-identity-unknown) has NO escape hatch at all — not
|
|
82
|
+
// even an env var: the owner's own path is `hs source identity set`, typed by
|
|
83
|
+
// a human, so there is nothing a hatch would enable that the sanctioned form
|
|
84
|
+
// does not already do. Agents read git identity; they never write it.
|
|
85
|
+
const ALWAYS_ON = new Set(['env-vault-guard', 'research-method-guard', 'research-protocol-gate', 'git-identity-guard']);
|
|
86
|
+
|
|
87
|
+
function isAlwaysOn(name) {
|
|
88
|
+
const base = String(name)
|
|
89
|
+
.replace(/^.*[\\/]/, '')
|
|
90
|
+
.replace(/\.mjs$/i, '');
|
|
91
|
+
return ALWAYS_ON.has(base);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── project roots the mode file may live in ─────────────────────────────────
|
|
95
|
+
// `hs mode off` writes <project>/.heyspex/mode.json. In a customer repo whose hooks come from the plugin /
|
|
96
|
+
// node_modules, REPO_ROOT is the package, not the project, so the dev's switch was ignored. Look in the PROJECT
|
|
97
|
+
// first: CLAUDE_PROJECT_DIR, then the hook payload's cwd, then walk up from process.cwd(); REPO_ROOT stays the
|
|
98
|
+
// last resort. HEYSPEX_MODE (env) keeps precedence over every file.
|
|
99
|
+
function projectRoots(payload) {
|
|
100
|
+
const roots = [];
|
|
101
|
+
const add = (r) => {
|
|
102
|
+
if (typeof r === 'string' && r.trim() && !roots.includes(r.trim())) roots.push(r.trim());
|
|
103
|
+
};
|
|
104
|
+
add(process.env.CLAUDE_PROJECT_DIR);
|
|
105
|
+
add(payload?.cwd);
|
|
106
|
+
try {
|
|
107
|
+
let d = process.cwd();
|
|
108
|
+
for (;;) {
|
|
109
|
+
if (fs.existsSync(path.join(d, '.heyspex', 'mode.json'))) {
|
|
110
|
+
add(d);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
const up = path.dirname(d);
|
|
114
|
+
if (up === d) break;
|
|
115
|
+
d = up;
|
|
116
|
+
}
|
|
117
|
+
} catch { /* ignore */ }
|
|
118
|
+
add(REPO_ROOT);
|
|
119
|
+
return roots;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function modeOffFromProject(payload) {
|
|
123
|
+
try {
|
|
124
|
+
const raw = String(process.env.HEYSPEX_MODE ?? '').trim().toLowerCase();
|
|
125
|
+
if (raw) return ['off', '0', 'false', 'disabled', 'disable', 'no'].includes(raw);
|
|
126
|
+
for (const root of projectRoots(payload)) {
|
|
127
|
+
const file = path.join(root, '.heyspex', 'mode.json');
|
|
128
|
+
if (!fs.existsSync(file)) continue;
|
|
129
|
+
// ANY root saying off wins: the dev's `hs mode off` lives in the project, an older switch in the platform root.
|
|
130
|
+
if (JSON.parse(fs.readFileSync(file, 'utf8'))?.mode === 'off') return true;
|
|
131
|
+
}
|
|
132
|
+
} catch { /* fail-open: on */ }
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── fail-open, outermost ([[HS-20SW-DRSM-DIAG-heyspex-capture-on-idea-and-workflow]]) ─────────────
|
|
137
|
+
// GitHub Copilot turns ANY non-zero hook exit into a deny (and 2 is always a deny with the reason lost), so a
|
|
138
|
+
// crash anywhere - resolution, import, a hook's own async throw - must exit 0. Exit 2 survives only where a hook
|
|
139
|
+
// truly decided "deny" (Claude Code: stderr reason; Copilot: translated to the JSON deny below).
|
|
140
|
+
process.on('uncaughtException', (err) => {
|
|
141
|
+
try { process.stderr.write(`[heyspex] hook ${target} crashed (failing open): ${err?.message ?? err}\n`); } catch { /* ignore */ }
|
|
142
|
+
process.exit(0);
|
|
143
|
+
});
|
|
144
|
+
process.on('unhandledRejection', (err) => {
|
|
145
|
+
try { process.stderr.write(`[heyspex] hook ${target} rejected (failing open): ${err?.message ?? err}\n`); } catch { /* ignore */ }
|
|
146
|
+
process.exit(0);
|
|
147
|
+
});
|
|
148
|
+
process.on('exit', (code) => {
|
|
149
|
+
if (code !== 0 && code !== 2) process.exitCode = 0;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
function readAll(stream, ms) {
|
|
153
|
+
return new Promise((resolve) => {
|
|
154
|
+
let buf = '';
|
|
155
|
+
const t = setTimeout(() => resolve(buf), ms);
|
|
156
|
+
t.unref?.();
|
|
157
|
+
stream.setEncoding('utf8');
|
|
158
|
+
stream.on('data', (c) => { buf += c; });
|
|
159
|
+
stream.on('end', () => { clearTimeout(t); resolve(buf); });
|
|
160
|
+
stream.on('error', () => resolve(buf));
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Copilot helpers (all best-effort; every failure means "run the hook normally") ─────────────────────────────
|
|
165
|
+
function readJson(file) {
|
|
166
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// The command text after the shim: `node -e "<bootstrap>" hook [args]` or `node ".../run-hook.mjs" hook [args]`.
|
|
170
|
+
function rowTokens(command) {
|
|
171
|
+
const c = String(command ?? '');
|
|
172
|
+
const m = c.match(/^node -e "[^"]*"\s+([\s\S]*)$/) || c.match(/\.mjs"?\s+([\s\S]*)$/);
|
|
173
|
+
return m ? m[1].trim().split(/\s+/) : [];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The matcher(s) registered for this hook + event, read from the project's Claude settings file and the plugin's
|
|
178
|
+
* hooks.json. A hook may sit under several matchers; ALL are returned (an unset matcher is ''). null = the hook
|
|
179
|
+
* is in no row we can read, so no filtering.
|
|
180
|
+
*/
|
|
181
|
+
function matchersForHook(eventName, payload) {
|
|
182
|
+
const files = [];
|
|
183
|
+
for (const r of projectRoots(payload)) files.push(path.join(r, '.claude', 'settings.json'));
|
|
184
|
+
for (const r of [process.env.CLAUDE_PLUGIN_ROOT, process.env.COPILOT_PLUGIN_ROOT, process.env.PLUGIN_ROOT]) {
|
|
185
|
+
if (r && r.trim()) files.push(path.join(r.trim(), 'hooks', 'hooks.json'));
|
|
186
|
+
}
|
|
187
|
+
files.push(path.join(REPO_ROOT, 'plugins', 'heyspex', 'hooks', 'hooks.json'));
|
|
188
|
+
const want = [target, ...rest].join(' ');
|
|
189
|
+
const matchers = [];
|
|
190
|
+
let found = false;
|
|
191
|
+
for (const file of [...new Set(files)]) {
|
|
192
|
+
const groups = readJson(file)?.hooks?.[eventName];
|
|
193
|
+
if (!Array.isArray(groups)) continue;
|
|
194
|
+
for (const g of groups) {
|
|
195
|
+
for (const h of g?.hooks ?? []) {
|
|
196
|
+
const t = rowTokens(h?.command);
|
|
197
|
+
if (t.length && t.join(' ') === want) { found = true; matchers.push(typeof g.matcher === 'string' ? g.matcher : ''); }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return found ? matchers : null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const DEDUPE_WINDOW_MS = 2000;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Double-firing guard. The first run of a key atomically creates a stamp (flag 'wx'); a second run inside 2 s is
|
|
208
|
+
* a duplicate. The first run stores its final stdout in the stamp so a duplicate replays a DENY instead of
|
|
209
|
+
* silently allowing. Keyed by (session, event, hook+args, tool, timestamp | input hash, project).
|
|
210
|
+
*/
|
|
211
|
+
function claimDedupe(payload, eventName) {
|
|
212
|
+
const none = { duplicate: false };
|
|
213
|
+
try {
|
|
214
|
+
if (String(process.env.HEYSPEX_COPILOT_DEDUPE_OFF ?? '').trim() === '1') return none;
|
|
215
|
+
const session = payload.session_id ?? payload.sessionId;
|
|
216
|
+
if (typeof session !== 'string' || !session) return none;
|
|
217
|
+
const ident = payload.timestamp !== undefined
|
|
218
|
+
? String(payload.timestamp)
|
|
219
|
+
: crypto.createHash('sha1').update(JSON.stringify([payload.tool_input ?? null, payload.prompt ?? null])).digest('hex');
|
|
220
|
+
const key = crypto.createHash('sha1')
|
|
221
|
+
.update(JSON.stringify([session, eventName, target, rest, payload.tool_name ?? '', ident, payload.cwd ?? '']))
|
|
222
|
+
.digest('hex');
|
|
223
|
+
const dir = process.env.HEYSPEX_COPILOT_DEDUPE_DIR?.trim() || path.join(os.tmpdir(), 'heyspex-copilot-dedupe');
|
|
224
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
225
|
+
const file = path.join(dir, `${key}.json`);
|
|
226
|
+
const now = Date.now();
|
|
227
|
+
try {
|
|
228
|
+
fs.writeFileSync(file, JSON.stringify({ t: now, out: '' }), { flag: 'wx' });
|
|
229
|
+
} catch (e) {
|
|
230
|
+
if (e?.code !== 'EEXIST') return none;
|
|
231
|
+
const prev = readJson(file);
|
|
232
|
+
if (prev && typeof prev.t === 'number' && now - prev.t >= 0 && now - prev.t < DEDUPE_WINDOW_MS) {
|
|
233
|
+
return { duplicate: true, replay: typeof prev.out === 'string' ? prev.out : '' };
|
|
234
|
+
}
|
|
235
|
+
fs.writeFileSync(file, JSON.stringify({ t: now, out: '' })); // stale stamp: this run owns it
|
|
236
|
+
}
|
|
237
|
+
try { // opportunistic cleanup of old stamps
|
|
238
|
+
for (const f of fs.readdirSync(dir)) {
|
|
239
|
+
const fp = path.join(dir, f);
|
|
240
|
+
if (now - fs.statSync(fp).mtimeMs > 60000) fs.rmSync(fp, { force: true });
|
|
241
|
+
}
|
|
242
|
+
} catch { /* ignore */ }
|
|
243
|
+
return { duplicate: false, store: (final) => { if (final) fs.writeFileSync(file, JSON.stringify({ t: now, out: final })); } };
|
|
244
|
+
} catch {
|
|
245
|
+
return none;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** True exactly once per (project, session): creates <project>/<state dir>/copilot-style-<session>.stamp. */
|
|
250
|
+
function claimSessionStyleStamp(payload) {
|
|
251
|
+
try {
|
|
252
|
+
const session = String(payload.session_id ?? payload.sessionId ?? '').replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 80);
|
|
253
|
+
if (!session) return false;
|
|
254
|
+
const root = projectRoots(payload).find((r) => { try { return fs.statSync(r).isDirectory(); } catch { return false; } });
|
|
255
|
+
if (!root) return false;
|
|
256
|
+
const dir = path.join(root, STATE_DIR);
|
|
257
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
258
|
+
try { fs.writeFileSync(path.join(dir, `copilot-style-${session}.stamp`), new Date().toISOString(), { flag: 'wx' }); } catch { return false; }
|
|
259
|
+
try { // drop stamps older than a week
|
|
260
|
+
for (const f of fs.readdirSync(dir)) {
|
|
261
|
+
if (f.startsWith('copilot-style-') && f.endsWith('.stamp') && Date.now() - fs.statSync(path.join(dir, f)).mtimeMs > 7 * 86400000) fs.rmSync(path.join(dir, f), { force: true });
|
|
262
|
+
}
|
|
263
|
+
} catch { /* ignore */ }
|
|
264
|
+
return true;
|
|
265
|
+
} catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function main() {
|
|
271
|
+
// stdin is read ONCE here (harness detection and the mode file's project root both need the payload) and
|
|
272
|
+
// handed on to the hook as a fresh stream, so every hook still reads process.stdin exactly as before.
|
|
273
|
+
let text = '';
|
|
274
|
+
try {
|
|
275
|
+
if (!process.stdin.isTTY) text = await readAll(process.stdin, 4000);
|
|
276
|
+
} catch { /* ignore */ }
|
|
277
|
+
let payload = null;
|
|
278
|
+
try { payload = text.trim() ? JSON.parse(text) : null; } catch { payload = null; }
|
|
279
|
+
|
|
280
|
+
if (!isAlwaysOn(target) && modeOffFromProject(payload)) process.exit(0);
|
|
281
|
+
|
|
282
|
+
// ── harness detection + the Copilot dialect ──────────────────────────────
|
|
283
|
+
let harness = 'unknown';
|
|
284
|
+
let copilot = null;
|
|
285
|
+
try {
|
|
286
|
+
const { detectHarness } = await import(pathToFileURL(path.join(REPO_ROOT, 'spexharness', 'src', 'adapters', 'detect-harness.mjs')).href);
|
|
287
|
+
harness = detectHarness(payload, process.env);
|
|
288
|
+
} catch { /* no adapter in this install: behave exactly as Claude Code */ }
|
|
289
|
+
|
|
290
|
+
if (harness === 'copilot') {
|
|
291
|
+
try {
|
|
292
|
+
copilot = await import(pathToFileURL(path.join(REPO_ROOT, 'spexharness', 'src', 'adapters', 'copilot.mjs')).href);
|
|
293
|
+
} catch {
|
|
294
|
+
// Copilot denies on exit 2 and on any crash, and without the adapter we cannot speak its dialect: fail OPEN.
|
|
295
|
+
process.stderr.write(`[heyspex] hook ${target}: Copilot adapter unavailable (failing open)\n`);
|
|
296
|
+
process.exit(0);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (copilot) await copilotBranch(copilot);
|
|
300
|
+
|
|
301
|
+
// ── the Copilot branch ([[HS-20SW-DRSM-DIAG-heyspex-capture-on-idea-and-workflow]]) ──────────────────────
|
|
302
|
+
// Everything below runs ONLY when the harness is GitHub Copilot. Guarantees for Copilot, whatever fails:
|
|
303
|
+
// * the process exits 0 (Copilot turns ANY non-zero exit into a deny, and 2 loses the reason);
|
|
304
|
+
// * stdout is empty (allow) or ONE valid JSON object (a gate's own decision) - never partial / malformed text;
|
|
305
|
+
// * a crash, a throwing hook, an unhandled rejection or a bad import is an ALLOW (the _run.mjs handlers above
|
|
306
|
+
// exit 0; the exit hook below translates the buffered output).
|
|
307
|
+
// Behaviors, in order:
|
|
308
|
+
// 1. opt-out: HEYSPEX_COPILOT_HOOKS_OFF=1 / config agent_behaviors.harness_hooks.copilot.enabled: false
|
|
309
|
+
// silences every hook except the ALWAYS_ON security gates.
|
|
310
|
+
// 2. normalize the payload (camelCase or PascalCase, MCP tool-name spellings -> mcp__heyspex__*).
|
|
311
|
+
// 3. script-side tool filter: the VS Code Local harness ignores matchers and fires every hook on every tool,
|
|
312
|
+
// so the matcher of the registered row (.claude/settings.json or the plugin hooks.json) is re-applied here.
|
|
313
|
+
// 4. double-firing guard: Copilot CLI loads hooks from the plugin AND .claude/settings.json; a duplicate
|
|
314
|
+
// (same session, event, hook, tool, timestamp) inside 2 s exits 0 (replaying a stored decision, if any).
|
|
315
|
+
// 5. prompt-time context: Copilot DROPS UserPromptSubmit output, so a context-only result prints nothing
|
|
316
|
+
// there. SessionStart accepts additionalContext, so ONCE PER SESSION (stamp file under the project's
|
|
317
|
+
// state dir) the SessionStart run also emits inject-communication-style's static text as additionalContext.
|
|
318
|
+
async function copilotBranch(copilot) {
|
|
319
|
+
if (!isAlwaysOn(target)) {
|
|
320
|
+
let off = String(process.env.HEYSPEX_COPILOT_HOOKS_OFF ?? '').trim() === '1';
|
|
321
|
+
if (!off) {
|
|
322
|
+
try {
|
|
323
|
+
const { isConfigKeyEnabled } = await import(pathToFileURL(path.join(HOOK_DIR, '_guardrail-loader.mjs')).href);
|
|
324
|
+
off = !isConfigKeyEnabled('agent_behaviors.harness_hooks.copilot.enabled');
|
|
325
|
+
} catch { /* config unreadable: stay on */ }
|
|
326
|
+
}
|
|
327
|
+
if (off) process.exit(0);
|
|
328
|
+
}
|
|
329
|
+
payload = copilot.normalizeCopilotPayload(payload);
|
|
330
|
+
text = JSON.stringify(payload);
|
|
331
|
+
const eventName = typeof payload.hook_event_name === 'string' ? payload.hook_event_name : '';
|
|
332
|
+
|
|
333
|
+
// (3) script-side tool filter
|
|
334
|
+
let skip = false;
|
|
335
|
+
try {
|
|
336
|
+
if (eventName && typeof payload.tool_name === 'string') {
|
|
337
|
+
const matchers = matchersForHook(eventName, payload);
|
|
338
|
+
skip = !!matchers && !copilot.toolMatchesAnyMatcher(matchers, copilot.toolNameCandidates(payload));
|
|
339
|
+
}
|
|
340
|
+
} catch { /* lookup failure: run the hook */ }
|
|
341
|
+
if (skip) process.exit(0);
|
|
342
|
+
|
|
343
|
+
// (4) double-firing guard
|
|
344
|
+
const dedupe = claimDedupe(payload, eventName);
|
|
345
|
+
if (dedupe.duplicate) {
|
|
346
|
+
if (dedupe.replay) { try { fs.writeSync(1, dedupe.replay); } catch { /* ignore */ } }
|
|
347
|
+
process.exit(0);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// (5) once-per-session static advisory at SessionStart
|
|
351
|
+
let extraContext = '';
|
|
352
|
+
if (eventName === 'SessionStart' && claimSessionStyleStamp(payload)) {
|
|
353
|
+
try {
|
|
354
|
+
const mod = await import(pathToFileURL(path.join(HOOK_DIR, 'inject-communication-style.mjs')).href);
|
|
355
|
+
extraContext = typeof mod.copilotSessionStartText === 'function' ? await mod.copilotSessionStartText() : '';
|
|
356
|
+
} catch { /* advisory only */ }
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Copilot reads ONE top-level {permissionDecision, permissionDecisionReason}; buffer stdout/stderr and
|
|
360
|
+
// translate at exit: Claude-dialect JSON -> top-level, and a legacy `exit 2 + stderr` deny -> JSON deny, exit 0.
|
|
361
|
+
let out = '';
|
|
362
|
+
let err = '';
|
|
363
|
+
const realStderr = process.stderr.write.bind(process.stderr);
|
|
364
|
+
process.stdout.write = (chunk, enc, cb) => { out += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'); if (typeof enc === 'function') enc(); else if (typeof cb === 'function') cb(); return true; };
|
|
365
|
+
process.stderr.write = (chunk, enc, cb) => { err += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'); if (typeof enc === 'function') enc(); else if (typeof cb === 'function') cb(); return true; };
|
|
366
|
+
process.on('exit', (code) => {
|
|
367
|
+
process.exitCode = 0; // Copilot: never non-zero, whatever happened
|
|
368
|
+
let final = '';
|
|
369
|
+
try {
|
|
370
|
+
final = out ? copilot.finalizeCopilotStdout(out, eventName) : '';
|
|
371
|
+
if (code === 2 && !final) final = copilot.denyFromLegacyExit(err);
|
|
372
|
+
else if (err && code !== 2) realStderr(err);
|
|
373
|
+
if (extraContext) final = copilot.mergeAdditionalContext(final, extraContext);
|
|
374
|
+
} catch { final = ''; }
|
|
375
|
+
try { dedupe.store?.(final); } catch { /* best effort */ }
|
|
376
|
+
try { if (final) fs.writeSync(1, final); } catch { /* never let the translation break the hook */ }
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// hand the (already read) payload on as a fresh stdin
|
|
381
|
+
if (!process.stdin.isTTY) {
|
|
382
|
+
try {
|
|
383
|
+
const { Readable } = await import('node:stream');
|
|
384
|
+
const fresh = new Readable({ read() {} });
|
|
385
|
+
if (text) fresh.push(Buffer.from(text, 'utf8'));
|
|
386
|
+
fresh.push(null);
|
|
387
|
+
Object.defineProperty(process, 'stdin', { value: fresh, configurable: true });
|
|
388
|
+
} catch { /* hooks will see an empty stdin and fail open themselves */ }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const hookPath = resolveHook(target);
|
|
392
|
+
if (!hookPath || !fs.existsSync(hookPath)) {
|
|
393
|
+
process.stderr.write(`[heyspex] _run.mjs: hook not found: ${target}\n`);
|
|
394
|
+
process.exit(0);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Make the hook believe it was invoked directly.
|
|
398
|
+
process.argv = [process.argv[0], hookPath, ...rest];
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
await import(pathToFileURL(hookPath).href);
|
|
402
|
+
} catch (err) {
|
|
403
|
+
process.stderr.write(`[heyspex] hook ${target} failed: ${err?.message ?? err}\n`);
|
|
404
|
+
process.exit(0);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
await main();
|
|
410
|
+
} catch (err) {
|
|
411
|
+
process.stderr.write(`[heyspex] _run.mjs failed (failing open): ${err?.message ?? err}\n`);
|
|
412
|
+
process.exit(0);
|
|
413
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Shared Stop-hook input helpers: read the hook's stdin JSON payload and pull the
|
|
3
|
+
// agent's own last message out of the transcript JSONL. Split out of
|
|
4
|
+
// naughty-list-gate.mjs (its original sole owner) so a second Stop-hook scanner
|
|
5
|
+
// (id-display-format-gate.mjs) doesn't fork a copy of the same 40 lines.
|
|
6
|
+
//
|
|
7
|
+
// DEPENDENCY-FREE, same contract as _guardrail-loader.mjs: every function degrades
|
|
8
|
+
// to null/'' on any read/parse failure rather than throwing, so a caller's own
|
|
9
|
+
// fail-safe wrapper never has to guard against this module misbehaving.
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
|
|
13
|
+
/** Read and parse the hook's stdin JSON payload. Resolves to null on any failure. */
|
|
14
|
+
export async function readStdinJson() {
|
|
15
|
+
if (process.stdin.isTTY) return null;
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
let buf = '';
|
|
18
|
+
let settled = false;
|
|
19
|
+
const finish = (value) => {
|
|
20
|
+
if (settled) return;
|
|
21
|
+
settled = true;
|
|
22
|
+
resolve(value);
|
|
23
|
+
};
|
|
24
|
+
const timer = setTimeout(() => finish(buf || ''), 200);
|
|
25
|
+
process.stdin.setEncoding('utf8');
|
|
26
|
+
process.stdin.on('data', (chunk) => { buf += chunk; });
|
|
27
|
+
process.stdin.on('end', () => { clearTimeout(timer); finish(buf); });
|
|
28
|
+
process.stdin.on('error', () => { clearTimeout(timer); finish(buf); });
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Read the last assistant message's text out of a transcript JSONL file. */
|
|
33
|
+
export function lastAssistantText(transcriptPath) {
|
|
34
|
+
let raw;
|
|
35
|
+
try {
|
|
36
|
+
raw = fs.readFileSync(transcriptPath, 'utf8');
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const lines = raw.split(/\r?\n/).filter(Boolean);
|
|
41
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
42
|
+
let evt;
|
|
43
|
+
try { evt = JSON.parse(lines[i]); } catch { continue; }
|
|
44
|
+
const msg = evt?.message;
|
|
45
|
+
const role = msg?.role ?? evt?.role;
|
|
46
|
+
if (role !== 'assistant') continue;
|
|
47
|
+
const content = msg?.content;
|
|
48
|
+
if (typeof content === 'string') return content;
|
|
49
|
+
if (Array.isArray(content)) {
|
|
50
|
+
const text = content
|
|
51
|
+
.filter((b) => b && b.type === 'text' && typeof b.text === 'string')
|
|
52
|
+
.map((b) => b.text)
|
|
53
|
+
.join('\n')
|
|
54
|
+
.trim();
|
|
55
|
+
return text || null;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|