heyspex 0.1.0 → 0.2.0
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/README.md +21 -0
- package/bin/hs.mjs +931 -98
- 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 +1085 -103
- package/dist/cli/hs-claim.mjs +936 -63
- package/dist/cli/hs-connect.mjs +9786 -1910
- package/dist/cli/hs-defer.mjs +5267 -3053
- package/dist/cli/hs-dev.mjs +564 -0
- package/dist/cli/hs-env.mjs +385 -299
- package/dist/cli/hs-index-rebuild.mjs +7129 -0
- package/dist/cli/hs-index-reconcile.mjs +7852 -0
- package/dist/cli/hs-init.mjs +777 -138
- package/dist/cli/hs-install.mjs +6583 -485
- package/dist/cli/hs-join.mjs +10137 -2368
- package/dist/cli/hs-vault-pull.mjs +4659 -5882
- package/dist/cli/hs-vault.mjs +3793 -1537
- package/dist/cli/hs-workspace.mjs +2550 -524
- 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 +28 -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 +26 -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 +31 -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 +30 -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/mcp-client-config.md +259 -6
- package/package.json +231 -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/bin/launch-mcp.mjs +95 -0
- package/plugins/heyspex/bin/run-hook.mjs +62 -0
- package/plugins/heyspex/hooks/cursor-hooks.json +227 -0
- package/plugins/heyspex/hooks/hooks.json +361 -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 +410 -0
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +351 -0
- package/scripts/claude-hooks/_run.mjs +120 -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-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 +575 -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/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 +66 -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 +247 -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 +449 -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/wip-checkpoint.mjs +298 -0
- package/scripts/cursor-hooks/manifest.mjs +235 -0
- package/scripts/dev-server-status.mjs +34 -13
- package/scripts/dev-server-stop.mjs +313 -0
- package/scripts/heyspex-mode.mjs +268 -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 +333 -0
- package/scripts/hs-worktree.mjs +201 -0
- package/scripts/install-claude-code-hooks.mjs +270 -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 +197 -60
- package/scripts/lib/customer-kit.mjs +886 -0
- package/scripts/lib/dev-mode.mjs +244 -28
- package/scripts/lib/heyspex-home.mjs +72 -0
- package/scripts/lib/hs-command.mjs +277 -0
- package/scripts/lib/hs-companion-settings.mjs +472 -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 +11 -21
- package/scripts/lib/hs-source-push.mjs +107 -58
- 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/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 +441 -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/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 +274 -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/git-reflog-audit.mjs +294 -0
- package/spexharness/src/intent-classifier.mjs +194 -0
- package/spexharness/src/intent-directive.mjs +541 -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 +819 -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 +95 -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 +46 -5
- package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
- package/vendor/heyspex-mcp/capture-console-client.mjs +80 -1
- package/vendor/heyspex-mcp/capture-focus.mjs +1035 -29
- package/vendor/heyspex-mcp/capture-outbox.mjs +5 -1
- package/vendor/heyspex-mcp/capture-tools.mjs +1219 -0
- package/vendor/heyspex-mcp/claim-tools.mjs +1124 -0
- package/vendor/heyspex-mcp/cli-verbs.mjs +2076 -0
- package/vendor/heyspex-mcp/cli.mjs +201 -15524
- package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +412 -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 +657 -16
- package/vendor/heyspex-mcp/console-transport.mjs +166 -0
- package/vendor/heyspex-mcp/credentials.mjs +584 -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 +124 -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 +688 -0
- package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +69 -14
- package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
- package/vendor/heyspex-mcp/folder-trust-store.mjs +59 -0
- package/vendor/heyspex-mcp/http-transport.mjs +300 -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 +94 -0
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
- package/vendor/heyspex-mcp/occupied-keys.mjs +141 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +3360 -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 +906 -21
- 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 +325 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +540 -351
- package/vendor/heyspex-mcp/promote-capture.mjs +386 -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 +660 -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 +29 -9
- 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 +59 -0
- package/vendor/heyspex-mcp/research-focus.mjs +207 -4
- package/vendor/heyspex-mcp/research-handlers.mjs +623 -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/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 +108 -0
- package/vendor/heyspex-mcp/session-tools.mjs +152 -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 +446 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
- package/vendor/heyspex-mcp/split-research.mjs +301 -56
- package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +132 -6
- package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
- package/vendor/heyspex-mcp/summary-write-tools.mjs +224 -0
- package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +108 -1
- package/vendor/heyspex-mcp/tool-catalog.mjs +2813 -0
- package/vendor/heyspex-mcp/tool-dispatch.mjs +639 -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 +724 -0
- package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +366 -68
- package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
- package/vendor/heyspex-mcp/whoami.mjs +91 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// vault-less-endpoints.mjs — the ONE list of console endpoints that have no
|
|
2
|
+
// vault yet ([[HS-MVG4-WW9R-TASK-mcp-and-hs-name-account-vault-on-every]],
|
|
3
|
+
// epic [[HS-BIWR-WW9R-EPIC-explicit-vault-scope-on-every-console]] invariant 2).
|
|
4
|
+
//
|
|
5
|
+
// Every other console request from the MCP server (console-fetch.mjs) and from
|
|
6
|
+
// the `hs` HTTP client (spex-console/src/lib/cli/control-shared.ts) MUST name
|
|
7
|
+
// `{ account, vault }` in its headers or refuse LOCALLY — a call that sends no
|
|
8
|
+
// vault is served from the account's leftover pointer, which is the 2026-10-02
|
|
9
|
+
// incident ([[HS-M95C-QQAV-DIAG-in-prod-mode-every-mcp-call-from-a]]). The
|
|
10
|
+
// endpoints below are the explicit exceptions: pairing, sign-in identity,
|
|
11
|
+
// creating/listing the vaults and workspaces themselves, and the dev-mode
|
|
12
|
+
// switch. They are matched by METHOD + exact pathname (query string ignored),
|
|
13
|
+
// never by prefix — `/api/vaults/active` is vault-scoped and stays gated.
|
|
14
|
+
//
|
|
15
|
+
// Plain Node ESM, zero dependencies, so the TS CLI imports it bare (same as
|
|
16
|
+
// `credentials.mjs`) and the two runtimes can never drift apart on the list.
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `"<METHOD> <pathname>"` rows; `*` matches any method.
|
|
20
|
+
* @type {readonly string[]}
|
|
21
|
+
*/
|
|
22
|
+
export const VAULT_LESS_ENDPOINTS = Object.freeze([
|
|
23
|
+
'* /api/mcp/session',
|
|
24
|
+
'* /api/mcp/pairing-codes',
|
|
25
|
+
'POST /api/vaults',
|
|
26
|
+
'GET /api/vaults',
|
|
27
|
+
'POST /api/workspaces',
|
|
28
|
+
'GET /api/workspaces',
|
|
29
|
+
'* /api/admin/dev-mode',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/** Pathname of a request path — the part before `?`, trailing slash dropped. */
|
|
33
|
+
export function pathnameOf(path) {
|
|
34
|
+
const raw = String(path ?? '').split('?')[0].split('#')[0];
|
|
35
|
+
const noTrailing = raw.length > 1 ? raw.replace(/\/+$/, '') : raw;
|
|
36
|
+
return noTrailing || '/';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* True when `method` + `path` name an endpoint that legitimately has no vault
|
|
41
|
+
* yet, so the vault header is omitted and no local refusal fires.
|
|
42
|
+
* @param {string | null | undefined} method HTTP method (any case; missing → GET)
|
|
43
|
+
* @param {string} path request path, with or without a query string
|
|
44
|
+
*/
|
|
45
|
+
export function isVaultLessEndpoint(method, path) {
|
|
46
|
+
const m = String(method || 'GET').toUpperCase();
|
|
47
|
+
const pathname = pathnameOf(path);
|
|
48
|
+
return VAULT_LESS_ENDPOINTS.some((row) => {
|
|
49
|
+
const [rowMethod, rowPath] = row.split(' ');
|
|
50
|
+
return rowPath === pathname && (rowMethod === '*' || rowMethod === m);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -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
|
*
|
|
@@ -51,7 +66,7 @@
|
|
|
51
66
|
*/
|
|
52
67
|
|
|
53
68
|
import { existsSync, readFileSync } from 'node:fs';
|
|
54
|
-
import { homedir } from 'node:os';
|
|
69
|
+
import { homedir, tmpdir } from 'node:os';
|
|
55
70
|
import { dirname, join, resolve } from 'node:path';
|
|
56
71
|
|
|
57
72
|
/**
|
|
@@ -90,6 +105,8 @@ function isNonEmptyString(v) {
|
|
|
90
105
|
* Read + defensively parse `~/.heyspex/registry.json`. A missing or malformed
|
|
91
106
|
* file returns `null` (never throws) so callers fall through to env/cwd tiers —
|
|
92
107
|
* mirroring registry-file.ts `loadVaultRegistry`'s fail-closed contract.
|
|
108
|
+
* Reads schema v1 and v2 alike; `active_vault_id` is the pointer
|
|
109
|
+
* {@link registryPointerVaultId} picks.
|
|
93
110
|
*
|
|
94
111
|
* @param {{ registryDir?: string }} [opts]
|
|
95
112
|
* @returns {{ vaults: Array<{ id: string, repo_root_abs: string }>, active_vault_id: string|null }|null}
|
|
@@ -113,8 +130,49 @@ function loadRegistry(opts = {}) {
|
|
|
113
130
|
if (!isNonEmptyString(raw.repo_root_abs)) continue;
|
|
114
131
|
vaults.push({ id: raw.id, repo_root_abs: raw.repo_root_abs });
|
|
115
132
|
}
|
|
116
|
-
|
|
117
|
-
|
|
133
|
+
return { vaults, active_vault_id: registryPointerVaultId(json) };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Mirrors registry-file.ts `LEGACY_REGISTRY_ACCOUNT_ID`. */
|
|
137
|
+
const LEGACY_REGISTRY_ACCOUNT_ID = 'legacy';
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The offline active-vault pointer of a parsed registry file, schema v1 or v2
|
|
141
|
+
* ([[HS-RXLG-WW9R-TASK-key-registry-json-per-account-schema-v2]]). Mirrors
|
|
142
|
+
* registry-file.ts `currentAccountPointers`:
|
|
143
|
+
* - v2: `accounts[current_account_id].active_vault_id`. An ABSENT field
|
|
144
|
+
* reads the pre-account `legacy` row a not-yet-migrated machine left
|
|
145
|
+
* behind; an explicit `null` is a clear (reads null) when the account
|
|
146
|
+
* owns a row here, else it too reads `legacy`. No current account → the
|
|
147
|
+
* `legacy` value or null — never some other account's.
|
|
148
|
+
* - v1: the machine-global `active_vault_id`.
|
|
149
|
+
*
|
|
150
|
+
* @param {Record<string, unknown>} json
|
|
151
|
+
* @returns {string|null}
|
|
152
|
+
*/
|
|
153
|
+
export function registryPointerVaultId(json) {
|
|
154
|
+
if (!json || typeof json !== 'object') return null;
|
|
155
|
+
if (json.schema_version !== 2) {
|
|
156
|
+
return isNonEmptyString(json.active_vault_id) ? json.active_vault_id : null;
|
|
157
|
+
}
|
|
158
|
+
const accounts =
|
|
159
|
+
json.accounts && typeof json.accounts === 'object' && !Array.isArray(json.accounts)
|
|
160
|
+
? json.accounts
|
|
161
|
+
: {};
|
|
162
|
+
const current = isNonEmptyString(json.current_account_id) ? json.current_account_id : null;
|
|
163
|
+
const rowOf = (id) => (id && accounts[id] && typeof accounts[id] === 'object' ? accounts[id] : null);
|
|
164
|
+
const own = rowOf(current);
|
|
165
|
+
if (own && isNonEmptyString(own.active_vault_id)) return own.active_vault_id;
|
|
166
|
+
// An explicit null is a CLEAR — but only from an account that owns a row
|
|
167
|
+
// here; a stray row of an account that owns nothing still reads legacy.
|
|
168
|
+
const ownsRows = [json.vaults, json.workspaces].some(
|
|
169
|
+
(rows) => Array.isArray(rows) && rows.some((r) => r && r.account_id === current),
|
|
170
|
+
);
|
|
171
|
+
if (own && own.active_vault_id === null && ownsRows && current !== LEGACY_REGISTRY_ACCOUNT_ID) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const legacy = rowOf(LEGACY_REGISTRY_ACCOUNT_ID);
|
|
175
|
+
return legacy && isNonEmptyString(legacy.active_vault_id) ? legacy.active_vault_id : null;
|
|
118
176
|
}
|
|
119
177
|
|
|
120
178
|
/** Find a vault row by id. Returns null when absent. */
|
|
@@ -124,40 +182,214 @@ function findVaultById(registry, vaultId) {
|
|
|
124
182
|
}
|
|
125
183
|
|
|
126
184
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
185
|
+
* Normalize a path for comparison: forward slashes, no trailing separator,
|
|
186
|
+
* lower-cased (this CLI's primary platform is Windows, where `C:\Dev` and
|
|
187
|
+
* `c:\dev` name one directory). Mirrors `satellite.ts`'s
|
|
188
|
+
* `normalizePathForCompare`.
|
|
189
|
+
*/
|
|
190
|
+
function normalizeRoot(p) {
|
|
191
|
+
return resolve(p)
|
|
192
|
+
.replace(/[\\/]+/g, '/')
|
|
193
|
+
.replace(/\/+$/, '')
|
|
194
|
+
.toLowerCase();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* REVERSE-resolve a filesystem repo root to its registered vault row — the
|
|
199
|
+
* `.mjs` twin of the TS resolver's `registryLookupByRoot`. Without it a
|
|
200
|
+
* path-first tier reports the `unregistered` sentinel even when the registry
|
|
201
|
+
* knows that exact repo, which misfiles the whole index under `unregistered`
|
|
202
|
+
* ([[HS-XOZ3-DIAG-NWG6-vault-identity-split-reconciled-to]]).
|
|
203
|
+
*
|
|
204
|
+
* @param {{ vaults: Array<{id: string, repo_root_abs: string}> }|null} registry
|
|
205
|
+
* @param {string} repoRootAbs
|
|
206
|
+
*/
|
|
207
|
+
function findVaultByRoot(registry, repoRootAbs) {
|
|
208
|
+
if (!registry || !isNonEmptyString(repoRootAbs)) return null;
|
|
209
|
+
const target = normalizeRoot(repoRootAbs);
|
|
210
|
+
return registry.vaults.find((v) => normalizeRoot(v.repo_root_abs) === target) ?? null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The MCP manifests a SATELLITE repo records its vault attachment in, and the
|
|
215
|
+
* env key inside them. Mirrors `spex-console/src/lib/cli/satellite.ts`
|
|
216
|
+
* (`SATELLITE_MANIFEST_RELS` / `SATELLITE_VAULT_ENV`) — keep the two in sync;
|
|
217
|
+
* the TS module carries the full rationale.
|
|
218
|
+
*/
|
|
219
|
+
const SATELLITE_MANIFESTS = [['.mcp.json'], ['.cursor', 'mcp.json']];
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Is `p` an ABSOLUTE path? Pure string test over the three shapes a real pin
|
|
223
|
+
* can carry: a Windows drive path (`C:\vault`, `C:/vault`), a UNC share
|
|
224
|
+
* (`\\server\share\vault`) and a POSIX root (`/vault`). Mirrors
|
|
225
|
+
* `satellite.ts`'s `isAbsolutePathForCompare` — keep the two in sync; the TS
|
|
226
|
+
* module carries the full rationale for why a non-absolute pin is refused
|
|
227
|
+
* rather than resolved.
|
|
228
|
+
*
|
|
229
|
+
* @param {string} p
|
|
230
|
+
* @returns {boolean}
|
|
231
|
+
*/
|
|
232
|
+
function isAbsolutePin(p) {
|
|
233
|
+
const s = p.trim();
|
|
234
|
+
return /^[\\/]/.test(s) || /^[a-zA-Z]:[\\/]/.test(s);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Read a satellite attachment out of `dir`'s MCP manifests: the heyspex
|
|
239
|
+
* server's `env.HEYSPEX_VAULT_ROOT` pin, when it names a path OUTSIDE `dir`.
|
|
240
|
+
* Two kinds of value are NOT an attachment:
|
|
241
|
+
*
|
|
242
|
+
* - one still carrying an unexpanded `${...}` — the client's own
|
|
243
|
+
* project-dir placeholder that a normal (this-repo-IS-the-vault) install
|
|
244
|
+
* writes; and
|
|
245
|
+
* - one that is not an ABSOLUTE path ({@link isAbsolutePin}). This used to
|
|
246
|
+
* be `resolve(trimmed)`, which resolved a relative pin against
|
|
247
|
+
* `process.cwd()` — NOT the manifest's own directory — so a manifest in
|
|
248
|
+
* `<repo>` pinning `../some-vault` returned a directory beside whatever
|
|
249
|
+
* folder the MCP server happened to be launched from. No resolution is
|
|
250
|
+
* defensible here: the same string also reaches the server as the
|
|
251
|
+
* `HEYSPEX_VAULT_ROOT` env var, where tier 2 resolves it against the
|
|
252
|
+
* server process's cwd, so a manifest-dir rule would contradict the
|
|
253
|
+
* runtime. Refusing fails safe and matches `satellite.ts`.
|
|
254
|
+
*
|
|
255
|
+
* Pure + synchronous and never throws — an unreadable/unparseable manifest
|
|
256
|
+
* reads as "no attachment", the same fail-open posture as the registry
|
|
257
|
+
* loader above.
|
|
258
|
+
*
|
|
259
|
+
* @param {string} dir
|
|
260
|
+
* @returns {string|null} the attached vault's absolute repo root
|
|
261
|
+
*/
|
|
262
|
+
export function readSatellitePin(dir) {
|
|
263
|
+
for (const rel of SATELLITE_MANIFESTS) {
|
|
264
|
+
let raw;
|
|
265
|
+
try {
|
|
266
|
+
raw = readFileSync(join(dir, ...rel), 'utf8');
|
|
267
|
+
} catch {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
let value;
|
|
271
|
+
try {
|
|
272
|
+
value = JSON.parse(raw)?.mcpServers?.heyspex?.env?.HEYSPEX_VAULT_ROOT;
|
|
273
|
+
} catch {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (!isNonEmptyString(value)) continue;
|
|
277
|
+
const trimmed = value.trim();
|
|
278
|
+
if (trimmed.includes('${')) continue;
|
|
279
|
+
if (!isAbsolutePin(trimmed)) continue;
|
|
280
|
+
const pinned = normalizeRoot(trimmed);
|
|
281
|
+
const here = normalizeRoot(dir);
|
|
282
|
+
if (pinned === here || pinned.startsWith(`${here}/`)) continue; // ordinary install
|
|
283
|
+
return resolve(trimmed);
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Is `dir` a vault root? True when it contains a `heyspex.yaml` marker file
|
|
290
|
+
* OR a `specs/` dir — the two on-disk signals that a folder IS a vault root
|
|
291
|
+
* (mirrors `hs init`'s marker + the entity-model's folder layout). This is
|
|
292
|
+
* the ONE definition of "is this a vault root" — every walk/check in this
|
|
293
|
+
* file (and in `bin/hs.mjs`) routes through it rather than re-testing the
|
|
294
|
+
* two signals inline. Pure and synchronous: no config reads, no walking, no
|
|
295
|
+
* side effects — just the two existsSync checks.
|
|
296
|
+
*
|
|
297
|
+
* One exclusion ([[HS-VVNQ]]): the OS temp directory ITSELF is never a vault
|
|
298
|
+
* root. It is shared scratch for every process on the machine, test runs
|
|
299
|
+
* leave a stray `specs/` there, and once the walk outranks env and the
|
|
300
|
+
* pointer, every process started anywhere under it would otherwise resolve
|
|
301
|
+
* to that junk folder. Subdirectories of it (a real scratch vault) still
|
|
302
|
+
* count. The TS resolver applies the same rule.
|
|
303
|
+
*
|
|
304
|
+
* @param {string} dir
|
|
305
|
+
* @returns {boolean}
|
|
306
|
+
*/
|
|
307
|
+
export function isVaultRoot(dir) {
|
|
308
|
+
if (normalizeRoot(dir) === normalizeRoot(tmpdir())) return false;
|
|
309
|
+
return existsSync(join(dir, 'heyspex.yaml')) || existsSync(join(dir, 'specs'));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Extract `vault_id: <value>` from raw heyspex.yaml text, or `null` if absent.
|
|
314
|
+
* Same semantics as `spex-console/src/lib/cli/init.ts` `parseVaultId` (the
|
|
315
|
+
* writer's own reader): first line-anchored `vault_id:` key, value trimmed,
|
|
316
|
+
* one matching pair of surrounding quotes stripped, empty -> `null`. Kept to
|
|
317
|
+
* this one line-regex on purpose — `hs init` writes the marker as flat
|
|
318
|
+
* `key: value` lines, so a YAML parser buys nothing here.
|
|
319
|
+
*
|
|
320
|
+
* @param {string} content
|
|
321
|
+
* @returns {string|null}
|
|
322
|
+
*/
|
|
323
|
+
export function parseMarkerVaultId(content) {
|
|
324
|
+
const m = String(content ?? '').match(/^vault_id:\s*(.+?)\s*$/m);
|
|
325
|
+
if (!m) return null;
|
|
326
|
+
const unquoted = m[1].trim().replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1');
|
|
327
|
+
return unquoted.length > 0 ? unquoted : null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* The `vault_id` written in `<dir>/heyspex.yaml`, or `null` (no marker, no
|
|
332
|
+
* key, unreadable). Never throws.
|
|
333
|
+
*
|
|
334
|
+
* @param {string} dir
|
|
335
|
+
* @returns {string|null}
|
|
336
|
+
*/
|
|
337
|
+
function readMarkerVaultId(dir) {
|
|
338
|
+
try {
|
|
339
|
+
return parseMarkerVaultId(readFileSync(join(dir, 'heyspex.yaml'), 'utf8'));
|
|
340
|
+
} catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* The marker id and the registry row for the same root disagree. The marker
|
|
347
|
+
* wins (it is the repo's own on-disk declaration, written by `hs init` /
|
|
348
|
+
* `hs connect`); this notice names the command whose `id_agreement` row
|
|
349
|
+
* reports the same disagreement so the operator can repair the registry.
|
|
350
|
+
*
|
|
351
|
+
* @param {string} repoRootAbs
|
|
352
|
+
* @param {string} markerVaultId
|
|
353
|
+
* @param {string} registryVaultId
|
|
354
|
+
*/
|
|
355
|
+
function logMarkerRegistryMismatch(repoRootAbs, markerVaultId, registryVaultId) {
|
|
356
|
+
process.stderr.write(
|
|
357
|
+
`[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`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Walk UP from `startDir` taking the NEAREST of the two tier-2 (walk) signals: a
|
|
363
|
+
* vault root (see {@link isVaultRoot}) or a satellite attachment (see
|
|
364
|
+
* {@link readSatellitePin}). The attachment is checked FIRST at each level,
|
|
365
|
+
* because a repo that explicitly names its vault is never also asking to be
|
|
366
|
+
* one.
|
|
134
367
|
*
|
|
135
368
|
* @param {string} startDir
|
|
136
369
|
* @param {string|undefined} stopAt - test-only walk boundary (checked, then
|
|
137
370
|
* the walk stops — never climbs past it). Unset in production, so the walk
|
|
138
371
|
* 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}
|
|
372
|
+
* inside a disposable tmp fixture so ambient/unrelated directories higher
|
|
373
|
+
* up the real tree cannot produce a false match.
|
|
374
|
+
* @returns {{ repoRootAbs: string, satellite: boolean }|null}
|
|
143
375
|
*/
|
|
144
|
-
function
|
|
376
|
+
function findCwdVaultOrAttachment(startDir, stopAt) {
|
|
145
377
|
let dir = startDir;
|
|
146
378
|
const boundary = isNonEmptyString(stopAt) ? resolve(stopAt) : null;
|
|
147
379
|
for (let i = 0; i < 256; i += 1) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
if (boundary && dir === boundary) break;
|
|
380
|
+
const pinned = readSatellitePin(dir);
|
|
381
|
+
if (pinned) return { repoRootAbs: pinned, satellite: true };
|
|
382
|
+
if (isVaultRoot(dir)) return { repoRootAbs: dir, satellite: false };
|
|
383
|
+
if (boundary && dir === boundary) break;
|
|
152
384
|
const parent = dirname(dir);
|
|
153
|
-
if (parent === dir) break;
|
|
385
|
+
if (parent === dir) break;
|
|
154
386
|
dir = parent;
|
|
155
387
|
}
|
|
156
388
|
return null;
|
|
157
389
|
}
|
|
158
390
|
|
|
159
391
|
/**
|
|
160
|
-
* When the cwd-contained vault (tier
|
|
392
|
+
* When the cwd-contained vault (tier 2) wins, tell the operator if that
|
|
161
393
|
* silently overrode the registry's active vault — the customer-visible
|
|
162
394
|
* failure this task fixes (packaged server writing bookkeeping into the
|
|
163
395
|
* WRONG repo with no warning). Prints nothing when there is no registry
|
|
@@ -186,7 +418,19 @@ function maybeLogCwdOverrideNotice(cwdVaultRoot, registry) {
|
|
|
186
418
|
*
|
|
187
419
|
* @param {string} vaultId
|
|
188
420
|
* @param {string} repoRootAbs
|
|
189
|
-
*
|
|
421
|
+
* `source` names WHICH tier answered, and callers gate on it — `bin/hs.mjs`'s
|
|
422
|
+
* `findRegistryVaultRoot`, `dev-factory/judge.mjs`'s `resolveVaultRoot`,
|
|
423
|
+
* `dev-factory/lib/vault-root.mjs` and `dev-factory/lib/worker-lifecycle.mjs`
|
|
424
|
+
* all accept anything that is not the last-resort `'cwd-heuristic'`. So the
|
|
425
|
+
* label has to be true: a satellite attachment found by the walk (tier 2) is
|
|
426
|
+
* `'satellite-pin'`, NOT `'env-vault-root'` (which it reported until
|
|
427
|
+
* 2026-09-18, even with no env var set anywhere). Both are accepted by every
|
|
428
|
+
* gate above, so the rename is behavior-preserving.
|
|
429
|
+
*
|
|
430
|
+
* `'cwd-marker'` and `'marker-registry-mismatch'` (a heyspex.yaml `vault_id`
|
|
431
|
+
* named the vault) are likewise real answers, accepted by the same gates.
|
|
432
|
+
*
|
|
433
|
+
* @param {'explicit'|'account-pointer'|'env-vault-root'|'env-vault-dir'|'satellite-pin'|'cwd-marker'|'marker-registry-mismatch'|'cwd-heuristic'} source
|
|
190
434
|
*/
|
|
191
435
|
function finalize(vaultId, repoRootAbs, source) {
|
|
192
436
|
const resolvedRoot = resolve(repoRootAbs);
|
|
@@ -203,11 +447,11 @@ function finalize(vaultId, repoRootAbs, source) {
|
|
|
203
447
|
*
|
|
204
448
|
* @param {{ vaultId?: string, cwd?: string, registryDir?: string, cwdWalkRoot?: string }} [options]
|
|
205
449
|
* - `vaultId`: explicit override (tier 1).
|
|
206
|
-
* - `cwd`: working dir for the cwd-contained-vault walk-up (tier
|
|
450
|
+
* - `cwd`: working dir for the cwd-contained-vault walk-up (tier 2; defaults
|
|
207
451
|
* to `CLAUDE_PROJECT_DIR` then `process.cwd()`).
|
|
208
452
|
* - `registryDir`: registry dir override (tests only; defaults to ~/.heyspex).
|
|
209
|
-
* - `cwdWalkRoot`: test-only fence for the tier-
|
|
210
|
-
* {@link
|
|
453
|
+
* - `cwdWalkRoot`: test-only fence for the tier-2 walk-up (see
|
|
454
|
+
* {@link findCwdVaultOrAttachment}); production code never sets this.
|
|
211
455
|
* @returns {{ vaultId: string, repoRootAbs: string, specsDirAbs: string, source: string }}
|
|
212
456
|
*/
|
|
213
457
|
export function resolveActiveVault(options = {}) {
|
|
@@ -221,23 +465,7 @@ export function resolveActiveVault(options = {}) {
|
|
|
221
465
|
// Unknown explicit id: cascade rather than fail closed (matches TS resolver).
|
|
222
466
|
}
|
|
223
467
|
|
|
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
|
|
468
|
+
// Tier 2 — cwd-contained vault (HS-C87T). Backward-compat note: the OLD
|
|
241
469
|
// getConfig() fell back to CLAUDE_PROJECT_DIR then process.cwd() as a bare
|
|
242
470
|
// repo root. We keep honoring CLAUDE_PROJECT_DIR as the cwd base (an
|
|
243
471
|
// explicit `options.cwd` caller/test override still takes precedence over
|
|
@@ -252,15 +480,85 @@ export function resolveActiveVault(options = {}) {
|
|
|
252
480
|
? process.env.CLAUDE_PROJECT_DIR
|
|
253
481
|
: process.cwd()),
|
|
254
482
|
);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
483
|
+
//
|
|
484
|
+
// A SATELLITE repo ([[satellite.ts]]) has no marker of its own — it records
|
|
485
|
+
// an explicit attachment in its MCP manifests instead — so the walk checks
|
|
486
|
+
// BOTH signals at each level and takes the nearest: an explicit attachment
|
|
487
|
+
// in this repo beats a vault that merely happens to sit further up the
|
|
488
|
+
// tree (e.g. two sibling checkouts under one parent that has a stray
|
|
489
|
+
// `specs/`), and both still beat the machine-global pointer at tier 4.
|
|
490
|
+
const cwdMatch = findCwdVaultOrAttachment(cwdBase, options.cwdWalkRoot);
|
|
491
|
+
if (cwdMatch) {
|
|
492
|
+
// The override notice is for an ACCIDENT of where the process started —
|
|
493
|
+
// "you are inside vault X, the registry says Y". A satellite attachment
|
|
494
|
+
// is not an accident: the repo names its vault on disk, deliberately, and
|
|
495
|
+
// that pin is meant to outrank the machine-global pointer. Warning on it
|
|
496
|
+
// would fire on every single satellite session. Stays suppressed.
|
|
497
|
+
if (!cwdMatch.satellite) maybeLogCwdOverrideNotice(cwdMatch.repoRootAbs, registry);
|
|
498
|
+
if (cwdMatch.satellite) {
|
|
499
|
+
// `'satellite-pin'`, not `'env-vault-root'`: nothing set an env var —
|
|
500
|
+
// the walk read an on-disk manifest pin. See finalize()'s doc comment
|
|
501
|
+
// for the gate sites that consume this (all of them reject only
|
|
502
|
+
// `'cwd-heuristic'`, so both labels are accepted identically).
|
|
503
|
+
const pinnedRecord = findVaultByRoot(registry, cwdMatch.repoRootAbs);
|
|
504
|
+
return finalize(
|
|
505
|
+
pinnedRecord?.id ?? UNREGISTERED_VAULT_ID,
|
|
506
|
+
cwdMatch.repoRootAbs,
|
|
507
|
+
'satellite-pin',
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
// A heyspex.yaml `vault_id` IS the vault's id ([[HS-VVNQ]]): `hs init` /
|
|
511
|
+
// `hs connect` wrote it there. Before this, the id came ONLY from a
|
|
512
|
+
// registry row keyed by this root, so a connected repo on a machine
|
|
513
|
+
// whose registry lacked (or mis-keyed) that row resolved to the
|
|
514
|
+
// `unregistered` sentinel and every console call was refused. The
|
|
515
|
+
// registry row is still consulted — as the thing the marker is checked
|
|
516
|
+
// against, not as the source.
|
|
517
|
+
const markerRecord = findVaultByRoot(registry, cwdMatch.repoRootAbs);
|
|
518
|
+
const markerVaultId = readMarkerVaultId(cwdMatch.repoRootAbs);
|
|
519
|
+
if (markerVaultId) {
|
|
520
|
+
if (markerRecord && markerRecord.id !== markerVaultId) {
|
|
521
|
+
logMarkerRegistryMismatch(cwdMatch.repoRootAbs, markerVaultId, markerRecord.id);
|
|
522
|
+
return finalize(markerVaultId, cwdMatch.repoRootAbs, 'marker-registry-mismatch');
|
|
523
|
+
}
|
|
524
|
+
return finalize(markerVaultId, cwdMatch.repoRootAbs, 'cwd-marker');
|
|
525
|
+
}
|
|
526
|
+
// No `vault_id` on the marker (or a bare specs/ dir): same reverse lookup
|
|
527
|
+
// as the satellite and env tiers — a vault the registry knows reports its
|
|
528
|
+
// REGISTERED id, so the console-scoping header (console-fetch.mjs
|
|
529
|
+
// `getVaultIdHeader`) can name it ([[HS-M95C]]).
|
|
530
|
+
return finalize(markerRecord?.id ?? UNREGISTERED_VAULT_ID, cwdMatch.repoRootAbs, 'cwd-heuristic');
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Tier 3 — HEYSPEX_VAULT_ROOT / HEYSPEX_VAULT_DIR env (legacy escape hatch).
|
|
534
|
+
// [[HS-VVNQ]]: BELOW the walk. The plugin manifest sets
|
|
535
|
+
// HEYSPEX_VAULT_ROOT=${CLAUDE_PROJECT_DIR} on every session, so an env tier
|
|
536
|
+
// above the walk outranked every satellite pin and heyspex.yaml marker on
|
|
537
|
+
// disk. It still outranks the machine-global registry pointer.
|
|
538
|
+
const envRoot = process.env.HEYSPEX_VAULT_ROOT;
|
|
539
|
+
if (isNonEmptyString(envRoot)) {
|
|
540
|
+
// Reverse-resolve the pinned root to its registered id when the registry
|
|
541
|
+
// knows that repo ([[HS-XOZ3-DIAG-NWG6-vault-identity-split-reconciled-to]]).
|
|
542
|
+
const envRecord = findVaultByRoot(registry, envRoot);
|
|
543
|
+
return finalize(envRecord?.id ?? UNREGISTERED_VAULT_ID, resolve(envRoot), 'env-vault-root');
|
|
544
|
+
}
|
|
545
|
+
const envDir = process.env.HEYSPEX_VAULT_DIR;
|
|
546
|
+
if (isNonEmptyString(envDir)) {
|
|
547
|
+
// Historically points AT the specs/ dir itself, so repo root = its parent
|
|
548
|
+
// (specsDirAbs then round-trips back).
|
|
549
|
+
// Same reverse lookup as HEYSPEX_VAULT_ROOT, matching the TS resolver.
|
|
550
|
+
const specsDirFromEnv = resolve(envDir);
|
|
551
|
+
const envDirRecord = findVaultByRoot(registry, dirname(specsDirFromEnv));
|
|
552
|
+
return finalize(
|
|
553
|
+
envDirRecord?.id ?? UNREGISTERED_VAULT_ID,
|
|
554
|
+
dirname(specsDirFromEnv),
|
|
555
|
+
'env-vault-dir',
|
|
556
|
+
);
|
|
259
557
|
}
|
|
260
558
|
|
|
261
559
|
// 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)
|
|
560
|
+
// only when cwd sits inside no vault at all (the walk found neither marker
|
|
561
|
+
// anywhere up the tree) and no env var names one. This is how an MCP
|
|
264
562
|
// restart picks up a CLI `hs vault use` switch without Neon. Mirrors the TS
|
|
265
563
|
// resolver's account-pointer tier, sourced from the registry file here.
|
|
266
564
|
if (registry && isNonEmptyString(registry.active_vault_id)) {
|