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
package/bin/hs.mjs
CHANGED
|
@@ -12,16 +12,245 @@
|
|
|
12
12
|
* node_modules) executes the .ts entrypoints.
|
|
13
13
|
*/
|
|
14
14
|
import { spawn } from "node:child_process";
|
|
15
|
-
import { existsSync, readFileSync,
|
|
15
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import process from "node:process";
|
|
18
18
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
foldConnectionReport,
|
|
22
|
+
runBundledConnectCheck,
|
|
23
|
+
runtimeFilesCheckRow,
|
|
24
|
+
templateCheckRow,
|
|
25
|
+
vaultConfigCheckRow,
|
|
26
|
+
} from "../scripts/lib/standalone-doctor.mjs";
|
|
27
|
+
import {
|
|
28
|
+
findBrokenSatellitePin,
|
|
29
|
+
isVaultRoot,
|
|
30
|
+
readMarkerSatellite,
|
|
31
|
+
readSatellitePin,
|
|
32
|
+
resolveActiveVault,
|
|
33
|
+
} from "../vendor/heyspex-mcp/vault-resolve.mjs";
|
|
21
34
|
|
|
22
35
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
36
|
const PKG_ROOT = path.resolve(__dirname, "..");
|
|
24
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Every script path `hs` spawns directly by filesystem path (as opposed to
|
|
40
|
+
* `dispatchSpexConsoleEntry`'s own three-case tsx/bundle/missing resolution,
|
|
41
|
+
* which already carries its own existence handling). Each entry is
|
|
42
|
+
* PKG_ROOT-relative, posix-separated.
|
|
43
|
+
*
|
|
44
|
+
* Why this exists ([[HS-561M]]): `scripts/verify-mcp-package.mjs`'s
|
|
45
|
+
* `checkClosure` proves the STATIC IMPORT closure of vendor/heyspex-mcp/cli.mjs
|
|
46
|
+
* is fully packed — it says nothing about the ~15 verbs below that reach a
|
|
47
|
+
* script by `spawn()`ing a path string, which is invisible to any import-graph
|
|
48
|
+
* walk. Every dispatch site below resolves its script path THROUGH this
|
|
49
|
+
* manifest (`path.join(PKG_ROOT, SPAWN_TARGETS[key].path)`) instead of
|
|
50
|
+
* building the path inline, so `verify-mcp-package.mjs`'s `spawnTargets` check
|
|
51
|
+
* (which imports this const) can assert every non-`devOnly` entry is actually
|
|
52
|
+
* in the packed tarball file list, naming whichever one is missing.
|
|
53
|
+
*
|
|
54
|
+
* `devOnly: true` entries are scripts this package deliberately does NOT ship
|
|
55
|
+
* (see each `reason`) — the pack gate skips them, and the dispatch site for
|
|
56
|
+
* each already guards the missing case at runtime with a clear message
|
|
57
|
+
* (`doctor` falls back to `runStandaloneDoctor`; `skills` prints a
|
|
58
|
+
* published-install notice — see their dispatch sites below).
|
|
59
|
+
*/
|
|
60
|
+
export const SPAWN_TARGETS = {
|
|
61
|
+
"setup:claude": { path: "scripts/install-claude-code-hooks.mjs" },
|
|
62
|
+
"setup:cursor": { path: "scripts/install-cursor-hooks.mjs" },
|
|
63
|
+
"skills:sync": {
|
|
64
|
+
path: "scripts/sync-skills.mjs",
|
|
65
|
+
devOnly: true,
|
|
66
|
+
reason:
|
|
67
|
+
"sync-skills.mjs propagates THIS platform repo's OWN .commandcode/skills " +
|
|
68
|
+
"source content (Command Code SSOT, resolved from the script's own file " +
|
|
69
|
+
"location, not the caller's vault) — that content has no meaning outside " +
|
|
70
|
+
"a clone of the HeySpex platform repo and is not shipped.",
|
|
71
|
+
},
|
|
72
|
+
"heyspex-mcp-cli": { path: "vendor/heyspex-mcp/cli.mjs" }, // `hs login` + `hs mcp *`
|
|
73
|
+
doctor: {
|
|
74
|
+
path: "scripts/heyspex-doctor.mjs",
|
|
75
|
+
devOnly: true,
|
|
76
|
+
reason:
|
|
77
|
+
"the full doctor audits dozens of source-checkout-only surfaces (git " +
|
|
78
|
+
"hooks, Cursor wiring, console secrets, ...) a published install never " +
|
|
79
|
+
"has by design — the dispatch site below already falls back to " +
|
|
80
|
+
"runStandaloneDoctor() when this file is absent.",
|
|
81
|
+
},
|
|
82
|
+
"server:status": { path: "scripts/dev-server-status.mjs" },
|
|
83
|
+
"server:stop": { path: "scripts/dev-server-stop.mjs" },
|
|
84
|
+
wip: { path: "scripts/wip-ledger-health.mjs" },
|
|
85
|
+
worktree: { path: "scripts/hs-worktree.mjs" },
|
|
86
|
+
"source:status": { path: "scripts/lib/hs-source-preflight.mjs" },
|
|
87
|
+
"source:preflight": { path: "scripts/lib/hs-source-preflight.mjs" },
|
|
88
|
+
"source:commit": { path: "scripts/lib/hs-source-commit.mjs" },
|
|
89
|
+
"source:push": { path: "scripts/lib/hs-source-push.mjs" },
|
|
90
|
+
"source:branch": { path: "scripts/lib/hs-source-branch.mjs" },
|
|
91
|
+
"source:log": { path: "scripts/lib/hs-source-log.mjs" },
|
|
92
|
+
// `hs source identity` — the ONE sanctioned git-identity writer (human-only
|
|
93
|
+
// set/clear, read-only show). Incident 2026-09-19, vault lesson
|
|
94
|
+
// `when-git-says-author-identity-unknown`; docs/admin/workflow/git-identity.md.
|
|
95
|
+
"source:identity": { path: "scripts/lib/hs-source-identity.mjs" },
|
|
96
|
+
view: { path: "scripts/hs-view.mjs" },
|
|
97
|
+
capture: { path: "scripts/hs-capture-cli.mjs" },
|
|
98
|
+
companion: { path: "scripts/hs-companion.mjs" },
|
|
99
|
+
next: { path: "scripts/hs-next.mjs" },
|
|
100
|
+
mode: { path: "scripts/hs-mode.mjs" },
|
|
101
|
+
gateway: { path: "scripts/hs-gateway.mjs" },
|
|
102
|
+
settings: { path: "scripts/hs-settings.mjs" },
|
|
103
|
+
classifier: { path: "scripts/hs-classifier.mjs" },
|
|
104
|
+
auto: { path: "scripts/hs-auto.mjs" },
|
|
105
|
+
"plugin:launch-mcp": { path: "plugins/heyspex/bin/launch-mcp.mjs" },
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Neon control-plane verbs ([[HS-EPIC-CLMW-hs-cli-mcp-wrappers-for-control-plane]]):
|
|
110
|
+
* each namespace maps to a spex-console TS entry, dispatched via
|
|
111
|
+
* `dispatchSpexConsoleEntry` in `main()` below. Hoisted to module scope (was
|
|
112
|
+
* previously declared inline inside `main()`) and exported alongside
|
|
113
|
+
* `SPAWN_TARGETS` so `BUILTIN_VERBS` below — and anything else that needs the
|
|
114
|
+
* full verb manifest, e.g. `scripts/verify-verb-sweep.mjs` — can derive these
|
|
115
|
+
* eight verb names from the dispatcher's own table instead of hand-copying it.
|
|
116
|
+
*/
|
|
117
|
+
export const CONTROL_ENTRYPOINTS = {
|
|
118
|
+
pin: "hs-pin.ts",
|
|
119
|
+
claim: "hs-claim.ts",
|
|
120
|
+
handoff: "hs-handoff.ts",
|
|
121
|
+
events: "hs-events.ts",
|
|
122
|
+
"export-events": "hs-export-events.ts",
|
|
123
|
+
prereq: "hs-prereq.ts",
|
|
124
|
+
queue: "hs-queue.ts",
|
|
125
|
+
// `hs model` - one read-only list of every seat that has a model behind it
|
|
126
|
+
// (launch seats from settings.json, SpexFleet seats from routing-table.json).
|
|
127
|
+
model: "hs-model.ts",
|
|
128
|
+
// `hs trash` - the bin every spec delete soft-deletes into. Nothing looked
|
|
129
|
+
// at it before, so deleted specs accumulated on disk forever.
|
|
130
|
+
trash: "hs-trash.ts",
|
|
131
|
+
// `hs admin test-user create` — mint one throwaway test customer against a
|
|
132
|
+
// stated environment ([[HS-4Z1N-SMXX-EPIC-hs-admin-test-user-create-one-command]]).
|
|
133
|
+
// Wraps POST /api/admin/users/create, so it belongs with the other verbs
|
|
134
|
+
// that are thin shells over a console endpoint.
|
|
135
|
+
admin: "hs-admin.ts",
|
|
136
|
+
// `hs seat` — read/add/update/disable a SpexFleet seat, matching
|
|
137
|
+
// /api/seat-registry's operations 1:1 ([[HS-W2H1-NYPR-TASK-create-the-api-endpoint-that-lets-a]]
|
|
138
|
+
// of epic [[HS-RJVM-NYPR-EPIC-seat-registry-every-role-spexfleet-runs]]).
|
|
139
|
+
// Reads/writes dev-factory/seats.json directly through `@/lib/seat-registry`
|
|
140
|
+
// — the same core the API route calls — so it works with the console
|
|
141
|
+
// stopped, same shape as `hs model` / `hs registry`.
|
|
142
|
+
seat: "hs-seat.ts",
|
|
143
|
+
// `hs voice` — add/build/show/set/test/refresh/export a coded voice profile,
|
|
144
|
+
// matching /api/voice's operations 1:1
|
|
145
|
+
// ([[HS-PQPJ-DOJY-TASK-build-the-hs-voice-cli-and-matching]] of epic
|
|
146
|
+
// [[HS-8PG1-DOJY-EPIC-voice-as-a-deterministic-system-a-coded]]). Reads/
|
|
147
|
+
// writes .heyspex/voice-profiles directly through @/lib/voice-cli — the
|
|
148
|
+
// same core the API route calls — so it works with the console stopped,
|
|
149
|
+
// same shape as `hs model` / `hs seat`.
|
|
150
|
+
voice: "hs-voice.ts",
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Every top-level `hs <namespace>` verb `main()` recognizes below, listed in
|
|
155
|
+
* the same order as their dispatch branches — the single manifest
|
|
156
|
+
* [[HS-G65Z-TASK-every-hs-verb-sweeps-clean-against-a]]'s
|
|
157
|
+
* `scripts/verify-verb-sweep.mjs` imports to enumerate what to sweep, instead
|
|
158
|
+
* of hand-maintaining a second, driftable copy of this list in the sweep
|
|
159
|
+
* script. Deleting a verb's dispatch branch above and its entry here (both in
|
|
160
|
+
* this file) is enough to drop it from the sweep with no sweep-script edit.
|
|
161
|
+
* Does NOT distinguish spawn-backed vs. spex-console-TS-backed vs. bundled —
|
|
162
|
+
* `SPAWN_TARGETS` above already carries that detail for the verbs it covers;
|
|
163
|
+
* this list is deliberately just "what does `hs` accept as argv[0]".
|
|
164
|
+
*/
|
|
165
|
+
export const BUILTIN_VERBS = [
|
|
166
|
+
"init",
|
|
167
|
+
"install",
|
|
168
|
+
"join",
|
|
169
|
+
"invite",
|
|
170
|
+
"setup",
|
|
171
|
+
"skills",
|
|
172
|
+
"vault",
|
|
173
|
+
"workspace",
|
|
174
|
+
"login",
|
|
175
|
+
"logout",
|
|
176
|
+
"whoami",
|
|
177
|
+
"connect",
|
|
178
|
+
"doctor",
|
|
179
|
+
"index",
|
|
180
|
+
"pull",
|
|
181
|
+
"push",
|
|
182
|
+
"import",
|
|
183
|
+
"milestone",
|
|
184
|
+
"factory",
|
|
185
|
+
"code-watcher",
|
|
186
|
+
"server",
|
|
187
|
+
"wip",
|
|
188
|
+
"worktree",
|
|
189
|
+
"source",
|
|
190
|
+
"view",
|
|
191
|
+
"dev",
|
|
192
|
+
"defer",
|
|
193
|
+
"undefer",
|
|
194
|
+
"spawn",
|
|
195
|
+
"capture",
|
|
196
|
+
"companion",
|
|
197
|
+
"registry",
|
|
198
|
+
"env",
|
|
199
|
+
"next",
|
|
200
|
+
"mode",
|
|
201
|
+
"gateway",
|
|
202
|
+
"settings",
|
|
203
|
+
"classifier",
|
|
204
|
+
"auto",
|
|
205
|
+
"mcp",
|
|
206
|
+
"test",
|
|
207
|
+
"harness",
|
|
208
|
+
"hooks",
|
|
209
|
+
"bench",
|
|
210
|
+
"check",
|
|
211
|
+
"revalidate",
|
|
212
|
+
"agent",
|
|
213
|
+
"chat",
|
|
214
|
+
"trust",
|
|
215
|
+
"doctrine",
|
|
216
|
+
...Object.keys(CONTROL_ENTRYPOINTS),
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Does this `hs next ...` invocation need the console? Mirror of
|
|
221
|
+
* `isNextConsoleCommand` in `spex-console/src/lib/cli/next.ts` - that module
|
|
222
|
+
* owns the rule; this copy exists because the dispatcher must choose a half
|
|
223
|
+
* BEFORE spawning either one, and cannot import TypeScript. Keep the two in
|
|
224
|
+
* step: any launch/settings flag added there belongs here too.
|
|
225
|
+
*/
|
|
226
|
+
export function isNextConsoleArgv(args) {
|
|
227
|
+
if (
|
|
228
|
+
args[0] === "model" ||
|
|
229
|
+
args[0] === "permission-mode" ||
|
|
230
|
+
args[0] === "claim" ||
|
|
231
|
+
args[0] === "front"
|
|
232
|
+
)
|
|
233
|
+
return true;
|
|
234
|
+
return args.some(
|
|
235
|
+
(a) =>
|
|
236
|
+
a === "--launch" ||
|
|
237
|
+
a === "--force" ||
|
|
238
|
+
a === "--claim" ||
|
|
239
|
+
a === "--no-claim" ||
|
|
240
|
+
a === "--model" ||
|
|
241
|
+
a.startsWith("--model=") ||
|
|
242
|
+
a === "--permission-mode" ||
|
|
243
|
+
a.startsWith("--permission-mode="),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Resolve a SPAWN_TARGETS entry to an absolute path under PKG_ROOT. */
|
|
248
|
+
function spawnTargetPath(key) {
|
|
249
|
+
const entry = SPAWN_TARGETS[key];
|
|
250
|
+
if (!entry) throw new Error(`hs: unknown SPAWN_TARGETS key "${key}"`);
|
|
251
|
+
return path.join(PKG_ROOT, ...entry.path.split("/"));
|
|
252
|
+
}
|
|
253
|
+
|
|
25
254
|
// Captured before anything in this process could ever chdir (it doesn't
|
|
26
255
|
// today, but every dispatched entry below is spawned with its own `cwd`
|
|
27
256
|
// override — see dispatchSpexConsoleEntry). This is the ONE true record of
|
|
@@ -42,20 +271,244 @@ const INVOKED_CWD = process.cwd();
|
|
|
42
271
|
process.env.HS_INVOKED_CWD = INVOKED_CWD;
|
|
43
272
|
|
|
44
273
|
/**
|
|
45
|
-
* Walk up from `start` until we find a directory containing
|
|
46
|
-
*
|
|
274
|
+
* Walk up from `start` until we find a vault root — a directory containing
|
|
275
|
+
* a `heyspex.yaml` marker file OR a `specs/` dir. Delegates the "is this a
|
|
276
|
+
* vault root" test to the shared `isVaultRoot` helper (same one the MCP
|
|
277
|
+
* resolver's cwd walk uses) so the two walks can never disagree about what
|
|
278
|
+
* counts as a vault ([[HS-GKAI]]).
|
|
47
279
|
*/
|
|
48
|
-
function findVaultRoot(start) {
|
|
280
|
+
export function findVaultRoot(start, stopAt) {
|
|
49
281
|
let dir = path.resolve(start);
|
|
282
|
+
const boundary = stopAt ? path.resolve(stopAt) : null;
|
|
50
283
|
while (true) {
|
|
51
|
-
|
|
52
|
-
|
|
284
|
+
// A SATELLITE repo (a code repo attached to an existing vault -- see
|
|
285
|
+
// spex-console/src/lib/cli/satellite.ts) carries no marker of its own and
|
|
286
|
+
// names its vault in its MCP manifests instead. Checked FIRST at each
|
|
287
|
+
// level so an explicit attachment beats a vault that merely happens to
|
|
288
|
+
// sit further up the tree, and so `hs` in a satellite follows THAT vault
|
|
289
|
+
// rather than whatever the machine-global active pointer last named.
|
|
290
|
+
const pinned = readSatellitePin(dir);
|
|
291
|
+
if (pinned) {
|
|
292
|
+
return pinned;
|
|
293
|
+
}
|
|
294
|
+
// A committed, portable satellite marker (heyspex.yaml with a relative
|
|
295
|
+
// `vault_path`): the attached vault's clone when it is on this machine,
|
|
296
|
+
// else this repo itself (cloud-only; the marker's vault_id binds it).
|
|
297
|
+
const marked = readMarkerSatellite(dir);
|
|
298
|
+
if (marked) {
|
|
299
|
+
return marked.vaultRootAbs ?? dir;
|
|
300
|
+
}
|
|
301
|
+
if (isVaultRoot(dir)) {
|
|
53
302
|
return dir;
|
|
54
303
|
}
|
|
304
|
+
// `stopAt` is a TEST-ONLY walk boundary (mirrors vault-resolve.mjs's
|
|
305
|
+
// `cwdWalkRoot`) — checked, then the walk stops, never climbing past it.
|
|
306
|
+
// Unset in production, so the walk always reaches the real filesystem
|
|
307
|
+
// root; tests fence it inside a disposable fixture so an unrelated
|
|
308
|
+
// ancestor higher up the real tree (e.g. a stray `specs/` dir left over
|
|
309
|
+
// in a temp folder) cannot produce a false match ([[HS-0I1G]]).
|
|
310
|
+
if (boundary && dir === boundary) return null;
|
|
311
|
+
const parent = path.dirname(dir);
|
|
312
|
+
if (parent === dir) return null;
|
|
313
|
+
dir = parent;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Registry fallback for when the specs/ walk-up finds nothing — since the
|
|
319
|
+
* repo split (2026-08-31) the vault lives outside the code repo, so `hs`
|
|
320
|
+
* run from the code repo would otherwise exit "no vault detected".
|
|
321
|
+
* Delegates to the shared resolver (same one the MCP server trusts) but
|
|
322
|
+
* only accepts a result that names a real vault: the resolver's last-resort
|
|
323
|
+
* cwd fallback is rejected so the "no vault detected" error below still
|
|
324
|
+
* fires when there is genuinely no vault anywhere.
|
|
325
|
+
*
|
|
326
|
+
* The test is deliberately "anything but `cwd-heuristic`", so every real
|
|
327
|
+
* tier passes it: `explicit`, `account-pointer`, `env-vault-root`,
|
|
328
|
+
* `env-vault-dir`, and `satellite-pin` (an on-disk manifest attachment found
|
|
329
|
+
* by the resolver's tier-3 walk -- that tier reported the untrue
|
|
330
|
+
* `env-vault-root` until 2026-09-18; both labels are accepted here, so the
|
|
331
|
+
* rename changed nothing). `cwd-marker` and `marker-registry-mismatch` (a
|
|
332
|
+
* heyspex.yaml `vault_id` named the vault, [[HS-VVNQ]]) pass too.
|
|
333
|
+
*/
|
|
334
|
+
export function findRegistryVaultRoot(options = {}) {
|
|
335
|
+
let resolved;
|
|
336
|
+
try {
|
|
337
|
+
resolved = resolveActiveVault(options);
|
|
338
|
+
} catch {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
if (!resolved || resolved.source === "cwd-heuristic") return null;
|
|
342
|
+
if (!existsSync(resolved.specsDirAbs)) return null;
|
|
343
|
+
return resolved.repoRootAbs;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Env hatch: follow the account-wide active-vault pointer on purpose (same name the MCP transport uses). */
|
|
347
|
+
export function pointerVaultAllowed(env = process.env) {
|
|
348
|
+
const v = String(env.HEYSPEX_ALLOW_POINTER_VAULT ?? "").trim().toLowerCase();
|
|
349
|
+
return v === "1" || v === "true" || v === "yes";
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Verbs that may legitimately run with only the machine's last-active vault
|
|
354
|
+
* (or none): they onboard, sign in, or manage the registry itself. EVERY other
|
|
355
|
+
* verb acts on a vault, and P1 (owner, 2026-10-16) says that vault is the one
|
|
356
|
+
* bound to the repo the agent runs in -- never the account-wide pointer.
|
|
357
|
+
*/
|
|
358
|
+
const POINTER_FALLBACK_NAMESPACES = new Set([
|
|
359
|
+
"init", "install", "join", "setup", "skills", "vault", "workspace",
|
|
360
|
+
"login", "whoami", "logout", "doctor", "registry", "companion", "trust",
|
|
361
|
+
]);
|
|
362
|
+
|
|
363
|
+
/** True when `hs <namespace> <command>` may run on the pointer-only fallback. */
|
|
364
|
+
export function pointerFallbackAllowedFor(namespace, command) {
|
|
365
|
+
if (namespace === "mcp") return command === "pair" || command === "login" || command === "install";
|
|
366
|
+
return POINTER_FALLBACK_NAMESPACES.has(namespace);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** The refusal printed when a satellite's pin / marker cannot be honoured. */
|
|
370
|
+
export function brokenPinRefusalLine(broken) {
|
|
371
|
+
return (
|
|
372
|
+
`hs: this repo's vault pin cannot be honoured, so nothing was run. ${broken.dir}/${broken.source} ` +
|
|
373
|
+
`names \`${broken.raw}\`, but ${broken.reason}. ` +
|
|
374
|
+
"Fix the pin (a relative path such as ../<vault-repo> or ${CLAUDE_PROJECT_DIR:-.}/../<vault-repo> is portable), " +
|
|
375
|
+
"or commit a heyspex.yaml with vault_id and vault_path; hs will NOT fall back to the account-wide active vault.\n"
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Nearest directory at/above `start` whose heyspex.yaml is a satellite marker (has vault_path), else null. */
|
|
380
|
+
export function markerSatelliteDirFrom(start) {
|
|
381
|
+
let dir = path.resolve(start);
|
|
382
|
+
for (let i = 0; i < 256; i += 1) {
|
|
383
|
+
if (readMarkerSatellite(dir)) return dir;
|
|
55
384
|
const parent = path.dirname(dir);
|
|
56
385
|
if (parent === dir) return null;
|
|
57
386
|
dir = parent;
|
|
58
387
|
}
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** The refusal for disk-writing verbs in a satellite whose vault clone is missing. */
|
|
392
|
+
export function unclonedVaultRefusalLine(repo) {
|
|
393
|
+
const m = readMarkerSatellite(repo);
|
|
394
|
+
return (
|
|
395
|
+
`hs: ${repo} is bound to vault ${m?.vaultId ?? "(no vault_id)"} by its heyspex.yaml, but the vault ` +
|
|
396
|
+
`is not cloned at ${m?.vaultPath ?? "its vault_path"} on this machine, so this verb has no local vault to act on. ` +
|
|
397
|
+
"Clone the vault repo to that relative path (agents and cloud-routed MCP writes work without it).\n"
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** The refusal printed when only the account-wide pointer answered and the verb acts on a vault. */
|
|
402
|
+
export function pointerOnlyRefusalLine(cwd, vaultRoot) {
|
|
403
|
+
return (
|
|
404
|
+
`hs: ${cwd} is not bound to a HeySpex vault (no heyspex.yaml, no specs/, no vault pin in .mcp.json), ` +
|
|
405
|
+
`so \`hs\` refused to follow the account-wide active vault${vaultRoot ? ` (${vaultRoot})` : ""}. ` +
|
|
406
|
+
"Bind this repo: run `hs connect` in a vault repo, `hs install --attach-vault <id> --target <repo>` for a code repo, " +
|
|
407
|
+
"or commit a heyspex.yaml with vault_id and a relative vault_path. " +
|
|
408
|
+
"To follow the pointer on purpose set HEYSPEX_ALLOW_POINTER_VAULT=1.\n"
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* The vault-resolution ladder `main()` runs at the top of every invocation,
|
|
414
|
+
* factored out so it is unit-testable without spawning a child process
|
|
415
|
+
* ([[HS-0I1G-B0IT-TASK-track-and-announce-the-registry-active]]).
|
|
416
|
+
*
|
|
417
|
+
* Reports `usedRegistryFallback: true` only when NONE of `HS_VAULT_ROOT`, an
|
|
418
|
+
* explicit `--target`-equivalent, or a marker/satellite pin found walking up
|
|
419
|
+
* from `cwd` answered, and `findRegistryVaultRoot()` (the machine's
|
|
420
|
+
* last-active vault, `~/.heyspex/registry.json`) is what resolved the root.
|
|
421
|
+
* `hs install` uses that flag to warn (always) and — when `--register-vault`
|
|
422
|
+
* is given without `--dry-run` — refuse, so a customer onboarding an empty
|
|
423
|
+
* directory doesn't silently write into whatever vault this machine last had
|
|
424
|
+
* active in some other terminal/console tab.
|
|
425
|
+
*
|
|
426
|
+
* `stopAt` is a TEST-ONLY walk-boundary fence, threaded through to both
|
|
427
|
+
* `findVaultRoot`'s own walk and `resolveActiveVault`'s inner cwd walk (via
|
|
428
|
+
* its `cwdWalkRoot` option) — production callers (`main()`) omit it.
|
|
429
|
+
*
|
|
430
|
+
* @param {string} cwd
|
|
431
|
+
* @param {NodeJS.ProcessEnv} env
|
|
432
|
+
* @param {string} [stopAt]
|
|
433
|
+
* @returns {{ vaultRoot: string|null, usedRegistryFallback: boolean }}
|
|
434
|
+
*/
|
|
435
|
+
export function resolveVaultRootForMain(cwd, env, stopAt) {
|
|
436
|
+
if (env.HS_VAULT_ROOT) {
|
|
437
|
+
return { vaultRoot: env.HS_VAULT_ROOT, usedRegistryFallback: false };
|
|
438
|
+
}
|
|
439
|
+
// A pin that cannot be honoured is a REFUSAL, never a reason to fall
|
|
440
|
+
// through to the account-wide pointer (owner principle P1, 2026-10-16).
|
|
441
|
+
const brokenPin = findBrokenSatellitePin(cwd, stopAt);
|
|
442
|
+
if (brokenPin) {
|
|
443
|
+
return { vaultRoot: null, usedRegistryFallback: false, brokenPin };
|
|
444
|
+
}
|
|
445
|
+
const walkedRoot = findVaultRoot(cwd, stopAt);
|
|
446
|
+
if (walkedRoot) {
|
|
447
|
+
return { vaultRoot: walkedRoot, usedRegistryFallback: false };
|
|
448
|
+
}
|
|
449
|
+
// Same ONE order as both resolvers ([[HS-VVNQ]], shared fixture
|
|
450
|
+
// spex-console/src/lib/vault-resolve/precedence-fixtures.json):
|
|
451
|
+
// `HS_VAULT_ROOT` (this CLI's explicit tier) → the walk above (satellite
|
|
452
|
+
// pin, then marker) → `HEYSPEX_VAULT_ROOT`/`HEYSPEX_VAULT_DIR` env → the
|
|
453
|
+
// registry pointer, the last two via the shared resolver. `cwd` is passed
|
|
454
|
+
// explicitly so the resolver's own walk starts where ours did — left to
|
|
455
|
+
// its default it would start from `CLAUDE_PROJECT_DIR`, and a marker found
|
|
456
|
+
// THERE (a different folder) would be mislabelled a registry fallback.
|
|
457
|
+
const registryRoot = findRegistryVaultRoot(
|
|
458
|
+
stopAt ? { cwd, cwdWalkRoot: stopAt } : { cwd },
|
|
459
|
+
);
|
|
460
|
+
return {
|
|
461
|
+
vaultRoot: registryRoot,
|
|
462
|
+
usedRegistryFallback: Boolean(registryRoot),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** True when `argv` carries `--attach-vault` (bare or `=value` form). */
|
|
467
|
+
export function hasAttachVaultFlag(argv) {
|
|
468
|
+
return argv.some(
|
|
469
|
+
(a) => a === "--attach-vault" || a.startsWith("--attach-vault="),
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* True when `argv` carries an explicit `--target` (bare or `=value` form) —
|
|
475
|
+
* mirrors `install.ts`'s own `parseInstallArgs` flag test. `hs-install.ts`'s
|
|
476
|
+
* `resolveInstallTargetDir` already honors `--target` over `HS_VAULT_ROOT`
|
|
477
|
+
* unconditionally, so a caller who named their target explicitly is never
|
|
478
|
+
* actually affected by the registry fallback `bin/hs.mjs` computed for
|
|
479
|
+
* `HS_VAULT_ROOT` — warning or refusing anyway would be a false alarm about a
|
|
480
|
+
* value the install never consults.
|
|
481
|
+
*/
|
|
482
|
+
export function hasExplicitTargetFlag(argv) {
|
|
483
|
+
return argv.some((a) => a === "--target" || a.startsWith("--target="));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* The warning `hs install` prints (to stderr, in both `--dry-run` and real
|
|
488
|
+
* output) when the target it is about to use came ONLY from the registry
|
|
489
|
+
* active-vault fallback — i.e. cwd sits under no vault at all and this
|
|
490
|
+
* machine's last-active vault pointer answered instead.
|
|
491
|
+
*/
|
|
492
|
+
export function registryFallbackWarningLine(cwd, vaultRoot) {
|
|
493
|
+
return (
|
|
494
|
+
`hs install: no vault found in or above ${cwd} — falling back to this ` +
|
|
495
|
+
`machine's last-active vault (${vaultRoot}, from ~/.heyspex/registry.json). ` +
|
|
496
|
+
"Pass --target <dir> to install into this directory instead.\n"
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* The refusal `hs install --register-vault` (without `--dry-run`) prints and
|
|
502
|
+
* exits non-zero on instead of registering, when the fallback above is what
|
|
503
|
+
* answered — registering here would file the machine's last-active vault
|
|
504
|
+
* under a fresh pointer, not the one the caller meant.
|
|
505
|
+
*/
|
|
506
|
+
export function registryFallbackRefusalLine() {
|
|
507
|
+
return (
|
|
508
|
+
"hs install: refusing to run --register-vault from an empty directory that " +
|
|
509
|
+
"resolved only through the registry active-vault fallback. Pass --target <dir> " +
|
|
510
|
+
"naming the vault to register, or --dry-run to preview first.\n"
|
|
511
|
+
);
|
|
59
512
|
}
|
|
60
513
|
|
|
61
514
|
/**
|
|
@@ -93,8 +546,14 @@ function usage() {
|
|
|
93
546
|
"",
|
|
94
547
|
"Usage:",
|
|
95
548
|
" hs login [--console-url <url>] [--label <t>] [--json] Sign in via your browser (no code to type)",
|
|
549
|
+
" hs login --switch-account [<id|email>] | --create-account Switch to a stored account, or choose/create one in the browser",
|
|
550
|
+
" hs login --migrate [--dry-run] Re-key a single-account credentials file under its confirmed account",
|
|
551
|
+
" hs whoami [--account <id|email>] [--json] Which account this machine is signed in as (email, id, source, console)",
|
|
552
|
+
" hs logout [--account <id|email>] [--json] Sign one account out of this machine (default: the current one)",
|
|
96
553
|
" hs init [--console-url <url>] [--force] Write the heyspex.yaml vault marker",
|
|
97
554
|
" hs install [--target <dir>] [--platform <dir>] Bootstrap vault + Cursor/Claude MCP",
|
|
555
|
+
" hs install --attach-vault <id|path> Wire THIS code repo to an existing vault",
|
|
556
|
+
" (satellite: no specs/, no heyspex.yaml here)",
|
|
98
557
|
" hs invite [<name>] Mint a short pairing code (other computer: hs join CODE)",
|
|
99
558
|
" hs join <code> Join with a pairing code (LAN auto-find; no IP to type)",
|
|
100
559
|
" hs join [<repo>] [--dir d] [--code c] [--at url] [--profile p] [--skip steps] [--json] [--yes] Fresh-machine onboarding (clone → toolchain → deps → doctor → agent layer → secrets → verify)",
|
|
@@ -108,20 +567,35 @@ function usage() {
|
|
|
108
567
|
" hs dev [mode:local|mode:cloud|mode:prod] Dev/prod mode + local/cloud reliance (status when bare)",
|
|
109
568
|
" hs setup claude|cursor [--check|--uninstall] Install agent lifecycle hooks",
|
|
110
569
|
" hs skills sync|check Propagate skills to Cursor/Codex/Claude (cwd-safe)",
|
|
570
|
+
" hs skills add <module-path> --name <n> --trigger </slash> [--export <n>] [--description <t>] Register a skill (extension seam)",
|
|
571
|
+
" hs skills remove <name> Remove a customer-registered skill",
|
|
572
|
+
" hs skills list [--json] List registered skills (built-ins first, locked)",
|
|
111
573
|
" hs index rebuild [--verbose] [--dry-run] Full idempotent rebuild of the index",
|
|
112
|
-
" hs
|
|
574
|
+
" hs index reconcile [--apply] [--cap-docs keep-out|index] Per-spec newest-wins between disk and cloud (reports unless --apply)",
|
|
575
|
+
" hs pull [--apply] [--only <id>] [--overwrite] Bring cloud-written specs down to the disk vault (reports unless --apply; --watch --push adds the disk -> cloud leg)",
|
|
576
|
+
" hs push [--apply] [--only <id>] [--force] Send local spec edits up to the cloud vault (reports unless --apply)",
|
|
577
|
+
" hs import <source-dir> [--folders a,b] [--plan f] [--dry-run] [--limit N] Import a doc tree as GROUNDED spec-import captures (never raw, never research)",
|
|
578
|
+
" hs source status|preflight|commit|push|branch Local git: tree status, path-scoped checks, commit, safe push, session branch",
|
|
579
|
+
" hs source identity [show|set|clear] Git author identity: show (read-only) / set, clear (HUMAN-ONLY)",
|
|
113
580
|
" hs milestone show <id> [--json] Show a milestone (flags needs-breakdown when epicless)",
|
|
114
581
|
" hs factory status|fleet|results [--json] Dev-Factory read-only status (fleet, routing, calibrations, results)",
|
|
115
582
|
" hs factory run status [--json] Live assisted/autonomous dispatcher run (pids, queue, stop hints)",
|
|
116
583
|
" hs code-watcher scan [--json] Capture build-correctness failures (tsc) → Resolution",
|
|
117
584
|
" hs server status [--json] Ground-truth dev-server status (running? which port[s]?)",
|
|
585
|
+
" hs wip status [--json] WIP ledger health report (lines vs 600-line budget, age tiers, archive)",
|
|
586
|
+
" hs wip restore <id> [--json] [--force] Restore an archived WIP ledger entry back onto the live ledger",
|
|
587
|
+
" hs worktree list|alloc|release|reap [--json] [--dry-run] Session/build worktree registry (HS-03DO)",
|
|
118
588
|
" hs view <surface> [spec-id] [--width N --height N --browser edge|chrome] Open one surface in a chromeless app-mode window",
|
|
119
589
|
" hs server stop [--json] Stop the spex-console dev server (any terminal)",
|
|
590
|
+
" hs source status|preflight|commit|push SpexSource local git operations",
|
|
591
|
+
" hs source identity [show|set|clear] Git author identity (set/clear are human-only; see hs source --help)",
|
|
120
592
|
" hs defer <id> [--until <when>] [--json] Time-shift a spec (defer_until)",
|
|
121
593
|
" hs undefer <id> [--json] Clear a spec's defer_until",
|
|
122
594
|
" hs spawn [<id>] [--dry-run] [--json] Launch a fresh /hs-next agent in a new terminal",
|
|
123
|
-
|
|
595
|
+
' hs capture "<text>"|--clipboard|--file <p> [--add] Capture → console drafts (--add chains a group)',
|
|
124
596
|
" hs companion start|stop|status|build Desktop companion tray app",
|
|
597
|
+
" hs companion settings [list|get <k>|set <k> <v>] [--json] Every Companion setting (console-backed)",
|
|
598
|
+
" hs companion mute|unmute|volume|orb|wake|activation|hotkey [<v>] Setting shortcuts (omit <v> to read)",
|
|
125
599
|
" hs registry list [--json] List launch registry entries",
|
|
126
600
|
" hs env init|set|list|run|export|rotate Encrypted env vault (ADR-0020)",
|
|
127
601
|
" hs env profiles | key export|import Named profiles + second-machine key transfer",
|
|
@@ -129,23 +603,52 @@ function usage() {
|
|
|
129
603
|
" hs settings list|get|set Read/change workflow settings (.heyspex/config.yaml)",
|
|
130
604
|
" hs classifier stats [--json] [--recent N] Intent Classifier decision stats (read-only)",
|
|
131
605
|
" hs next [spec_id] [--summary] [--json] Resolve on-deck brief (MCP fallback)",
|
|
606
|
+
" hs next <id> --launch [--dry-run] [--claim] [--backend <anthropic|ollama|deepseek>] Launch a session on that spec",
|
|
607
|
+
" hs next model|permission-mode [<kind>] [<v>] Per-kind launch defaults",
|
|
608
|
+
" hs next backend [<kind>] [<v>] Per-kind launch backend default (anthropic|ollama|deepseek)",
|
|
609
|
+
" hs next claim [on|off] Auto-claim vs ask-first default",
|
|
610
|
+
" hs next front [handheld|assist|auto] Default front: one task with you, or a dispatcher run",
|
|
611
|
+
" hs next assist|auto <milestone...> --launch Arm a dispatcher run on one or more milestones",
|
|
612
|
+
" hs model [<seat>] [--launch|--fleet] Every seat and the model set for it",
|
|
613
|
+
" hs seat list|add|update|disable [--json] Read/add/update/disable a seat in the registry (dev-factory/seats.json)",
|
|
614
|
+
" hs voice add|build|show|set|test|refresh|export Build and manage a coded voice profile",
|
|
615
|
+
" hs trash [list|restore|empty] The bin spec deletes go into",
|
|
132
616
|
" hs auto [filename] [--json] Resolve /hs-auto dispatch plan",
|
|
133
617
|
" hs mcp call <tool> [--arg key=value ...] [--args '<json>'] [--args-file <path>] [--json] One-shot MCP tool",
|
|
134
618
|
" hs mcp pair <code> [--console-url <url>] Redeem a console pairing code (cross-machine fallback for hs login)",
|
|
619
|
+
" hs mcp install --remote [--write] [--json] Write/print .mcp.json for the cloud (remote) MCP endpoint",
|
|
135
620
|
" hs test run <id|manifest> [--format ...] Run open-format API tests (Hurl/.http) per AC",
|
|
136
621
|
" hs test gate [dir] [--report <j>] [--threshold n] Mutation-testing gate — reject suites that kill no mutants",
|
|
137
622
|
" hs test conform <spec> --url <u> | --report <x> OpenAPI conformance / docs↔impl drift (Schemathesis)",
|
|
623
|
+
" hs chat [--model <name>] [--script <file>] [--seat <seat>] HeySpex's own chat front (any model); !<cmd> or /shell for CLI passthrough",
|
|
624
|
+
" hs trust list List spexcode's stored folder-trust decisions",
|
|
625
|
+
" hs trust revoke [path] Clear one folder's trust decision (default: cwd) so the next launch asks again",
|
|
626
|
+
" hs doctrine extract [--since <iso>] [--project <name>] [--dry-run] Stream every session/taste source into .heyspex/doctrine/observations.jsonl",
|
|
627
|
+
" hs doctrine learn [--full] [--max-batches <n>] [--dry-run] Extract, then synthesize into spexharness/doctrine/SPEXCODE-DOCTRINE.md/json",
|
|
628
|
+
" hs doctrine show Print the current SPEXCODE-DOCTRINE.md",
|
|
629
|
+
" hs harness gates [--json] List every SpexHarness gate: id, event, severity, last fixture result",
|
|
630
|
+
" hs hooks add <module-path> --event <event> [--id <id>] [--priority <n>] Register a hook (extension seam)",
|
|
631
|
+
" hs hooks remove <id> Remove a customer-registered hook",
|
|
632
|
+
" hs hooks list [--json] List registered hooks (built-ins first, locked)",
|
|
138
633
|
" hs check run <id|manifest> [--json] Verify acceptance criteria vs the frozen checks manifest (per-AC pass/fail)",
|
|
634
|
+
" hs check judge <id> [--session <id>] [--dry-run] Decorrelated judge seat: verdicts on method:human criteria, posts to the verification API",
|
|
139
635
|
" hs revalidate [--json] [--apply] Re-run the frozen checks of already-stamped work; report what has decayed",
|
|
140
636
|
" hs agent import <collection> <role> Import a community agent into a type:agent vault spec",
|
|
141
637
|
" hs agent list [collection] List vendored agent collections / roles",
|
|
142
638
|
"",
|
|
639
|
+
" node dev-factory/spexcrawl/spexcrawl-cli.mjs search|scrape|crawl SpexCrawl — web",
|
|
640
|
+
" search/scrape/crawl CLI (standalone script,",
|
|
641
|
+
" not an `hs` verb; also reachable from the",
|
|
642
|
+
" Companion)",
|
|
643
|
+
"",
|
|
143
644
|
"Control plane (Neon — requires a reachable console):",
|
|
144
645
|
" hs pin get|set <id>|clear Read/set/clear the on-deck pin",
|
|
145
646
|
" hs claim acquire|release <id> | list Atomic spec claims",
|
|
146
647
|
" hs claim progress <id> --stage <verb> Report live work-stage",
|
|
147
|
-
" hs handoff record|load [--lane <k>] Resume-context coordination",
|
|
648
|
+
" hs handoff record|load [--lane <k>] Resume-context coordination (never launches)",
|
|
649
|
+
" hs handoff <CODE> [--no-launch] Resolve + LAUNCH the lane's resume by default",
|
|
148
650
|
" hs events show <id> [--kind --since] Per-spec audit log",
|
|
651
|
+
" hs admin test-user create --env <e> Mint one throwaway test customer",
|
|
149
652
|
" hs export-events [--since <date>] Idempotent audit export → specs/events/",
|
|
150
653
|
" hs prereq <blocked-id> [<action> <prereq>] Cross-milestone prerequisite resolution",
|
|
151
654
|
" hs queue submit|next|claim|complete|fail|list Fleet-queue dispatch (Neon, direct — no console hop)",
|
|
@@ -155,7 +658,13 @@ function usage() {
|
|
|
155
658
|
" hs --version Show CLI version",
|
|
156
659
|
"",
|
|
157
660
|
"Vault root is detected by walking up from the current directory until a",
|
|
158
|
-
"specs/ folder is found
|
|
661
|
+
"heyspex.yaml marker or a specs/ folder is found, then falling back to the",
|
|
662
|
+
"registry's active vault (~/.heyspex/registry.json). Override with HS_VAULT_ROOT.",
|
|
663
|
+
"When hs install resolves ONLY through that registry fallback (a fully empty",
|
|
664
|
+
"cwd — no heyspex.yaml/specs/ found walking up, no HS_VAULT_ROOT, no --target),",
|
|
665
|
+
"it warns, naming the fallback vault, in both --dry-run and real output; and",
|
|
666
|
+
"`hs install --register-vault` without --dry-run in that case refuses instead of",
|
|
667
|
+
"registering, asking for --target. --attach-vault (satellite mode) is exempt.",
|
|
159
668
|
"",
|
|
160
669
|
].join("\n"),
|
|
161
670
|
);
|
|
@@ -169,9 +678,13 @@ function usage() {
|
|
|
169
678
|
* Three cases, in order ([[HS-JOQX-0JNK-TASK-published-package-runs-hs-init-and-hs]]):
|
|
170
679
|
* 1. Monorepo checkout: `.ts` source AND `tsx` both present — run via tsx,
|
|
171
680
|
* exactly as before this task.
|
|
172
|
-
* 2. Published install of a BUNDLED verb
|
|
173
|
-
* `hs-install.ts`
|
|
174
|
-
*
|
|
681
|
+
* 2. Published install of a BUNDLED verb — currently `hs-init.ts`,
|
|
682
|
+
* `hs-install.ts`, `hs-claim.ts`, `hs-check.ts`, `hs-env.ts`,
|
|
683
|
+
* `hs-connect.ts`, `hs-join.ts`, `hs-vault.ts`, `hs-workspace.ts`,
|
|
684
|
+
* `hs-defer.ts`, `hs-vault-pull.ts`, `hs-dev.ts`, `hs-index-rebuild.ts`,
|
|
685
|
+
* `hs-index-reconcile.ts` — see `scripts/build-cli-bundle.mjs`'s
|
|
686
|
+
* `ENTRIES` array and doc comment for the authoritative list and why
|
|
687
|
+
* each qualifies: neither the source nor tsx exists, but
|
|
175
688
|
* `dist/cli/<name>.mjs` does — run the plain-JS bundle directly with
|
|
176
689
|
* `node`, no tsx/TypeScript involved.
|
|
177
690
|
* 3. Neither exists: the package install is incomplete (a verb this
|
|
@@ -256,7 +769,7 @@ function dispatchSpexConsoleEntry(entryFile, args, vaultRoot, extraEnv = {}) {
|
|
|
256
769
|
* it), so there is nothing to check about `hs` other than reporting its
|
|
257
770
|
* version.
|
|
258
771
|
*/
|
|
259
|
-
function runStandaloneDoctor(args) {
|
|
772
|
+
function runStandaloneDoctor(args, vaultRoot) {
|
|
260
773
|
const jsonOut = args.includes("--json");
|
|
261
774
|
let version = "0.0.0";
|
|
262
775
|
try {
|
|
@@ -292,6 +805,46 @@ function runStandaloneDoctor(args) {
|
|
|
292
805
|
if (runtimeFilesRow.status !== "ok") report.ok = false;
|
|
293
806
|
report.checks.push(runtimeFilesRow);
|
|
294
807
|
|
|
808
|
+
// `vault-config` ([[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]]
|
|
809
|
+
// fix 5): the vault's `.heyspex/config.yaml` (ADR-0009: vault root, git-
|
|
810
|
+
// tracked) must exist — without it every settings reader runs on defaults.
|
|
811
|
+
// `vaultRoot` is main()'s resolution (HS_VAULT_ROOT → walk-up → registry);
|
|
812
|
+
// null means no vault at all, which the row reports as its own failure.
|
|
813
|
+
const vaultConfigRow = vaultConfigCheckRow(vaultRoot ?? null);
|
|
814
|
+
if (vaultConfigRow.status !== "ok") report.ok = false;
|
|
815
|
+
report.checks.push(vaultConfigRow);
|
|
816
|
+
|
|
817
|
+
// `vault-template` (HS-KH0Q): shipped core docs + template_version pin present.
|
|
818
|
+
const templateRow = templateCheckRow(vaultRoot ?? null);
|
|
819
|
+
if (templateRow.status === "fail") report.ok = false;
|
|
820
|
+
report.checks.push(templateRow);
|
|
821
|
+
|
|
822
|
+
// `connection` ([[HS-W73V-WW9R-TASK-make-both-doctor-shapes-fail-on-cloud]],
|
|
823
|
+
// invariants 8 and 10): the BUNDLED `hs connect --check --json` from the
|
|
824
|
+
// invoked cwd, folded by the SAME function the full doctor uses
|
|
825
|
+
// (`foldConnectionReport`), so a customer machine gets the local-vs-cloud
|
|
826
|
+
// rows too. A disagreeing row fails the run with its repair command; an
|
|
827
|
+
// unreadable cloud side (offline, signed out) is a warn and leaves `ok`
|
|
828
|
+
// alone. The check reads the console with GET only. Its subject is the repo
|
|
829
|
+
// the caller stands in (the walk-up from the invoked cwd, else the cwd
|
|
830
|
+
// itself), NOT `vaultRoot`: that can be an inherited HS_VAULT_ROOT or the
|
|
831
|
+
// registry fallback, i.e. ANOTHER repo (seen when `hs connect`'s acceptance
|
|
832
|
+
// pass ran this doctor in a fresh repo and it audited the previous one).
|
|
833
|
+
const connectSubject = findVaultRoot(INVOKED_CWD) ?? INVOKED_CWD;
|
|
834
|
+
const connection = foldConnectionReport(
|
|
835
|
+
runBundledConnectCheck(PKG_ROOT, {
|
|
836
|
+
cwd: INVOKED_CWD,
|
|
837
|
+
env: {
|
|
838
|
+
...process.env,
|
|
839
|
+
HS_INVOKED_CWD: INVOKED_CWD,
|
|
840
|
+
HS_VAULT_ROOT: connectSubject,
|
|
841
|
+
},
|
|
842
|
+
}),
|
|
843
|
+
);
|
|
844
|
+
if (!connection.ok) report.ok = false;
|
|
845
|
+
report.checks.push(...connection.rows);
|
|
846
|
+
report.connection = connection.json;
|
|
847
|
+
|
|
295
848
|
const hsLinkLib = path.join(PKG_ROOT, "scripts", "lib", "hs-link.mjs");
|
|
296
849
|
if (existsSync(hsLinkLib)) {
|
|
297
850
|
// Deliberately synchronous top-level work below via a nested async IIFE
|
|
@@ -340,8 +893,15 @@ function printStandaloneDoctorReport(report, jsonOut) {
|
|
|
340
893
|
"HeySpex setup check — published-package install (not a source clone)\n\n",
|
|
341
894
|
);
|
|
342
895
|
for (const check of report.checks) {
|
|
343
|
-
const label =
|
|
344
|
-
|
|
896
|
+
const label =
|
|
897
|
+
check.status === "ok"
|
|
898
|
+
? " ok "
|
|
899
|
+
: check.status === "warn"
|
|
900
|
+
? " warn "
|
|
901
|
+
: " FAIL ";
|
|
902
|
+
process.stdout.write(
|
|
903
|
+
`[ ${label} ] ${check.label ? `${check.label}: ` : ""}${check.detail}\n`,
|
|
904
|
+
);
|
|
345
905
|
if (check.human) process.stdout.write(` ${check.human}\n`);
|
|
346
906
|
}
|
|
347
907
|
process.stdout.write(
|
|
@@ -384,24 +944,88 @@ function main(argv) {
|
|
|
384
944
|
const command = argv[1];
|
|
385
945
|
const rest = argv.slice(2);
|
|
386
946
|
|
|
387
|
-
const
|
|
947
|
+
const resolvedMain = resolveVaultRootForMain(process.cwd(), process.env);
|
|
948
|
+
const { usedRegistryFallback, brokenPin } = resolvedMain;
|
|
949
|
+
// P1: the pointer is not a binding. A verb that acts on a vault runs with
|
|
950
|
+
// NO vault (and is refused at the gate) rather than on whatever vault the
|
|
951
|
+
// console last selected; onboarding/registry verbs keep the old fallback.
|
|
952
|
+
const pointerOnly =
|
|
953
|
+
usedRegistryFallback &&
|
|
954
|
+
!pointerVaultAllowed(process.env) &&
|
|
955
|
+
!pointerFallbackAllowedFor(namespace, command);
|
|
956
|
+
const vaultRoot = pointerOnly ? null : resolvedMain.vaultRoot;
|
|
957
|
+
if (pointerOnly) {
|
|
958
|
+
process.stderr.write(pointerOnlyRefusalLine(process.cwd(), resolvedMain.vaultRoot));
|
|
959
|
+
}
|
|
960
|
+
if (
|
|
961
|
+
brokenPin &&
|
|
962
|
+
namespace !== "doctor" &&
|
|
963
|
+
!pointerFallbackAllowedFor(namespace, command)
|
|
964
|
+
) {
|
|
965
|
+
process.stderr.write(brokenPinRefusalLine(brokenPin));
|
|
966
|
+
process.exit(2);
|
|
967
|
+
}
|
|
388
968
|
|
|
389
|
-
// `hs init` writes the top-level heyspex.yaml vault marker
|
|
390
|
-
// ([[HS-EPIC-AEHR-audit-export-heyspex-yaml-and-heyspex-retirement]]
|
|
391
|
-
//
|
|
392
|
-
//
|
|
969
|
+
// `hs init` writes the top-level heyspex.yaml vault marker AND scaffolds
|
|
970
|
+
// specs/ ([[HS-EPIC-AEHR-audit-export-heyspex-yaml-and-heyspex-retirement]]
|
|
971
|
+
// §2). It always acts on the caller's invocation directory (`INVOKED_CWD`,
|
|
972
|
+
// captured before any dispatch below runs) — NEVER on `vaultRoot`'s
|
|
973
|
+
// walk-up/registry result. `hs init` may legitimately run *inside* a stray
|
|
974
|
+
// ancestor's `specs/` folder (a half-set-up nested checkout, a doc repo
|
|
975
|
+
// that happens to have a `specs/` dir, etc.); using `vaultRoot` here would
|
|
976
|
+
// silently write the marker into that unrelated ancestor instead of where
|
|
977
|
+
// the user actually typed `hs init` (HS-HISX: first fresh-machine walkthrough
|
|
978
|
+
// break — the vault marker landed in the wrong folder).
|
|
979
|
+
// HS_PKG_ROOT is forwarded so the entrypoint can resolve the vendored
|
|
980
|
+
// `.heyspex/config.yaml` seed template (`vendor/heyspex-mcp/config-seed.yaml`)
|
|
981
|
+
// from the PACKAGE root, never from the caller's cwd (HS-4C76 fix 4).
|
|
393
982
|
if (namespace === "init") {
|
|
394
|
-
dispatchSpexConsoleEntry("hs-init.ts", argv,
|
|
983
|
+
dispatchSpexConsoleEntry("hs-init.ts", argv, INVOKED_CWD, {
|
|
984
|
+
HS_PKG_ROOT: PKG_ROOT,
|
|
985
|
+
});
|
|
395
986
|
return;
|
|
396
987
|
}
|
|
397
988
|
|
|
398
989
|
// `hs install` bootstraps a customer/project vault + agent MCP wiring. May run
|
|
399
990
|
// on a fresh directory with no specs/ yet (like init).
|
|
991
|
+
//
|
|
992
|
+
// [[HS-0I1G-B0IT-TASK-track-and-announce-the-registry-active]]: when the
|
|
993
|
+
// resolution above bottomed out at the registry active-vault fallback
|
|
994
|
+
// (nothing set HS_VAULT_ROOT, and no marker/satellite pin was found walking
|
|
995
|
+
// up from cwd), say so — silently bootstrapping into whatever vault this
|
|
996
|
+
// machine last had active in some OTHER terminal/console tab is exactly the
|
|
997
|
+
// wrong-vault failure this task closes. Two flags are exempt because
|
|
998
|
+
// `hs-install.ts` never actually consults the vaultRoot this fallback
|
|
999
|
+
// computed for them, so warning/refusing about it would be a false alarm:
|
|
1000
|
+
// `--attach-vault` (satellite mode) — its target resolution
|
|
1001
|
+
// (resolveInstallTargetDir's satellite branch) ignores HS_VAULT_ROOT
|
|
1002
|
+
// entirely and reads HS_INVOKED_CWD instead (and `--register-vault` is
|
|
1003
|
+
// already mutually exclusive with it via assertSatelliteFlagCombination, so
|
|
1004
|
+
// the refusal below can never apply either way) — and an explicit
|
|
1005
|
+
// `--target`, which resolveInstallTargetDir honors over HS_VAULT_ROOT
|
|
1006
|
+
// unconditionally (see hasExplicitTargetFlag's doc comment).
|
|
400
1007
|
if (namespace === "install") {
|
|
1008
|
+
if (
|
|
1009
|
+
usedRegistryFallback &&
|
|
1010
|
+
!hasAttachVaultFlag(argv) &&
|
|
1011
|
+
!hasExplicitTargetFlag(argv)
|
|
1012
|
+
) {
|
|
1013
|
+
process.stderr.write(
|
|
1014
|
+
registryFallbackWarningLine(process.cwd(), vaultRoot),
|
|
1015
|
+
);
|
|
1016
|
+
const isDryRun = argv.includes("--dry-run");
|
|
1017
|
+
const isRegisterVault = argv.includes("--register-vault");
|
|
1018
|
+
if (isRegisterVault && !isDryRun) {
|
|
1019
|
+
process.stderr.write(registryFallbackRefusalLine());
|
|
1020
|
+
process.exit(1);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
401
1023
|
dispatchSpexConsoleEntry(
|
|
402
1024
|
"hs-install.ts",
|
|
403
1025
|
argv,
|
|
404
|
-
|
|
1026
|
+
// In a committed-marker satellite the install target is the CODE repo
|
|
1027
|
+
// (MCP entry, hooks, commands), never the vault clone the marker names.
|
|
1028
|
+
markerSatelliteDirFrom(INVOKED_CWD) ?? (vaultRoot || process.cwd()),
|
|
405
1029
|
{
|
|
406
1030
|
HS_PKG_ROOT: PKG_ROOT,
|
|
407
1031
|
},
|
|
@@ -426,24 +1050,28 @@ function main(argv) {
|
|
|
426
1050
|
if (namespace === "invite") {
|
|
427
1051
|
if (!vaultRoot) {
|
|
428
1052
|
process.stderr.write(
|
|
429
|
-
"hs: no vault detected — could not find a
|
|
430
|
-
"walking up from the current directory. " +
|
|
1053
|
+
"hs: no vault detected — could not find a heyspex.yaml marker or a " +
|
|
1054
|
+
"specs/ folder walking up from the current directory. " +
|
|
431
1055
|
"Set HS_VAULT_ROOT or run from inside a vault.\n",
|
|
432
1056
|
);
|
|
433
1057
|
process.exit(1);
|
|
434
1058
|
}
|
|
435
|
-
dispatchSpexConsoleEntry(
|
|
1059
|
+
dispatchSpexConsoleEntry(
|
|
1060
|
+
"hs-env.ts",
|
|
1061
|
+
["env", "invite", ...argv.slice(1)],
|
|
1062
|
+
vaultRoot,
|
|
1063
|
+
);
|
|
436
1064
|
return;
|
|
437
1065
|
}
|
|
438
1066
|
|
|
439
1067
|
// `hs setup claude|cursor` — install agent lifecycle hooks into the vault root.
|
|
440
1068
|
if (namespace === "setup") {
|
|
441
1069
|
const agent = command?.toLowerCase();
|
|
442
|
-
const
|
|
443
|
-
claude: "
|
|
444
|
-
cursor: "
|
|
1070
|
+
const SPAWN_KEYS = {
|
|
1071
|
+
claude: "setup:claude",
|
|
1072
|
+
cursor: "setup:cursor",
|
|
445
1073
|
};
|
|
446
|
-
if (!agent || !Object.hasOwn(
|
|
1074
|
+
if (!agent || !Object.hasOwn(SPAWN_KEYS, agent)) {
|
|
447
1075
|
process.stderr.write(
|
|
448
1076
|
`hs setup: expected \`claude\` or \`cursor\` (got \`${command ?? ""}\`)\n` +
|
|
449
1077
|
" hs setup claude [--check|--uninstall] Claude Code lifecycle hooks\n" +
|
|
@@ -454,7 +1082,7 @@ function main(argv) {
|
|
|
454
1082
|
const setupFlags = rest.filter(
|
|
455
1083
|
(a) => a === "--check" || a === "--uninstall",
|
|
456
1084
|
);
|
|
457
|
-
const script =
|
|
1085
|
+
const script = spawnTargetPath(SPAWN_KEYS[agent]);
|
|
458
1086
|
const target = vaultRoot || process.cwd();
|
|
459
1087
|
const child = spawn(process.execPath, [script, ...setupFlags], {
|
|
460
1088
|
cwd: target,
|
|
@@ -464,7 +1092,7 @@ function main(argv) {
|
|
|
464
1092
|
child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
|
|
465
1093
|
child.on("error", (err) => {
|
|
466
1094
|
process.stderr.write(
|
|
467
|
-
`hs setup: failed to run ${
|
|
1095
|
+
`hs setup: failed to run ${SPAWN_TARGETS[SPAWN_KEYS[agent]].path}: ${err.message}\n`,
|
|
468
1096
|
);
|
|
469
1097
|
process.exit(1);
|
|
470
1098
|
});
|
|
@@ -472,17 +1100,44 @@ function main(argv) {
|
|
|
472
1100
|
}
|
|
473
1101
|
|
|
474
1102
|
// `hs skills sync|check` — propagate .commandcode/skills → Cursor/Codex/Claude shims.
|
|
1103
|
+
// `hs skills add|remove|list` — a DIFFERENT verb: the extension seam's skill
|
|
1104
|
+
// registry ([[HS-8N3K-NYPR-TASK-add-cli-verbs-so-a-developer-can]],
|
|
1105
|
+
// [[HS-8EDJ-NYPR-EPIC-extension-seam-for-other-developers]]), routed to its
|
|
1106
|
+
// own entrypoint below so it never touches the sync/check spawn path.
|
|
475
1107
|
if (namespace === "skills") {
|
|
476
1108
|
const action = command?.toLowerCase();
|
|
1109
|
+
if (action === "add" || action === "remove" || action === "list") {
|
|
1110
|
+
dispatchSpexConsoleEntry(
|
|
1111
|
+
"hs-skills.ts",
|
|
1112
|
+
argv,
|
|
1113
|
+
vaultRoot || process.cwd(),
|
|
1114
|
+
);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
477
1117
|
if (action !== "sync" && action !== "check") {
|
|
478
1118
|
process.stderr.write(
|
|
479
|
-
`hs skills: expected \`sync\` or \`
|
|
480
|
-
" hs skills sync
|
|
481
|
-
" hs skills check
|
|
1119
|
+
`hs skills: expected \`sync\`, \`check\`, \`add\`, \`remove\`, or \`list\` (got \`${command ?? ""}\`)\n` +
|
|
1120
|
+
" hs skills sync Copy skills + slash shims to all agent harnesses\n" +
|
|
1121
|
+
" hs skills check Exit 1 if any harness copy is stale\n" +
|
|
1122
|
+
" hs skills add <module-path> --name <n> --trigger </slash> Register a skill (extension seam)\n" +
|
|
1123
|
+
" hs skills remove <name> Remove a customer-registered skill\n" +
|
|
1124
|
+
" hs skills list [--json] List registered skills\n",
|
|
482
1125
|
);
|
|
483
1126
|
process.exit(2);
|
|
484
1127
|
}
|
|
485
|
-
const script =
|
|
1128
|
+
const script = spawnTargetPath("skills:sync");
|
|
1129
|
+
if (!existsSync(script)) {
|
|
1130
|
+
// `skills:sync` is a deliberately dev-only SPAWN_TARGETS entry — see its
|
|
1131
|
+
// `reason` above. A published `npm install heyspex` never has
|
|
1132
|
+
// `.commandcode/skills` (the platform's own SSOT content) on disk, so
|
|
1133
|
+
// report that plainly instead of a bare MODULE_NOT_FOUND.
|
|
1134
|
+
process.stderr.write(
|
|
1135
|
+
"hs skills: not available in a published `heyspex` install — this verb " +
|
|
1136
|
+
"propagates the HeySpex platform repo's OWN `.commandcode/skills` " +
|
|
1137
|
+
"source content, which only exists inside a clone of that repo.\n",
|
|
1138
|
+
);
|
|
1139
|
+
process.exit(1);
|
|
1140
|
+
}
|
|
486
1141
|
const target = vaultRoot || PKG_ROOT;
|
|
487
1142
|
const childArgs = action === "check" ? ["--check"] : [];
|
|
488
1143
|
const child = spawn(process.execPath, [script, ...childArgs], {
|
|
@@ -532,18 +1187,22 @@ function main(argv) {
|
|
|
532
1187
|
// auth verb, so it bypasses the hard vault gate below like init/install —
|
|
533
1188
|
// signing in is exactly what you do BEFORE you have a vault here.
|
|
534
1189
|
if (namespace === "login") {
|
|
535
|
-
const cliPath =
|
|
1190
|
+
const cliPath = spawnTargetPath("heyspex-mcp-cli");
|
|
536
1191
|
const loginRoot = vaultRoot || process.cwd();
|
|
537
|
-
const child = spawn(
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
1192
|
+
const child = spawn(
|
|
1193
|
+
process.execPath,
|
|
1194
|
+
[cliPath, "login", ...argv.slice(1)],
|
|
1195
|
+
{
|
|
1196
|
+
cwd: PKG_ROOT,
|
|
1197
|
+
stdio: "inherit",
|
|
1198
|
+
env: {
|
|
1199
|
+
...process.env,
|
|
1200
|
+
HEYSPEX_VAULT_ROOT: loginRoot,
|
|
1201
|
+
HS_VAULT_ROOT: loginRoot,
|
|
1202
|
+
HEYSPEX_DISABLE_STDIO_BOOTSTRAP: "1",
|
|
1203
|
+
},
|
|
545
1204
|
},
|
|
546
|
-
|
|
1205
|
+
);
|
|
547
1206
|
child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
|
|
548
1207
|
child.on("error", (err) => {
|
|
549
1208
|
process.stderr.write(`hs: failed to run login: ${err.message}\n`);
|
|
@@ -552,15 +1211,53 @@ function main(argv) {
|
|
|
552
1211
|
return;
|
|
553
1212
|
}
|
|
554
1213
|
|
|
1214
|
+
// `hs whoami` — which account this machine's credential belongs to (email,
|
|
1215
|
+
// account id, console), asked of GET /api/mcp/session. The account a repo
|
|
1216
|
+
// would be attached to by `hs connect` / `hs install`
|
|
1217
|
+
// ([[HS-HAQI-KODY-DIAG-hs-install-and-hs-connect-bind-a-new]]). Auth verb:
|
|
1218
|
+
// bypasses the vault gate like `hs login`.
|
|
1219
|
+
// `hs logout [--account <id|email>]` — remove one stored account's token
|
|
1220
|
+
// ([[HS-UA9S-WW9R-TASK-store-credentials-per-account-login]]). Auth verb:
|
|
1221
|
+
// bypasses the vault gate like `hs login` / `hs whoami`.
|
|
1222
|
+
if (namespace === "whoami" || namespace === "logout") {
|
|
1223
|
+
const cliPath = spawnTargetPath("heyspex-mcp-cli");
|
|
1224
|
+
const whoamiRoot = vaultRoot || process.cwd();
|
|
1225
|
+
const child = spawn(
|
|
1226
|
+
process.execPath,
|
|
1227
|
+
[cliPath, namespace, ...argv.slice(1)],
|
|
1228
|
+
{
|
|
1229
|
+
cwd: PKG_ROOT,
|
|
1230
|
+
stdio: "inherit",
|
|
1231
|
+
env: {
|
|
1232
|
+
...process.env,
|
|
1233
|
+
HEYSPEX_VAULT_ROOT: whoamiRoot,
|
|
1234
|
+
HS_VAULT_ROOT: whoamiRoot,
|
|
1235
|
+
HEYSPEX_DISABLE_STDIO_BOOTSTRAP: "1",
|
|
1236
|
+
},
|
|
1237
|
+
},
|
|
1238
|
+
);
|
|
1239
|
+
child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
|
|
1240
|
+
child.on("error", (err) => {
|
|
1241
|
+
process.stderr.write(`hs: failed to run ${namespace}: ${err.message}\n`);
|
|
1242
|
+
process.exit(1);
|
|
1243
|
+
});
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
555
1247
|
// `hs connect --check [--json]` — the read-only connect-checks report
|
|
556
1248
|
// ([[HS-I2OO-EKLJ-TASK-shared-connect-checks-module-with-a]] on
|
|
557
1249
|
// [[HS-QYH8-EKLJ-EPIC-hs-connect-one-command-repo-connect-verb]]). May run
|
|
558
1250
|
// in an unconnected repo (no specs/ yet — that is the point), so it
|
|
559
1251
|
// bypasses the hard vault gate below like init/install/vault/workspace.
|
|
560
1252
|
if (namespace === "connect") {
|
|
561
|
-
dispatchSpexConsoleEntry(
|
|
562
|
-
|
|
563
|
-
|
|
1253
|
+
dispatchSpexConsoleEntry(
|
|
1254
|
+
"hs-connect.ts",
|
|
1255
|
+
argv,
|
|
1256
|
+
vaultRoot || process.cwd(),
|
|
1257
|
+
{
|
|
1258
|
+
HS_INVOKED_CWD: INVOKED_CWD,
|
|
1259
|
+
},
|
|
1260
|
+
);
|
|
564
1261
|
return;
|
|
565
1262
|
}
|
|
566
1263
|
|
|
@@ -572,7 +1269,7 @@ function main(argv) {
|
|
|
572
1269
|
// the very first thing a stranger runs after `npm install heyspex` in an
|
|
573
1270
|
// empty project is exactly this, from a directory with no `specs/` yet.
|
|
574
1271
|
if (namespace === "doctor") {
|
|
575
|
-
const script =
|
|
1272
|
+
const script = spawnTargetPath("doctor");
|
|
576
1273
|
if (!existsSync(script)) {
|
|
577
1274
|
// Published-package install (per HS-ZJSP-0JNK): the full doctor audits
|
|
578
1275
|
// dozens of source-checkout-only surfaces (git hooks, Cursor wiring,
|
|
@@ -580,7 +1277,7 @@ function main(argv) {
|
|
|
580
1277
|
// it is deliberately not in the package's `files`. Run the small
|
|
581
1278
|
// subset that DOES apply to a published install instead of crashing
|
|
582
1279
|
// with MODULE_NOT_FOUND.
|
|
583
|
-
runStandaloneDoctor(argv.slice(1));
|
|
1280
|
+
runStandaloneDoctor(argv.slice(1), vaultRoot);
|
|
584
1281
|
return;
|
|
585
1282
|
}
|
|
586
1283
|
const child = spawn(process.execPath, [script, ...argv.slice(1)], {
|
|
@@ -598,15 +1295,40 @@ function main(argv) {
|
|
|
598
1295
|
|
|
599
1296
|
if (!vaultRoot) {
|
|
600
1297
|
process.stderr.write(
|
|
601
|
-
"hs: no vault detected — could not find a
|
|
602
|
-
"walking up from the current directory. " +
|
|
1298
|
+
"hs: no vault detected — could not find a heyspex.yaml marker or a " +
|
|
1299
|
+
"specs/ folder walking up from the current directory. " +
|
|
603
1300
|
"Set HS_VAULT_ROOT or run from inside a vault.\n",
|
|
604
1301
|
);
|
|
605
1302
|
process.exit(1);
|
|
606
1303
|
}
|
|
607
1304
|
|
|
1305
|
+
// A committed satellite marker whose vault clone is NOT on this machine
|
|
1306
|
+
// resolves to the code repo itself (cloud-only binding). Verbs that write the
|
|
1307
|
+
// vault's files to disk must never scaffold specs/ into a code repo.
|
|
1308
|
+
if (
|
|
1309
|
+
["pull", "push", "index"].includes(namespace) &&
|
|
1310
|
+
readMarkerSatellite(vaultRoot) &&
|
|
1311
|
+
!existsSync(path.join(vaultRoot, "specs"))
|
|
1312
|
+
) {
|
|
1313
|
+
process.stderr.write(unclonedVaultRefusalLine(vaultRoot));
|
|
1314
|
+
process.exit(2);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
608
1317
|
if (namespace === "index" && command === "rebuild") {
|
|
609
|
-
dispatchSpexConsoleEntry("hs-index-rebuild.ts", rest, vaultRoot
|
|
1318
|
+
dispatchSpexConsoleEntry("hs-index-rebuild.ts", rest, vaultRoot, {
|
|
1319
|
+
...(vaultIdOverride ? { HS_VAULT_ID_OVERRIDE: vaultIdOverride } : {}),
|
|
1320
|
+
});
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// `hs index reconcile` — per-spec newest-wins between the disk vault and the
|
|
1325
|
+
// cloud index ([[HS-MS75]]). Read-only unless `--apply`; status moves go
|
|
1326
|
+
// through the status route (refusals are listed), content through the
|
|
1327
|
+
// phased reindex route with preserve_status; never deletes, never finalizes.
|
|
1328
|
+
if (namespace === "index" && command === "reconcile") {
|
|
1329
|
+
dispatchSpexConsoleEntry("hs-index-reconcile.ts", rest, vaultRoot, {
|
|
1330
|
+
...(vaultIdOverride ? { HS_VAULT_ID_OVERRIDE: vaultIdOverride } : {}),
|
|
1331
|
+
});
|
|
610
1332
|
return;
|
|
611
1333
|
}
|
|
612
1334
|
|
|
@@ -617,7 +1339,34 @@ function main(argv) {
|
|
|
617
1339
|
// pushes, never deletes, and only REPORTS unless `--apply` is given.
|
|
618
1340
|
// Forwards the full argv; the entrypoint drops the leading `pull` token.
|
|
619
1341
|
if (namespace === "pull") {
|
|
620
|
-
dispatchSpexConsoleEntry("hs-vault-pull.ts", argv, vaultRoot
|
|
1342
|
+
dispatchSpexConsoleEntry("hs-vault-pull.ts", argv, vaultRoot, {
|
|
1343
|
+
...(vaultIdOverride ? { HS_VAULT_ID_OVERRIDE: vaultIdOverride } : {}),
|
|
1344
|
+
});
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// `hs import <source-dir>` — a doc tree becomes GROUNDED spec-import captures
|
|
1349
|
+
// in the cwd's vault (heyspex.yaml marker, never the account pointer): group-
|
|
1350
|
+
// intent pass, foundational-to-specific order, sequential Ground=Expand with
|
|
1351
|
+
// a running batch ledger, one intent-map doc, source links; never promotes
|
|
1352
|
+
// ([[HS-0GOV-43G9-CAP-the-spec-import-function-should-not-stop]]). Forwards
|
|
1353
|
+
// the full argv; the entrypoint drops the leading `import` token and resolves
|
|
1354
|
+
// a relative <source-dir> against HS_INVOKED_CWD.
|
|
1355
|
+
if (namespace === "import") {
|
|
1356
|
+
dispatchSpexConsoleEntry("hs-import.ts", argv, vaultRoot, {
|
|
1357
|
+
HS_INVOKED_CWD: INVOKED_CWD,
|
|
1358
|
+
...(vaultIdOverride ? { HS_VAULT_ID_OVERRIDE: vaultIdOverride } : {}),
|
|
1359
|
+
});
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// `hs push` — disk -> cloud (SpexRelay local leg, HS-FCNH). Report-only
|
|
1364
|
+
// unless --apply; never creates specs, never pushes status, refuses over a
|
|
1365
|
+
// newer cloud copy. Forwards the full argv; the entrypoint drops `push`.
|
|
1366
|
+
if (namespace === "push") {
|
|
1367
|
+
dispatchSpexConsoleEntry("hs-vault-push.ts", argv, vaultRoot, {
|
|
1368
|
+
...(vaultIdOverride ? { HS_VAULT_ID_OVERRIDE: vaultIdOverride } : {}),
|
|
1369
|
+
});
|
|
621
1370
|
return;
|
|
622
1371
|
}
|
|
623
1372
|
|
|
@@ -629,6 +1378,14 @@ function main(argv) {
|
|
|
629
1378
|
return;
|
|
630
1379
|
}
|
|
631
1380
|
|
|
1381
|
+
// NOTE: `hs source` is handled further down by the plain-Node
|
|
1382
|
+
// scripts/lib/hs-source-*.mjs cores ([[HS-Q0K8-CWPX-EPIC-spexsource-local-git-preflight-commit]]),
|
|
1383
|
+
// which ship inside the published package. An earlier TypeScript build of the
|
|
1384
|
+
// same verb ([[HS-KN93-V4RB-EPIC-local-git-preflight-commit-and-push-for]],
|
|
1385
|
+
// spex-console/bin/hs-source.ts + src/lib/source-local/) used to be dispatched
|
|
1386
|
+
// from this spot; it was superseded when the two branches merged and must not
|
|
1387
|
+
// be re-wired here, or the package-shipping handler below becomes dead code.
|
|
1388
|
+
|
|
632
1389
|
if (namespace === "factory" && command === "run") {
|
|
633
1390
|
dispatchSpexConsoleEntry("hs-factory-run.ts", argv, vaultRoot);
|
|
634
1391
|
return;
|
|
@@ -659,9 +1416,8 @@ function main(argv) {
|
|
|
659
1416
|
// server-impacting task; stop from any terminal (not only the one that
|
|
660
1417
|
// started it). See [[dev-server-lifecycle-and-db-safety]].
|
|
661
1418
|
if (namespace === "server" && (command === "status" || command === "stop")) {
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
const script = path.join(PKG_ROOT, "scripts", scriptName);
|
|
1419
|
+
const spawnKey = command === "stop" ? "server:stop" : "server:status";
|
|
1420
|
+
const script = spawnTargetPath(spawnKey);
|
|
665
1421
|
const child = spawn(process.execPath, [script, ...rest], {
|
|
666
1422
|
cwd: PKG_ROOT,
|
|
667
1423
|
stdio: "inherit",
|
|
@@ -670,7 +1426,55 @@ function main(argv) {
|
|
|
670
1426
|
child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
|
|
671
1427
|
child.on("error", (err) => {
|
|
672
1428
|
process.stderr.write(
|
|
673
|
-
`hs: failed to run ${
|
|
1429
|
+
`hs: failed to run ${SPAWN_TARGETS[spawnKey].path}: ${err.message}\n`,
|
|
1430
|
+
);
|
|
1431
|
+
process.exit(1);
|
|
1432
|
+
});
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// `hs wip status` / `hs wip restore <id>` — health report + restore for the
|
|
1437
|
+
// WIP hand-off ledger (`.heyspex/wip-state.json`) and its archive
|
|
1438
|
+
// (`.heyspex/wip-archive/*.jsonl`). Pure Node core under scripts/ (same
|
|
1439
|
+
// pattern as `hs server status/stop` above). See
|
|
1440
|
+
// [[HS-B8OK-PFMN]] / capture [[HS-817Y-83XQ]] ("the WIP ledger maintains
|
|
1441
|
+
// itself"). Forwards `command` plus the rest of argv; the script itself
|
|
1442
|
+
// decides status vs restore off that first token.
|
|
1443
|
+
if (namespace === "wip" && (command === "status" || command === "restore")) {
|
|
1444
|
+
const script = spawnTargetPath("wip");
|
|
1445
|
+
const child = spawn(process.execPath, [script, command, ...rest], {
|
|
1446
|
+
cwd: PKG_ROOT,
|
|
1447
|
+
stdio: "inherit",
|
|
1448
|
+
env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
|
|
1449
|
+
});
|
|
1450
|
+
child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
|
|
1451
|
+
child.on("error", (err) => {
|
|
1452
|
+
process.stderr.write(
|
|
1453
|
+
`hs: failed to run wip-ledger-health.mjs: ${err.message}\n`,
|
|
1454
|
+
);
|
|
1455
|
+
process.exit(1);
|
|
1456
|
+
});
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// `hs worktree list|alloc|release|reap|rebind` — session/build worktree
|
|
1461
|
+
// registry (HS-03DO / HS-V9XH). Pure Node core under scripts/, itself a
|
|
1462
|
+
// thin shell over dev-factory/lib/worktree-allocator.mjs (called in-process,
|
|
1463
|
+
// never shelled out to). Same dispatch shape as `hs server status/stop` and
|
|
1464
|
+
// `hs wip status/restore` above. Forwards the full sub-argv; the entrypoint
|
|
1465
|
+
// drops the leading `worktree` token itself.
|
|
1466
|
+
if (namespace === "worktree") {
|
|
1467
|
+
const script = spawnTargetPath("worktree");
|
|
1468
|
+
const worktreeArgs = command ? [command, ...rest] : rest;
|
|
1469
|
+
const child = spawn(process.execPath, [script, ...worktreeArgs], {
|
|
1470
|
+
cwd: PKG_ROOT,
|
|
1471
|
+
stdio: "inherit",
|
|
1472
|
+
env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
|
|
1473
|
+
});
|
|
1474
|
+
child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
|
|
1475
|
+
child.on("error", (err) => {
|
|
1476
|
+
process.stderr.write(
|
|
1477
|
+
`hs: failed to run hs-worktree.mjs: ${err.message}\n`,
|
|
674
1478
|
);
|
|
675
1479
|
process.exit(1);
|
|
676
1480
|
});
|
|
@@ -695,27 +1499,41 @@ function main(argv) {
|
|
|
695
1499
|
" hs source status Read-only tree status + path-scoped stage plan\n" +
|
|
696
1500
|
" hs source preflight Alias of status\n" +
|
|
697
1501
|
" hs source commit Stage + commit per the preflight plan\n" +
|
|
698
|
-
" hs source push
|
|
1502
|
+
" hs source push [branch] Push the committed branch (no branch: the shared session-branch resolver's target)\n" +
|
|
1503
|
+
" hs source branch declare [<branch>] | show\n" +
|
|
1504
|
+
" Declare the branch this session works on (cloud spec saves then land on it) / show the target\n" +
|
|
699
1505
|
" hs source log <spec-id> Commit-history lookup for a spec (console settlement,\n" +
|
|
700
|
-
" falling back to local git log)\n"
|
|
1506
|
+
" falling back to local git log)\n" +
|
|
1507
|
+
" hs source identity [show] [--json]\n" +
|
|
1508
|
+
" Effective git user.name/user.email, where each comes from,\n" +
|
|
1509
|
+
" and whether a repo-local override is authorized (read-only)\n" +
|
|
1510
|
+
" hs source identity set --email <email> [--name <name>] [--global] [--human]\n" +
|
|
1511
|
+
" HUMAN-ONLY: write the identity and record the authorization\n" +
|
|
1512
|
+
" in .heyspex/git-identity.json (agents are refused, exit 3)\n" +
|
|
1513
|
+
" hs source identity clear [--human]\n" +
|
|
1514
|
+
" HUMAN-ONLY: remove the repo-local [user] override + authorization\n" +
|
|
1515
|
+
" Type set/clear yourself — terminal: hs source identity set --email you@example.com\n" +
|
|
1516
|
+
" inside Claude Code: !hs source identity set --email you@example.com --human\n",
|
|
701
1517
|
);
|
|
702
1518
|
process.exit(0);
|
|
703
1519
|
}
|
|
704
|
-
const
|
|
705
|
-
status: "
|
|
706
|
-
preflight: "
|
|
707
|
-
commit: "
|
|
708
|
-
push: "
|
|
709
|
-
|
|
1520
|
+
const SOURCE_SPAWN_KEYS = {
|
|
1521
|
+
status: "source:status",
|
|
1522
|
+
preflight: "source:preflight",
|
|
1523
|
+
commit: "source:commit",
|
|
1524
|
+
push: "source:push",
|
|
1525
|
+
branch: "source:branch",
|
|
1526
|
+
log: "source:log",
|
|
1527
|
+
identity: "source:identity",
|
|
710
1528
|
};
|
|
711
|
-
const
|
|
712
|
-
if (!
|
|
1529
|
+
const spawnKey = SOURCE_SPAWN_KEYS[command];
|
|
1530
|
+
if (!spawnKey) {
|
|
713
1531
|
process.stderr.write(
|
|
714
|
-
`hs: unknown \`hs source\` verb \`${command ?? ""}\` — expected one of: ${Object.keys(
|
|
1532
|
+
`hs: unknown \`hs source\` verb \`${command ?? ""}\` — expected one of: ${Object.keys(SOURCE_SPAWN_KEYS).join(", ")}\n`,
|
|
715
1533
|
);
|
|
716
1534
|
process.exit(2);
|
|
717
1535
|
}
|
|
718
|
-
const script =
|
|
1536
|
+
const script = spawnTargetPath(spawnKey);
|
|
719
1537
|
const child = spawn(process.execPath, [script, command, ...rest], {
|
|
720
1538
|
cwd: PKG_ROOT,
|
|
721
1539
|
stdio: "inherit",
|
|
@@ -723,7 +1541,9 @@ function main(argv) {
|
|
|
723
1541
|
});
|
|
724
1542
|
child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
|
|
725
1543
|
child.on("error", (err) => {
|
|
726
|
-
process.stderr.write(
|
|
1544
|
+
process.stderr.write(
|
|
1545
|
+
`hs: failed to run ${SPAWN_TARGETS[spawnKey].path}: ${err.message}\n`,
|
|
1546
|
+
);
|
|
727
1547
|
process.exit(1);
|
|
728
1548
|
});
|
|
729
1549
|
return;
|
|
@@ -734,7 +1554,7 @@ function main(argv) {
|
|
|
734
1554
|
// [[HS-RU2R-I87U-RES-solo-surface-cli-launcher-single]]). Probes ground-truth
|
|
735
1555
|
// server status first; a down console prints a clear message, never a blank window.
|
|
736
1556
|
if (namespace === "view") {
|
|
737
|
-
const script =
|
|
1557
|
+
const script = spawnTargetPath("view");
|
|
738
1558
|
const child = spawn(process.execPath, [script, ...argv.slice(1)], {
|
|
739
1559
|
cwd: PKG_ROOT,
|
|
740
1560
|
stdio: "inherit",
|
|
@@ -779,7 +1599,7 @@ function main(argv) {
|
|
|
779
1599
|
// group (the multi-capture "add another?" gesture). Pure Node, posts the
|
|
780
1600
|
// same heyspex.capture/0.1 envelope the companion sends to /api/drafts.
|
|
781
1601
|
if (namespace === "capture") {
|
|
782
|
-
const script =
|
|
1602
|
+
const script = spawnTargetPath("capture");
|
|
783
1603
|
const captureArgs = command ? [command, ...rest] : rest;
|
|
784
1604
|
const child = spawn(process.execPath, [script, ...captureArgs], {
|
|
785
1605
|
cwd: PKG_ROOT,
|
|
@@ -795,12 +1615,17 @@ function main(argv) {
|
|
|
795
1615
|
}
|
|
796
1616
|
|
|
797
1617
|
// `hs companion start|stop|status|build` — companion tray host (companion/src/HeySpex.Companion.Host).
|
|
1618
|
+
// `hs companion settings ...` and the shortcuts (mute|volume|orb|wake|...) — every
|
|
1619
|
+
// Companion setting via the console's /api/companion/settings ([[HS-UDH1]]); the
|
|
1620
|
+
// script resolves the console through the shared resolver, which needs the
|
|
1621
|
+
// vault root for the `.heyspex/console-url` discovery file (same as `capture`).
|
|
798
1622
|
if (namespace === "companion") {
|
|
799
|
-
const script =
|
|
1623
|
+
const script = spawnTargetPath("companion");
|
|
800
1624
|
const companionArgs = command ? [command, ...rest] : ["start", ...rest];
|
|
801
1625
|
const child = spawn(process.execPath, [script, ...companionArgs], {
|
|
802
1626
|
cwd: PKG_ROOT,
|
|
803
1627
|
stdio: "inherit",
|
|
1628
|
+
env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
|
|
804
1629
|
});
|
|
805
1630
|
child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
|
|
806
1631
|
child.on("error", (err) => {
|
|
@@ -825,12 +1650,28 @@ function main(argv) {
|
|
|
825
1650
|
|
|
826
1651
|
// `hs next [spec_id] [--summary] [--json]` — resolve the on-deck brief without
|
|
827
1652
|
// MCP ([[HS-TASK-ISK7]] / [[tool-agnostic-mcp-first-cli-second]]).
|
|
1653
|
+
// `hs next` has TWO halves and this branch picks one.
|
|
1654
|
+
//
|
|
1655
|
+
// - `hs next [spec_id] [--summary] [--json]` resolves the on-deck brief
|
|
1656
|
+
// with no console and no MCP (`scripts/hs-next.mjs`).
|
|
1657
|
+
// - `hs next <id> --launch`, and the `model` / `permission-mode` / `claim`
|
|
1658
|
+
// subcommands, need the console (POST /api/agent/start, PUT
|
|
1659
|
+
// /api/settings) and run through `spex-console/bin/hs-next-launch.ts` -
|
|
1660
|
+
// the same shape `hs handoff` uses.
|
|
828
1661
|
if (namespace === "next") {
|
|
829
|
-
|
|
1662
|
+
if (isNextConsoleArgv(argv.slice(1))) {
|
|
1663
|
+
dispatchSpexConsoleEntry("hs-next-launch.ts", argv, vaultRoot);
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
const script = spawnTargetPath("next");
|
|
830
1667
|
const child = spawn(process.execPath, [script, ...argv.slice(1)], {
|
|
831
1668
|
cwd: PKG_ROOT,
|
|
832
1669
|
stdio: "inherit",
|
|
833
|
-
|
|
1670
|
+
// hs-next.mjs loads vendor/heyspex-mcp/cli.mjs, which is CODE, not vault
|
|
1671
|
+
// content — since the repo split, HS_VAULT_ROOT can point at a checkout
|
|
1672
|
+
// (e.g. heyspex-specs) with no vendor/ at all
|
|
1673
|
+
// ([[HS-1LCC-4XKS-DIAG-frozen-checks-run-against-the-vault]]).
|
|
1674
|
+
env: { ...process.env, HS_VAULT_ROOT: vaultRoot, HS_PKG_ROOT: PKG_ROOT },
|
|
834
1675
|
});
|
|
835
1676
|
child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
|
|
836
1677
|
child.on("error", (err) => {
|
|
@@ -845,7 +1686,7 @@ function main(argv) {
|
|
|
845
1686
|
// Turning it off makes this repo behave like a plain codebase in Claude Code.
|
|
846
1687
|
// Core: scripts/heyspex-mode.mjs · docs/claude-code-setup.md.
|
|
847
1688
|
if (namespace === "mode") {
|
|
848
|
-
const script =
|
|
1689
|
+
const script = spawnTargetPath("mode");
|
|
849
1690
|
const child = spawn(process.execPath, [script, ...argv.slice(1)], {
|
|
850
1691
|
cwd: PKG_ROOT,
|
|
851
1692
|
stdio: "inherit",
|
|
@@ -864,7 +1705,7 @@ function main(argv) {
|
|
|
864
1705
|
// Registration subcommands print a blocked-on-HS-DCCP notice until the
|
|
865
1706
|
// connector's server-registration table exists.
|
|
866
1707
|
if (namespace === "gateway") {
|
|
867
|
-
const script =
|
|
1708
|
+
const script = spawnTargetPath("gateway");
|
|
868
1709
|
const child = spawn(process.execPath, [script, ...argv.slice(1)], {
|
|
869
1710
|
cwd: PKG_ROOT,
|
|
870
1711
|
stdio: "inherit",
|
|
@@ -882,7 +1723,7 @@ function main(argv) {
|
|
|
882
1723
|
// .heyspex/config.yaml (settings capability; core: vendor/heyspex-mcp/
|
|
883
1724
|
// config-settings.mjs; write-permission rules: agent-settings-protocol).
|
|
884
1725
|
if (namespace === "settings") {
|
|
885
|
-
const script =
|
|
1726
|
+
const script = spawnTargetPath("settings");
|
|
886
1727
|
const child = spawn(process.execPath, [script, ...argv], {
|
|
887
1728
|
cwd: PKG_ROOT,
|
|
888
1729
|
stdio: "inherit",
|
|
@@ -899,7 +1740,7 @@ function main(argv) {
|
|
|
899
1740
|
// `hs classifier stats` — read-only view of HeySpex Intent Classifier decisions
|
|
900
1741
|
// (.heyspex/intent-classifier-history.jsonl). Pure local read.
|
|
901
1742
|
if (namespace === "classifier") {
|
|
902
|
-
const script =
|
|
1743
|
+
const script = spawnTargetPath("classifier");
|
|
903
1744
|
const child = spawn(process.execPath, [script, ...argv], {
|
|
904
1745
|
cwd: PKG_ROOT,
|
|
905
1746
|
stdio: "inherit",
|
|
@@ -915,7 +1756,7 @@ function main(argv) {
|
|
|
915
1756
|
|
|
916
1757
|
// `hs auto [filename] [--json]` — resolve /hs-auto COO dispatch plan.
|
|
917
1758
|
if (namespace === "auto") {
|
|
918
|
-
const script =
|
|
1759
|
+
const script = spawnTargetPath("auto");
|
|
919
1760
|
const child = spawn(process.execPath, [script, ...argv.slice(1)], {
|
|
920
1761
|
cwd: PKG_ROOT,
|
|
921
1762
|
stdio: "inherit",
|
|
@@ -936,30 +1777,45 @@ function main(argv) {
|
|
|
936
1777
|
// code for a persistent `hsmcp_` bearer, stored in ~/.heyspex/credentials.yaml
|
|
937
1778
|
// ([[HS-GD45-MCPC-TASK-mcp-cloud-auth-shared-console-resolver]]).
|
|
938
1779
|
if (namespace === "mcp") {
|
|
939
|
-
if (
|
|
1780
|
+
if (
|
|
1781
|
+
command !== "call" &&
|
|
1782
|
+
command !== "pair" &&
|
|
1783
|
+
command !== "login" &&
|
|
1784
|
+
command !== "install"
|
|
1785
|
+
) {
|
|
940
1786
|
process.stderr.write(
|
|
941
|
-
`hs: unknown mcp subcommand \`${command ?? ""}\` (expected: call | pair | login)\n` +
|
|
1787
|
+
`hs: unknown mcp subcommand \`${command ?? ""}\` (expected: call | pair | login | install)\n` +
|
|
942
1788
|
" hs mcp call <tool> [--arg key=value ...] [--args '<json>'] [--args-file <path>] [--json]\n" +
|
|
943
1789
|
" hs mcp pair <code> [--console-url <url>] [--label <text>] [--json]\n" +
|
|
944
|
-
" hs mcp login [--console-url <url>] [--label <text>] [--json] (alias of `hs login`)\n"
|
|
1790
|
+
" hs mcp login [--console-url <url>] [--label <text>] [--json] (alias of `hs login`)\n" +
|
|
1791
|
+
" hs mcp install --remote [--console-url <url>] [--write] [--json] Wire a client to the cloud endpoint\n",
|
|
945
1792
|
);
|
|
946
1793
|
process.exit(2);
|
|
947
1794
|
}
|
|
948
|
-
const cliPath =
|
|
1795
|
+
const cliPath = spawnTargetPath("heyspex-mcp-cli");
|
|
1796
|
+
// The MCP engine resolves its vault by walking UP from ITS cwd. Launching
|
|
1797
|
+
// it with cwd = the HeySpex checkout made every `hs mcp call` read the
|
|
1798
|
+
// HeySpex vault no matter where the agent stood (found 2026-10-16 from
|
|
1799
|
+
// mlc-specs / mlc-app). Run it from the repo the caller is in; an explicit
|
|
1800
|
+
// HS_VAULT_ROOT keeps winning (the engine is started inside that vault).
|
|
1801
|
+
const mcpCwd = process.env.HS_VAULT_ROOT && vaultRoot ? vaultRoot : INVOKED_CWD;
|
|
949
1802
|
const child = spawn(process.execPath, [cliPath, command, ...rest], {
|
|
950
|
-
cwd:
|
|
1803
|
+
cwd: mcpCwd,
|
|
951
1804
|
stdio: "inherit",
|
|
952
1805
|
env: {
|
|
953
1806
|
...process.env,
|
|
954
|
-
|
|
955
|
-
|
|
1807
|
+
CLAUDE_PROJECT_DIR: mcpCwd,
|
|
1808
|
+
HEYSPEX_VAULT_ROOT: vaultRoot ?? "",
|
|
1809
|
+
HS_VAULT_ROOT: vaultRoot ?? "",
|
|
956
1810
|
// One-shot MCP tool — no stdio loop, no stderr brief-tier noise in PS.
|
|
957
1811
|
HEYSPEX_DISABLE_STDIO_BOOTSTRAP: "1",
|
|
958
1812
|
},
|
|
959
1813
|
});
|
|
960
1814
|
child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
|
|
961
1815
|
child.on("error", (err) => {
|
|
962
|
-
process.stderr.write(
|
|
1816
|
+
process.stderr.write(
|
|
1817
|
+
`hs: failed to run mcp ${command}: ${err.message}\n`,
|
|
1818
|
+
);
|
|
963
1819
|
process.exit(1);
|
|
964
1820
|
});
|
|
965
1821
|
return;
|
|
@@ -974,13 +1830,54 @@ function main(argv) {
|
|
|
974
1830
|
return;
|
|
975
1831
|
}
|
|
976
1832
|
|
|
1833
|
+
// `hs harness gates [--json]` — list every registered SpexHarness gate (id,
|
|
1834
|
+
// event, severity, last fixture result), reading the live gate registry +
|
|
1835
|
+
// rule table straight from spexharness/ — no vault/console dependency
|
|
1836
|
+
// ([[HS-XKYY-NYPR-TASK-hs-harness-gates-and-a-console]]).
|
|
1837
|
+
if (namespace === "harness") {
|
|
1838
|
+
dispatchSpexConsoleEntry("hs-harness.ts", argv, vaultRoot || process.cwd());
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// `hs hooks add|remove|list` — the CLI face of the extension seam
|
|
1843
|
+
// ([[HS-8N3K-NYPR-TASK-add-cli-verbs-so-a-developer-can]],
|
|
1844
|
+
// [[HS-8EDJ-NYPR-EPIC-extension-seam-for-other-developers]]). Reads/writes
|
|
1845
|
+
// `.heyspex/harness.json` straight through `spexharness/src/extension/
|
|
1846
|
+
// cli-face.mjs` — the SAME core the API face
|
|
1847
|
+
// (spex-console/src/app/api/harness/extension/route.ts) calls — no
|
|
1848
|
+
// vault/console dependency, same shell shape as `harness gates` above.
|
|
1849
|
+
if (namespace === "hooks") {
|
|
1850
|
+
dispatchSpexConsoleEntry("hs-hooks.ts", argv, vaultRoot || process.cwd());
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// `hs bench <phase1|phase2> --model <name> --harness <on|off>` — run the
|
|
1855
|
+
// harness-on-versus-harness-off benchmark's task set once and write a dated
|
|
1856
|
+
// report under dev-factory/reports/calibrations/bench/
|
|
1857
|
+
// ([[HS-DQ0S-NYPR-TASK-add-a-bench-command-that-runs-the-same]], epic
|
|
1858
|
+
// [[HS-IZCF-NYPR-EPIC-harness-on-versus-harness-off-benchmark]]). Thin CLI
|
|
1859
|
+
// verb only — the real orchestration lives in spexharness/src/bench/*.mjs;
|
|
1860
|
+
// this dispatch branch just reaches the thin shell like `harness` above.
|
|
1861
|
+
if (namespace === "bench") {
|
|
1862
|
+
dispatchSpexConsoleEntry("hs-bench.ts", argv, vaultRoot || process.cwd());
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
977
1866
|
// `hs check run <id|manifest>` — verify a spec's acceptance criteria against its
|
|
978
1867
|
// frozen checks manifest, per-criterion pass/fail against the real tree
|
|
979
1868
|
// ([[HS-EPIC-DCEP-acceptance-criteria-checks-manifest-dete]], the deterministic
|
|
980
1869
|
// kernel of [[HS-MIL-Y5DN-verification-engine-shared-deterministic]]). Forwards
|
|
981
1870
|
// the full argv; the entrypoint drops the leading `check` token.
|
|
1871
|
+
//
|
|
1872
|
+
// HS_PKG_ROOT is forwarded so the entrypoint can resolve a check's `cwd`
|
|
1873
|
+
// against the CODE root rather than `vaultRoot` — since the repo split, the
|
|
1874
|
+
// vault and the code can be two different checkouts, and a check almost
|
|
1875
|
+
// always runs code, not vault content
|
|
1876
|
+
// ([[HS-1LCC-4XKS-DIAG-frozen-checks-run-against-the-vault]]).
|
|
982
1877
|
if (namespace === "check") {
|
|
983
|
-
dispatchSpexConsoleEntry("hs-check.ts", argv, vaultRoot
|
|
1878
|
+
dispatchSpexConsoleEntry("hs-check.ts", argv, vaultRoot, {
|
|
1879
|
+
HS_PKG_ROOT: PKG_ROOT,
|
|
1880
|
+
});
|
|
984
1881
|
return;
|
|
985
1882
|
}
|
|
986
1883
|
|
|
@@ -988,8 +1885,15 @@ function main(argv) {
|
|
|
988
1885
|
// `review_pending`/`in_review` and report what has decayed since the stamp
|
|
989
1886
|
// ([[HS-R5LF-36QL-EPIC-stamped-work-is-re-verified-on-a]]). Read-only with
|
|
990
1887
|
// respect to the repo; the Resolution plan is a dry run unless `--apply`.
|
|
1888
|
+
//
|
|
1889
|
+
// HS_PKG_ROOT is forwarded for the same reason `check` forwards it: the sweep
|
|
1890
|
+
// re-runs those same frozen manifests, so a check's `cwd` has to resolve
|
|
1891
|
+
// against the CODE root, not the vault
|
|
1892
|
+
// ([[HS-1LCC-4XKS-DIAG-frozen-checks-run-against-the-vault]]).
|
|
991
1893
|
if (namespace === "revalidate") {
|
|
992
|
-
dispatchSpexConsoleEntry("hs-revalidate.ts", argv, vaultRoot
|
|
1894
|
+
dispatchSpexConsoleEntry("hs-revalidate.ts", argv, vaultRoot, {
|
|
1895
|
+
HS_PKG_ROOT: PKG_ROOT,
|
|
1896
|
+
});
|
|
993
1897
|
return;
|
|
994
1898
|
}
|
|
995
1899
|
|
|
@@ -1003,18 +1907,49 @@ function main(argv) {
|
|
|
1003
1907
|
return;
|
|
1004
1908
|
}
|
|
1005
1909
|
|
|
1910
|
+
// `hs chat [--model <name>] [--script <file>] [--seat <seat>]` — HeySpex's own chat
|
|
1911
|
+
// front (step 0 of [[HS-2KYS-NYPR-EPIC-phase-1-end-to-end-on-deepseek-and-on-a]], the
|
|
1912
|
+
// prerequisite that finishes [[HS-F7N3-NYPR-EPIC-the-powershell-front-hs-chat]]). Thin
|
|
1913
|
+
// CLI verb only — every real behavior lives in spexharness/src/chat/*.mjs; this
|
|
1914
|
+
// dispatch branch just reaches the thin shell like every other verb here. Forwards the
|
|
1915
|
+
// full argv; the entrypoint drops the leading `chat` token.
|
|
1916
|
+
if (namespace === "chat") {
|
|
1917
|
+
dispatchSpexConsoleEntry("hs-chat.ts", argv, vaultRoot || process.cwd());
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// `hs trust list` / `hs trust revoke [path]` — standalone management of the machine-local
|
|
1922
|
+
// folder-trust store `hs chat`'s first-run prompt writes to
|
|
1923
|
+
// ([[HS-25AJ-8CMW-TASK-folder-trust-revoke-through-the]], deferred from
|
|
1924
|
+
// [[HS-ZBEB-8CMW-TASK-folder-trust-for-spexcode-first-run]], which shipped only `hs chat
|
|
1925
|
+
// --revoke-trust`). Thin CLI verb — same shell shape as `chat`/`hooks`/`bench` above; the real
|
|
1926
|
+
// logic lives in `spex-console/src/lib/cli/trust-cli.ts` over `folder-trust.ts`'s pure
|
|
1927
|
+
// functions. Forwards the full argv; the entrypoint drops the leading `trust` token.
|
|
1928
|
+
if (namespace === "trust") {
|
|
1929
|
+
dispatchSpexConsoleEntry("hs-trust.ts", argv, vaultRoot || process.cwd());
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// `hs doctrine extract|learn|show` — SpexCode Doctrine corpus reader + synthesis
|
|
1934
|
+
// ([[HS-TWTC-8CMW-TASK-dr1-doctrine-corpus-reader-stream-every]],
|
|
1935
|
+
// [[HS-YMPY-8CMW-TASK-dr2-doctrine-synthesis-a-routed]] of epic
|
|
1936
|
+
// [[HS-KNHZ-8CMW-EPIC-spexcode-doctrine-v0-the-foundational]]). Thin CLI verb —
|
|
1937
|
+
// same shell shape as `chat`/`trust`/`harness` above; the real logic lives in
|
|
1938
|
+
// `spexharness/src/doctrine/*.mjs`. Forwards the full argv; the entrypoint drops
|
|
1939
|
+
// the leading `doctrine` token.
|
|
1940
|
+
if (namespace === "doctrine") {
|
|
1941
|
+
dispatchSpexConsoleEntry(
|
|
1942
|
+
"hs-doctrine.ts",
|
|
1943
|
+
argv,
|
|
1944
|
+
vaultRoot || process.cwd(),
|
|
1945
|
+
);
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1006
1949
|
// Neon control-plane verbs ([[HS-EPIC-CLMW-hs-cli-mcp-wrappers-for-control-plane]]).
|
|
1007
1950
|
// Each forwards the full argv (namespace/verb + args); the entrypoint drops
|
|
1008
|
-
// the leading namespace token and parses the rest.
|
|
1009
|
-
|
|
1010
|
-
pin: "hs-pin.ts",
|
|
1011
|
-
claim: "hs-claim.ts",
|
|
1012
|
-
handoff: "hs-handoff.ts",
|
|
1013
|
-
events: "hs-events.ts",
|
|
1014
|
-
"export-events": "hs-export-events.ts",
|
|
1015
|
-
prereq: "hs-prereq.ts",
|
|
1016
|
-
queue: "hs-queue.ts",
|
|
1017
|
-
};
|
|
1951
|
+
// the leading namespace token and parses the rest. (Table itself now lives
|
|
1952
|
+
// at module scope as CONTROL_ENTRYPOINTS — see its definition above main().)
|
|
1018
1953
|
if (Object.hasOwn(CONTROL_ENTRYPOINTS, namespace)) {
|
|
1019
1954
|
dispatchSpexConsoleEntry(CONTROL_ENTRYPOINTS[namespace], argv, vaultRoot);
|
|
1020
1955
|
return;
|
|
@@ -1025,4 +1960,54 @@ function main(argv) {
|
|
|
1025
1960
|
process.exit(2);
|
|
1026
1961
|
}
|
|
1027
1962
|
|
|
1028
|
-
|
|
1963
|
+
// Only run the CLI when this file is executed directly (`node bin/hs.mjs` /
|
|
1964
|
+
// the `hs`/`heyspex` bin shims), not when another script imports it — e.g.
|
|
1965
|
+
// `scripts/verify-mcp-package.mjs`'s `spawnTargets` check and
|
|
1966
|
+
// `scripts/lib/spawn-target-manifest.test.mjs` import `SPAWN_TARGETS` above
|
|
1967
|
+
// without wanting `main()` to run the dispatcher against THEIR argv.
|
|
1968
|
+
//
|
|
1969
|
+
// A naive `import.meta.url === pathToFileURL(process.argv[1]).href` (the
|
|
1970
|
+
// pattern `scripts/verify-mcp-package.mjs`'s own `isMain` guard uses — safe
|
|
1971
|
+
// there because that script is never reached through an npm bin shim) is
|
|
1972
|
+
// NOT enough here: an `npm link`ed / globally-installed `hs` runs
|
|
1973
|
+
// `node "<npm-prefix>/node_modules/heyspex/bin/hs.mjs"`, and when that path
|
|
1974
|
+
// is a symlink (npm link's install shape), Node's default module resolution
|
|
1975
|
+
// realpaths it for `import.meta.url` but leaves `process.argv[1]` as the
|
|
1976
|
+
// ORIGINAL (symlinked) path the shim passed — so the two never matched,
|
|
1977
|
+
// `isMain` came out false, and the globally-installed `hs` silently printed
|
|
1978
|
+
// nothing and exited 0 for every verb ([[HS-561M]] regression). Fix:
|
|
1979
|
+
// realpath BOTH sides before comparing (so the symlink resolves the same way
|
|
1980
|
+
// on each side), case-insensitively on Windows (drive-letter casing can
|
|
1981
|
+
// differ between how a shim built the path and how realpath reports it).
|
|
1982
|
+
function isSameFile(a, b) {
|
|
1983
|
+
const normalize = (p) => {
|
|
1984
|
+
let real;
|
|
1985
|
+
try {
|
|
1986
|
+
real = realpathSync.native(p);
|
|
1987
|
+
} catch {
|
|
1988
|
+
real = path.resolve(p);
|
|
1989
|
+
}
|
|
1990
|
+
return process.platform === "win32" ? real.toLowerCase() : real;
|
|
1991
|
+
};
|
|
1992
|
+
return normalize(a) === normalize(b);
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
const isMain = (() => {
|
|
1996
|
+
if (!process.argv[1]) return false;
|
|
1997
|
+
try {
|
|
1998
|
+
if (isSameFile(process.argv[1], fileURLToPath(import.meta.url)))
|
|
1999
|
+
return true;
|
|
2000
|
+
} catch {
|
|
2001
|
+
// fall through to the basename fallback below
|
|
2002
|
+
}
|
|
2003
|
+
// Belt-and-suspenders fallback for install shapes realpath can't reconcile
|
|
2004
|
+
// (a shim invoking through an intermediate wrapper, a path realpath can't
|
|
2005
|
+
// stat, etc.): recognize this file by its own known bin names rather than
|
|
2006
|
+
// failing closed into silent no-output.
|
|
2007
|
+
const base = path.basename(process.argv[1]).toLowerCase();
|
|
2008
|
+
return base === "hs.mjs" || base === "hs" || base === "heyspex";
|
|
2009
|
+
})();
|
|
2010
|
+
|
|
2011
|
+
if (isMain) {
|
|
2012
|
+
main(process.argv.slice(2));
|
|
2013
|
+
}
|