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,176 @@
|
|
|
1
|
+
// spexharness/src/doctrine/writer.mjs
|
|
2
|
+
//
|
|
3
|
+
// DR2 (epic [[HS-KNHZ-8CMW-EPIC-spexcode-doctrine-v0-the-foundational]], task
|
|
4
|
+
// [[HS-YMPY-8CMW-TASK-dr2-doctrine-synthesis-a-routed]]) — renders the merged
|
|
5
|
+
// principle set (merge.mjs's output) into the two committed product artifacts:
|
|
6
|
+
// `spexharness/doctrine/SPEXCODE-DOCTRINE.json` (schema-versioned, machine-readable,
|
|
7
|
+
// cited) and `SPEXCODE-DOCTRINE.md` (the human-readable, sectioned document). Pure
|
|
8
|
+
// rendering — {@link renderDoctrineMarkdown}/{@link buildDoctrineJson} do no I/O, so
|
|
9
|
+
// the golden Markdown test never touches disk; {@link writeDoctrineFiles} is the one
|
|
10
|
+
// function that does.
|
|
11
|
+
|
|
12
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
export const DOCTRINE_SCHEMA_VERSION = 1;
|
|
16
|
+
|
|
17
|
+
/** Section order the Markdown writer renders in — matches the epic's own category list. */
|
|
18
|
+
export const CATEGORY_ORDER = [
|
|
19
|
+
'delegation',
|
|
20
|
+
'verification',
|
|
21
|
+
'communication',
|
|
22
|
+
'naming',
|
|
23
|
+
'safety-permissions',
|
|
24
|
+
'research-grounding',
|
|
25
|
+
'session-hygiene',
|
|
26
|
+
'product-taste',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export const CATEGORY_LABELS = {
|
|
30
|
+
delegation: 'Delegation',
|
|
31
|
+
verification: 'Verification',
|
|
32
|
+
communication: 'Communication',
|
|
33
|
+
naming: 'Naming',
|
|
34
|
+
'safety-permissions': 'Safety & Permissions',
|
|
35
|
+
'research-grounding': 'Research & Grounding',
|
|
36
|
+
'session-hygiene': 'Session Hygiene',
|
|
37
|
+
'product-taste': 'Product Taste',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const DOCTRINE_HEADER =
|
|
41
|
+
'This is SpexCode Doctrine — the foundational operating principles distilled from the owner\'s ' +
|
|
42
|
+
'Claude Code and Command Code sessions and the Command Code taste profile. It is layer 2 of three: ' +
|
|
43
|
+
'1) SpexDoctrine (the enforced gates in spexharness/src/gates/, never learned), 2) SpexCode Doctrine ' +
|
|
44
|
+
'(this document — the base every developer profile is built on), 3) SpexCode Profile (the per-' +
|
|
45
|
+
'developer layer on top, which may refine a principle here but may not silently contradict one).';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Assemble the schema-versioned JSON document. Pure — `learnedAt` must be supplied by
|
|
49
|
+
* the caller (never `Date.now()` internally) so the golden test is deterministic.
|
|
50
|
+
*
|
|
51
|
+
* @param {{ principles: object[], conflicts?: object[], sources?: object, learnedAt: string }} input
|
|
52
|
+
* @returns {{ schemaVersion: number, learnedAt: string, sources: object, principles: object[], conflicts: object[] }}
|
|
53
|
+
*/
|
|
54
|
+
export function buildDoctrineJson({ principles, conflicts = [], sources = {}, learnedAt }) {
|
|
55
|
+
return {
|
|
56
|
+
schemaVersion: DOCTRINE_SCHEMA_VERSION,
|
|
57
|
+
learnedAt,
|
|
58
|
+
sources,
|
|
59
|
+
principles,
|
|
60
|
+
conflicts,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** `claude-code ×5, taste ×1, command-code ×1` — counts an evidence-id list by its `<source>:` prefix. */
|
|
65
|
+
function evidenceBreakdown(evidence) {
|
|
66
|
+
const counts = new Map();
|
|
67
|
+
for (const id of evidence ?? []) {
|
|
68
|
+
const src = typeof id === 'string' && id.includes(':') ? id.slice(0, id.indexOf(':')) : 'unknown';
|
|
69
|
+
counts.set(src, (counts.get(src) ?? 0) + 1);
|
|
70
|
+
}
|
|
71
|
+
return [...counts.entries()].map(([k, v]) => `${k} ×${v}`).join(', ');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function renderPrincipleLine(p) {
|
|
75
|
+
const conf = typeof p.confidence === 'number' ? p.confidence.toFixed(2) : '0.00';
|
|
76
|
+
const evidence = Array.isArray(p.evidence) ? p.evidence : [];
|
|
77
|
+
return `- ${p.text} — confidence ${conf} · evidence ${evidence.length} (${evidenceBreakdown(evidence)})`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Render the sectioned Markdown document from a JSON-shaped doctrine object
|
|
82
|
+
* ({@link buildDoctrineJson}'s output, or anything with the same shape). Deterministic:
|
|
83
|
+
* the same `doc` always renders the same string (no clock reads, no random order —
|
|
84
|
+
* principles within a category render in their given array order).
|
|
85
|
+
*
|
|
86
|
+
* @param {{ learnedAt: string, principles: object[], conflicts?: object[], sources?: object }} doc
|
|
87
|
+
* @returns {string}
|
|
88
|
+
*/
|
|
89
|
+
export function renderDoctrineMarkdown(doc) {
|
|
90
|
+
const lines = [];
|
|
91
|
+
lines.push('# SpexCode Doctrine');
|
|
92
|
+
lines.push('');
|
|
93
|
+
lines.push(`_Learned ${doc.learnedAt}_`);
|
|
94
|
+
lines.push('');
|
|
95
|
+
lines.push(DOCTRINE_HEADER);
|
|
96
|
+
lines.push('');
|
|
97
|
+
|
|
98
|
+
const byCategory = new Map();
|
|
99
|
+
for (const p of doc.principles ?? []) {
|
|
100
|
+
const cat = p.category ?? 'product-taste';
|
|
101
|
+
const list = byCategory.get(cat) ?? [];
|
|
102
|
+
list.push(p);
|
|
103
|
+
byCategory.set(cat, list);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const renderSection = (cat, list) => {
|
|
107
|
+
lines.push(`## ${CATEGORY_LABELS[cat] ?? cat}`);
|
|
108
|
+
lines.push('');
|
|
109
|
+
for (const p of list) lines.push(renderPrincipleLine(p));
|
|
110
|
+
lines.push('');
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
for (const cat of CATEGORY_ORDER) {
|
|
114
|
+
const list = byCategory.get(cat);
|
|
115
|
+
if (list && list.length) renderSection(cat, list);
|
|
116
|
+
byCategory.delete(cat);
|
|
117
|
+
}
|
|
118
|
+
// Defensive: any category the seat invented outside CATEGORY_ORDER still renders,
|
|
119
|
+
// in the stable order it was first seen, rather than being silently dropped.
|
|
120
|
+
for (const [cat, list] of byCategory) renderSection(cat, list);
|
|
121
|
+
|
|
122
|
+
lines.push('## Conflicts');
|
|
123
|
+
lines.push('');
|
|
124
|
+
const conflicts = doc.conflicts ?? [];
|
|
125
|
+
if (!conflicts.length) {
|
|
126
|
+
lines.push('_None recorded._');
|
|
127
|
+
} else {
|
|
128
|
+
for (const c of conflicts) lines.push(`- **${c.category}**: "${c.a}" vs "${c.b}" — headline: ${c.headline}`);
|
|
129
|
+
}
|
|
130
|
+
lines.push('');
|
|
131
|
+
|
|
132
|
+
lines.push('## Sources');
|
|
133
|
+
lines.push('');
|
|
134
|
+
const sources = doc.sources ?? {};
|
|
135
|
+
lines.push(`- Observations: ${sources.totalObservations ?? 0}`);
|
|
136
|
+
const bySource = sources.bySource ?? {};
|
|
137
|
+
for (const [k, v] of Object.entries(bySource)) lines.push(` - ${k}: ${v}`);
|
|
138
|
+
// DR5 ([[HS-7Z9E-8CMW-TASK-dr5-vault-knowledge-as-a-doctrine]]) — the vault sets
|
|
139
|
+
// (decisions, lessons, protocol enforcement, ADRs, the naughty list, CLAUDE.md, ...)
|
|
140
|
+
// that fed the `vault` row above, broken out with their own per-set counts.
|
|
141
|
+
const vaultSets = sources.vaultSets ?? {};
|
|
142
|
+
if (Object.keys(vaultSets).length) {
|
|
143
|
+
lines.push(' - vault sets:');
|
|
144
|
+
for (const [k, v] of Object.entries(vaultSets)) lines.push(` - ${k}: ${v}`);
|
|
145
|
+
}
|
|
146
|
+
lines.push('');
|
|
147
|
+
|
|
148
|
+
return lines.join('\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function defaultDoctrineOutDir(repoRoot) {
|
|
152
|
+
return join(repoRoot, 'spexharness', 'doctrine');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function defaultDoctrineMdPath(repoRoot) {
|
|
156
|
+
return join(defaultDoctrineOutDir(repoRoot), 'SPEXCODE-DOCTRINE.md');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function defaultDoctrineJsonPath(repoRoot) {
|
|
160
|
+
return join(defaultDoctrineOutDir(repoRoot), 'SPEXCODE-DOCTRINE.json');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Write both product artifacts for `repoRoot`. The only I/O in this module.
|
|
165
|
+
* @param {string} repoRoot
|
|
166
|
+
* @param {ReturnType<typeof buildDoctrineJson>} doc
|
|
167
|
+
* @returns {{ jsonPath: string, mdPath: string }}
|
|
168
|
+
*/
|
|
169
|
+
export function writeDoctrineFiles(repoRoot, doc) {
|
|
170
|
+
mkdirSync(defaultDoctrineOutDir(repoRoot), { recursive: true });
|
|
171
|
+
const jsonPath = defaultDoctrineJsonPath(repoRoot);
|
|
172
|
+
const mdPath = defaultDoctrineMdPath(repoRoot);
|
|
173
|
+
writeFileSync(jsonPath, `${JSON.stringify(doc, null, 2)}\n`);
|
|
174
|
+
writeFileSync(mdPath, `${renderDoctrineMarkdown(doc)}\n`);
|
|
175
|
+
return { jsonPath, mdPath };
|
|
176
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// spexharness/src/events.mjs
|
|
2
|
+
//
|
|
3
|
+
// The harness's normalized event model. Every front (Claude Code plugin, `hs
|
|
4
|
+
// chat`, a headless seat) translates whatever its own runtime hands it into
|
|
5
|
+
// one of these six shapes before calling into the gate registry / decider —
|
|
6
|
+
// so a gate is written once against this shape and never against a
|
|
7
|
+
// front-specific payload.
|
|
8
|
+
//
|
|
9
|
+
// This module is pure: no I/O, no process.stdin/stdout, no imports outside
|
|
10
|
+
// node:* stdlib.
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The six event points a gate may run on, in the order they occur during a
|
|
14
|
+
* session:
|
|
15
|
+
* 1. session_start — a session begins.
|
|
16
|
+
* 2. prompt_submit — the user (or caller) submits a prompt.
|
|
17
|
+
* 3. before_tool — a tool is about to run (pre-flight; may deny/rewrite).
|
|
18
|
+
* 4. after_tool — a tool finished running (post-flight; may inject).
|
|
19
|
+
* 5. stop — the agent is about to stop / hand control back.
|
|
20
|
+
* 6. session_end — a session ends.
|
|
21
|
+
* @type {ReadonlyArray<string>}
|
|
22
|
+
*/
|
|
23
|
+
export const EVENT_POINTS = Object.freeze([
|
|
24
|
+
'session_start',
|
|
25
|
+
'prompt_submit',
|
|
26
|
+
'before_tool',
|
|
27
|
+
'after_tool',
|
|
28
|
+
'stop',
|
|
29
|
+
'session_end',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const EVENT_POINT_SET = new Set(EVENT_POINTS);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Documented field set per event point. `sessionId` and `ts` are common to
|
|
36
|
+
* every event (added by {@link createEvent} itself); the arrays below are
|
|
37
|
+
* the additional fields each event point's `fields` argument is expected to
|
|
38
|
+
* carry. This is documentation, not a runtime schema — createEvent does not
|
|
39
|
+
* reject extra or missing fields, so a front can pass whatever its own
|
|
40
|
+
* runtime already has without a translation step per gate.
|
|
41
|
+
*
|
|
42
|
+
* - session_start: cwd, agent
|
|
43
|
+
* - prompt_submit: prompt
|
|
44
|
+
* - before_tool: toolName, toolInput
|
|
45
|
+
* - after_tool: toolName, toolInput, toolResult
|
|
46
|
+
* - stop: (none beyond the common fields)
|
|
47
|
+
* - session_end: reason
|
|
48
|
+
* @type {Readonly<Record<string, ReadonlyArray<string>>>}
|
|
49
|
+
*/
|
|
50
|
+
export const EVENT_FIELDS = Object.freeze({
|
|
51
|
+
session_start: Object.freeze(['cwd', 'agent']),
|
|
52
|
+
prompt_submit: Object.freeze(['prompt']),
|
|
53
|
+
before_tool: Object.freeze(['toolName', 'toolInput']),
|
|
54
|
+
after_tool: Object.freeze(['toolName', 'toolInput', 'toolResult']),
|
|
55
|
+
stop: Object.freeze([]),
|
|
56
|
+
session_end: Object.freeze(['reason']),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build one normalized event object for a gate to run against.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} point - one of {@link EVENT_POINTS}. Throws on any other
|
|
63
|
+
* value, including a Claude-Code-shaped name like `"PreToolUse"` — callers
|
|
64
|
+
* translate into this vocabulary at the edge, not inside a gate.
|
|
65
|
+
* @param {Record<string, unknown>} [fields] - the event point's documented
|
|
66
|
+
* fields (see {@link EVENT_FIELDS}), plus any extra context a front wants
|
|
67
|
+
* to carry through to its own gates. Not validated against the field list;
|
|
68
|
+
* the list is documentation for gate authors, not a runtime schema.
|
|
69
|
+
* @returns {Readonly<{ point: string, ts: number } & Record<string, unknown>>}
|
|
70
|
+
* A frozen, normalized event object. `ts` defaults to `Date.now()` when
|
|
71
|
+
* `fields.ts` is not supplied.
|
|
72
|
+
*/
|
|
73
|
+
export function createEvent(point, fields = {}) {
|
|
74
|
+
if (!EVENT_POINT_SET.has(point)) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`spexharness: unknown event point "${point}" — expected one of: ${EVENT_POINTS.join(', ')}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return Object.freeze({
|
|
80
|
+
ts: Date.now(),
|
|
81
|
+
...fields,
|
|
82
|
+
point,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// spexharness/src/gates/_shell-write-targets.mjs
|
|
2
|
+
//
|
|
3
|
+
// Shared conservative shell-write scanner. Extracted from
|
|
4
|
+
// env-vault-guard.decide.mjs (originally the same scanner as
|
|
5
|
+
// guard-auto-memory-writes.mjs) so env-vault-guard and vault-write-tier-guard
|
|
6
|
+
// (HS-FCNH) import ONE implementation instead of drifting copies.
|
|
7
|
+
//
|
|
8
|
+
// Pure: no fs, no process. It only recognises write-SHAPED commands whose
|
|
9
|
+
// targets it can positively identify; callers pair it with a fail-closed
|
|
10
|
+
// text scan (see `mentionsWriteVerb`) for everything it cannot parse
|
|
11
|
+
// (heredocs, `python -c`, `node -e`, variable-expanded targets).
|
|
12
|
+
|
|
13
|
+
const REDIRECT_RE = /(?:>>?|\|\s*tee(?:\s+-a)?)\s+("([^"]+)"|'([^']+)'|(\S+))/g;
|
|
14
|
+
const CMD_RE =
|
|
15
|
+
/\b(?:mv|cp|touch|install|rename|tee|rm|del|Remove-Item|Move-Item|Copy-Item|New-Item|Set-Content|Out-File|Add-Content|Clear-Content)\s+([^|;&\n]+)/g;
|
|
16
|
+
// In-place editors: `sed -i`, `sed -ibak`, `sed --in-place`, `perl -pi -e`, `perl -i.bak`.
|
|
17
|
+
const INPLACE_RE = /\b(?:sed|perl)\b([^|;&\n]*)/g;
|
|
18
|
+
|
|
19
|
+
function isInPlaceFlag(token) {
|
|
20
|
+
return (
|
|
21
|
+
token === '--in-place' ||
|
|
22
|
+
token.startsWith('--in-place=') ||
|
|
23
|
+
/^-[A-Za-z]*i[A-Za-z0-9.]*$/.test(token)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Same conservative shell scanner as guard-auto-memory-writes.mjs, plus in-place editors. */
|
|
28
|
+
export function extractShellTargets(command) {
|
|
29
|
+
if (!command || typeof command !== 'string') return [];
|
|
30
|
+
const targets = new Set();
|
|
31
|
+
for (const m of command.matchAll(REDIRECT_RE)) targets.add(m[2] ?? m[3] ?? m[4]);
|
|
32
|
+
for (const m of command.matchAll(CMD_RE)) {
|
|
33
|
+
const head = m[0].split(/\s/)[0];
|
|
34
|
+
const args = m[1]
|
|
35
|
+
.trim()
|
|
36
|
+
.split(/\s+/)
|
|
37
|
+
.filter((a) => a && !a.startsWith('-'));
|
|
38
|
+
if (args.length === 0) continue;
|
|
39
|
+
if (/^(?:mv|cp|install|rename|Move-Item|Copy-Item)$/.test(head)) {
|
|
40
|
+
targets.add(args[args.length - 1]);
|
|
41
|
+
} else {
|
|
42
|
+
for (const a of args) targets.add(a);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// sed -i / perl -i / --in-place: every non-flag operand is a candidate
|
|
46
|
+
// (the script operand rides along harmlessly — callers match by path).
|
|
47
|
+
for (const m of command.matchAll(INPLACE_RE)) {
|
|
48
|
+
const tokens = m[1].trim().split(/\s+/).filter(Boolean);
|
|
49
|
+
if (!tokens.some(isInPlaceFlag)) continue;
|
|
50
|
+
for (const t of tokens) if (!t.startsWith('-')) targets.add(t);
|
|
51
|
+
}
|
|
52
|
+
// PowerShell: -Path / -FilePath / -LiteralPath <target> on a write cmdlet.
|
|
53
|
+
if (/\b(?:New-Item|Set-Content|Out-File|Add-Content|Clear-Content)\b/i.test(command)) {
|
|
54
|
+
for (const m of command.matchAll(/-(?:Path|FilePath|LiteralPath)\s+("([^"]+)"|'([^']+)'|(\S+))/gi))
|
|
55
|
+
targets.add(m[2] ?? m[3] ?? m[4]);
|
|
56
|
+
}
|
|
57
|
+
for (const t of extractInterpreterWriteTargets(command)) targets.add(t);
|
|
58
|
+
return [...targets]
|
|
59
|
+
.map((t) => t.replace(/^['"]|['"]$/g, ''))
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.filter((t) => !/[$`(){}]/.test(t));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Literal-path writes inside inline interpreter bodies: python `open('p','w')`,
|
|
66
|
+
* node `writeFileSync('p', ...)`. Only a literal path that is the write call's
|
|
67
|
+
* FIRST argument counts; a body that merely reads or prints is not a write.
|
|
68
|
+
*/
|
|
69
|
+
export function extractInterpreterWriteTargets(command) {
|
|
70
|
+
if (!command || typeof command !== 'string') return [];
|
|
71
|
+
const out = [];
|
|
72
|
+
const pyOpen = /\bopen\s*\(\s*(['"])([^'"]+)\1\s*,\s*['"][wax]/g;
|
|
73
|
+
const nodeWrite = /\b(?:writeFile|writeFileSync|appendFile|appendFileSync)\s*\(\s*(['"`])([^'"`]+)\1/g;
|
|
74
|
+
for (const m of command.matchAll(pyOpen)) out.push(m[2]);
|
|
75
|
+
for (const m of command.matchAll(nodeWrite)) out.push(m[2]);
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// spexharness/src/gates/capture-workflow-gate.mjs
|
|
2
|
+
//
|
|
3
|
+
// Engine port of scripts/claude-hooks/capture-workflow-gate.mjs — the
|
|
4
|
+
// PreToolUse EARLY-WARNING gate for filing an implementation spec
|
|
5
|
+
// (epic/task/milestone) that references the capture currently on deck. See
|
|
6
|
+
// ./PORTING.md for the recipe this follows and ./env-vault-guard.mjs for the
|
|
7
|
+
// reference port.
|
|
8
|
+
//
|
|
9
|
+
// THIS HOOK NEVER DENIES (by design — see the original's header). The real
|
|
10
|
+
// refusal lives in heyspex_file_spec / vendor/heyspex-mcp/capture-stage-gate.mjs,
|
|
11
|
+
// which reads the LIVE capture-on-deck pin (the console / Neon control
|
|
12
|
+
// plane); this hook can only see the OFFLINE file pin
|
|
13
|
+
// (.heyspex/capture-focus.yaml), so it is kept as a narrow, always-silent-
|
|
14
|
+
// unless-matched early warning, never a blocker.
|
|
15
|
+
//
|
|
16
|
+
// The original script does NOT export a pure decision function — its logic
|
|
17
|
+
// is tangled inside `main()` (stdin read, env checks, then the match logic).
|
|
18
|
+
// Per PORTING.md step 1's "otherwise" branch that logic would normally be
|
|
19
|
+
// extracted into the original script itself, but this task's Do-nots forbid
|
|
20
|
+
// touching anything under scripts/claude-hooks/. Rather than hand-copy the
|
|
21
|
+
// original's rule/regex logic (the thing PORTING.md warns will drift), this
|
|
22
|
+
// port imports the SAME shared, already-pure library modules the original
|
|
23
|
+
// hook itself imports for the actual decision surface:
|
|
24
|
+
// - vendor/heyspex-mcp/capture-stage-gate.mjs — BLOCKED_FILE_SPEC_TYPES,
|
|
25
|
+
// ALLOWED_FILE_SPEC_TYPES, referencesCapture(). This is the canonical
|
|
26
|
+
// module the original hook's header says it shares "so the two can
|
|
27
|
+
// never disagree about which filings count" — importing it here keeps
|
|
28
|
+
// that same guarantee for the port.
|
|
29
|
+
// - vendor/heyspex-mcp/capture-focus.mjs — resolveActiveCapture(), which
|
|
30
|
+
// reads the offline pin + the pinned capture's frontmatter/body.
|
|
31
|
+
// The only code re-derived here is the original's small, non-security
|
|
32
|
+
// glue: stripping the MCP tool-name prefix (bareToolName), and the tiny
|
|
33
|
+
// offline frontmatter scanner + spec loader the original defines locally
|
|
34
|
+
// (parseFrontmatter / loadSpec) purely to satisfy resolveActiveCapture's
|
|
35
|
+
// callback-shaped contract — neither is a rule/regex guard, both are
|
|
36
|
+
// deterministic parsing utilities with no security surface of their own.
|
|
37
|
+
//
|
|
38
|
+
// Decision mapping (documented judgment call, same shape as
|
|
39
|
+
// env-vault-guard.mjs's "ask" → rewrite mapping): a match produces a WARNING
|
|
40
|
+
// carrying the capture id and the sanctioned-exit message — richer than a
|
|
41
|
+
// plain allow, and the original never denies, so silently collapsing this to
|
|
42
|
+
// `allow` would drop information a caller might want to surface (e.g. show
|
|
43
|
+
// the same warning text the Claude Code hook would have written to stderr).
|
|
44
|
+
// Mapped to `rewrite` with `{ warn: true, reason, captureId, type }` rather
|
|
45
|
+
// than `deny` (the hook's own header is explicit: "it never denies") or a
|
|
46
|
+
// silent `allow` (loses the warning). No match, or any early-out (kill
|
|
47
|
+
// switch, wrong tool, allowed/unmatched type, no capture on deck, unrelated
|
|
48
|
+
// filing) → plain `allow`.
|
|
49
|
+
//
|
|
50
|
+
// This gate never hard-denies (matching the original) — "deny" coverage for
|
|
51
|
+
// AC3 ("allow, deny and rewrite each covered by at least one test per gate",
|
|
52
|
+
// checked as one combined `node --test` run across all three ported gates in
|
|
53
|
+
// this task) comes from the sibling enforce-session-sequence gate. See
|
|
54
|
+
// ./enforce-session-sequence.test.mjs.
|
|
55
|
+
//
|
|
56
|
+
// Env hatches (unchanged, either one silences the warning — matching the
|
|
57
|
+
// original, which honors the tool's own break-glass too so one variable
|
|
58
|
+
// covers both layers):
|
|
59
|
+
// HEYSPEX_CAPTURE_WORKFLOW_GATE_OFF=1
|
|
60
|
+
// HEYSPEX_CAPTURE_STAGE_GATE_OFF=1
|
|
61
|
+
//
|
|
62
|
+
// Not ported HERE: the original's `appendGuardrailEvent(...)` audit-log
|
|
63
|
+
// write on a match. A gate module stays a pure decision function (AC1: no
|
|
64
|
+
// stdout writes; by the same spirit, no other observability side effects
|
|
65
|
+
// either) — matching env-vault-guard.mjs's port. The `rewrite.captureId` /
|
|
66
|
+
// `rewrite.type` fields below already carry everything a front needs to
|
|
67
|
+
// rebuild that same audit row — see spexharness/src/adapters/audit.mjs,
|
|
68
|
+
// called from spexharness/src/adapters/claude-code.mjs's runGateAsHook.
|
|
69
|
+
//
|
|
70
|
+
// `ctx` contract (all optional; every field defaults to a real fs read, same
|
|
71
|
+
// as the original hook and as spexharness/fixtures/lib/setups.mjs's
|
|
72
|
+
// captureActiveVault — the golden/regression tests below point ctx.cwd at a
|
|
73
|
+
// real scratch vault rather than injecting a fake fs):
|
|
74
|
+
// ctx.cwd - repo/vault root to resolve the offline capture pin from.
|
|
75
|
+
// Defaults to CLAUDE_PROJECT_DIR, else this repo's root.
|
|
76
|
+
// ctx.env - env var overrides (tests use this instead of mutating
|
|
77
|
+
// process.env).
|
|
78
|
+
|
|
79
|
+
import fs from 'node:fs';
|
|
80
|
+
import path from 'node:path';
|
|
81
|
+
import { fileURLToPath } from 'node:url';
|
|
82
|
+
|
|
83
|
+
import { resolveActiveCapture } from '../../../vendor/heyspex-mcp/capture-focus.mjs';
|
|
84
|
+
import {
|
|
85
|
+
ALLOWED_FILE_SPEC_TYPES,
|
|
86
|
+
BLOCKED_FILE_SPEC_TYPES,
|
|
87
|
+
referencesCapture,
|
|
88
|
+
} from '../../../vendor/heyspex-mcp/capture-stage-gate.mjs';
|
|
89
|
+
|
|
90
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
91
|
+
const REPO_ROOT = path.resolve(HERE, '..', '..', '..');
|
|
92
|
+
|
|
93
|
+
// The bare (server-stripped) MCP tool name this gate acts on.
|
|
94
|
+
const FILE_SPEC_TOOL = 'heyspex_file_spec';
|
|
95
|
+
const OFF_ENV_WORKFLOW = 'HEYSPEX_CAPTURE_WORKFLOW_GATE_OFF';
|
|
96
|
+
const OFF_ENV_STAGE = 'HEYSPEX_CAPTURE_STAGE_GATE_OFF';
|
|
97
|
+
|
|
98
|
+
// Minimal, sync, offline frontmatter parser — mirrors the original hook's
|
|
99
|
+
// local helper of the same name byte-for-byte. Enough for
|
|
100
|
+
// resolveActiveCapture's callbacks (it only reads frontmatter.id / status /
|
|
101
|
+
// promoted_to and the body). Not a full YAML parser; capture frontmatter is
|
|
102
|
+
// flat scalars + simple arrays.
|
|
103
|
+
function parseFrontmatter(content) {
|
|
104
|
+
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
105
|
+
if (!m) return { frontmatter: {}, body: content };
|
|
106
|
+
const frontmatter = {};
|
|
107
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
108
|
+
const trimmed = line.trim();
|
|
109
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('- ')) continue;
|
|
110
|
+
const colon = trimmed.indexOf(':');
|
|
111
|
+
if (colon === -1) continue;
|
|
112
|
+
const key = trimmed.slice(0, colon).trim();
|
|
113
|
+
let val = trimmed.slice(colon + 1).trim();
|
|
114
|
+
val = val.replace(/^["']|["']$/g, '');
|
|
115
|
+
frontmatter[key] = val;
|
|
116
|
+
}
|
|
117
|
+
return { frontmatter, body: m[2] ?? '' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// readFileSync loader for resolveActiveCapture — returns { frontmatter, body }
|
|
121
|
+
// or null when the pinned capture file is missing/unreadable. Mirrors the
|
|
122
|
+
// original hook's local helper of the same name.
|
|
123
|
+
function loadSpec(vaultRoot, specPath) {
|
|
124
|
+
try {
|
|
125
|
+
const full = path.join(vaultRoot, specPath);
|
|
126
|
+
const content = fs.readFileSync(full, 'utf8');
|
|
127
|
+
return parseFrontmatter(content);
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Strip the Claude Code MCP tool prefix (mcp__<server>__<tool>) → bare tool
|
|
134
|
+
// name. Mirrors the original hook's local helper of the same name.
|
|
135
|
+
function bareToolName(name) {
|
|
136
|
+
if (typeof name !== 'string') return '';
|
|
137
|
+
const m = name.match(/^mcp__[^_]+(?:_[^_]+)*?__(.+)$/);
|
|
138
|
+
if (m) return m[1];
|
|
139
|
+
const idx = name.lastIndexOf('__');
|
|
140
|
+
return idx === -1 ? name : name.slice(idx + 2);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function warnText(captureId) {
|
|
144
|
+
return (
|
|
145
|
+
`[capture-workflow-gate] warning — this filing links the open capture ${captureId}, ` +
|
|
146
|
+
`which a capture must not become. The sanctioned exit is promote-to-research (\`/hs-res\`). ` +
|
|
147
|
+
`This hook only warns. heyspex_file_spec is where the rule is enforced, and it ` +
|
|
148
|
+
`reads the live capture pin rather than the offline file this hook can see.`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The capture-workflow gate's id — unique across the built-in registry. */
|
|
153
|
+
export const id = 'capture-workflow-gate';
|
|
154
|
+
/** Runs pre-tool, same as its PreToolUse Claude Code hook registration. */
|
|
155
|
+
export const event = 'before_tool';
|
|
156
|
+
/** Caller-defined severity label; this gate only ever warns (rewrites) or allows. */
|
|
157
|
+
export const severity = 'warn';
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {{ point: string, toolName?: string, toolInput?: Record<string, unknown> }} evt
|
|
161
|
+
* @param {{ cwd?: string, env?: Record<string, string | undefined> }} [ctx]
|
|
162
|
+
* @returns {{ decision: 'allow'|'rewrite', reason?: string, rewrite?: object }}
|
|
163
|
+
*/
|
|
164
|
+
export function run(evt, ctx = {}) {
|
|
165
|
+
const env = ctx.env ?? process.env;
|
|
166
|
+
if (String(env[OFF_ENV_WORKFLOW] ?? '') === '1') return { decision: 'allow' };
|
|
167
|
+
if (String(env[OFF_ENV_STAGE] ?? '') === '1') return { decision: 'allow' };
|
|
168
|
+
|
|
169
|
+
const tool = bareToolName(evt?.toolName);
|
|
170
|
+
if (tool !== FILE_SPEC_TOOL) return { decision: 'allow' };
|
|
171
|
+
|
|
172
|
+
const toolInput = evt?.toolInput ?? {};
|
|
173
|
+
const type = String(toolInput?.type ?? '').trim().toLowerCase();
|
|
174
|
+
|
|
175
|
+
// Only implementation specs are the premature-promotion case. Research /
|
|
176
|
+
// diagnose / memory / pillar / review filings are sanctioned exits or
|
|
177
|
+
// unrelated — never warned about.
|
|
178
|
+
if (!BLOCKED_FILE_SPEC_TYPES.has(type) || ALLOWED_FILE_SPEC_TYPES.has(type)) {
|
|
179
|
+
return { decision: 'allow' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const root = ctx.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? REPO_ROOT;
|
|
183
|
+
let resolved;
|
|
184
|
+
try {
|
|
185
|
+
resolved = resolveActiveCapture(root, parseFrontmatter, loadSpec);
|
|
186
|
+
} catch {
|
|
187
|
+
return { decision: 'allow' }; // discovery failure → stay quiet, fail-safe
|
|
188
|
+
}
|
|
189
|
+
if (!resolved || resolved.error || !resolved.spec_id) return { decision: 'allow' };
|
|
190
|
+
|
|
191
|
+
// Arg-targeted: only warn when the spec being filed references THIS capture.
|
|
192
|
+
if (!referencesCapture(toolInput, resolved.spec_id)) return { decision: 'allow' };
|
|
193
|
+
|
|
194
|
+
const reason = warnText(resolved.spec_id);
|
|
195
|
+
return {
|
|
196
|
+
decision: 'rewrite',
|
|
197
|
+
reason,
|
|
198
|
+
rewrite: { warn: true, reason, captureId: resolved.spec_id, type },
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export default { id, event, severity, run };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// spexharness/src/gates/claim-before-code-gate.decide.mjs
|
|
2
|
+
//
|
|
3
|
+
// Pure decision logic relocated from scripts/claude-hooks/claim-before-code-gate.mjs
|
|
4
|
+
// (HS-IQBF, per PORTING.md's "move the pure decision logic into the gate
|
|
5
|
+
// module itself or a sibling pure module" — the original hook now delegates
|
|
6
|
+
// to this file too, so ./claim-before-code-gate.mjs (the gate) and the
|
|
7
|
+
// original hook's own smoke test both import the SAME code; nothing here is
|
|
8
|
+
// a second copy that can drift). No stdin/stdout/process.exit anywhere in
|
|
9
|
+
// this file.
|
|
10
|
+
|
|
11
|
+
const EDIT_TOOL_NAMES = new Set(['Edit', 'Write', 'MultiEdit']);
|
|
12
|
+
|
|
13
|
+
// Paths this gate never blocks: the vault itself (filing the epic IS the
|
|
14
|
+
// required step), hook/state machinery, and anything outside the repo tree.
|
|
15
|
+
// Everything else counts as "code" for this gate's purposes.
|
|
16
|
+
const EXEMPT_PATH_RE = /(?:^|[\\/])(?:specs|\.heyspex|\.claude|\.git)(?:[\\/]|$)/;
|
|
17
|
+
|
|
18
|
+
function isCodeEditPath(filePath) {
|
|
19
|
+
if (typeof filePath !== 'string' || !filePath.trim()) return false;
|
|
20
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
21
|
+
return !EXEMPT_PATH_RE.test(normalized);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Pure decision function (exported for the smoke test and the gate): given
|
|
26
|
+
* the resolved inputs, should this edit be denied?
|
|
27
|
+
*/
|
|
28
|
+
export function shouldDenyEdit({ toolName, filePath, hasMarker, hasNoClaimSignal }) {
|
|
29
|
+
if (!EDIT_TOOL_NAMES.has(toolName)) return false;
|
|
30
|
+
if (!isCodeEditPath(filePath)) return false;
|
|
31
|
+
if (!hasMarker) return false;
|
|
32
|
+
return hasNoClaimSignal;
|
|
33
|
+
}
|