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
|
@@ -19,28 +19,43 @@
|
|
|
19
19
|
* offline pointer is how an MCP restart picks up a CLI `hs vault use` switch
|
|
20
20
|
* without Neon.
|
|
21
21
|
*
|
|
22
|
-
* Precedence stack (highest wins) —
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
22
|
+
* Precedence stack (highest wins) — ONE order shared with the TS resolver
|
|
23
|
+
* (`spex-console/src/lib/vault-resolve/index.ts`), pinned row-by-row by the
|
|
24
|
+
* shared fixture `spex-console/src/lib/vault-resolve/precedence-fixtures.json`
|
|
25
|
+
* ([[HS-VVNQ]], decision memory `vault-resolver-precedence`). History: HS-C87T
|
|
26
|
+
* put the folder the process started in above the registry pointer; HS-VVNQ
|
|
27
|
+
* then put it above the env vars too, because the plugin manifest
|
|
28
|
+
* (`plugins/heyspex/.mcp.json`) sets `HEYSPEX_VAULT_ROOT=${CLAUDE_PROJECT_DIR}`
|
|
29
|
+
* on EVERY session — an env tier above the walk silently outranked every
|
|
30
|
+
* on-disk pin and marker:
|
|
29
31
|
* 1. explicit vaultId -> registry lookup (vaults[].id)
|
|
30
|
-
* 2.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* registry
|
|
41
|
-
*
|
|
32
|
+
* 2. cwd-contained vault OR a -> walk UP from cwd through every ancestor
|
|
33
|
+
* satellite attachment looking for a heyspex.yaml marker, a
|
|
34
|
+
* specs/ dir, or a satellite repo's MCP
|
|
35
|
+
* manifest pin (readSatellitePin); the
|
|
36
|
+
* nearest match wins, attachment first.
|
|
37
|
+
* Sources: 'satellite-pin' (attachment);
|
|
38
|
+
* 'cwd-marker' (heyspex.yaml carries a
|
|
39
|
+
* `vault_id` — that id IS the vault id,
|
|
40
|
+
* registry row or not); 'marker-registry-
|
|
41
|
+
* mismatch' (the marker id disagrees with
|
|
42
|
+
* the registry row for that root — the
|
|
43
|
+
* marker id wins and stderr names
|
|
44
|
+
* `hs connect --check`); 'cwd-heuristic'
|
|
45
|
+
* (a marker without `vault_id`, or a bare
|
|
46
|
+
* specs/ dir: id from the registry row, else
|
|
47
|
+
* the `unregistered` sentinel). Logs one
|
|
48
|
+
* stderr notice iff the cwd vault differs
|
|
49
|
+
* from the registry's active vault — silent
|
|
50
|
+
* when they agree.
|
|
51
|
+
* 3. HEYSPEX_VAULT_ROOT env -> repo root (legacy escape hatch)
|
|
52
|
+
* 3. HEYSPEX_VAULT_DIR env -> the specs/ dir itself (repo root = parent)
|
|
53
|
+
* 4. registry active_vault_id -> the offline active pointer (vaults[].id);
|
|
54
|
+
* schema v2: the current account's row
|
|
55
|
+
* (`accounts[current_account_id]`), v1:
|
|
56
|
+
* the machine-global field
|
|
42
57
|
* — reached only when cwd sits inside no
|
|
43
|
-
* vault
|
|
58
|
+
* vault and no env var names one
|
|
44
59
|
* 5. cwd fallback -> cwd itself (no marker found anywhere;
|
|
45
60
|
* preserves the pre-existing default)
|
|
46
61
|
*
|
|
@@ -50,8 +65,9 @@
|
|
|
50
65
|
* behavior, so nothing regresses when no registry exists.
|
|
51
66
|
*/
|
|
52
67
|
|
|
68
|
+
import { heyspexHome } from '../../scripts/lib/heyspex-home.mjs';
|
|
53
69
|
import { existsSync, readFileSync } from 'node:fs';
|
|
54
|
-
import { homedir } from 'node:os';
|
|
70
|
+
import { homedir, tmpdir } from 'node:os';
|
|
55
71
|
import { dirname, join, resolve } from 'node:path';
|
|
56
72
|
|
|
57
73
|
/**
|
|
@@ -70,7 +86,7 @@ export const UNREGISTERED_VAULT_ID = 'unregistered';
|
|
|
70
86
|
* @returns {string}
|
|
71
87
|
*/
|
|
72
88
|
function vaultRegistryDir(opts = {}) {
|
|
73
|
-
return opts.registryDir ?? join(
|
|
89
|
+
return opts.registryDir ?? join(heyspexHome(), '.heyspex');
|
|
74
90
|
}
|
|
75
91
|
|
|
76
92
|
/**
|
|
@@ -90,6 +106,8 @@ function isNonEmptyString(v) {
|
|
|
90
106
|
* Read + defensively parse `~/.heyspex/registry.json`. A missing or malformed
|
|
91
107
|
* file returns `null` (never throws) so callers fall through to env/cwd tiers —
|
|
92
108
|
* mirroring registry-file.ts `loadVaultRegistry`'s fail-closed contract.
|
|
109
|
+
* Reads schema v1 and v2 alike; `active_vault_id` is the pointer
|
|
110
|
+
* {@link registryPointerVaultId} picks.
|
|
93
111
|
*
|
|
94
112
|
* @param {{ registryDir?: string }} [opts]
|
|
95
113
|
* @returns {{ vaults: Array<{ id: string, repo_root_abs: string }>, active_vault_id: string|null }|null}
|
|
@@ -113,8 +131,76 @@ function loadRegistry(opts = {}) {
|
|
|
113
131
|
if (!isNonEmptyString(raw.repo_root_abs)) continue;
|
|
114
132
|
vaults.push({ id: raw.id, repo_root_abs: raw.repo_root_abs });
|
|
115
133
|
}
|
|
116
|
-
|
|
117
|
-
|
|
134
|
+
return { vaults, active_vault_id: registryPointerVaultId(json) };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Mirrors registry-file.ts `LEGACY_REGISTRY_ACCOUNT_ID`. */
|
|
138
|
+
const LEGACY_REGISTRY_ACCOUNT_ID = 'legacy';
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The offline active-vault pointer of a parsed registry file, schema v1 or v2
|
|
142
|
+
* ([[HS-RXLG-WW9R-TASK-key-registry-json-per-account-schema-v2]]). Mirrors
|
|
143
|
+
* registry-file.ts `currentAccountPointers`:
|
|
144
|
+
* - v2: `accounts[current_account_id].active_vault_id`. An ABSENT field
|
|
145
|
+
* reads the pre-account `legacy` row a not-yet-migrated machine left
|
|
146
|
+
* behind; an explicit `null` is a clear (reads null) when the account
|
|
147
|
+
* owns a row here, else it too reads `legacy`. No current account → the
|
|
148
|
+
* `legacy` value or null — never some other account's.
|
|
149
|
+
* - v1: the machine-global `active_vault_id`.
|
|
150
|
+
*
|
|
151
|
+
* @param {Record<string, unknown>} json
|
|
152
|
+
* @returns {string|null}
|
|
153
|
+
*/
|
|
154
|
+
export function registryPointerVaultId(json) {
|
|
155
|
+
if (!json || typeof json !== 'object') return null;
|
|
156
|
+
if (json.schema_version !== 2) {
|
|
157
|
+
return isNonEmptyString(json.active_vault_id) ? json.active_vault_id : null;
|
|
158
|
+
}
|
|
159
|
+
const accounts =
|
|
160
|
+
json.accounts && typeof json.accounts === 'object' && !Array.isArray(json.accounts)
|
|
161
|
+
? json.accounts
|
|
162
|
+
: {};
|
|
163
|
+
const current = isNonEmptyString(json.current_account_id) ? json.current_account_id : null;
|
|
164
|
+
const rowOf = (id) => (id && accounts[id] && typeof accounts[id] === 'object' ? accounts[id] : null);
|
|
165
|
+
const own = rowOf(current);
|
|
166
|
+
if (own && isNonEmptyString(own.active_vault_id)) return own.active_vault_id;
|
|
167
|
+
// An explicit null is a CLEAR — but only from an account that owns a row
|
|
168
|
+
// here; a stray row of an account that owns nothing still reads legacy.
|
|
169
|
+
const ownsRows = [json.vaults, json.workspaces].some(
|
|
170
|
+
(rows) => Array.isArray(rows) && rows.some((r) => r && r.account_id === current),
|
|
171
|
+
);
|
|
172
|
+
if (own && own.active_vault_id === null && ownsRows && current !== LEGACY_REGISTRY_ACCOUNT_ID) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const legacy = rowOf(LEGACY_REGISTRY_ACCOUNT_ID);
|
|
176
|
+
return legacy && isNonEmptyString(legacy.active_vault_id) ? legacy.active_vault_id : null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The account the MACHINE-LOCAL registry row for `repoRootAbs` was registered
|
|
181
|
+
* under (`vaults[].account_id`), or null (no row, or the pre-account `legacy`
|
|
182
|
+
* bucket). The repo-to-account binding lives here - per machine, per user -
|
|
183
|
+
* and NEVER in the committed heyspex.yaml: a second developer with another
|
|
184
|
+
* account must not inherit the first one's account (owner ruling 2026-10-16).
|
|
185
|
+
*
|
|
186
|
+
* @param {string} repoRootAbs
|
|
187
|
+
* @param {{ registryDir?: string }} [opts]
|
|
188
|
+
* @returns {string|null}
|
|
189
|
+
*/
|
|
190
|
+
export function registryAccountIdForRoot(repoRootAbs, opts = {}) {
|
|
191
|
+
if (!isNonEmptyString(repoRootAbs)) return null;
|
|
192
|
+
const file = vaultRegistryFilePath(opts);
|
|
193
|
+
let json;
|
|
194
|
+
try {
|
|
195
|
+
json = JSON.parse(readFileSync(file, 'utf8'));
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const target = normalizeRoot(repoRootAbs);
|
|
200
|
+
const rows = Array.isArray(json?.vaults) ? json.vaults : [];
|
|
201
|
+
const row = rows.find((v) => v && isNonEmptyString(v.repo_root_abs) && normalizeRoot(v.repo_root_abs) === target);
|
|
202
|
+
const id = row?.account_id;
|
|
203
|
+
return isNonEmptyString(id) && id !== LEGACY_REGISTRY_ACCOUNT_ID ? id.trim() : null;
|
|
118
204
|
}
|
|
119
205
|
|
|
120
206
|
/** Find a vault row by id. Returns null when absent. */
|
|
@@ -124,40 +210,358 @@ function findVaultById(registry, vaultId) {
|
|
|
124
210
|
}
|
|
125
211
|
|
|
126
212
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
213
|
+
* Normalize a path for comparison: forward slashes, no trailing separator,
|
|
214
|
+
* lower-cased (this CLI's primary platform is Windows, where `C:\Dev` and
|
|
215
|
+
* `c:\dev` name one directory). Mirrors `satellite.ts`'s
|
|
216
|
+
* `normalizePathForCompare`.
|
|
217
|
+
*/
|
|
218
|
+
function normalizeRoot(p) {
|
|
219
|
+
return resolve(p)
|
|
220
|
+
.replace(/[\\/]+/g, '/')
|
|
221
|
+
.replace(/\/+$/, '')
|
|
222
|
+
.toLowerCase();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* REVERSE-resolve a filesystem repo root to its registered vault row — the
|
|
227
|
+
* `.mjs` twin of the TS resolver's `registryLookupByRoot`. Without it a
|
|
228
|
+
* path-first tier reports the `unregistered` sentinel even when the registry
|
|
229
|
+
* knows that exact repo, which misfiles the whole index under `unregistered`
|
|
230
|
+
* ([[HS-XOZ3-DIAG-NWG6-vault-identity-split-reconciled-to]]).
|
|
231
|
+
*
|
|
232
|
+
* @param {{ vaults: Array<{id: string, repo_root_abs: string}> }|null} registry
|
|
233
|
+
* @param {string} repoRootAbs
|
|
234
|
+
*/
|
|
235
|
+
function findVaultByRoot(registry, repoRootAbs) {
|
|
236
|
+
if (!registry || !isNonEmptyString(repoRootAbs)) return null;
|
|
237
|
+
const target = normalizeRoot(repoRootAbs);
|
|
238
|
+
return registry.vaults.find((v) => normalizeRoot(v.repo_root_abs) === target) ?? null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The MCP manifests a SATELLITE repo records its vault attachment in, and the
|
|
243
|
+
* env key inside them. Mirrors `spex-console/src/lib/cli/satellite.ts`
|
|
244
|
+
* (`SATELLITE_MANIFEST_RELS` / `SATELLITE_VAULT_ENV`) — keep the two in sync;
|
|
245
|
+
* the TS module carries the full rationale.
|
|
246
|
+
*/
|
|
247
|
+
const SATELLITE_MANIFESTS = [['.mcp.json'], ['.cursor', 'mcp.json']];
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Is `p` an ABSOLUTE path? Pure string test over the three shapes a real pin
|
|
251
|
+
* can carry: a Windows drive path (`C:\vault`, `C:/vault`), a UNC share
|
|
252
|
+
* (`\\server\share\vault`) and a POSIX root (`/vault`). Mirrors
|
|
253
|
+
* `satellite.ts`'s `isAbsolutePathForCompare` — keep the two in sync; the TS
|
|
254
|
+
* module carries the full rationale for why a non-absolute pin is refused
|
|
255
|
+
* rather than resolved.
|
|
256
|
+
*
|
|
257
|
+
* @param {string} p
|
|
258
|
+
* @returns {boolean}
|
|
259
|
+
*/
|
|
260
|
+
function isAbsolutePin(p) {
|
|
261
|
+
const s = p.trim();
|
|
262
|
+
return /^[\\/]/.test(s) || /^[a-zA-Z]:[\\/]/.test(s);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Read a satellite attachment out of `dir`'s MCP manifests: the heyspex
|
|
267
|
+
* server's `env.HEYSPEX_VAULT_ROOT` pin, when it names a path OUTSIDE `dir`.
|
|
268
|
+
* Two kinds of value are NOT an attachment:
|
|
269
|
+
*
|
|
270
|
+
* - one still carrying an unexpanded `${...}` — the client's own
|
|
271
|
+
* project-dir placeholder that a normal (this-repo-IS-the-vault) install
|
|
272
|
+
* writes; and
|
|
273
|
+
* - one that is not an ABSOLUTE path ({@link isAbsolutePin}). This used to
|
|
274
|
+
* be `resolve(trimmed)`, which resolved a relative pin against
|
|
275
|
+
* `process.cwd()` — NOT the manifest's own directory — so a manifest in
|
|
276
|
+
* `<repo>` pinning `../some-vault` returned a directory beside whatever
|
|
277
|
+
* folder the MCP server happened to be launched from. No resolution is
|
|
278
|
+
* defensible here: the same string also reaches the server as the
|
|
279
|
+
* `HEYSPEX_VAULT_ROOT` env var, where tier 2 resolves it against the
|
|
280
|
+
* server process's cwd, so a manifest-dir rule would contradict the
|
|
281
|
+
* runtime. Refusing fails safe and matches `satellite.ts`.
|
|
282
|
+
*
|
|
283
|
+
* Pure + synchronous and never throws — an unreadable/unparseable manifest
|
|
284
|
+
* reads as "no attachment", the same fail-open posture as the registry
|
|
285
|
+
* loader above.
|
|
286
|
+
*
|
|
287
|
+
* @param {string} dir
|
|
288
|
+
* @returns {string|null} the attached vault's absolute repo root
|
|
289
|
+
*/
|
|
290
|
+
export function readSatellitePin(dir) {
|
|
291
|
+
const d = inspectSatellitePin(dir);
|
|
292
|
+
return d && d.kind === 'pin' ? d.root : null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Expand the client placeholders a PORTABLE pin may carry against the
|
|
297
|
+
* manifest's own directory: ${CLAUDE_PROJECT_DIR[:-default]} (Claude Code) and
|
|
298
|
+
* ${workspaceFolder} (Cursor). Anything else left as ${...} is unresolvable.
|
|
299
|
+
*/
|
|
300
|
+
function expandPinPlaceholders(value, dir) {
|
|
301
|
+
return value.replace(/\$\{(?:CLAUDE_PROJECT_DIR|workspaceFolder)(?::-[^}]*)?\}/g, dir);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Classify the heyspex server's HEYSPEX_VAULT_ROOT in `dir`'s MCP manifests.
|
|
306
|
+
* Returns null when no manifest names a pin at all, else one of:
|
|
307
|
+
* { kind: 'pin', root, raw, source } an attachment to a vault OUTSIDE dir
|
|
308
|
+
* { kind: 'self' } ordinary install (pin is dir or under it)
|
|
309
|
+
* { kind: 'broken', raw, source, reason } a pin that cannot be resolved
|
|
310
|
+
* (unknown ${...} placeholder). A broken pin must REFUSE, never fall
|
|
311
|
+
* through to the account-wide pointer (owner principle P1).
|
|
312
|
+
*
|
|
313
|
+
* PORTABLE pins (owner principle P3, 2026-10-16): a pin may be RELATIVE
|
|
314
|
+
* ('../mlc-specs') or built on the project-dir placeholder
|
|
315
|
+
* ('${CLAUDE_PROJECT_DIR:-.}/../mlc-specs'); both resolve against the
|
|
316
|
+
* MANIFEST'S OWN directory, never the process cwd, so the committed manifest
|
|
317
|
+
* carries no machine-specific absolute path.
|
|
318
|
+
*
|
|
319
|
+
* @param {string} dir
|
|
320
|
+
*/
|
|
321
|
+
export function inspectSatellitePin(dir) {
|
|
322
|
+
// ONE precedence rule, identical in the TS twin (satellite.ts
|
|
323
|
+
// readPinnedVaultRoot + the resolver walk): at each directory a committed
|
|
324
|
+
// satellite MARKER (heyspex.yaml with vault_path) beats any manifest pin; of
|
|
325
|
+
// the manifests, the first VALID outside pin in read order (.mcp.json, then
|
|
326
|
+
// .cursor/mcp.json) wins no matter what the other manifest says; a broken
|
|
327
|
+
// pin refuses only when no manifest carries a valid one; 'self' (an ordinary
|
|
328
|
+
// install) is the weakest signal. Shared table: satellite-precedence-fixtures.json.
|
|
329
|
+
let broken = null;
|
|
330
|
+
let self = false;
|
|
331
|
+
for (const rel of SATELLITE_MANIFESTS) {
|
|
332
|
+
let raw;
|
|
333
|
+
try {
|
|
334
|
+
raw = readFileSync(join(dir, ...rel), 'utf8');
|
|
335
|
+
} catch {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
let value;
|
|
339
|
+
try {
|
|
340
|
+
value = JSON.parse(raw)?.mcpServers?.heyspex?.env?.HEYSPEX_VAULT_ROOT;
|
|
341
|
+
} catch {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (!isNonEmptyString(value)) continue;
|
|
345
|
+
const source = rel.join('/');
|
|
346
|
+
const trimmed = value.trim();
|
|
347
|
+
const expanded = expandPinPlaceholders(trimmed, dir);
|
|
348
|
+
if (expanded.includes('${')) {
|
|
349
|
+
broken ??= { kind: 'broken', raw: trimmed, source, reason: 'it contains a placeholder other than ${CLAUDE_PROJECT_DIR} / ${workspaceFolder}' };
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (/^[a-zA-Z]:(?![\\/])/.test(expanded)) {
|
|
353
|
+
broken ??= { kind: 'broken', raw: trimmed, source, reason: 'a drive-relative path (C:dir) names no single directory' };
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const abs = isAbsolutePin(expanded) ? resolve(expanded) : resolve(dir, expanded);
|
|
357
|
+
const pinned = normalizeRoot(abs);
|
|
358
|
+
const here = normalizeRoot(dir);
|
|
359
|
+
if (pinned === here || pinned.startsWith(`${here}/`)) {
|
|
360
|
+
self = true;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
return { kind: 'pin', root: abs, raw: trimmed, source };
|
|
364
|
+
}
|
|
365
|
+
if (broken) return broken;
|
|
366
|
+
return self ? { kind: 'self' } : null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Walk up from `startDir` looking for the NEAREST manifest pin that cannot be
|
|
371
|
+
* honoured: unresolvable placeholder, or a pin whose target is not a vault
|
|
372
|
+
* root on this machine. Returns null when the nearest pin (or marker) is fine
|
|
373
|
+
* or there is none. `hs` uses it to REFUSE instead of falling back to the
|
|
374
|
+
* account-wide pointer ([[HS-O40H]], owner principle P1).
|
|
375
|
+
*
|
|
376
|
+
* @param {string} startDir
|
|
377
|
+
* @param {string} [stopAt]
|
|
378
|
+
* @returns {{ dir: string, raw: string, source: string, reason: string }|null}
|
|
379
|
+
*/
|
|
380
|
+
export function findBrokenSatellitePin(startDir, stopAt) {
|
|
381
|
+
let dir = resolve(startDir);
|
|
382
|
+
const boundary = isNonEmptyString(stopAt) ? resolve(stopAt) : null;
|
|
383
|
+
for (let i = 0; i < 256; i += 1) {
|
|
384
|
+
if (readMarkerSatellite(dir)) return null; // marker outranks any pin
|
|
385
|
+
const d = inspectSatellitePin(dir);
|
|
386
|
+
if (d?.kind === 'broken') return { dir, raw: d.raw, source: d.source, reason: d.reason };
|
|
387
|
+
if (d?.kind === 'pin') {
|
|
388
|
+
if (isVaultRoot(d.root)) return null;
|
|
389
|
+
return { dir, raw: d.raw, source: d.source, reason: `${d.root} is not a vault on this machine (no heyspex.yaml or specs/ there; clone the vault repo there or fix the pin)` };
|
|
390
|
+
}
|
|
391
|
+
if (d?.kind === 'self') return null;
|
|
392
|
+
if (readMarkerSatellite(dir) || isVaultRoot(dir)) return null;
|
|
393
|
+
if (boundary && dir === boundary) return null;
|
|
394
|
+
const parent = dirname(dir);
|
|
395
|
+
if (parent === dir) return null;
|
|
396
|
+
dir = parent;
|
|
397
|
+
}
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Is `dir` a vault root? True when it contains a `heyspex.yaml` marker file
|
|
403
|
+
* OR a `specs/` dir — the two on-disk signals that a folder IS a vault root
|
|
404
|
+
* (mirrors `hs init`'s marker + the entity-model's folder layout). This is
|
|
405
|
+
* the ONE definition of "is this a vault root" — every walk/check in this
|
|
406
|
+
* file (and in `bin/hs.mjs`) routes through it rather than re-testing the
|
|
407
|
+
* two signals inline. Pure and synchronous: no config reads, no walking, no
|
|
408
|
+
* side effects — just the two existsSync checks.
|
|
409
|
+
*
|
|
410
|
+
* One exclusion ([[HS-VVNQ]]): the OS temp directory ITSELF is never a vault
|
|
411
|
+
* root. It is shared scratch for every process on the machine, test runs
|
|
412
|
+
* leave a stray `specs/` there, and once the walk outranks env and the
|
|
413
|
+
* pointer, every process started anywhere under it would otherwise resolve
|
|
414
|
+
* to that junk folder. Subdirectories of it (a real scratch vault) still
|
|
415
|
+
* count. The TS resolver applies the same rule.
|
|
416
|
+
*
|
|
417
|
+
* @param {string} dir
|
|
418
|
+
* @returns {boolean}
|
|
419
|
+
*/
|
|
420
|
+
export function isVaultRoot(dir) {
|
|
421
|
+
if (normalizeRoot(dir) === normalizeRoot(tmpdir())) return false;
|
|
422
|
+
return existsSync(join(dir, 'heyspex.yaml')) || existsSync(join(dir, 'specs'));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Extract `vault_id: <value>` from raw heyspex.yaml text, or `null` if absent.
|
|
427
|
+
* Same semantics as `spex-console/src/lib/cli/init.ts` `parseVaultId` (the
|
|
428
|
+
* writer's own reader): first line-anchored `vault_id:` key, value trimmed,
|
|
429
|
+
* one matching pair of surrounding quotes stripped, empty -> `null`. Kept to
|
|
430
|
+
* this one line-regex on purpose — `hs init` writes the marker as flat
|
|
431
|
+
* `key: value` lines, so a YAML parser buys nothing here.
|
|
432
|
+
*
|
|
433
|
+
* @param {string} content
|
|
434
|
+
* @returns {string|null}
|
|
435
|
+
*/
|
|
436
|
+
export function parseMarkerVaultId(content) {
|
|
437
|
+
const m = String(content ?? '').match(/^vault_id:\s*(.+?)\s*$/m);
|
|
438
|
+
if (!m) return null;
|
|
439
|
+
const unquoted = m[1].trim().replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1');
|
|
440
|
+
return unquoted.length > 0 ? unquoted : null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* The `vault_id` written in `<dir>/heyspex.yaml`, or `null` (no marker, no
|
|
445
|
+
* key, unreadable). Never throws.
|
|
446
|
+
*
|
|
447
|
+
* @param {string} dir
|
|
448
|
+
* @returns {string|null}
|
|
449
|
+
*/
|
|
450
|
+
function readMarkerVaultId(dir) {
|
|
451
|
+
try {
|
|
452
|
+
return parseMarkerVaultId(readFileSync(join(dir, 'heyspex.yaml'), 'utf8'));
|
|
453
|
+
} catch {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Extract `vault_path: <value>` from raw heyspex.yaml text, or `null`.
|
|
460
|
+
* The PORTABLE satellite declaration (owner principles P1/P3, 2026-10-16): a
|
|
461
|
+
* code repo (e.g. mlc-app) commits a `heyspex.yaml` carrying its `vault_id`
|
|
462
|
+
* plus a RELATIVE `vault_path` (e.g. `../mlc-specs`) to where the attached
|
|
463
|
+
* vault is usually cloned. No absolute path is ever committed. An absolute,
|
|
464
|
+
* empty or unexpanded `${...}` value is not a declaration (returns null).
|
|
465
|
+
*
|
|
466
|
+
* @param {string} content
|
|
467
|
+
* @returns {string|null}
|
|
468
|
+
*/
|
|
469
|
+
export function parseMarkerVaultPath(content) {
|
|
470
|
+
const m = String(content ?? '').match(/^vault_path:[ \t]*(.+?)[ \t]*$/m);
|
|
471
|
+
if (!m) return null;
|
|
472
|
+
const v = m[1].trim().replace(/\s+#.*$/, '').replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1').trim();
|
|
473
|
+
if (!v || v.includes('${') || isAbsolutePin(v)) return null;
|
|
474
|
+
return v;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Is `<dir>/heyspex.yaml` a committed SATELLITE marker (it carries a relative
|
|
479
|
+
* `vault_path`)? Returns the attachment it declares, or `null`.
|
|
480
|
+
*
|
|
481
|
+
* - `vaultId`: the marker's `vault_id` (the vault this repo is bound to; the
|
|
482
|
+
* ONLY thing a fresh machine without the vault clone needs, because
|
|
483
|
+
* cloud-routed writes name the vault by id).
|
|
484
|
+
* - `vaultRootAbs`: `resolve(dir, vault_path)` when that directory exists on
|
|
485
|
+
* THIS machine and is a vault root whose own marker does not name a
|
|
486
|
+
* DIFFERENT vault id; otherwise `null` (not cloned here, or a different
|
|
487
|
+
* vault sits at that path - never read the wrong vault's disk).
|
|
488
|
+
*
|
|
489
|
+
* @param {string} dir
|
|
490
|
+
* @returns {{ vaultId: string|null, vaultPath: string, vaultRootAbs: string|null }|null}
|
|
491
|
+
*/
|
|
492
|
+
export function readMarkerSatellite(dir) {
|
|
493
|
+
let raw;
|
|
494
|
+
try {
|
|
495
|
+
raw = readFileSync(join(dir, 'heyspex.yaml'), 'utf8');
|
|
496
|
+
} catch {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
const vaultPath = parseMarkerVaultPath(raw);
|
|
500
|
+
if (!vaultPath) return null;
|
|
501
|
+
const vaultId = parseMarkerVaultId(raw);
|
|
502
|
+
const candidate = resolve(dir, vaultPath);
|
|
503
|
+
let vaultRootAbs = null;
|
|
504
|
+
if (normalizeRoot(candidate) !== normalizeRoot(dir) && isVaultRoot(candidate)) {
|
|
505
|
+
const theirs = readMarkerVaultId(candidate);
|
|
506
|
+
if (!vaultId || !theirs || theirs === vaultId) vaultRootAbs = candidate;
|
|
507
|
+
}
|
|
508
|
+
return { vaultId, vaultPath, vaultRootAbs };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* The marker id and the registry row for the same root disagree. The marker
|
|
513
|
+
* wins (it is the repo's own on-disk declaration, written by `hs init` /
|
|
514
|
+
* `hs connect`); this notice names the command whose `id_agreement` row
|
|
515
|
+
* reports the same disagreement so the operator can repair the registry.
|
|
516
|
+
*
|
|
517
|
+
* @param {string} repoRootAbs
|
|
518
|
+
* @param {string} markerVaultId
|
|
519
|
+
* @param {string} registryVaultId
|
|
520
|
+
*/
|
|
521
|
+
function logMarkerRegistryMismatch(repoRootAbs, markerVaultId, registryVaultId) {
|
|
522
|
+
process.stderr.write(
|
|
523
|
+
`[heyspex-mcp] vault resolution: ${resolve(repoRootAbs)}/heyspex.yaml says vault_id ${markerVaultId} but the registry row for that root says ${registryVaultId} — using the marker id. Run \`hs connect --check\` to see and repair the disagreement.\n`,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Walk UP from `startDir` taking the NEAREST of the two tier-2 (walk) signals: a
|
|
529
|
+
* vault root (see {@link isVaultRoot}) or a satellite attachment (see
|
|
530
|
+
* {@link readSatellitePin}). The attachment is checked FIRST at each level,
|
|
531
|
+
* because a repo that explicitly names its vault is never also asking to be
|
|
532
|
+
* one.
|
|
134
533
|
*
|
|
135
534
|
* @param {string} startDir
|
|
136
535
|
* @param {string|undefined} stopAt - test-only walk boundary (checked, then
|
|
137
536
|
* the walk stops — never climbs past it). Unset in production, so the walk
|
|
138
537
|
* always reaches the real filesystem root; tests use it to fence the walk
|
|
139
|
-
* inside a disposable tmp fixture so ambient/unrelated directories higher
|
|
140
|
-
* the real tree
|
|
141
|
-
*
|
|
142
|
-
* @returns {string|null}
|
|
538
|
+
* inside a disposable tmp fixture so ambient/unrelated directories higher
|
|
539
|
+
* up the real tree cannot produce a false match.
|
|
540
|
+
* @returns {{ repoRootAbs: string, satellite: boolean, markerSatellite?: ReturnType<typeof readMarkerSatellite> }|null}
|
|
143
541
|
*/
|
|
144
|
-
function
|
|
542
|
+
function findCwdVaultOrAttachment(startDir, stopAt) {
|
|
145
543
|
let dir = startDir;
|
|
146
544
|
const boundary = isNonEmptyString(stopAt) ? resolve(stopAt) : null;
|
|
147
545
|
for (let i = 0; i < 256; i += 1) {
|
|
148
|
-
|
|
149
|
-
|
|
546
|
+
// Precedence (see inspectSatellitePin): committed satellite marker, then
|
|
547
|
+
// any valid manifest pin, then the generic vault-root test.
|
|
548
|
+
const marked = readMarkerSatellite(dir);
|
|
549
|
+
if (marked) {
|
|
550
|
+
return { repoRootAbs: marked.vaultRootAbs ?? dir, satellite: true, markerSatellite: marked };
|
|
150
551
|
}
|
|
151
|
-
|
|
552
|
+
const pinned = readSatellitePin(dir);
|
|
553
|
+
if (pinned) return { repoRootAbs: pinned, satellite: true };
|
|
554
|
+
if (isVaultRoot(dir)) return { repoRootAbs: dir, satellite: false };
|
|
555
|
+
if (boundary && dir === boundary) break;
|
|
152
556
|
const parent = dirname(dir);
|
|
153
|
-
if (parent === dir) break;
|
|
557
|
+
if (parent === dir) break;
|
|
154
558
|
dir = parent;
|
|
155
559
|
}
|
|
156
560
|
return null;
|
|
157
561
|
}
|
|
158
562
|
|
|
159
563
|
/**
|
|
160
|
-
* When the cwd-contained vault (tier
|
|
564
|
+
* When the cwd-contained vault (tier 2) wins, tell the operator if that
|
|
161
565
|
* silently overrode the registry's active vault — the customer-visible
|
|
162
566
|
* failure this task fixes (packaged server writing bookkeeping into the
|
|
163
567
|
* WRONG repo with no warning). Prints nothing when there is no registry
|
|
@@ -186,7 +590,19 @@ function maybeLogCwdOverrideNotice(cwdVaultRoot, registry) {
|
|
|
186
590
|
*
|
|
187
591
|
* @param {string} vaultId
|
|
188
592
|
* @param {string} repoRootAbs
|
|
189
|
-
*
|
|
593
|
+
* `source` names WHICH tier answered, and callers gate on it — `bin/hs.mjs`'s
|
|
594
|
+
* `findRegistryVaultRoot`, `dev-factory/judge.mjs`'s `resolveVaultRoot`,
|
|
595
|
+
* `dev-factory/lib/vault-root.mjs` and `dev-factory/lib/worker-lifecycle.mjs`
|
|
596
|
+
* all accept anything that is not the last-resort `'cwd-heuristic'`. So the
|
|
597
|
+
* label has to be true: a satellite attachment found by the walk (tier 2) is
|
|
598
|
+
* `'satellite-pin'`, NOT `'env-vault-root'` (which it reported until
|
|
599
|
+
* 2026-09-18, even with no env var set anywhere). Both are accepted by every
|
|
600
|
+
* gate above, so the rename is behavior-preserving.
|
|
601
|
+
*
|
|
602
|
+
* `'cwd-marker'` and `'marker-registry-mismatch'` (a heyspex.yaml `vault_id`
|
|
603
|
+
* named the vault) are likewise real answers, accepted by the same gates.
|
|
604
|
+
*
|
|
605
|
+
* @param {'explicit'|'account-pointer'|'env-vault-root'|'env-vault-dir'|'satellite-pin'|'cwd-marker'|'marker-registry-mismatch'|'cwd-heuristic'} source
|
|
190
606
|
*/
|
|
191
607
|
function finalize(vaultId, repoRootAbs, source) {
|
|
192
608
|
const resolvedRoot = resolve(repoRootAbs);
|
|
@@ -203,11 +619,11 @@ function finalize(vaultId, repoRootAbs, source) {
|
|
|
203
619
|
*
|
|
204
620
|
* @param {{ vaultId?: string, cwd?: string, registryDir?: string, cwdWalkRoot?: string }} [options]
|
|
205
621
|
* - `vaultId`: explicit override (tier 1).
|
|
206
|
-
* - `cwd`: working dir for the cwd-contained-vault walk-up (tier
|
|
622
|
+
* - `cwd`: working dir for the cwd-contained-vault walk-up (tier 2; defaults
|
|
207
623
|
* to `CLAUDE_PROJECT_DIR` then `process.cwd()`).
|
|
208
624
|
* - `registryDir`: registry dir override (tests only; defaults to ~/.heyspex).
|
|
209
|
-
* - `cwdWalkRoot`: test-only fence for the tier-
|
|
210
|
-
* {@link
|
|
625
|
+
* - `cwdWalkRoot`: test-only fence for the tier-2 walk-up (see
|
|
626
|
+
* {@link findCwdVaultOrAttachment}); production code never sets this.
|
|
211
627
|
* @returns {{ vaultId: string, repoRootAbs: string, specsDirAbs: string, source: string }}
|
|
212
628
|
*/
|
|
213
629
|
export function resolveActiveVault(options = {}) {
|
|
@@ -221,23 +637,7 @@ export function resolveActiveVault(options = {}) {
|
|
|
221
637
|
// Unknown explicit id: cascade rather than fail closed (matches TS resolver).
|
|
222
638
|
}
|
|
223
639
|
|
|
224
|
-
// Tier 2 —
|
|
225
|
-
// [[HS-C87T-0JNK-TASK]]: moved ABOVE both the cwd-contained vault and the
|
|
226
|
-
// registry pointer — an explicit env override must win "regardless" of
|
|
227
|
-
// where the process was started or what the registry says is active.
|
|
228
|
-
const envRoot = process.env.HEYSPEX_VAULT_ROOT;
|
|
229
|
-
if (isNonEmptyString(envRoot)) {
|
|
230
|
-
return finalize(UNREGISTERED_VAULT_ID, resolve(envRoot), 'env-vault-root');
|
|
231
|
-
}
|
|
232
|
-
const envDir = process.env.HEYSPEX_VAULT_DIR;
|
|
233
|
-
if (isNonEmptyString(envDir)) {
|
|
234
|
-
// Historically points AT the specs/ dir itself, so repo root = its parent
|
|
235
|
-
// (specsDirAbs then round-trips back).
|
|
236
|
-
const specsDirFromEnv = resolve(envDir);
|
|
237
|
-
return finalize(UNREGISTERED_VAULT_ID, dirname(specsDirFromEnv), 'env-vault-dir');
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Tier 3 — cwd-contained vault (HS-C87T). Backward-compat note: the OLD
|
|
640
|
+
// Tier 2 — cwd-contained vault (HS-C87T). Backward-compat note: the OLD
|
|
241
641
|
// getConfig() fell back to CLAUDE_PROJECT_DIR then process.cwd() as a bare
|
|
242
642
|
// repo root. We keep honoring CLAUDE_PROJECT_DIR as the cwd base (an
|
|
243
643
|
// explicit `options.cwd` caller/test override still takes precedence over
|
|
@@ -252,17 +652,103 @@ export function resolveActiveVault(options = {}) {
|
|
|
252
652
|
? process.env.CLAUDE_PROJECT_DIR
|
|
253
653
|
: process.cwd()),
|
|
254
654
|
);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
655
|
+
//
|
|
656
|
+
// A SATELLITE repo ([[satellite.ts]]) has no marker of its own — it records
|
|
657
|
+
// an explicit attachment in its MCP manifests instead — so the walk checks
|
|
658
|
+
// BOTH signals at each level and takes the nearest: an explicit attachment
|
|
659
|
+
// in this repo beats a vault that merely happens to sit further up the
|
|
660
|
+
// tree (e.g. two sibling checkouts under one parent that has a stray
|
|
661
|
+
// `specs/`), and both still beat the machine-global pointer at tier 4.
|
|
662
|
+
const cwdMatch = findCwdVaultOrAttachment(cwdBase, options.cwdWalkRoot);
|
|
663
|
+
if (cwdMatch) {
|
|
664
|
+
// The override notice is for an ACCIDENT of where the process started —
|
|
665
|
+
// "you are inside vault X, the registry says Y". A satellite attachment
|
|
666
|
+
// is not an accident: the repo names its vault on disk, deliberately, and
|
|
667
|
+
// that pin is meant to outrank the machine-global pointer. Warning on it
|
|
668
|
+
// would fire on every single satellite session. Stays suppressed.
|
|
669
|
+
if (!cwdMatch.satellite) maybeLogCwdOverrideNotice(cwdMatch.repoRootAbs, registry);
|
|
670
|
+
if (cwdMatch.satellite) {
|
|
671
|
+
// `'satellite-pin'`, not `'env-vault-root'`: nothing set an env var —
|
|
672
|
+
// the walk read an on-disk manifest pin. See finalize()'s doc comment
|
|
673
|
+
// for the gate sites that consume this (all of them reject only
|
|
674
|
+
// `'cwd-heuristic'`, so both labels are accepted identically).
|
|
675
|
+
const pinnedRecord = findVaultByRoot(registry, cwdMatch.repoRootAbs);
|
|
676
|
+
// A committed satellite marker names the vault by id: that id IS the
|
|
677
|
+
// binding (works on a fresh machine with no registry row and no clone).
|
|
678
|
+
const markerId = cwdMatch.markerSatellite?.vaultId ?? null;
|
|
679
|
+
const bound = finalize(
|
|
680
|
+
markerId ?? pinnedRecord?.id ?? UNREGISTERED_VAULT_ID,
|
|
681
|
+
cwdMatch.repoRootAbs,
|
|
682
|
+
'satellite-pin',
|
|
683
|
+
);
|
|
684
|
+
// The marker names a vault that is NOT cloned on this machine: the
|
|
685
|
+
// binding is cloud-only. Disk readers must not report an empty vault -
|
|
686
|
+
// they answer from the cloud and say so (read-path.mjs).
|
|
687
|
+
if (cwdMatch.markerSatellite && !cwdMatch.markerSatellite.vaultRootAbs) {
|
|
688
|
+
return { ...bound, vaultCloned: false, vaultPath: cwdMatch.markerSatellite.vaultPath };
|
|
689
|
+
}
|
|
690
|
+
return bound;
|
|
691
|
+
}
|
|
692
|
+
// A heyspex.yaml `vault_id` IS the vault's id ([[HS-VVNQ]]): `hs init` /
|
|
693
|
+
// `hs connect` wrote it there. Before this, the id came ONLY from a
|
|
694
|
+
// registry row keyed by this root, so a connected repo on a machine
|
|
695
|
+
// whose registry lacked (or mis-keyed) that row resolved to the
|
|
696
|
+
// `unregistered` sentinel and every console call was refused. The
|
|
697
|
+
// registry row is still consulted — as the thing the marker is checked
|
|
698
|
+
// against, not as the source.
|
|
699
|
+
const markerRecord = findVaultByRoot(registry, cwdMatch.repoRootAbs);
|
|
700
|
+
const markerVaultId = readMarkerVaultId(cwdMatch.repoRootAbs);
|
|
701
|
+
if (markerVaultId) {
|
|
702
|
+
if (markerRecord && markerRecord.id !== markerVaultId) {
|
|
703
|
+
logMarkerRegistryMismatch(cwdMatch.repoRootAbs, markerVaultId, markerRecord.id);
|
|
704
|
+
return finalize(markerVaultId, cwdMatch.repoRootAbs, 'marker-registry-mismatch');
|
|
705
|
+
}
|
|
706
|
+
return finalize(markerVaultId, cwdMatch.repoRootAbs, 'cwd-marker');
|
|
707
|
+
}
|
|
708
|
+
// No `vault_id` on the marker (or a bare specs/ dir): same reverse lookup
|
|
709
|
+
// as the satellite and env tiers — a vault the registry knows reports its
|
|
710
|
+
// REGISTERED id, so the console-scoping header (console-fetch.mjs
|
|
711
|
+
// `getVaultIdHeader`) can name it ([[HS-M95C]]).
|
|
712
|
+
return finalize(markerRecord?.id ?? UNREGISTERED_VAULT_ID, cwdMatch.repoRootAbs, 'cwd-heuristic');
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Tier 3 — HEYSPEX_VAULT_ROOT / HEYSPEX_VAULT_DIR env (legacy escape hatch).
|
|
716
|
+
// [[HS-VVNQ]]: BELOW the walk. The plugin manifest sets
|
|
717
|
+
// HEYSPEX_VAULT_ROOT=${CLAUDE_PROJECT_DIR} on every session, so an env tier
|
|
718
|
+
// above the walk outranked every satellite pin and heyspex.yaml marker on
|
|
719
|
+
// disk. It still outranks the machine-global registry pointer.
|
|
720
|
+
const envRoot = process.env.HEYSPEX_VAULT_ROOT;
|
|
721
|
+
if (isNonEmptyString(envRoot)) {
|
|
722
|
+
// Reverse-resolve the pinned root to its registered id when the registry
|
|
723
|
+
// knows that repo ([[HS-XOZ3-DIAG-NWG6-vault-identity-split-reconciled-to]]).
|
|
724
|
+
const envRecord = findVaultByRoot(registry, envRoot);
|
|
725
|
+
return finalize(envRecord?.id ?? UNREGISTERED_VAULT_ID, resolve(envRoot), 'env-vault-root');
|
|
726
|
+
}
|
|
727
|
+
const envDir = process.env.HEYSPEX_VAULT_DIR;
|
|
728
|
+
if (isNonEmptyString(envDir)) {
|
|
729
|
+
// Historically points AT the specs/ dir itself, so repo root = its parent
|
|
730
|
+
// (specsDirAbs then round-trips back).
|
|
731
|
+
// Same reverse lookup as HEYSPEX_VAULT_ROOT, matching the TS resolver.
|
|
732
|
+
const specsDirFromEnv = resolve(envDir);
|
|
733
|
+
const envDirRecord = findVaultByRoot(registry, dirname(specsDirFromEnv));
|
|
734
|
+
return finalize(
|
|
735
|
+
envDirRecord?.id ?? UNREGISTERED_VAULT_ID,
|
|
736
|
+
dirname(specsDirFromEnv),
|
|
737
|
+
'env-vault-dir',
|
|
738
|
+
);
|
|
259
739
|
}
|
|
260
740
|
|
|
261
741
|
// Tier 4 — registry active_vault_id (the OFFLINE active pointer). Reached
|
|
262
|
-
// only when cwd sits inside no vault at all (
|
|
263
|
-
// anywhere up the tree)
|
|
742
|
+
// only when cwd sits inside no vault at all (the walk found neither marker
|
|
743
|
+
// anywhere up the tree) and no env var names one. This is how an MCP
|
|
264
744
|
// restart picks up a CLI `hs vault use` switch without Neon. Mirrors the TS
|
|
265
745
|
// resolver's account-pointer tier, sourced from the registry file here.
|
|
746
|
+
// The ORDER is pinned (decision memory `vault-resolver-precedence`); what a
|
|
747
|
+
// consumer does with `source: 'account-pointer'` is its own call — the
|
|
748
|
+
// console transport (console-fetch.mjs `vaultIdHeaderFor`) REFUSES
|
|
749
|
+
// vault-scoped requests on it (`VAULT_NOT_PINNED`) unless
|
|
750
|
+
// HEYSPEX_ALLOW_POINTER_VAULT=1, because the pointer mirrors whatever the
|
|
751
|
+
// owner last clicked in the console, not anything this repo declared.
|
|
266
752
|
if (registry && isNonEmptyString(registry.active_vault_id)) {
|
|
267
753
|
const record = findVaultById(registry, registry.active_vault_id);
|
|
268
754
|
if (record) return finalize(record.id, record.repo_root_abs, 'account-pointer');
|