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,54 @@
|
|
|
1
|
+
// spexharness/src/config-root.mjs
|
|
2
|
+
//
|
|
3
|
+
// The ONE answer to "which directory holds `.heyspex/config.yaml`?" for every
|
|
4
|
+
// hook-side reader: the plugin loader (scripts/claude-hooks/_guardrail-loader.mjs),
|
|
5
|
+
// the harness's own leaf reader (./intent-directive.mjs) and the gate-rows
|
|
6
|
+
// severity resolver (./gate-rows.mjs) all default to this.
|
|
7
|
+
//
|
|
8
|
+
// Per ADR-0009 §1 the workflow-settings file lives at the VAULT root — the
|
|
9
|
+
// directory the active-vault resolver (vendor/heyspex-mcp/vault-resolve.mjs)
|
|
10
|
+
// reports as `repoRootAbs`. Since the 2026-08-31 repo split that is a separate
|
|
11
|
+
// checkout from this code repo, and every hook-side reader that still joined
|
|
12
|
+
// `config.yaml` onto the CODE root (DEFAULT_REPO_ROOT / DEFAULT_APP_ROOT) was
|
|
13
|
+
// silently reading a stale copy while the MCP, CLI and console read the vault —
|
|
14
|
+
// [[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]].
|
|
15
|
+
//
|
|
16
|
+
// ONLY `config.yaml` moves. The guardrail registry (`.heyspex/guardrails.json`),
|
|
17
|
+
// the terminology override, the event/history logs, the dev-server lock and the
|
|
18
|
+
// fleet-queue lanes are code-repo artifacts and keep resolving from the code
|
|
19
|
+
// root — the same CODE_ROOT vs vault-root split scripts/lib/resolve-validate-root.mjs
|
|
20
|
+
// makes for the validators (memory lesson [[after-the-repo-split-every-live-specs]]).
|
|
21
|
+
//
|
|
22
|
+
// FAIL-SAFE: resolution is fs/env only (no network) and `getActiveConfig()`
|
|
23
|
+
// memoizes it once per process; if it throws for any reason the code root is
|
|
24
|
+
// returned so a hook never strands the agent. Deliberately NOT memoized a second
|
|
25
|
+
// time here — tests call `resetActiveConfigCache()` between cases and expect the
|
|
26
|
+
// next read to re-resolve.
|
|
27
|
+
//
|
|
28
|
+
// Layering: a harness module importing from vendor/heyspex-mcp, the same
|
|
29
|
+
// direction ./intent-directive.mjs and ./gates/enforce-session-sequence.mjs
|
|
30
|
+
// already take. Never imports from scripts/claude-hooks.
|
|
31
|
+
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
import { fileURLToPath } from 'node:url';
|
|
34
|
+
|
|
35
|
+
import { getActiveConfig } from '../../vendor/heyspex-mcp/vault-resolve.mjs';
|
|
36
|
+
|
|
37
|
+
/** spexharness/src/ → the code checkout is two levels up. */
|
|
38
|
+
export const CODE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The directory `.heyspex/config.yaml` is read from when a caller passes no
|
|
42
|
+
* explicit root: the active vault's repo root, else (resolution threw) the code
|
|
43
|
+
* checkout. Explicit `repoRoot` arguments on the readers always win over this.
|
|
44
|
+
*
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
export function defaultConfigRoot() {
|
|
48
|
+
try {
|
|
49
|
+
const root = getActiveConfig()?.repoRootAbs;
|
|
50
|
+
return typeof root === 'string' && root.trim() ? root : CODE_ROOT;
|
|
51
|
+
} catch {
|
|
52
|
+
return CODE_ROOT;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// spexharness/src/context-budget.mjs
|
|
2
|
+
//
|
|
3
|
+
// The harness's single context-injection measurement path (HS-7UAQ, epic HS-1YDJ). Every
|
|
4
|
+
// frontend that puts something into a seat's context window — a Claude Code hook shell run
|
|
5
|
+
// through spexharness/src/adapters/claude-code.mjs, the plugin's own SessionStart pieces,
|
|
6
|
+
// a headless seat's bootstrap (session-bootstrap.mjs), an overnight loop run — reports that
|
|
7
|
+
// injection through {@link recordContextInjection}. Nothing else in the harness writes a
|
|
8
|
+
// context-injection line into a session record; this is the one path every frontend goes
|
|
9
|
+
// through, which is what makes the resulting inventory trustworthy across fronts.
|
|
10
|
+
//
|
|
11
|
+
// Sizing is NOT reimplemented here: measuring one piece delegates to
|
|
12
|
+
// spexharness/src/token-inventory.mjs's `measurePiece` (itself built on
|
|
13
|
+
// dev-factory/lib/context-budget.mjs's `estimateTokens`), so this module owns WHERE a
|
|
14
|
+
// measurement is recorded, not HOW big a piece of text is. Recording delegates to
|
|
15
|
+
// spexharness/src/session-record.mjs's `recordEvent`, so the injection lands as an ordinary
|
|
16
|
+
// event on the session record (`point: 'context_injection'`) rather than a bespoke array —
|
|
17
|
+
// a session record inspector that already walks `record.events` sees these for free.
|
|
18
|
+
//
|
|
19
|
+
// Pure: no I/O, no console. `record` is mutated in place (matching recordEvent's and
|
|
20
|
+
// recordToolCall's own contract) and the measured piece is returned so a caller can log or
|
|
21
|
+
// assert on it without re-reading the record.
|
|
22
|
+
|
|
23
|
+
import { measurePiece } from './token-inventory.mjs';
|
|
24
|
+
import { recordEvent } from './session-record.mjs';
|
|
25
|
+
|
|
26
|
+
/** The session-record event point every context injection is recorded under. */
|
|
27
|
+
export const CONTEXT_INJECTION_POINT = 'context_injection';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {object} ContextInjectionMeasurement
|
|
31
|
+
* @property {string} source who injected it — a hook id, a gate id, 'seat-context',
|
|
32
|
+
* 'wip', 'memories', 'procedures', etc. Required in spirit (falls back to 'unknown'
|
|
33
|
+
* rather than throwing, matching the rest of the harness's fail-soft posture).
|
|
34
|
+
* @property {string} name a human label for the piece; defaults to `source`.
|
|
35
|
+
* @property {number} tokens the measured (or caller-supplied) token count.
|
|
36
|
+
* @property {number} chars the measured byte/char length of the injected text.
|
|
37
|
+
* @property {boolean} estimated true when `tokens` came from the chars/4 estimate
|
|
38
|
+
* rather than a caller-supplied exact count.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Measure and record ONE context injection into `record`. This is the single reporting
|
|
43
|
+
* path (AC2): a chat front, a plugin hook shell, and a headless seat all call this same
|
|
44
|
+
* function — none of them measures or records a piece any other way.
|
|
45
|
+
*
|
|
46
|
+
* @param {ReturnType<import('./session-record.mjs').createSessionRecord>} record
|
|
47
|
+
* @param {{ source?: string, name?: string, text?: string, tokens?: number, priority?: number, ts?: number }} [injection]
|
|
48
|
+
* @returns {ContextInjectionMeasurement} the measurement that was recorded.
|
|
49
|
+
*/
|
|
50
|
+
export function recordContextInjection(record, injection = {}) {
|
|
51
|
+
const { source, name, text, tokens, priority, ts } = injection;
|
|
52
|
+
const measured = measurePiece({ name: name ?? source, text, tokens, priority }, record?.events?.length ?? 0);
|
|
53
|
+
const decision = {
|
|
54
|
+
source: source ? String(source) : 'unknown',
|
|
55
|
+
name: measured.name,
|
|
56
|
+
tokens: measured.tokens,
|
|
57
|
+
chars: measured.chars,
|
|
58
|
+
estimated: measured.estimated,
|
|
59
|
+
};
|
|
60
|
+
recordEvent(record, { point: CONTEXT_INJECTION_POINT, ts }, decision);
|
|
61
|
+
return decision;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Every context-injection measurement recorded on `record` so far, in the order they were
|
|
66
|
+
* recorded. This is what "the session record shows bytes per injection with a source"
|
|
67
|
+
* (AC3) means in practice — a caller reads this instead of re-deriving it from raw events.
|
|
68
|
+
*
|
|
69
|
+
* @param {ReturnType<import('./session-record.mjs').createSessionRecord>|null} record
|
|
70
|
+
* @returns {ContextInjectionMeasurement[]}
|
|
71
|
+
*/
|
|
72
|
+
export function contextInjectionsOf(record) {
|
|
73
|
+
return (record?.events ?? [])
|
|
74
|
+
.filter((e) => e && e.point === CONTEXT_INJECTION_POINT)
|
|
75
|
+
.map((e) => e.decision)
|
|
76
|
+
.filter(Boolean);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Roll up every context injection recorded on `record`: total tokens/chars, and a
|
|
81
|
+
* per-source breakdown — the shape a `hs harness context` printer (or a test asserting a
|
|
82
|
+
* budget target) reads from.
|
|
83
|
+
*
|
|
84
|
+
* @param {ReturnType<import('./session-record.mjs').createSessionRecord>|null} record
|
|
85
|
+
* @returns {{
|
|
86
|
+
* injections: ContextInjectionMeasurement[],
|
|
87
|
+
* totalTokens: number,
|
|
88
|
+
* totalChars: number,
|
|
89
|
+
* bySource: Record<string, { tokens: number, chars: number, count: number }>,
|
|
90
|
+
* }}
|
|
91
|
+
*/
|
|
92
|
+
export function summarizeContextInjections(record) {
|
|
93
|
+
const injections = contextInjectionsOf(record);
|
|
94
|
+
const bySource = {};
|
|
95
|
+
let totalTokens = 0;
|
|
96
|
+
let totalChars = 0;
|
|
97
|
+
for (const injection of injections) {
|
|
98
|
+
totalTokens += injection.tokens || 0;
|
|
99
|
+
totalChars += injection.chars || 0;
|
|
100
|
+
const bucket = bySource[injection.source] ?? { tokens: 0, chars: 0, count: 0 };
|
|
101
|
+
bucket.tokens += injection.tokens || 0;
|
|
102
|
+
bucket.chars += injection.chars || 0;
|
|
103
|
+
bucket.count += 1;
|
|
104
|
+
bySource[injection.source] = bucket;
|
|
105
|
+
}
|
|
106
|
+
return { injections, totalTokens, totalChars, bySource };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Context-window-used percentage for a non-Claude-Code session (HS-CUWH, epic HS-8CMW) — the
|
|
111
|
+
* one figure the spexcode status line needs to show a real context number for a seat that
|
|
112
|
+
* isn't Claude Code (which reports its own). Deliberately NOT a second sizing path: the
|
|
113
|
+
* caller resolves `usedTokens` however it already knows tokens (a real usage count from a
|
|
114
|
+
* model response, or this module's own chars/4 `estimateTokens` from token-inventory.mjs when
|
|
115
|
+
* only characters are tracked — see spexharness/src/chat/chat-loop.mjs's `windowTokens` wiring
|
|
116
|
+
* for that fallback in practice) and `windowTokens` from the model's context window; this
|
|
117
|
+
* function only turns the two into a percentage.
|
|
118
|
+
*
|
|
119
|
+
* Returns the explicit marker `null` — never an invented number — when either input is
|
|
120
|
+
* missing, non-numeric, or non-positive, so a caller/renderer can print "unknown" instead of
|
|
121
|
+
* a misleading 0%.
|
|
122
|
+
*
|
|
123
|
+
* @param {{ usedTokens?: number, windowTokens?: number }} [args]
|
|
124
|
+
* @returns {number|null} an integer 0-100 (rounded, clamped at 100), or `null` when unknown.
|
|
125
|
+
*/
|
|
126
|
+
export function contextUsedPercent({ usedTokens, windowTokens } = {}) {
|
|
127
|
+
const used = Number(usedTokens);
|
|
128
|
+
const window = Number(windowTokens);
|
|
129
|
+
if (!Number.isFinite(used) || used <= 0) return null;
|
|
130
|
+
if (!Number.isFinite(window) || window <= 0) return null;
|
|
131
|
+
const pct = Math.round((used / window) * 100);
|
|
132
|
+
return Math.min(100, pct);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export default {
|
|
136
|
+
recordContextInjection,
|
|
137
|
+
contextInjectionsOf,
|
|
138
|
+
summarizeContextInjections,
|
|
139
|
+
contextUsedPercent,
|
|
140
|
+
CONTEXT_INJECTION_POINT,
|
|
141
|
+
};
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// spexharness/src/dev-server-control.mjs
|
|
2
|
+
//
|
|
3
|
+
// The SHARED dev-server protection logic — moved here verbatim from
|
|
4
|
+
// scripts/claude-hooks/dev-server-task-reminder.mjs (task HS-SJD3-D0UC,
|
|
5
|
+
// protocol [[protected-shared-actions-gate]]) as wave 6 of [[HS-AZ61-NYPR]]
|
|
6
|
+
// turned that hook into a thin shim over ./gates/dev-server-task-reminder.mjs.
|
|
7
|
+
// There is exactly ONE copy of the start/stop classification, the worktree
|
|
8
|
+
// bypass, the front read, and the fleet-queue claim wrapper in the tree: this
|
|
9
|
+
// one. The gate imports it; the Claude Code shim reaches it through the gate;
|
|
10
|
+
// scripts/claude-hooks/dev-server-task-reminder.smoke.mjs drives it directly.
|
|
11
|
+
//
|
|
12
|
+
// TIER 1 (hard gate): starting or stopping the shared spex-console dev server
|
|
13
|
+
// is a PROTECTED action. In Assisted front it ALWAYS asks (approving IS the
|
|
14
|
+
// permission). In Autonomous front it requires a fleet-queue claim on this
|
|
15
|
+
// checkout's `dev-server-<checkout-key>` lane: a second agent's concurrent
|
|
16
|
+
// attempt is rejected while the claim is held, and can retry after
|
|
17
|
+
// release/TTL expiry. Worktree-isolated agents bypass entirely — each has its
|
|
18
|
+
// own server/port ([[parallel-agent-isolation-guidance]]), so there is no
|
|
19
|
+
// shared resource to gate.
|
|
20
|
+
//
|
|
21
|
+
// TIER 2 (soft reminder): a server-impacting command (migration, Neon branch
|
|
22
|
+
// reset, dependency install, .next delete, a second `next dev`) gets a warning
|
|
23
|
+
// naming the live port, and only when the ground-truth status core says a
|
|
24
|
+
// server is actually up. It NEVER blocks.
|
|
25
|
+
//
|
|
26
|
+
// Pure-ish: the classification/reminder half is pure; the claim, audit, lock
|
|
27
|
+
// and status halves do I/O and every path they touch is a parameter.
|
|
28
|
+
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { spawnSync } from 'node:child_process';
|
|
32
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
33
|
+
|
|
34
|
+
// The runnable form of a HeySpex command on THIS machine — `hs …` when the bin
|
|
35
|
+
// is on PATH, else an absolute-path `node "<install>/bin/hs.mjs" …`. Never an
|
|
36
|
+
// `npx` form ([[HS-I6HT-EKYU-DIAG-every-message-tells-the-user-to-run-hs]]).
|
|
37
|
+
import { hsCommand } from '../../scripts/lib/hs-command.mjs';
|
|
38
|
+
import { enqueue, claim, complete, list } from '../../dev-factory/lib/fleet-queue.mjs';
|
|
39
|
+
|
|
40
|
+
/** spexharness/src/ → repo root is two levels up. */
|
|
41
|
+
const DEFAULT_REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
42
|
+
|
|
43
|
+
/** The `.heyspex/config.yaml` leaf that can revert tier 1 to legacy warn-only. */
|
|
44
|
+
export const TOGGLE_KEY = 'agent_behaviors.dev_server_control.enabled';
|
|
45
|
+
|
|
46
|
+
/** Env-first repo-root resolution, matching every pre-cutover hook's own helper. */
|
|
47
|
+
export function repoRoot(env = process.env) {
|
|
48
|
+
const fromEnv = env.CLAUDE_PROJECT_DIR?.trim();
|
|
49
|
+
if (fromEnv) return fromEnv.replace(/[\\/]+$/, '');
|
|
50
|
+
return DEFAULT_REPO_ROOT;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── TIER 2 — warn-only server-impacting detection ───────────────────────────
|
|
54
|
+
|
|
55
|
+
/** Shell-command patterns that can break / collide with a running dev server. */
|
|
56
|
+
export const SERVER_IMPACTING_COMMAND_PATTERNS = [
|
|
57
|
+
/\bdb:(?:migrate|baseline|generate|push)\b/, // npm run db:* (Drizzle migrations)
|
|
58
|
+
/\bdrizzle-kit\b/,
|
|
59
|
+
/\bmigrate\.mjs\b/,
|
|
60
|
+
/\bnpm\s+run\s+dev\b/, // starting a second wrapped dev server
|
|
61
|
+
/\bnext\s+dev\b/, // starting a bare second dev server
|
|
62
|
+
/\b(?:npm|pnpm|yarn)\s+(?:i|install|ci|add)\b/, // dependency changes (need a restart)
|
|
63
|
+
// Deleting the .next build dir out from under a running server.
|
|
64
|
+
/\brm\s+-[a-zA-Z]*r[a-zA-Z]*f?[a-zA-Z]*\s+[^|;&\n]*\.next\b/,
|
|
65
|
+
/\bRemove-Item\b[^|;&\n]*\.next\b/i,
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Neon MCP tools that mutate the schema / endpoint the running server is bound
|
|
70
|
+
* to. Matched on tool name.
|
|
71
|
+
*/
|
|
72
|
+
export const SERVER_IMPACTING_TOOL_NAMES = new Set([
|
|
73
|
+
'mcp__neon__prepare_database_migration',
|
|
74
|
+
'mcp__neon__complete_database_migration',
|
|
75
|
+
'mcp__neon__reset_from_parent',
|
|
76
|
+
'mcp__neon__delete_branch',
|
|
77
|
+
'mcp__Neon__prepare_database_migration',
|
|
78
|
+
'mcp__Neon__complete_database_migration',
|
|
79
|
+
'mcp__Neon__reset_from_parent',
|
|
80
|
+
'mcp__Neon__delete_branch',
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
/** True when this tool call could break a running dev server. */
|
|
84
|
+
export function isServerImpacting(toolName, toolInput) {
|
|
85
|
+
if (typeof toolName === 'string' && SERVER_IMPACTING_TOOL_NAMES.has(toolName)) return true;
|
|
86
|
+
if (toolName === 'Bash' || toolName === 'PowerShell') {
|
|
87
|
+
const command = toolInput?.command;
|
|
88
|
+
if (typeof command === 'string' && command.trim()) {
|
|
89
|
+
return SERVER_IMPACTING_COMMAND_PATTERNS.some((re) => re.test(command));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Build the soft-reminder text from a status verdict, or null if no server is live. */
|
|
96
|
+
export function buildReminder(status) {
|
|
97
|
+
if (!status || !status.running) return null;
|
|
98
|
+
const live = (status.servers || []).filter((s) => s.probe === 'console');
|
|
99
|
+
const where = live.map((s) => `:${s.port}${s.pid ? ` (listener pid ${s.pid})` : ''}`).join(', ');
|
|
100
|
+
const launcherPid = status.lockfile?.present && status.lockfile?.pid ? status.lockfile.pid : null;
|
|
101
|
+
const stopHint = launcherPid
|
|
102
|
+
? `Ctrl+C in its terminal, or stop the launcher process (pid ${launcherPid}), or run /kill-ports`
|
|
103
|
+
: 'Ctrl+C in its terminal, or run /kill-ports';
|
|
104
|
+
return (
|
|
105
|
+
`⚠ A spex-console dev server is RUNNING (${where}) and this command can break it. ` +
|
|
106
|
+
`Running a migration / schema change under a live server strands its in-memory ` +
|
|
107
|
+
`schema and forces a restart. Recommended: stop the server first (${stopHint}), run ` +
|
|
108
|
+
`the task, then restart with \`npm run dev\`. Re-check with \`${hsCommand('server status')}\` / ` +
|
|
109
|
+
`heyspex_dev_server_status. (Proceeding anyway is allowed — this is a reminder, not a block. ` +
|
|
110
|
+
`Mute: HEYSPEX_DEV_SERVER_REMINDER_OFF=1.)`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Ground-truth dev-server status. The answer comes from
|
|
116
|
+
* scripts/dev-server-status.mjs — the SAME core `heyspex_dev_server_status`
|
|
117
|
+
* itself calls (vendor/heyspex-mcp/dev-tools.mjs's `handleDevServerStatus` is a
|
|
118
|
+
* three-line wrapper over it), so an in-process tool-catalog call and this
|
|
119
|
+
* function cannot disagree.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} root
|
|
122
|
+
* @returns {Promise<object>}
|
|
123
|
+
*/
|
|
124
|
+
export async function getStatus(root = repoRoot()) {
|
|
125
|
+
const coreUrl = pathToFileURL(path.join(root, 'scripts', 'dev-server-status.mjs')).href;
|
|
126
|
+
const mod = await import(coreUrl);
|
|
127
|
+
return mod.getDevServerStatus({ vaultRoot: root });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── TIER 1 — hard gate for dev-server START/STOP ────────────────────────────
|
|
131
|
+
|
|
132
|
+
// Narrow start/stop detection — deliberately NOT the broad
|
|
133
|
+
// SERVER_IMPACTING_COMMAND_PATTERNS set above (migration/install detection stays
|
|
134
|
+
// warn-only, unchanged). Per protected-shared-actions-gate.md Gate 1.
|
|
135
|
+
const START_PATTERNS = [/\bnpm\s+run\s+dev\b/, /\bnext\s+dev\b/];
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Generic process-kill commands (Stop-Process / taskkill / bare kill) are
|
|
139
|
+
* ambiguous on their own — an agent kills stray processes for all kinds of
|
|
140
|
+
* reasons. They only count as a server STOP when they target the recorded
|
|
141
|
+
* dev-server lock pid. `/kill-ports` is the one exception: it is the
|
|
142
|
+
* dev-server-aware skill/CLI, already scoped to the recorded lock port.
|
|
143
|
+
*/
|
|
144
|
+
export function extractKillTargetPid(command) {
|
|
145
|
+
let m = command.match(/Stop-Process\s+(?:-Id\s+)?(\d+)/i);
|
|
146
|
+
if (m) return Number(m[1]);
|
|
147
|
+
m = command.match(/taskkill\b[^\n]*?\/pid\s+(\d+)/i);
|
|
148
|
+
if (m) return Number(m[1]);
|
|
149
|
+
// `kill <pid>` / `kill -9 <pid>` — word-boundaried so it never matches
|
|
150
|
+
// "skill" or "killed" inside prose/paths.
|
|
151
|
+
m = command.match(/(?:^|[\s;&|(])kill\s+(?:-\d+\s+)?(\d+)\b/i);
|
|
152
|
+
if (m) return Number(m[1]);
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The recorded dev-server launcher pid, or null when there is no lockfile. */
|
|
157
|
+
export function readLockPid(repoRootPath) {
|
|
158
|
+
try {
|
|
159
|
+
const raw = JSON.parse(
|
|
160
|
+
fs.readFileSync(path.join(repoRootPath, '.heyspex', 'dev-server.lock.json'), 'utf8'),
|
|
161
|
+
);
|
|
162
|
+
return typeof raw?.pid === 'number' ? raw.pid : null;
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Classify a tool call as a dev-server 'start', 'stop', or null. `lockPid` (the
|
|
170
|
+
* currently-recorded dev-server launcher pid, or null) scopes the generic kill
|
|
171
|
+
* commands to the actual server — a `kill <unrelated-pid>` never counts.
|
|
172
|
+
*/
|
|
173
|
+
export function classifyServerStartStop(toolName, toolInput, { lockPid = null } = {}) {
|
|
174
|
+
if (
|
|
175
|
+
toolName === 'Skill' &&
|
|
176
|
+
typeof toolInput?.skill === 'string' &&
|
|
177
|
+
/kill-ports/i.test(toolInput.skill)
|
|
178
|
+
) {
|
|
179
|
+
return 'stop';
|
|
180
|
+
}
|
|
181
|
+
if (toolName !== 'Bash' && toolName !== 'PowerShell') return null;
|
|
182
|
+
const command = toolInput?.command;
|
|
183
|
+
if (typeof command !== 'string' || !command.trim()) return null;
|
|
184
|
+
if (START_PATTERNS.some((re) => re.test(command))) return 'start';
|
|
185
|
+
if (/kill-ports/i.test(command)) return 'stop';
|
|
186
|
+
const targetPid = extractKillTargetPid(command);
|
|
187
|
+
if (targetPid != null && lockPid != null && targetPid === lockPid) return 'stop';
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Pure comparison: true when `gitDir` and `gitCommonDir` differ, i.e. this
|
|
193
|
+
* checkout is a linked git worktree, not the main checkout.
|
|
194
|
+
*/
|
|
195
|
+
export function isWorktreePaths(gitDir, gitCommonDir) {
|
|
196
|
+
if (!gitDir || !gitCommonDir) return false; // can't determine → not isolated (stay gated)
|
|
197
|
+
return path.resolve(gitDir) !== path.resolve(gitCommonDir);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Shell out to git to answer "is this cwd a linked worktree (not the main checkout)?" */
|
|
201
|
+
export function detectWorktreeIsolation(cwd = process.cwd()) {
|
|
202
|
+
try {
|
|
203
|
+
const dir = spawnSync('git', ['rev-parse', '--git-dir'], { cwd, encoding: 'utf8', timeout: 3000 });
|
|
204
|
+
const common = spawnSync('git', ['rev-parse', '--git-common-dir'], {
|
|
205
|
+
cwd,
|
|
206
|
+
encoding: 'utf8',
|
|
207
|
+
timeout: 3000,
|
|
208
|
+
});
|
|
209
|
+
if (dir.status !== 0 || common.status !== 0 || !dir.stdout?.trim() || !common.stdout?.trim()) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
return isWorktreePaths(
|
|
213
|
+
path.resolve(cwd, dir.stdout.trim()),
|
|
214
|
+
path.resolve(cwd, common.stdout.trim()),
|
|
215
|
+
);
|
|
216
|
+
} catch {
|
|
217
|
+
return false; // undetectable → stay gated (fail toward protection, not bypass)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The claim-bound session worktree's owner marker, written by
|
|
222
|
+
* dev-factory/lib/worktree-allocator.mjs. The name is mirrored rather than
|
|
223
|
+
* imported so this module stays loadable in a checkout with no dev-factory. */
|
|
224
|
+
export const WORKTREE_OWNER_FILE = '.hsewt-owner.json';
|
|
225
|
+
|
|
226
|
+
/** The env var a SPAWNED worktree session carries (set by the spawner). */
|
|
227
|
+
export const WORKTREE_ENV = 'HEYSPEX_WORKTREE';
|
|
228
|
+
|
|
229
|
+
/** The one stderr line a bypassed Gate 1 emits, so a bypass is never silent. */
|
|
230
|
+
export const WORKTREE_BYPASS_LOG =
|
|
231
|
+
'protected-actions gate: worktree-isolated session, dev-server gate bypassed';
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Is this checkout a claim-bound SESSION worktree?
|
|
235
|
+
* ([[HS-QDQA]], epic [[HS-03DO]] — the bypass [[protected-shared-actions-gate]]
|
|
236
|
+
* Gate 1 documents ("A worktree-isolated agent bypasses entirely — its own
|
|
237
|
+
* server, its own port") but only ever implemented via git's own linked-worktree
|
|
238
|
+
* probe, which misses a spawned session whose shell never resolved git, and
|
|
239
|
+
* which cannot be exercised without a real worktree on disk.)
|
|
240
|
+
*
|
|
241
|
+
* Two independent signals, either sufficient on its own:
|
|
242
|
+
* - `<cwd>/.hsewt-owner.json` with `kind: "session"` — the allocator's record;
|
|
243
|
+
* - `HEYSPEX_WORKTREE=1` — the spawner's env stamp.
|
|
244
|
+
*
|
|
245
|
+
* A `kind: "build"` marker (dev-factory's per-candidate gate worktree) is NOT a
|
|
246
|
+
* session and gets no bypass. Undetectable → false, i.e. STAY GATED: this
|
|
247
|
+
* function may only ever say "yes, isolated", never guess one.
|
|
248
|
+
*
|
|
249
|
+
* SCOPE: Gate 1 (dev-server start/stop) ONLY. Every destructive GIT verb keeps
|
|
250
|
+
* its own, separate gating in ./gates/git-stash-gate.mjs — a worktree still
|
|
251
|
+
* shares the object store, the reflog and the branch namespace with the primary
|
|
252
|
+
* checkout, so `git stash` / `reset --hard` / `push --force` are exactly as
|
|
253
|
+
* dangerous there as anywhere else. Nothing here is wired into that gate.
|
|
254
|
+
*
|
|
255
|
+
* @param {{ cwd?: string, env?: Record<string, string|undefined>, fs?: object }} [input]
|
|
256
|
+
* @returns {boolean}
|
|
257
|
+
*/
|
|
258
|
+
export function isWorktreeIsolated({ cwd = process.cwd(), env = process.env, fs: fsImpl = fs } = {}) {
|
|
259
|
+
try {
|
|
260
|
+
const raw = fsImpl.readFileSync(path.join(String(cwd), WORKTREE_OWNER_FILE), 'utf8');
|
|
261
|
+
const marker = JSON.parse(raw);
|
|
262
|
+
if (marker && typeof marker === 'object' && marker.kind === 'session') return true;
|
|
263
|
+
} catch {
|
|
264
|
+
// No marker / unreadable / not JSON — fall through to the env signal.
|
|
265
|
+
}
|
|
266
|
+
return String(env?.[WORKTREE_ENV] ?? '') === '1';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Read the live front (`assisted` | `autonomous`) off
|
|
271
|
+
* dev-factory/state/coo-dispatcher-state.json — re-read fresh every call so a
|
|
272
|
+
* mid-session front flip takes effect on the very next gated action.
|
|
273
|
+
*/
|
|
274
|
+
export function readFront(repoRootPath) {
|
|
275
|
+
try {
|
|
276
|
+
const p = path.join(repoRootPath, 'dev-factory', 'state', 'coo-dispatcher-state.json');
|
|
277
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
278
|
+
return raw?.front === 'autonomous' ? 'autonomous' : 'assisted';
|
|
279
|
+
} catch {
|
|
280
|
+
return 'assisted';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Deterministic per-checkout fleet-queue lane name. */
|
|
285
|
+
export function devServerLane(repoRootPath) {
|
|
286
|
+
const key = path.basename(repoRootPath).toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
287
|
+
return `dev-server-${key || 'checkout'}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Path of the standing "who holds the dev server" marker for a checkout. */
|
|
291
|
+
export function activeClaimPath(repoRootPath) {
|
|
292
|
+
return path.join(repoRootPath, '.heyspex', 'dev-server-active-claim.json');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* THIN wrapper over dev-factory/lib/fleet-queue.mjs (enqueue/claim/complete/list
|
|
297
|
+
* — unmodified) implementing the one exclusive "who currently owns the shared
|
|
298
|
+
* dev server" resource per lane. All I/O is parameterized.
|
|
299
|
+
*/
|
|
300
|
+
export function attemptDevServerClaim({ lane, action, claimant, queuePath, activePath, now = Date.now() }) {
|
|
301
|
+
let active = null;
|
|
302
|
+
try {
|
|
303
|
+
active = JSON.parse(fs.readFileSync(activePath, 'utf8'));
|
|
304
|
+
} catch {
|
|
305
|
+
active = null;
|
|
306
|
+
}
|
|
307
|
+
if (active && active.lane === lane && active.claimant !== claimant) {
|
|
308
|
+
const heldItems = list(lane, 'claimed', { queuePath, now });
|
|
309
|
+
const stillHeld = heldItems.some((i) => i.id === active.id);
|
|
310
|
+
if (stillHeld) {
|
|
311
|
+
return {
|
|
312
|
+
ok: false,
|
|
313
|
+
reason:
|
|
314
|
+
`dev-server resource "${lane}" is already claimed by ${active.claimant} (since ${active.claimedAt}) — ` +
|
|
315
|
+
`back off and retry after release or TTL expiry.`,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const enq = enqueue({ lane, payload: { action }, requestedBy: claimant, queuePath, now });
|
|
321
|
+
const res = claim(enq.id, claimant, { queuePath, now });
|
|
322
|
+
if (!res.ok) {
|
|
323
|
+
return { ok: false, reason: `dev-server resource "${lane}" claim rejected: ${res.reason}` };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
fs.mkdirSync(path.dirname(activePath), { recursive: true });
|
|
328
|
+
fs.writeFileSync(
|
|
329
|
+
activePath,
|
|
330
|
+
JSON.stringify({ lane, id: enq.id, claimant, action, claimedAt: new Date(now).toISOString() }),
|
|
331
|
+
);
|
|
332
|
+
} catch {
|
|
333
|
+
/* best-effort marker write — the queue-level claim already holds regardless */
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (action === 'stop') {
|
|
337
|
+
complete(enq.id, { action }, { queuePath, now });
|
|
338
|
+
try {
|
|
339
|
+
fs.rmSync(activePath, { force: true });
|
|
340
|
+
} catch {
|
|
341
|
+
/* best-effort */
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return { ok: true, id: enq.id };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Append one row to `.heyspex/dev-server-history.jsonl`. Never throws. */
|
|
349
|
+
export function auditHistory(repoRootPath, record) {
|
|
350
|
+
try {
|
|
351
|
+
const dir = path.join(repoRootPath, '.heyspex');
|
|
352
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
353
|
+
fs.appendFileSync(
|
|
354
|
+
path.join(dir, 'dev-server-history.jsonl'),
|
|
355
|
+
JSON.stringify({ timestamp: new Date().toISOString(), ...record }) + '\n',
|
|
356
|
+
);
|
|
357
|
+
} catch {
|
|
358
|
+
/* observability must never break the gate */
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** The exact ask text the Assisted front shows. Pure; asserted by the goldens. */
|
|
363
|
+
export function askReason(action) {
|
|
364
|
+
return (
|
|
365
|
+
`Starting or stopping the shared spex-console dev server (${action}) is a PROTECTED action ` +
|
|
366
|
+
`(protected-shared-actions-gate) — it always requires explicit permission in Assisted front. ` +
|
|
367
|
+
`Approving this prompt IS the permission.`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export default {
|
|
372
|
+
TOGGLE_KEY,
|
|
373
|
+
activeClaimPath,
|
|
374
|
+
askReason,
|
|
375
|
+
attemptDevServerClaim,
|
|
376
|
+
auditHistory,
|
|
377
|
+
buildReminder,
|
|
378
|
+
classifyServerStartStop,
|
|
379
|
+
detectWorktreeIsolation,
|
|
380
|
+
devServerLane,
|
|
381
|
+
extractKillTargetPid,
|
|
382
|
+
getStatus,
|
|
383
|
+
isServerImpacting,
|
|
384
|
+
isWorktreeIsolated,
|
|
385
|
+
isWorktreePaths,
|
|
386
|
+
readFront,
|
|
387
|
+
readLockPid,
|
|
388
|
+
repoRoot,
|
|
389
|
+
};
|