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,129 @@
|
|
|
1
|
+
// spexharness/src/gates/claim-before-code-gate.mjs
|
|
2
|
+
//
|
|
3
|
+
// Engine port of scripts/claude-hooks/claim-before-code-gate.mjs — the
|
|
4
|
+
// PreToolUse gate that denies the first post-plan CODE edit made by a
|
|
5
|
+
// session that exited plan mode with no active claim
|
|
6
|
+
// ([[HS-38J5-2BXD-DIAG-plan-execute-boundary-has-no-ingest-clai]] item 2).
|
|
7
|
+
// See ./PORTING.md for the recipe this follows and ./env-vault-guard.mjs for
|
|
8
|
+
// the reference port.
|
|
9
|
+
//
|
|
10
|
+
// This module does NOT reimplement the guard's decision logic. The pure
|
|
11
|
+
// decision function — `shouldDenyEdit({ toolName, filePath, hasMarker,
|
|
12
|
+
// hasNoClaimSignal })` — with no stdin/stdout/process-exit of its own, lives
|
|
13
|
+
// in ./claim-before-code-gate.decide.mjs (relocated out of the original hook
|
|
14
|
+
// script, HS-IQBF; see PORTING.md and that sibling module's own header for
|
|
15
|
+
// why) and is IMPORTED here unchanged: it already encapsulates the
|
|
16
|
+
// tool-name filter (EDIT_TOOL_NAMES) and the code-path filter
|
|
17
|
+
// (isCodeEditPath) internally, so this module never needs its own copy of
|
|
18
|
+
// either.
|
|
19
|
+
//
|
|
20
|
+
// The two SIGNAL readers (hasMarker / hasNoClaimSignal) are also imported
|
|
21
|
+
// unchanged, not re-derived:
|
|
22
|
+
// - scripts/claude-hooks/plan-exit-ingest-gate.mjs — hasFreshPlanExitMarker,
|
|
23
|
+
// repoRoot. Also guarded by its own INVOKED_DIRECTLY check.
|
|
24
|
+
// - scripts/claude-hooks/claim-liveness-shared.mjs — resolveSessionId,
|
|
25
|
+
// hasFreshNoClaimSignal. A dependency-free shared module with no
|
|
26
|
+
// unconditional main() at all (its own async orchestration function is
|
|
27
|
+
// exported, never self-invoked at import time).
|
|
28
|
+
//
|
|
29
|
+
// Event mapping: PreToolUse (Edit|Write|MultiEdit) → before_tool.
|
|
30
|
+
// stdin.session_id / stdin.conversation_id → evt.sessionId (resolveSessionId
|
|
31
|
+
// also falls back to the real process env, unchanged).
|
|
32
|
+
// stdin.tool_input.file_path (or .path) → evt.toolInput.file_path/.path.
|
|
33
|
+
//
|
|
34
|
+
// Decision mapping — TWO rungs, same shape as plan-review-gate.mjs's port:
|
|
35
|
+
// CLAUDE_AGENT=true or HEYSPEX_CLAIM_GATE_INTERACTIVE=block ("forReal") maps
|
|
36
|
+
// a `shouldDenyEdit` true to a real `deny`; the interactive default maps the
|
|
37
|
+
// SAME finding to a `rewrite` envelope (`{ warn: true, reason, rule }`) — the
|
|
38
|
+
// edit still proceeds, matching the original's stderr-only warn path
|
|
39
|
+
// exactly. `shouldDenyEdit` false (no marker, no fresh no-claim signal, wrong
|
|
40
|
+
// tool, or a specs/.heyspex/.claude/.git path) → plain `allow`.
|
|
41
|
+
//
|
|
42
|
+
// Not ported: the original's `appendGuardrailEvent(...)` audit-log write on
|
|
43
|
+
// a fire — matching every other port in this set.
|
|
44
|
+
//
|
|
45
|
+
// Env hatches (unchanged):
|
|
46
|
+
// HEYSPEX_CLAIM_GATE_OFF=1 Hard kill switch — silences both rungs.
|
|
47
|
+
// HEYSPEX_CLAIM_GATE_INTERACTIVE=block Raise the interactive rung to a real deny.
|
|
48
|
+
// CLAUDE_AGENT=true Unattended session → real deny (unchanged).
|
|
49
|
+
//
|
|
50
|
+
// `ctx` contract (all optional; every field defaults to a real fs/env read):
|
|
51
|
+
// ctx.cwd - repo root to resolve the plan-exit marker and
|
|
52
|
+
// claim-liveness cache from. Defaults to
|
|
53
|
+
// plan-exit-ingest-gate.mjs's own repoRoot()
|
|
54
|
+
// (CLAUDE_PROJECT_DIR, else this repo's root).
|
|
55
|
+
// ctx.sessionId - override for the resolved session id (tests
|
|
56
|
+
// pin this directly instead of an env var).
|
|
57
|
+
// Defaults to resolveSessionId() against
|
|
58
|
+
// `{ session_id: evt.sessionId }`.
|
|
59
|
+
// ctx.env - env var overrides (tests use this instead of
|
|
60
|
+
// mutating process.env).
|
|
61
|
+
|
|
62
|
+
import { resolveSessionId, hasFreshNoClaimSignal } from '../../../scripts/claude-hooks/claim-liveness-shared.mjs';
|
|
63
|
+
import { hasFreshPlanExitMarker, repoRoot as planExitRepoRoot } from '../../../scripts/claude-hooks/plan-exit-ingest-gate.mjs';
|
|
64
|
+
import { shouldDenyEdit } from './claim-before-code-gate.decide.mjs';
|
|
65
|
+
|
|
66
|
+
// Copied verbatim from the original's own (unexported) denyReason.
|
|
67
|
+
function denyReason() {
|
|
68
|
+
return (
|
|
69
|
+
'HEYSPEX claim gate: this session exited plan mode and has no active claim — ' +
|
|
70
|
+
'the plan→execute boundary requires filing the epic (if not yet filed) and calling ' +
|
|
71
|
+
'heyspex_claim_spec before the first code edit ([[HS-38J5-2BXD-DIAG-plan-execute-boundary-has-no-ingest-clai]]). ' +
|
|
72
|
+
'Claim the spec you are working, then retry this edit. ' +
|
|
73
|
+
'(Break-glass: set HEYSPEX_CLAIM_GATE_OFF=1 for this session.)'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function shouldBlockForReal(env) {
|
|
78
|
+
return env.CLAUDE_AGENT === 'true' || env.HEYSPEX_CLAIM_GATE_INTERACTIVE === 'block';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The claim-before-code gate's id — unique across the built-in registry. */
|
|
82
|
+
export const id = 'claim-before-code-gate';
|
|
83
|
+
/** Runs pre-tool, same as its PreToolUse Claude Code hook registration. */
|
|
84
|
+
export const event = 'before_tool';
|
|
85
|
+
/** Caller-defined severity label; this gate can deny, so it is "block". */
|
|
86
|
+
export const severity = 'block';
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {{ point: string, toolName?: string, toolInput?: Record<string, unknown>, sessionId?: string }} evt
|
|
90
|
+
* @param {{ cwd?: string, sessionId?: string, env?: Record<string, string | undefined> }} [ctx]
|
|
91
|
+
* @returns {{ decision: 'allow'|'deny'|'rewrite', reason?: string, rewrite?: object }}
|
|
92
|
+
*/
|
|
93
|
+
export function run(evt, ctx = {}) {
|
|
94
|
+
const env = ctx.env ?? process.env;
|
|
95
|
+
if (String(env.HEYSPEX_CLAIM_GATE_OFF ?? '') === '1') {
|
|
96
|
+
return { decision: 'allow' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const toolName = evt?.toolName;
|
|
100
|
+
const toolInput = evt?.toolInput ?? {};
|
|
101
|
+
const filePath = toolInput.file_path ?? toolInput.path;
|
|
102
|
+
|
|
103
|
+
const sessionId = ctx.sessionId ?? resolveSessionId({ session_id: evt?.sessionId });
|
|
104
|
+
if (!sessionId) return { decision: 'allow' }; // can't scope the check — fail open
|
|
105
|
+
|
|
106
|
+
const root = ctx.cwd ?? planExitRepoRoot();
|
|
107
|
+
const hasMarker = hasFreshPlanExitMarker(sessionId, root);
|
|
108
|
+
const hasNoClaimSignal = hasFreshNoClaimSignal(sessionId, root);
|
|
109
|
+
|
|
110
|
+
if (!shouldDenyEdit({ toolName, filePath, hasMarker, hasNoClaimSignal })) {
|
|
111
|
+
return { decision: 'allow' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const reason = denyReason();
|
|
115
|
+
const forReal = shouldBlockForReal(env);
|
|
116
|
+
if (forReal) return { decision: 'deny', reason };
|
|
117
|
+
|
|
118
|
+
// Interactive warn path: byte-for-byte the original hook's fireDeny()
|
|
119
|
+
// stderr banner ([claim-before-code-gate] warning — ...\n(Interactive
|
|
120
|
+
// session: ...)\n) — kept here (not left to a generic adapter formatter)
|
|
121
|
+
// because it is gate-specific text, same convention as naughty-list-gate's
|
|
122
|
+
// and capture-workflow-gate's own self-contained warn reasons.
|
|
123
|
+
const warnReason =
|
|
124
|
+
`[claim-before-code-gate] warning — ${reason}\n` +
|
|
125
|
+
'(Interactive session: this only warns. Set HEYSPEX_CLAIM_GATE_INTERACTIVE=block to enforce it here too.)\n';
|
|
126
|
+
return { decision: 'rewrite', reason: warnReason, rewrite: { warn: true, reason: warnReason, rule: 'claim-before-code-gate' } };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default { id, event, severity, run };
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// spexharness/src/gates/deep-research-cost-gate.mjs
|
|
2
|
+
//
|
|
3
|
+
// Engine port of scripts/claude-hooks/deep-research-cost-gate.mjs — the
|
|
4
|
+
// PreToolUse cost-confirmation + grounding gate over a Workflow/Skill call
|
|
5
|
+
// that launches deep-research (HS-BLH1 grounding extension, epic HS-7HR9).
|
|
6
|
+
// See ./PORTING.md for the general recipe this follows.
|
|
7
|
+
//
|
|
8
|
+
// JUDGMENT CALL — bigger than git-stash-gate.mjs's, read before touching
|
|
9
|
+
// this file: the original script exports NOTHING — no pure decision
|
|
10
|
+
// function, not even its `COST` table or its regex matchers. Its entire
|
|
11
|
+
// decision (inlineScriptTarget / resolveTarget / groundingBlock /
|
|
12
|
+
// isGroundOnlyResearchLaunch / the COST table) is private to the module and
|
|
13
|
+
// tangled with `main()`'s stdin read, stdout write and exit. PORTING.md's
|
|
14
|
+
// step-1 default remedy — extract a pure function INSIDE the original — is
|
|
15
|
+
// exactly what this porting task's own brief forbids for this pass ("do NOT
|
|
16
|
+
// delete or edit anything under scripts/claude-hooks/ ... shells come
|
|
17
|
+
// later"). So, unlike the reference port and unlike git-stash-gate.mjs's
|
|
18
|
+
// port (which could reuse several already-exported primitives), THIS module
|
|
19
|
+
// re-derives the original's decision logic by hand — copied verbatim where
|
|
20
|
+
// the original's own strings/tables are copyable (the COST table, the
|
|
21
|
+
// deny/ask message templates, the grounding-artifact field list), so
|
|
22
|
+
// today's golden fixtures still match. The one real import is
|
|
23
|
+
// `loadResearchFocus` (vendor/heyspex-mcp/research-focus.mjs), which WAS
|
|
24
|
+
// already exported and pure — reused, not re-derived. If the original
|
|
25
|
+
// script is ever changed to export its logic (the "shells come later" plan
|
|
26
|
+
// this brief refers to), this module's private copies should be deleted in
|
|
27
|
+
// favor of importing the real thing — until then, a change to the
|
|
28
|
+
// original's regexes/COST table needs a matching edit here, and vice versa;
|
|
29
|
+
// there is no structural drift-proofing possible while the original stays
|
|
30
|
+
// unexported.
|
|
31
|
+
//
|
|
32
|
+
// Event mapping: PreToolUse (Workflow|Skill) → before_tool.
|
|
33
|
+
// Decision mapping: allow stays allow; the inline-script backstop's `deny`
|
|
34
|
+
// stays `deny`; the grounding block's `deny` stays `deny`; the cost
|
|
35
|
+
// confirmation's `ask` is carried forward as a `rewrite` envelope
|
|
36
|
+
// (`{ ask: true, reason }`) — the same mapping env-vault-guard.mjs's
|
|
37
|
+
// reference port uses for its own `ask` case, since the engine has no
|
|
38
|
+
// synchronous-approval primitive.
|
|
39
|
+
//
|
|
40
|
+
// Env hatches (unchanged): HEYSPEX_DEEP_RESEARCH_GATE_OFF=1 (mutes the whole
|
|
41
|
+
// gate), HEYSPEX_RESEARCH_GROUNDING_GATE_OFF=1 (mutes only the grounding
|
|
42
|
+
// check — the cost ask still fires).
|
|
43
|
+
//
|
|
44
|
+
// `ctx` contract (all optional):
|
|
45
|
+
// ctx.env - env var overrides (falls back to
|
|
46
|
+
// process.env).
|
|
47
|
+
// ctx.cwd - vault root to resolve the
|
|
48
|
+
// research-focus pin and grounding
|
|
49
|
+
// artifact from. Defaults to
|
|
50
|
+
// CLAUDE_PROJECT_DIR, else this repo's
|
|
51
|
+
// root.
|
|
52
|
+
// ctx.loadResearchFocus(root) - override for the research-focus
|
|
53
|
+
// reader. Defaults to the real
|
|
54
|
+
// loadResearchFocus.
|
|
55
|
+
// ctx.readGroundingArtifact(root) - override for the grounding-artifact
|
|
56
|
+
// reader. Tests inject fakes here
|
|
57
|
+
// instead of touching disk.
|
|
58
|
+
|
|
59
|
+
import fs from 'node:fs';
|
|
60
|
+
import path from 'node:path';
|
|
61
|
+
import { fileURLToPath } from 'node:url';
|
|
62
|
+
|
|
63
|
+
import { loadResearchFocus } from '../../../vendor/heyspex-mcp/research-focus.mjs';
|
|
64
|
+
|
|
65
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
66
|
+
const REPO_ROOT = path.resolve(HERE, '..', '..', '..');
|
|
67
|
+
|
|
68
|
+
// Copied verbatim from the original (private consts there) — see the
|
|
69
|
+
// judgment-call note above.
|
|
70
|
+
// Exported (additive, HS-KNJ8) so scripts/lib/capture-execution-guard.mjs reads the SAME record with the SAME shape.
|
|
71
|
+
export const GROUNDING_ARTIFACT_REL = path.join('.heyspex', 'research-grounding.json');
|
|
72
|
+
export const GROUNDING_FIELDS =['already_built', 'already_decided', 'owner_reference', 'residual_delta'];
|
|
73
|
+
|
|
74
|
+
const COST = {
|
|
75
|
+
'deep-research': {
|
|
76
|
+
agents: '~97',
|
|
77
|
+
label: 'the BUILT-IN deep-research',
|
|
78
|
+
extra:
|
|
79
|
+
' This is the EXPENSIVE built-in harness and it RATE-LIMITS. Strongly prefer a HeySpex bounded profile instead: deep-research-thorough (~31 agents) or deep-research-quick (~19 agents).',
|
|
80
|
+
},
|
|
81
|
+
'deep-research-thorough': {
|
|
82
|
+
agents: '~31',
|
|
83
|
+
label: 'deep-research-thorough (HeySpex bounded, 3-vote)',
|
|
84
|
+
extra: '',
|
|
85
|
+
},
|
|
86
|
+
'deep-research-medium': {
|
|
87
|
+
agents: '~24',
|
|
88
|
+
label: 'deep-research-medium (HeySpex bounded, 2-vote)',
|
|
89
|
+
extra: '',
|
|
90
|
+
},
|
|
91
|
+
'deep-research-quick': {
|
|
92
|
+
agents: '~19',
|
|
93
|
+
label: 'deep-research-quick (HeySpex bounded, single-vote)',
|
|
94
|
+
extra: '',
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Detect the recurring broken launch: a hand-authored inline `script` (often
|
|
99
|
+
// with a `title` like "dynamic workflow: deep-research-thorough") that does
|
|
100
|
+
// not lead with `export const meta` → the harness rejects it. We only trip
|
|
101
|
+
// when there is NO `name`/`scriptPath` (the correct paths) but an inline
|
|
102
|
+
// `script`/`title` clearly targets a deep-research profile. Copied verbatim
|
|
103
|
+
// from the original — see the judgment-call note above.
|
|
104
|
+
function inlineScriptTarget(input) {
|
|
105
|
+
if (typeof input.name === 'string' || typeof input.scriptPath === 'string') return null;
|
|
106
|
+
const hay = `${typeof input.script === 'string' ? input.script : ''}\n${typeof input.title === 'string' ? input.title : ''}`;
|
|
107
|
+
if (!hay.trim()) return null;
|
|
108
|
+
if (/deep-research-thorough/.test(hay)) return 'deep-research-thorough';
|
|
109
|
+
if (/deep-research-medium/.test(hay)) return 'deep-research-medium';
|
|
110
|
+
if (/deep-research-quick/.test(hay)) return 'deep-research-quick';
|
|
111
|
+
if (/deep-research/.test(hay)) return 'deep-research-thorough'; // steer built-in/unmapped to the bounded default
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Resolve which deep-research target a Workflow/Skill call refers to, or
|
|
116
|
+
// null. Copied verbatim from the original.
|
|
117
|
+
function resolveTarget(toolName, input) {
|
|
118
|
+
const candidates = [];
|
|
119
|
+
if (toolName === 'Workflow') {
|
|
120
|
+
if (typeof input.name === 'string') candidates.push(input.name);
|
|
121
|
+
if (typeof input.scriptPath === 'string') candidates.push(input.scriptPath);
|
|
122
|
+
} else if (toolName === 'Skill') {
|
|
123
|
+
if (typeof input.skill === 'string') candidates.push(input.skill);
|
|
124
|
+
}
|
|
125
|
+
for (const c of candidates) {
|
|
126
|
+
if (Object.prototype.hasOwnProperty.call(COST, c)) return c;
|
|
127
|
+
}
|
|
128
|
+
for (const c of candidates) {
|
|
129
|
+
if (/deep-research-thorough/.test(c)) return 'deep-research-thorough';
|
|
130
|
+
if (/deep-research-medium/.test(c)) return 'deep-research-medium';
|
|
131
|
+
if (/deep-research-quick/.test(c)) return 'deep-research-quick';
|
|
132
|
+
if (/deep-research/.test(c)) return 'deep-research';
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// `/hs-res` spends no Anthropic tokens (so no cost ask), but IS still a full
|
|
138
|
+
// external research pass and must still be grounded. Copied verbatim from
|
|
139
|
+
// the original.
|
|
140
|
+
function isGroundOnlyResearchLaunch(toolName, input) {
|
|
141
|
+
if (toolName !== 'Skill') return false;
|
|
142
|
+
return typeof input.skill === 'string' && /^hs-res$/i.test(input.skill.trim());
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function defaultReadGroundingArtifact(root) {
|
|
146
|
+
try {
|
|
147
|
+
const full = path.join(root, GROUNDING_ARTIFACT_REL);
|
|
148
|
+
if (!fs.existsSync(full)) return null;
|
|
149
|
+
return JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Returns a deny-reason string when a promotion-driven deep-research launch
|
|
156
|
+
// is NOT grounded, else null (allow). Copied verbatim from the original
|
|
157
|
+
// (parameterized on `root`/`deps` instead of calling repoRoot() and the
|
|
158
|
+
// module-level readers directly, so ctx can override both for tests).
|
|
159
|
+
function groundingBlock(root, deps) {
|
|
160
|
+
let focus;
|
|
161
|
+
try {
|
|
162
|
+
focus = deps.loadResearchFocus(root);
|
|
163
|
+
} catch {
|
|
164
|
+
return null; // discovery failure → fail open
|
|
165
|
+
}
|
|
166
|
+
const sourceIdeaId = focus?.source_idea_id;
|
|
167
|
+
if (!sourceIdeaId) return null; // no promotion pin → not gated
|
|
168
|
+
|
|
169
|
+
const artifact = deps.readGroundingArtifact(root);
|
|
170
|
+
const grounded =
|
|
171
|
+
artifact &&
|
|
172
|
+
typeof artifact === 'object' &&
|
|
173
|
+
artifact.source_idea_id === sourceIdeaId &&
|
|
174
|
+
GROUNDING_FIELDS.every((k) => typeof artifact[k] === 'string' && artifact[k].trim());
|
|
175
|
+
if (grounded) return null;
|
|
176
|
+
|
|
177
|
+
const why = !artifact
|
|
178
|
+
? 'no grounding artifact was found'
|
|
179
|
+
: artifact.source_idea_id !== sourceIdeaId
|
|
180
|
+
? `the grounding artifact is for a different idea (${artifact.source_idea_id ?? 'unknown'})`
|
|
181
|
+
: 'the grounding artifact is missing already_built / already_decided / owner_reference / residual_delta';
|
|
182
|
+
return (
|
|
183
|
+
`Deep-research launch blocked: this is a promotion-driven research run for ${sourceIdeaId}, but ${why}. ` +
|
|
184
|
+
`Ground FIRST so you research only the residual unknown, not work that already ships or is already decided ` +
|
|
185
|
+
`(the HS-VDPB → HS-LKCO burn re-derived companion features that already exist). Do both routes:\n` +
|
|
186
|
+
` route-1 (plans): heyspex_recall_memories + /hs-ground on the idea.\n` +
|
|
187
|
+
` route-2 (code): check companion/ · spex-console/ · scripts/ · the \`hs\` CLI, PLUS the feature's use case / roadmap / open tasks.\n` +
|
|
188
|
+
` route-3 (owner's working reference): does the owner ALREADY RUN something that does this — a service, container, script, or tool on their own machine? Inspect it and record what it showed. This is the cheapest and highest-authority evidence there is.\n` +
|
|
189
|
+
`Then record the result to ${GROUNDING_ARTIFACT_REL} as ` +
|
|
190
|
+
`{ "source_idea_id": "${sourceIdeaId}", "already_built": "...", "already_decided": "...", "owner_reference": "...", "residual_delta": "...", "recorded_at": "<ISO>" } ` +
|
|
191
|
+
`and relaunch with the RESIDUAL DELTA as the question. If grounding shows it is already built/decided, do NOT launch — record and stop. ` +
|
|
192
|
+
`(Break-glass: set HEYSPEX_RESEARCH_GROUNDING_GATE_OFF=1.)`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The deep-research cost/grounding gate's id — unique across the built-in registry. */
|
|
197
|
+
export const id = 'deep-research-cost-gate';
|
|
198
|
+
/** Runs pre-tool, same as its PreToolUse Claude Code hook registration. */
|
|
199
|
+
export const event = 'before_tool';
|
|
200
|
+
/** Caller-defined severity label; this gate can deny, so it is "block". */
|
|
201
|
+
export const severity = 'block';
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @param {{ point: string, toolName?: string, toolInput?: Record<string, unknown> }} evt
|
|
205
|
+
* @param {{ cwd?: string, loadResearchFocus?: (root: string) => object | null,
|
|
206
|
+
* readGroundingArtifact?: (root: string) => object | null,
|
|
207
|
+
* env?: Record<string, string | undefined> }} [ctx]
|
|
208
|
+
* @returns {{ decision: 'allow'|'deny'|'rewrite', reason?: string, rewrite?: object }}
|
|
209
|
+
*/
|
|
210
|
+
export function run(evt, ctx = {}) {
|
|
211
|
+
const env = ctx.env ?? process.env;
|
|
212
|
+
if (String(env.HEYSPEX_DEEP_RESEARCH_GATE_OFF ?? '') === '1') {
|
|
213
|
+
return { decision: 'allow' };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const toolName = evt?.toolName;
|
|
217
|
+
if (toolName !== 'Workflow' && toolName !== 'Skill') {
|
|
218
|
+
return { decision: 'allow' };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const input = evt?.toolInput ?? {};
|
|
222
|
+
const root = ctx.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? REPO_ROOT;
|
|
223
|
+
const deps = {
|
|
224
|
+
loadResearchFocus: ctx.loadResearchFocus ?? loadResearchFocus,
|
|
225
|
+
readGroundingArtifact: ctx.readGroundingArtifact ?? defaultReadGroundingArtifact,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Backstop: a hand-authored inline-script deep-research launch (no
|
|
229
|
+
// name/scriptPath) would fail the harness's own validator. Deny it with
|
|
230
|
+
// the exact corrective so the caller relaunches by scriptPath instead.
|
|
231
|
+
if (toolName === 'Workflow') {
|
|
232
|
+
const inlineProfile = inlineScriptTarget(input);
|
|
233
|
+
if (inlineProfile) {
|
|
234
|
+
return {
|
|
235
|
+
decision: 'deny',
|
|
236
|
+
reason:
|
|
237
|
+
`Don't hand-author an inline deep-research workflow script — that trips the ` +
|
|
238
|
+
`"export const meta must be the FIRST statement" validator (the recurring ` +
|
|
239
|
+
`"dynamic workflow: ${inlineProfile}" launch error). Relaunch it via scriptPath so the ` +
|
|
240
|
+
`validated file loads: Workflow({ scriptPath: '.claude/workflows/${inlineProfile}.js', args: '<the research question>' }). ` +
|
|
241
|
+
`Do NOT use the Workflow \`name\` parameter (its loader trips the same validator) and do NOT invoke it via the Skill tool.`,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const target = resolveTarget(toolName, input);
|
|
247
|
+
const groundingOff = String(env.HEYSPEX_RESEARCH_GROUNDING_GATE_OFF ?? '') === '1';
|
|
248
|
+
|
|
249
|
+
// Grounding applies to every external research pass, including the
|
|
250
|
+
// zero-cost /hs-res path — which is grounded but never cost-asked.
|
|
251
|
+
if (!target) {
|
|
252
|
+
if (isGroundOnlyResearchLaunch(toolName, input) && !groundingOff) {
|
|
253
|
+
const groundingDeny = groundingBlock(root, deps);
|
|
254
|
+
if (groundingDeny) return { decision: 'deny', reason: groundingDeny };
|
|
255
|
+
}
|
|
256
|
+
return { decision: 'allow' }; // not a cost-bearing deep-research invocation
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Grounding gate: a promotion-driven deep-research launch must be grounded
|
|
260
|
+
// first. Runs before the cost ask so an ungrounded launch is denied, not
|
|
261
|
+
// merely cost-confirmed. Muted independently of the cost gate.
|
|
262
|
+
if (!groundingOff) {
|
|
263
|
+
const groundingDeny = groundingBlock(root, deps);
|
|
264
|
+
if (groundingDeny) return { decision: 'deny', reason: groundingDeny };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const c = COST[target];
|
|
268
|
+
const reason =
|
|
269
|
+
`Deep-research about to run: ${c.label} — ${c.agents} agent calls ` +
|
|
270
|
+
`(fan-out web search → fetch → adversarially verify → synthesize).${c.extra} ` +
|
|
271
|
+
`Approve to spend those tokens, or cancel and choose the cheaper profile.`;
|
|
272
|
+
return { decision: 'rewrite', reason, rewrite: { ask: true, reason } };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export default { id, event, severity, run };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// spexharness/src/gates/diagnose-capture-reminder.mjs
|
|
2
|
+
//
|
|
3
|
+
// PreToolUse WARN-ONLY reminder (diagnose HS-IK2D, 2026-10-10). A diagnose (or research) filed straight
|
|
4
|
+
// through heyspex_file_spec passes every other gate: capture-workflow-gate and
|
|
5
|
+
// vendor/heyspex-mcp/capture-stage-gate.mjs deliberately ALLOW `type: diagnose|research` because they are
|
|
6
|
+
// the sanctioned exits. That left a hole — an owner's complaint could become a diagnose with NO capture
|
|
7
|
+
// ever holding their verbatim words. This gate closes the hole without blocking: when a diagnose/research
|
|
8
|
+
// is filed without `frontmatter.promoted_from` and the session transcript shows no capture tool call
|
|
9
|
+
// before it, it injects a reminder (additionalContext) telling the agent to capture first.
|
|
10
|
+
//
|
|
11
|
+
// Never denies. Fail-open on every unknowable (no transcript, unreadable, bad JSON, wrong tool).
|
|
12
|
+
// Kill switch: HEYSPEX_DIAGNOSE_CAPTURE_REMINDER_OFF=1.
|
|
13
|
+
//
|
|
14
|
+
// ctx (all optional): ctx.env, ctx.readFile(path) -> string|null, ctx.sessionRecord (non-Claude fronts:
|
|
15
|
+
// { toolCalls: [{ name }] }, used when no transcript path exists).
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
|
|
19
|
+
export const id = 'diagnose-capture-reminder';
|
|
20
|
+
export const event = 'before_tool';
|
|
21
|
+
export const severity = 'warn';
|
|
22
|
+
|
|
23
|
+
const OFF_ENV = 'HEYSPEX_DIAGNOSE_CAPTURE_REMINDER_OFF';
|
|
24
|
+
const FILE_SPEC_TOOL = 'heyspex_file_spec';
|
|
25
|
+
const REMINDED_TYPES = new Set(['diagnose', 'research']);
|
|
26
|
+
// A capture tool call anywhere earlier in the session counts as "the capture happened".
|
|
27
|
+
const CAPTURE_TOOL_RE = /heyspex_(?:active_capture|update_capture|promote_capture_to_diagnose|promote_capture)\b/;
|
|
28
|
+
|
|
29
|
+
export const REMINDER_TEXT =
|
|
30
|
+
"No capture precedes this diagnose — run heyspex_active_capture + heyspex_update_capture with the owner's " +
|
|
31
|
+
'verbatim words, then heyspex_promote_capture_to_diagnose. Direct filing is for bugs found mid-work only.';
|
|
32
|
+
|
|
33
|
+
function bareToolName(name) {
|
|
34
|
+
if (typeof name !== 'string') return '';
|
|
35
|
+
const idx = name.lastIndexOf('__');
|
|
36
|
+
return idx === -1 ? name : name.slice(idx + 2);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function defaultReadFile(p) {
|
|
40
|
+
try {
|
|
41
|
+
return fs.readFileSync(p, 'utf8');
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** True when the transcript JSONL holds an assistant tool_use of a capture tool. */
|
|
48
|
+
export function transcriptHasCaptureCall(raw) {
|
|
49
|
+
if (typeof raw !== 'string') return false;
|
|
50
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
51
|
+
if (!line || !line.includes('capture')) continue; // cheap pre-filter
|
|
52
|
+
let evt;
|
|
53
|
+
try {
|
|
54
|
+
evt = JSON.parse(line);
|
|
55
|
+
} catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const content = evt?.message?.content;
|
|
59
|
+
if (!Array.isArray(content)) continue;
|
|
60
|
+
for (const block of content) {
|
|
61
|
+
if (block?.type === 'tool_use' && typeof block.name === 'string' && CAPTURE_TOOL_RE.test(block.name)) return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function recordHasCaptureCall(record) {
|
|
68
|
+
const calls = Array.isArray(record?.toolCalls) ? record.toolCalls : [];
|
|
69
|
+
return calls.some((c) => typeof c?.name === 'string' && CAPTURE_TOOL_RE.test(c.name));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {{ point: string, toolName?: string, toolInput?: object, transcriptPath?: string, sessionRecord?: object }} evt
|
|
74
|
+
* @param {{ env?: object, readFile?: (p: string) => string|null, sessionRecord?: object }} [ctx]
|
|
75
|
+
* @returns {{ decision: 'allow'|'inject', reason?: string, inject?: { additionalContext: string } }}
|
|
76
|
+
*/
|
|
77
|
+
export function run(evt, ctx = {}) {
|
|
78
|
+
try {
|
|
79
|
+
const env = ctx.env ?? process.env;
|
|
80
|
+
if (String(env[OFF_ENV] ?? '') === '1') return { decision: 'allow' };
|
|
81
|
+
if (bareToolName(evt?.toolName) !== FILE_SPEC_TOOL) return { decision: 'allow' };
|
|
82
|
+
|
|
83
|
+
const input = evt?.toolInput ?? {};
|
|
84
|
+
const type = String(input?.type ?? '').trim().toLowerCase();
|
|
85
|
+
if (!REMINDED_TYPES.has(type)) return { decision: 'allow' };
|
|
86
|
+
|
|
87
|
+
const promotedFrom = input?.frontmatter?.promoted_from ?? input?.promoted_from;
|
|
88
|
+
if (promotedFrom != null && String(Array.isArray(promotedFrom) ? promotedFrom.join('') : promotedFrom).trim()) {
|
|
89
|
+
return { decision: 'allow' }; // came through a capture promotion
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const transcriptPath = evt?.transcriptPath;
|
|
93
|
+
const record = evt?.sessionRecord ?? ctx.sessionRecord ?? null;
|
|
94
|
+
if (!transcriptPath && !record) return { decision: 'allow' }; // unknowable -> fail open
|
|
95
|
+
|
|
96
|
+
let captured;
|
|
97
|
+
if (transcriptPath) {
|
|
98
|
+
const raw = (ctx.readFile ?? defaultReadFile)(transcriptPath);
|
|
99
|
+
if (raw == null) return { decision: 'allow' }; // unreadable -> fail open
|
|
100
|
+
captured = transcriptHasCaptureCall(raw);
|
|
101
|
+
} else {
|
|
102
|
+
captured = recordHasCaptureCall(record);
|
|
103
|
+
}
|
|
104
|
+
if (captured) return { decision: 'allow' };
|
|
105
|
+
|
|
106
|
+
return { decision: 'inject', reason: REMINDER_TEXT, inject: { additionalContext: REMINDER_TEXT } };
|
|
107
|
+
} catch {
|
|
108
|
+
return { decision: 'allow' };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default { id, event, severity, run };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// spexharness/src/gates/directive-obligation-gate.decide.mjs
|
|
2
|
+
//
|
|
3
|
+
// Pure decision logic for the directive-obligation-gate ([[HS-20SW-DRSM-DIAG-heyspex-capture-on-idea-and-workflow]]).
|
|
4
|
+
// No stdin/stdout/process.exit and no fs: every input arrives as an argument, so the gate module
|
|
5
|
+
// (./directive-obligation-gate.mjs) owns all I/O and a test drives these functions directly.
|
|
6
|
+
|
|
7
|
+
import { extractShellTargets } from './_shell-write-targets.mjs';
|
|
8
|
+
import { isProductCodePath } from '../../../scripts/lib/directive-obligations.mjs';
|
|
9
|
+
|
|
10
|
+
const WRITE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'write_file', 'edit_file']);
|
|
11
|
+
const SHELL_TOOLS = new Set(['Bash', 'PowerShell', 'Shell']);
|
|
12
|
+
const SPAWN_TOOLS = new Set(['Agent', 'Task']);
|
|
13
|
+
|
|
14
|
+
/** Sub-agent types that only read / plan / triage: spawning one never builds anything. */
|
|
15
|
+
export const READ_ONLY_AGENT_TYPES = new Set([
|
|
16
|
+
'explore',
|
|
17
|
+
'plan',
|
|
18
|
+
'claude-code-guide',
|
|
19
|
+
'capture-triager',
|
|
20
|
+
'plan-grounder',
|
|
21
|
+
'research-synthesizer',
|
|
22
|
+
'resolution-triager',
|
|
23
|
+
'statusline-setup',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
// Commands that build, install, serve or publish product code. Test runners are deliberately absent.
|
|
27
|
+
const BUILD_CMD_RE = new RegExp(
|
|
28
|
+
'(?:^|[\\s;&|(])(?:' +
|
|
29
|
+
'(?:npm|pnpm|yarn|bun)\\s+(?:run\\s+)?(?:build|dev|start|install|i|add|ci|publish|deploy)\\b' +
|
|
30
|
+
'|(?:npx\\s+)?(?:next|vite|webpack|turbo|esbuild)\\s+(?:build|dev)\\b' +
|
|
31
|
+
'|dotnet\\s+(?:build|publish|run)\\b' +
|
|
32
|
+
'|cargo\\s+(?:build|run|install)\\b' +
|
|
33
|
+
'|make\\b' +
|
|
34
|
+
'|vercel\\s+(?:deploy|--prod)\\b' +
|
|
35
|
+
'|(?:claude|spexcode)\\s+(?:-p|--print)\\b' +
|
|
36
|
+
')',
|
|
37
|
+
'i',
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
/** Is this a heyspex_* MCP call (any spelling)? Those are never gated. */
|
|
41
|
+
export function isHeyspexMcpTool(name) {
|
|
42
|
+
return /(?:^|__)heyspex_/.test(String(name ?? ''));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function pathOf(toolInput) {
|
|
46
|
+
return toolInput?.file_path ?? toolInput?.notebook_path ?? toolInput?.path ?? toolInput?.filePath ?? '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Classify one tool call into the surface this gate cares about.
|
|
51
|
+
* @returns {{ kind: 'exempt'|'spawn'|'code_write'|'shell_build'|'shell_write', detail?: string }}
|
|
52
|
+
*/
|
|
53
|
+
export function classifyToolCall({ toolName, toolInput }, { cwd, vaultRoot } = {}) {
|
|
54
|
+
const name = String(toolName ?? '');
|
|
55
|
+
if (!name || isHeyspexMcpTool(name)) return { kind: 'exempt' };
|
|
56
|
+
const input = toolInput ?? {};
|
|
57
|
+
|
|
58
|
+
if (SPAWN_TOOLS.has(name)) {
|
|
59
|
+
const type = String(input.subagent_type ?? '').trim().toLowerCase();
|
|
60
|
+
if (type && READ_ONLY_AGENT_TYPES.has(type)) return { kind: 'exempt' };
|
|
61
|
+
return { kind: 'spawn', detail: type || 'general-purpose' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (WRITE_TOOLS.has(name)) {
|
|
65
|
+
const target = pathOf(input);
|
|
66
|
+
return isProductCodePath(target, { cwd, vaultRoot }) ? { kind: 'code_write', detail: String(target) } : { kind: 'exempt' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (SHELL_TOOLS.has(name)) {
|
|
70
|
+
const command = String(input.command ?? '');
|
|
71
|
+
const product = extractShellTargets(command).find((t) => isProductCodePath(t, { cwd, vaultRoot }));
|
|
72
|
+
if (product) return { kind: 'shell_write', detail: product };
|
|
73
|
+
if (BUILD_CMD_RE.test(command)) return { kind: 'shell_build', detail: command.trim().slice(0, 80) };
|
|
74
|
+
return { kind: 'exempt' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { kind: 'exempt' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const HOW_TO_DISCHARGE =
|
|
81
|
+
'Capture the owner\'s words first: call heyspex_update_capture (or heyspex_file_spec / the /hs-capture skill) with their exact ' +
|
|
82
|
+
'message, then retry. Reads, greps, git, test runs, vault and .heyspex writes and every heyspex_* tool stay open meanwhile.';
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Rule 1: while a capture obligation is outstanding for this session, deny a sub-agent spawn, a
|
|
86
|
+
* product-code write and a build/product-write shell command.
|
|
87
|
+
* @returns {{ deny: boolean, reason?: string, rule?: string }}
|
|
88
|
+
*/
|
|
89
|
+
export function decideObligation(call, { outstandingCount, cwd, vaultRoot }) {
|
|
90
|
+
if (!outstandingCount) return { deny: false };
|
|
91
|
+
const c = classifyToolCall(call, { cwd, vaultRoot });
|
|
92
|
+
if (c.kind === 'exempt') return { deny: false };
|
|
93
|
+
const what =
|
|
94
|
+
c.kind === 'spawn' ? `spawn a ${c.detail} sub-agent`
|
|
95
|
+
: c.kind === 'code_write' ? `write product code (${c.detail})`
|
|
96
|
+
: c.kind === 'shell_write' ? `write product code from the shell (${c.detail})`
|
|
97
|
+
: `run a build command (${c.detail})`;
|
|
98
|
+
return {
|
|
99
|
+
deny: true,
|
|
100
|
+
rule: 'directive-obligation',
|
|
101
|
+
reason:
|
|
102
|
+
`HEYSPEX directive-obligation-gate: the capture_on_idea directive was issued for this session and the capture has not happened, so you may not ${what}. ` +
|
|
103
|
+
`${HOW_TO_DISCHARGE} (Break-glass, owner only: HEYSPEX_DIRECTIVE_OBLIGATION_GATE_OFF=1. [[HS-20SW-DRSM-DIAG-heyspex-capture-on-idea-and-workflow]])`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Rule 2 (claim-before-build): deny a build sub-agent spawn or a product-code write when the session
|
|
109
|
+
* has no claimed spec, no on-deck capture and no waiver. Independent of the plan-exit marker and CLAUDE_AGENT.
|
|
110
|
+
*/
|
|
111
|
+
export function decideClaimBeforeBuild(call, { hasRoadToCode, cwd, vaultRoot }) {
|
|
112
|
+
if (hasRoadToCode) return { deny: false };
|
|
113
|
+
const c = classifyToolCall(call, { cwd, vaultRoot });
|
|
114
|
+
if (c.kind !== 'spawn' && c.kind !== 'code_write') return { deny: false };
|
|
115
|
+
const what = c.kind === 'spawn' ? `spawn a ${c.detail} sub-agent` : `edit product code (${c.detail})`;
|
|
116
|
+
return {
|
|
117
|
+
deny: true,
|
|
118
|
+
rule: 'claim-before-build',
|
|
119
|
+
reason:
|
|
120
|
+
`HEYSPEX claim-before-build: this session holds no claimed spec (claim heartbeat confirmed), has no capture on deck and no waiver, so you may not ${what}. ` +
|
|
121
|
+
'Claim the spec you are building (heyspex_claim_spec) or capture the idea (heyspex_update_capture / /hs-capture), then retry. ' +
|
|
122
|
+
'(Break-glass, owner only: HEYSPEX_DIRECTIVE_OBLIGATION_GATE_OFF=1. [[HS-20SW-DRSM-DIAG-heyspex-capture-on-idea-and-workflow]])',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function stopReason(count) {
|
|
127
|
+
return (
|
|
128
|
+
`HEYSPEX directive-obligation-gate: you are about to stop with ${count} capture obligation(s) unmet - the capture_on_idea directive ` +
|
|
129
|
+
'was issued this session and no capture write was seen. Capture the owner\'s words now (heyspex_update_capture with their exact message, ' +
|
|
130
|
+
'or /hs-capture), then finish. This re-drive fires once; it will not block again. [[HS-20SW-DRSM-DIAG-heyspex-capture-on-idea-and-workflow]]'
|
|
131
|
+
);
|
|
132
|
+
}
|