heyspex 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +14 -0
- package/README.md +21 -0
- package/bin/hs.mjs +1089 -104
- package/bin/spexcode.mjs +262 -0
- package/dev-factory/engine/worktree.mjs +740 -0
- package/dev-factory/lib/code-repo-root.mjs +67 -0
- package/dev-factory/lib/context-budget.mjs +84 -0
- package/dev-factory/lib/epic-detect.mjs +190 -0
- package/dev-factory/lib/fleet-queue-file.mjs +846 -0
- package/dev-factory/lib/fleet-queue.mjs +426 -0
- package/dev-factory/lib/fleet-state.mjs +14 -18
- package/dev-factory/lib/model-catalog.mjs +338 -0
- package/dev-factory/lib/seat-context.mjs +199 -0
- package/dev-factory/lib/spec-io.mjs +93 -0
- package/dev-factory/lib/sufficiency-score.mjs +8 -2
- package/dev-factory/lib/worktree-allocator.mjs +744 -0
- package/dev-factory/model-catalog.json +222 -0
- package/dist/cli/hs-check.mjs +1127 -103
- package/dist/cli/hs-claim.mjs +981 -63
- package/dist/cli/hs-connect.mjs +10059 -1729
- package/dist/cli/hs-defer.mjs +5507 -3072
- package/dist/cli/hs-dev.mjs +576 -0
- package/dist/cli/hs-env.mjs +416 -302
- package/dist/cli/hs-index-rebuild.mjs +7334 -0
- package/dist/cli/hs-index-reconcile.mjs +8138 -0
- package/dist/cli/hs-init.mjs +2840 -149
- package/dist/cli/hs-install.mjs +7214 -594
- package/dist/cli/hs-join.mjs +10640 -2421
- package/dist/cli/hs-vault-pull.mjs +8157 -6370
- package/dist/cli/hs-vault-push.mjs +6433 -0
- package/dist/cli/hs-vault.mjs +4029 -1555
- package/dist/cli/hs-workspace.mjs +2702 -538
- package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
- package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
- package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
- package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
- package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
- package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
- package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
- package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
- package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
- package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -0
- package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
- package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
- package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
- package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
- package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -0
- package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
- package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
- package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
- package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
- package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
- package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
- package/dist/cli-templates/harness-commands/cursor/hs-ground.md +33 -0
- package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
- package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
- package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
- package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
- package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
- package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-view.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
- package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
- package/docs/copilot-plugin-install.md +162 -0
- package/docs/driving-heyspex-from-copilot.md +165 -0
- package/docs/mcp-client-config.md +259 -6
- package/package.json +249 -124
- package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
- package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
- package/plugins/heyspex/.mcp.json +12 -0
- package/plugins/heyspex/README.md +309 -0
- package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
- package/plugins/heyspex/bin/run-hook.mjs +80 -0
- package/plugins/heyspex/hooks/cursor-hooks.json +308 -0
- package/plugins/heyspex/hooks/hooks.json +418 -0
- package/plugins/heyspex/mcp.json +13 -0
- package/scripts/check-mcp-freshness.mjs +17 -9
- package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
- package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
- package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
- package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -0
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +356 -0
- package/scripts/claude-hooks/_run.mjs +413 -0
- package/scripts/claude-hooks/_transcript-text.mjs +60 -0
- package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
- package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
- package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
- package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
- package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
- package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
- package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
- package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
- package/scripts/claude-hooks/claim-liveness-shared.mjs +577 -0
- package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
- package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
- package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
- package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
- package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
- package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
- package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
- package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
- package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
- package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
- package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
- package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
- package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
- package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
- package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
- package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
- package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
- package/scripts/claude-hooks/inject-communication-style.mjs +86 -0
- package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
- package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
- package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
- package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
- package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -0
- package/scripts/claude-hooks/log-session-activity.mjs +341 -0
- package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
- package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
- package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
- package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
- package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
- package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
- package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
- package/scripts/claude-hooks/research-method-guard.mjs +398 -0
- package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
- package/scripts/claude-hooks/session-end.mjs +451 -0
- package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
- package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
- package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
- package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
- package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
- package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
- package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
- package/scripts/claude-hooks/wip-checkpoint.mjs +302 -0
- package/scripts/cursor-hooks/manifest.mjs +281 -0
- package/scripts/dev-server-status.mjs +34 -13
- package/scripts/dev-server-stop.mjs +313 -0
- package/scripts/heyspex-mode.mjs +288 -0
- package/scripts/hs-auto-resolve.mjs +240 -0
- package/scripts/hs-auto-source.mjs +398 -0
- package/scripts/hs-auto.mjs +134 -0
- package/scripts/hs-capture-cli.mjs +326 -0
- package/scripts/hs-classifier.mjs +136 -0
- package/scripts/hs-companion.mjs +88 -0
- package/scripts/hs-gateway.mjs +115 -0
- package/scripts/hs-mode.mjs +125 -0
- package/scripts/hs-next.mjs +441 -0
- package/scripts/hs-settings.mjs +338 -0
- package/scripts/hs-view.mjs +496 -0
- package/scripts/hs-worktree.mjs +201 -0
- package/scripts/install-claude-code-hooks.mjs +272 -0
- package/scripts/install-cursor-hooks.mjs +471 -0
- package/scripts/lib/agent-env-markers.mjs +38 -0
- package/scripts/lib/console-url-resolver.mjs +199 -61
- package/scripts/lib/customer-kit.mjs +1203 -0
- package/scripts/lib/dev-mode.mjs +249 -32
- package/scripts/lib/directive-obligations.mjs +349 -0
- package/scripts/lib/heyspex-home.mjs +72 -0
- package/scripts/lib/hook-dispositions.mjs +210 -0
- package/scripts/lib/hook-vault-header.mjs +30 -0
- package/scripts/lib/hs-command.mjs +277 -0
- package/scripts/lib/hs-companion-settings.mjs +472 -0
- package/scripts/lib/hs-source-branch.mjs +196 -0
- package/scripts/lib/hs-source-commit.mjs +59 -5
- package/scripts/lib/hs-source-identity.mjs +526 -0
- package/scripts/lib/hs-source-log.mjs +13 -22
- package/scripts/lib/hs-source-preflight.mjs +18 -4
- package/scripts/lib/hs-source-push.mjs +386 -307
- package/scripts/lib/hs-worktree-args.mjs +115 -0
- package/scripts/lib/shared-path-helpers.mjs +134 -0
- package/scripts/lib/standalone-doctor.mjs +540 -3
- package/scripts/lib/subagent-git-lock.mjs +109 -0
- package/scripts/lib/template-pin.mjs +34 -0
- package/scripts/lib/window-placement.mjs +109 -0
- package/scripts/provenance-lib.mjs +91 -0
- package/scripts/stamp-provenance-on-edit.mjs +222 -0
- package/scripts/wip-ledger-health.mjs +653 -0
- package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
- package/spexharness/src/adapters/audit.mjs +197 -0
- package/spexharness/src/adapters/claude-code.mjs +449 -0
- package/spexharness/src/adapters/copilot.mjs +236 -0
- package/spexharness/src/adapters/detect-harness.mjs +34 -0
- package/spexharness/src/chat/tui/ansi.mjs +448 -0
- package/spexharness/src/chat/tui/palette.mjs +22 -0
- package/spexharness/src/chat/tui/select.mjs +208 -0
- package/spexharness/src/config-root.mjs +54 -0
- package/spexharness/src/context-budget.mjs +141 -0
- package/spexharness/src/dev-server-control.mjs +389 -0
- package/spexharness/src/doctrine/behaviour.mjs +371 -0
- package/spexharness/src/doctrine/corpus.mjs +733 -0
- package/spexharness/src/doctrine/inject.mjs +338 -0
- package/spexharness/src/doctrine/merge.mjs +184 -0
- package/spexharness/src/doctrine/sources.mjs +252 -0
- package/spexharness/src/doctrine/synthesize.mjs +401 -0
- package/spexharness/src/doctrine/vault-source.mjs +574 -0
- package/spexharness/src/doctrine/writer.mjs +176 -0
- package/spexharness/src/events.mjs +84 -0
- package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
- package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
- package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
- package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
- package/spexharness/src/gates/deep-research-cost-gate.mjs +275 -0
- package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
- package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
- package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
- package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
- package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
- package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
- package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
- package/spexharness/src/gates/git-identity-guard.mjs +61 -0
- package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
- package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
- package/spexharness/src/gates/git-stash-gate.mjs +159 -0
- package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
- package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
- package/spexharness/src/gates/inject-vault-context.mjs +239 -0
- package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
- package/spexharness/src/gates/plan-review-gate.mjs +233 -0
- package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
- package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
- package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
- package/spexharness/src/gates/settings-write-gate.mjs +104 -0
- package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
- package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
- package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
- package/spexharness/src/git-reflog-audit.mjs +294 -0
- package/spexharness/src/intent-classifier.mjs +196 -0
- package/spexharness/src/intent-directive.mjs +626 -0
- package/spexharness/src/profile/learn.mjs +551 -0
- package/spexharness/src/profile/proposals.mjs +357 -0
- package/spexharness/src/profile/vault-inclusion.mjs +386 -0
- package/spexharness/src/redact.mjs +151 -0
- package/spexharness/src/rule-table.mjs +849 -0
- package/spexharness/src/seat-context.mjs +122 -0
- package/spexharness/src/session-bootstrap.mjs +512 -0
- package/spexharness/src/session-record.mjs +815 -0
- package/spexharness/src/token-inventory.mjs +207 -0
- package/vendor/heyspex-mcp/account-switch.mjs +96 -0
- package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
- package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
- package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
- package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
- package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +77 -8
- package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
- package/vendor/heyspex-mcp/capture-console-client.mjs +117 -3
- package/vendor/heyspex-mcp/capture-focus.mjs +1061 -31
- package/vendor/heyspex-mcp/capture-outbox.mjs +15 -5
- package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
- package/vendor/heyspex-mcp/capture-tools.mjs +1264 -0
- package/vendor/heyspex-mcp/claim-tools.mjs +1147 -0
- package/vendor/heyspex-mcp/cli-verbs.mjs +2074 -0
- package/vendor/heyspex-mcp/cli.mjs +207 -15524
- package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
- package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +602 -0
- package/vendor/heyspex-mcp/config-seed.yaml +15 -0
- package/vendor/heyspex-mcp/config-settings.mjs +290 -7
- package/vendor/heyspex-mcp/console-fetch.mjs +822 -17
- package/vendor/heyspex-mcp/console-transport.mjs +172 -0
- package/vendor/heyspex-mcp/credentials.mjs +585 -0
- package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
- package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
- package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
- package/vendor/heyspex-mcp/design-tools.mjs +4 -1
- package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
- package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
- package/vendor/heyspex-mcp/dev-tools.mjs +131 -0
- package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
- package/vendor/heyspex-mcp/events-tools.mjs +207 -0
- package/vendor/heyspex-mcp/filing-tools.mjs +723 -0
- package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +71 -15
- package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
- package/vendor/heyspex-mcp/folder-trust-store.mjs +60 -0
- package/vendor/heyspex-mcp/http-transport.mjs +302 -0
- package/vendor/heyspex-mcp/list-tools.mjs +257 -0
- package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
- package/vendor/heyspex-mcp/mcp-config.mjs +107 -0
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
- package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
- package/vendor/heyspex-mcp/occupied-keys.mjs +142 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +3377 -0
- package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
- package/vendor/heyspex-mcp/package-root.mjs +32 -0
- package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
- package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
- package/vendor/heyspex-mcp/plan-push.mjs +908 -22
- package/vendor/heyspex-mcp/plan-review.mjs +124 -30
- package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
- package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -970
- package/vendor/heyspex-mcp/promote-capture.mjs +388 -69
- package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
- package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
- package/vendor/heyspex-mcp/promote-research.mjs +666 -53
- package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
- package/vendor/heyspex-mcp/prompts.mjs +1460 -0
- package/vendor/heyspex-mcp/read-path.mjs +191 -15
- package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
- package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
- package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
- package/vendor/heyspex-mcp/register-spec.mjs +266 -54
- package/vendor/heyspex-mcp/request-context.mjs +83 -0
- package/vendor/heyspex-mcp/research-focus.mjs +207 -4
- package/vendor/heyspex-mcp/research-handlers.mjs +625 -0
- package/vendor/heyspex-mcp/research-tools.mjs +4 -4
- package/vendor/heyspex-mcp/response-budget.mjs +348 -0
- package/vendor/heyspex-mcp/review-focus.mjs +25 -1
- package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
- package/vendor/heyspex-mcp/search-specs.mjs +280 -0
- package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
- package/vendor/heyspex-mcp/session-owner.mjs +109 -0
- package/vendor/heyspex-mcp/session-tools.mjs +162 -0
- package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
- package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
- package/vendor/heyspex-mcp/spec-edit-tools.mjs +453 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
- package/vendor/heyspex-mcp/split-research.mjs +304 -56
- package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +150 -7
- package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
- package/vendor/heyspex-mcp/summary-write-tools.mjs +228 -0
- package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +123 -1
- package/vendor/heyspex-mcp/tool-catalog.mjs +2819 -0
- package/vendor/heyspex-mcp/tool-dispatch.mjs +657 -0
- package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
- package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
- package/vendor/heyspex-mcp/vault-io.mjs +720 -0
- package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +554 -68
- package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
- package/vendor/heyspex-mcp/whoami.mjs +92 -0
- package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* hs view <surface> [spec-id] — open ONE console surface in a chromeless
|
|
4
|
+
* app-mode browser window (the "solo surface" console gauge).
|
|
5
|
+
*
|
|
6
|
+
* Per [[HS-RU2R-I87U-RES-solo-surface-cli-launcher-single]]: launches
|
|
7
|
+
* Edge/Chrome with `--app=<url>` (NOT `--kiosk`, which forces fullscreen and
|
|
8
|
+
* ignores window sizing) pointed at the console's `/solo/<surface>` route.
|
|
9
|
+
* Probes ground-truth dev-server status first (same core as `hs server
|
|
10
|
+
* status`) so a down console yields a clear message, never a blank window.
|
|
11
|
+
*
|
|
12
|
+
* Open = run it; close = close the window; re-run = reopen (idempotent).
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* hs view # inspector, Pipeline tab, following the focus pin
|
|
16
|
+
* hs view HLFJ # same, anchored on HS-HLFJ (any case, HS- optional)
|
|
17
|
+
* hs view pipeline HS-I87U-CAP # same — "pipeline" is the inspector's Pipeline tab
|
|
18
|
+
* hs view inspector HS-I87U-CAP # inspector on its default tab
|
|
19
|
+
* hs view docs HS-NP1Y # doc + inspector Pipeline tab side by side
|
|
20
|
+
* hs view pipeline --follow capture # track the capture-on-deck pin
|
|
21
|
+
* hs view pipeline --follow capture --url https://console.example.com
|
|
22
|
+
* hs view pipeline --width 510 --height 900 --browser edge|chrome
|
|
23
|
+
*/
|
|
24
|
+
import { spawn } from 'node:child_process';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
27
|
+
|
|
28
|
+
import { getDevServerStatus } from './dev-server-status.mjs';
|
|
29
|
+
import { resolveConsoleUrl, DEFAULT_CONSOLE_URL } from './lib/console-url-resolver.mjs';
|
|
30
|
+
// The runnable form of a HeySpex command on THIS machine — `hs …` when the bin
|
|
31
|
+
// is on PATH, else an absolute-path `node "<install>/bin/hs.mjs" …`. Never an
|
|
32
|
+
// `npx` form: npx resolves from the READER's working directory, and outside a
|
|
33
|
+
// checkout `npx hs` installs the unrelated public package hs@2.0.1
|
|
34
|
+
// ([[HS-I6HT-EKYU-DIAG-every-message-tells-the-user-to-run-hs]]).
|
|
35
|
+
import { hsCommand } from './lib/hs-command.mjs';
|
|
36
|
+
import {
|
|
37
|
+
complementarySide,
|
|
38
|
+
computeWindowPlacement,
|
|
39
|
+
getPrimaryWorkArea,
|
|
40
|
+
parsePlaceSide,
|
|
41
|
+
placementFlags,
|
|
42
|
+
} from './lib/window-placement.mjs';
|
|
43
|
+
import { collectOccupiedFamilyKeysFromVault, parseSpecId } from '../vendor/heyspex-mcp/spec-id-lib.mjs';
|
|
44
|
+
|
|
45
|
+
function defaultVaultRoot() {
|
|
46
|
+
// This file lives at <vaultRoot>/scripts/, so one level up is the root.
|
|
47
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Levenshtein edit distance — small local helper, no dependency pulled in for it. */
|
|
51
|
+
function editDistance(a, b) {
|
|
52
|
+
const m = a.length;
|
|
53
|
+
const n = b.length;
|
|
54
|
+
const dp = Array.from({ length: m + 1 }, (_, i) => [i, ...Array(n).fill(0)]);
|
|
55
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
56
|
+
for (let i = 1; i <= m; i++) {
|
|
57
|
+
for (let j = 1; j <= n; j++) {
|
|
58
|
+
dp[i][j] =
|
|
59
|
+
a[i - 1] === b[j - 1]
|
|
60
|
+
? dp[i - 1][j - 1]
|
|
61
|
+
: 1 + Math.min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return dp[m][n];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Nearest occupied family keys to a typo'd spec token — walks the vault
|
|
69
|
+
* (fresh every call, unlike `.heyspex/vault-index.json`) via the shared
|
|
70
|
+
* spec-id-lib helper, never re-implements the vault scan.
|
|
71
|
+
*/
|
|
72
|
+
function suggestFamilyKeys(rawSpec, vaultRoot, limit = 5) {
|
|
73
|
+
const parsed = parseSpecId(rawSpec);
|
|
74
|
+
const needle = (parsed?.familyKey ?? rawSpec.replace(/^HS-/i, '')).toUpperCase();
|
|
75
|
+
const occupied = [...collectOccupiedFamilyKeysFromVault(vaultRoot)];
|
|
76
|
+
return occupied
|
|
77
|
+
.map((key) => ({ key, dist: editDistance(needle, key) }))
|
|
78
|
+
.sort((x, y) => x.dist - y.dist || x.key.localeCompare(y.key))
|
|
79
|
+
.slice(0, limit)
|
|
80
|
+
.filter((c) => c.dist <= Math.max(2, Math.ceil(needle.length / 2)))
|
|
81
|
+
.map((c) => `HS-${c.key}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolve a spec id/token against the running console before opening a
|
|
86
|
+
* browser window (same resolution rules as `GET /api/specs/:id`, including
|
|
87
|
+
* its bare-family-token prefix fallback). Returns `{ ok: true }`,
|
|
88
|
+
* `{ ok: false, kind: 'ambiguous', candidates }`, or
|
|
89
|
+
* `{ ok: false, kind: 'not_found', suggestions }`.
|
|
90
|
+
*/
|
|
91
|
+
async function resolveSpec(base, rawSpec, vaultRoot) {
|
|
92
|
+
let res;
|
|
93
|
+
try {
|
|
94
|
+
res = await fetch(`${base}/api/specs/${encodeURIComponent(rawSpec)}`);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
// Network hiccup against a console we already confirmed is up — don't
|
|
97
|
+
// block the window on it, just open unvalidated.
|
|
98
|
+
return { ok: true, skipped: true, reason: err?.message || String(err) };
|
|
99
|
+
}
|
|
100
|
+
if (res.ok) return { ok: true };
|
|
101
|
+
let body = null;
|
|
102
|
+
try {
|
|
103
|
+
body = await res.json();
|
|
104
|
+
} catch {
|
|
105
|
+
// non-JSON error body — fall through to generic not_found handling
|
|
106
|
+
}
|
|
107
|
+
if (res.status === 404 && body?.error === 'ambiguous_id' && Array.isArray(body.candidates)) {
|
|
108
|
+
return { ok: false, kind: 'ambiguous', candidates: body.candidates };
|
|
109
|
+
}
|
|
110
|
+
if (res.status !== 404) {
|
|
111
|
+
// Not a verdict on the spec — a hosted console answers an unauthenticated
|
|
112
|
+
// probe with a sign-in redirect or a 401/403. Open unvalidated rather than
|
|
113
|
+
// claiming the doc does not exist.
|
|
114
|
+
return { ok: true, skipped: true, reason: `HTTP ${res.status}` };
|
|
115
|
+
}
|
|
116
|
+
return { ok: false, kind: 'not_found', suggestions: suggestFamilyKeys(rawSpec, vaultRoot) };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The solo surfaces `/solo/<surface>` mounts — mirror of the registry in
|
|
121
|
+
* spex-console/src/app/solo/[surface]/page.tsx. A first positional that is not
|
|
122
|
+
* one of these is read as a spec id, opened on the pipeline surface.
|
|
123
|
+
*/
|
|
124
|
+
export const SOLO_SURFACES = new Set([
|
|
125
|
+
'pipeline',
|
|
126
|
+
'docs',
|
|
127
|
+
'inspector',
|
|
128
|
+
'whats-next',
|
|
129
|
+
'captures',
|
|
130
|
+
'session-log',
|
|
131
|
+
'settings',
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The view a bare `hs view` / `hs view <spec-id>` opens. There is no
|
|
136
|
+
* standalone pipeline window: the inspector already carries the Pipeline tab,
|
|
137
|
+
* so "pipeline" resolves to the inspector opened on that tab (owner ruling
|
|
138
|
+
* 2026-10-10) — see {@link INSPECTOR_TAB_ALIASES}.
|
|
139
|
+
*/
|
|
140
|
+
export const DEFAULT_SPEC_SURFACE = 'pipeline';
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Surface words that are really an inspector tab. `hs view pipeline <id>` and
|
|
144
|
+
* every older caller still work — they land on the inspector, that tab open.
|
|
145
|
+
*/
|
|
146
|
+
export const INSPECTOR_TAB_ALIASES = { pipeline: 'pipeline' };
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Canonicalise a typed spec token: `hlfj`, `hs-hlfj` and `HS-HLFJ` all become
|
|
150
|
+
* `HS-HLFJ`. A missing `HS-` prefix is added (the console's family-token
|
|
151
|
+
* fallback only matches `HS-<token>-…` prefixes, so a bare `hlfj` found
|
|
152
|
+
* nothing). Only a short key form (`HLFJ`, `HLFJ-AB12`, `HLFJ-TASK`) is
|
|
153
|
+
* upper-cased; a full basename keeps its lower-case slug so it still matches
|
|
154
|
+
* exactly.
|
|
155
|
+
*/
|
|
156
|
+
export function normalizeSpecToken(raw) {
|
|
157
|
+
const token = String(raw ?? '').trim();
|
|
158
|
+
if (!token) return token;
|
|
159
|
+
const body = token.replace(/^hs-/i, '');
|
|
160
|
+
if (/^[a-z0-9]{4}(?:-[a-z0-9]{2,8}){0,2}$/i.test(body)) return `HS-${body.toUpperCase()}`;
|
|
161
|
+
return `HS-${body}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Split positionals into `{ surface, panel, spec }`. `hs view <surface> [spec]`
|
|
166
|
+
* keeps its surface; `hs view <spec>` and bare `hs view` open the inspector on
|
|
167
|
+
* its Pipeline tab, as does the `pipeline` word. The spec token is normalised.
|
|
168
|
+
*/
|
|
169
|
+
export function resolveSurfaceAndSpec(positionals) {
|
|
170
|
+
let [surface = null, spec = null] = positionals;
|
|
171
|
+
if (surface && !SOLO_SURFACES.has(surface.toLowerCase()) && !spec) {
|
|
172
|
+
spec = surface;
|
|
173
|
+
surface = null;
|
|
174
|
+
}
|
|
175
|
+
surface = (surface ?? DEFAULT_SPEC_SURFACE).toLowerCase();
|
|
176
|
+
let panel = null;
|
|
177
|
+
if (INSPECTOR_TAB_ALIASES[surface]) {
|
|
178
|
+
panel = INSPECTOR_TAB_ALIASES[surface];
|
|
179
|
+
surface = 'inspector';
|
|
180
|
+
}
|
|
181
|
+
return { surface, panel, spec: spec ? normalizeSpecToken(spec) : null };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Anchor sources the window can follow live. `pin` is the default behavior. */
|
|
185
|
+
const FOLLOW_MODES = new Set(['pin', 'capture']);
|
|
186
|
+
|
|
187
|
+
/** Opening width for a surface with nothing better to say. */
|
|
188
|
+
export const DEFAULT_WINDOW_WIDTH = 510;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Surfaces that want a different opening width than the default.
|
|
192
|
+
*
|
|
193
|
+
* `inspector` is one narrow pane meant to sit beside a terminal — at 510 it
|
|
194
|
+
* takes screen it does not need. It opens at 350 and grows leftward to the
|
|
195
|
+
* `docs` width when a doc pane opens
|
|
196
|
+
* ([[HS-I2YN-ST07-EPIC-two-pane-layout-pipeline-view-on-the]]).
|
|
197
|
+
*/
|
|
198
|
+
export const SURFACE_WINDOW_WIDTH = {
|
|
199
|
+
inspector: 350,
|
|
200
|
+
// Two panes: the doc, then the inspector rail (360) to its right.
|
|
201
|
+
docs: 960,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Resolve the window width: an explicit `--width` always wins, otherwise the
|
|
206
|
+
* surface's own default, otherwise {@link DEFAULT_WINDOW_WIDTH}.
|
|
207
|
+
*
|
|
208
|
+
* `explicitWidth` is null/undefined when the flag was not passed. Deciding that
|
|
209
|
+
* by COMPARING against the fallback would silently drop `--width 510` on the
|
|
210
|
+
* inspector back to 350, so the caller tracks "was it passed" instead.
|
|
211
|
+
*/
|
|
212
|
+
export function resolveWindowWidth(surface, explicitWidth) {
|
|
213
|
+
if (typeof explicitWidth === 'number' && Number.isFinite(explicitWidth) && explicitWidth > 0) {
|
|
214
|
+
return explicitWidth;
|
|
215
|
+
}
|
|
216
|
+
return SURFACE_WINDOW_WIDTH[surface] ?? DEFAULT_WINDOW_WIDTH;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Which half of the primary monitor's work area the window takes.
|
|
221
|
+
*
|
|
222
|
+
* Explicit `--place` wins, then `HEYSPEX_VIEW_PLACE`, then the default: the
|
|
223
|
+
* two-pane capture view (`docs --follow capture`) sits on the RIGHT so the
|
|
224
|
+
* SpexCode agent window can take the left
|
|
225
|
+
* ([[HS-I2YN-ST07-EPIC-two-pane-layout-pipeline-view-on-the]]). Every other
|
|
226
|
+
* window keeps the OS's own placement (`none`), as before.
|
|
227
|
+
*/
|
|
228
|
+
export function resolvePlaceSide({ place, surface, follow }, env = process.env) {
|
|
229
|
+
const explicit = parsePlaceSide(place);
|
|
230
|
+
if (explicit) return explicit;
|
|
231
|
+
const fromEnv = parsePlaceSide(env.HEYSPEX_VIEW_PLACE);
|
|
232
|
+
if (fromEnv) return fromEnv;
|
|
233
|
+
return surface === 'docs' && follow === 'capture' ? 'right' : 'none';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The side the paired agent (terminal) window should take for this view. */
|
|
237
|
+
export const agentSideFor = (viewSide) => complementarySide(viewSide);
|
|
238
|
+
|
|
239
|
+
function parseArgs(argv) {
|
|
240
|
+
const args = {
|
|
241
|
+
place: null,
|
|
242
|
+
printOnly: false,
|
|
243
|
+
surface: null,
|
|
244
|
+
spec: null,
|
|
245
|
+
// null until `--width` is seen; resolved per-surface once the surface is known.
|
|
246
|
+
width: null,
|
|
247
|
+
height: 900,
|
|
248
|
+
browser: 'edge',
|
|
249
|
+
follow: null,
|
|
250
|
+
url: null,
|
|
251
|
+
};
|
|
252
|
+
const positionals = [];
|
|
253
|
+
for (let i = 0; i < argv.length; i++) {
|
|
254
|
+
const a = argv[i];
|
|
255
|
+
if (a === '--help' || a === '-h') return { help: true };
|
|
256
|
+
if (a === '--width') { args.width = Number(argv[++i]) || args.width; continue; }
|
|
257
|
+
if (a === '--height') { args.height = Number(argv[++i]) || args.height; args.heightExplicit = true; continue; }
|
|
258
|
+
if (a === '--place') {
|
|
259
|
+
const side = parsePlaceSide(argv[++i]);
|
|
260
|
+
if (!side) {
|
|
261
|
+
process.stderr.write('hs view: --place expects left, right or none\n');
|
|
262
|
+
process.exitCode = 2;
|
|
263
|
+
return { help: true };
|
|
264
|
+
}
|
|
265
|
+
args.place = side;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (a === '--print' || a === '--dry-run') { args.printOnly = true; continue; }
|
|
269
|
+
if (a === '--browser') { args.browser = String(argv[++i] || 'edge'); continue; }
|
|
270
|
+
if (a === '--url') {
|
|
271
|
+
const raw = String(argv[++i] || '').trim();
|
|
272
|
+
if (!/^https?:\/\/\S+$/.test(raw)) {
|
|
273
|
+
process.stderr.write(`hs view: --url expects an http(s) URL (got "${raw || '(nothing)'}")\n`);
|
|
274
|
+
process.exitCode = 2;
|
|
275
|
+
return { help: true };
|
|
276
|
+
}
|
|
277
|
+
args.url = raw.replace(/\/+$/, '');
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (a === '--follow') {
|
|
281
|
+
const mode = String(argv[++i] || '').trim().toLowerCase();
|
|
282
|
+
if (!FOLLOW_MODES.has(mode)) {
|
|
283
|
+
process.stderr.write(
|
|
284
|
+
`hs view: --follow expects ${[...FOLLOW_MODES].join(' or ')} (got "${mode || '(nothing)'}")\n`,
|
|
285
|
+
);
|
|
286
|
+
process.exitCode = 2;
|
|
287
|
+
return { help: true };
|
|
288
|
+
}
|
|
289
|
+
args.follow = mode;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (a.startsWith('--')) {
|
|
293
|
+
process.stderr.write(`hs view: unknown flag ${a}\n`);
|
|
294
|
+
process.exitCode = 2;
|
|
295
|
+
return { help: true };
|
|
296
|
+
}
|
|
297
|
+
positionals.push(a);
|
|
298
|
+
}
|
|
299
|
+
return { ...args, ...resolveSurfaceAndSpec(positionals) };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function usage() {
|
|
303
|
+
process.stdout.write(
|
|
304
|
+
[
|
|
305
|
+
'hs view <surface> [spec-id] [--follow pin|capture] [--url https://…]',
|
|
306
|
+
' [--width N] [--height N] [--browser edge|chrome]',
|
|
307
|
+
' [--place left|right|none] [--print]',
|
|
308
|
+
'hs view [spec-id] # inspector on its Pipeline tab',
|
|
309
|
+
'',
|
|
310
|
+
'Open one console surface in a chromeless app-mode browser window.',
|
|
311
|
+
'Surfaces: inspector, docs, whats-next, captures, session-log, settings',
|
|
312
|
+
' ("pipeline" = the inspector opened on its Pipeline tab; there is no',
|
|
313
|
+
' standalone pipeline window.)',
|
|
314
|
+
' (registry: src/app/solo/[surface]/page.tsx).',
|
|
315
|
+
'Spec ids are forgiving: hlfj, hs-hlfj and HS-HLFJ are the same spec.',
|
|
316
|
+
'Without a spec-id the inspector and docs views follow the focus pin live.',
|
|
317
|
+
'--follow capture tracks the capture-on-deck pin instead, so the window',
|
|
318
|
+
' re-anchors itself onto each new capture rather than opening another.',
|
|
319
|
+
'--url opens against a named console (a hosted one) instead of probing for',
|
|
320
|
+
' a local dev server — the cloud-relay path the desktop companion uses.',
|
|
321
|
+
'--place pins the window to the left or right half of the primary monitor',
|
|
322
|
+
' work area (taskbar excluded; never another monitor). Default: right for',
|
|
323
|
+
' `docs --follow capture` (the two-pane capture view, leaving the left half',
|
|
324
|
+
' for the SpexCode agent window), none otherwise. Env: HEYSPEX_VIEW_PLACE.',
|
|
325
|
+
'--print shows the browser command line and does not open a window.',
|
|
326
|
+
'',
|
|
327
|
+
].join('\n'),
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Only a localhost/loopback resolution still requires a running local dev
|
|
333
|
+
* server; anything else (a paired or `hs dev mode:cloud` hosted console) is
|
|
334
|
+
* opened directly. An unparseable URL is treated as local so it still goes
|
|
335
|
+
* through the dev-server probe instead of being launched blind.
|
|
336
|
+
*/
|
|
337
|
+
function isLocalConsoleUrl(url) {
|
|
338
|
+
try {
|
|
339
|
+
const host = new URL(url).hostname;
|
|
340
|
+
return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
|
|
341
|
+
} catch {
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* The exact command line a launch would run, as `{ file, args }`. Pure, so
|
|
348
|
+
* `--print` and the tests see precisely what `launchAppWindow` spawns.
|
|
349
|
+
*/
|
|
350
|
+
export function buildLaunchCommand(url, { width, height, browser }, placement = null, platform = process.platform) {
|
|
351
|
+
const exe = browser === 'chrome' ? 'chrome' : 'msedge';
|
|
352
|
+
const flags = [`--app=${url}`, ...(placement ? placementFlags(placement) : [`--window-size=${width},${height}`])];
|
|
353
|
+
if (platform === 'win32') return { file: 'cmd.exe', args: ['/c', 'start', '', exe, ...flags] };
|
|
354
|
+
if (platform === 'darwin') {
|
|
355
|
+
const app = browser === 'chrome' ? 'Google Chrome' : 'Microsoft Edge';
|
|
356
|
+
return { file: 'open', args: ['-na', app, '--args', ...flags] };
|
|
357
|
+
}
|
|
358
|
+
return { file: browser === 'chrome' ? 'google-chrome' : 'microsoft-edge', args: flags };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function launchAppWindow(url, opts, placement = null) {
|
|
362
|
+
const { width, height, browser } = opts;
|
|
363
|
+
const exe = browser === 'chrome' ? 'chrome' : 'msedge';
|
|
364
|
+
const flags = [`--app=${url}`, ...(placement ? placementFlags(placement) : [`--window-size=${width},${height}`])];
|
|
365
|
+
let child;
|
|
366
|
+
if (process.platform === 'win32') {
|
|
367
|
+
// `start` resolves msedge/chrome via the App Paths registry; the empty ''
|
|
368
|
+
// is start's window-title slot, without it the exe would be eaten as title.
|
|
369
|
+
child = spawn('cmd.exe', ['/c', 'start', '', exe, ...flags], {
|
|
370
|
+
detached: true,
|
|
371
|
+
stdio: 'ignore',
|
|
372
|
+
});
|
|
373
|
+
} else if (process.platform === 'darwin') {
|
|
374
|
+
const app = browser === 'chrome' ? 'Google Chrome' : 'Microsoft Edge';
|
|
375
|
+
child = spawn('open', ['-na', app, '--args', ...flags], { detached: true, stdio: 'ignore' });
|
|
376
|
+
} else {
|
|
377
|
+
const bin = browser === 'chrome' ? 'google-chrome' : 'microsoft-edge';
|
|
378
|
+
child = spawn(bin, flags, { detached: true, stdio: 'ignore' });
|
|
379
|
+
}
|
|
380
|
+
child.unref();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function main() {
|
|
384
|
+
const args = parseArgs(process.argv.slice(2));
|
|
385
|
+
if (args.help) {
|
|
386
|
+
usage();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Now that the surface is known, settle the width: an explicit `--width`
|
|
391
|
+
// stands, otherwise the surface's own opening width.
|
|
392
|
+
args.width = resolveWindowWidth(args.surface, args.width);
|
|
393
|
+
|
|
394
|
+
// `--url` names the console explicitly — the cloud-relay path, where the
|
|
395
|
+
// companion opens a window against the hosted console it is paired with and
|
|
396
|
+
// there is no local dev server to probe. Without it, the machine's own
|
|
397
|
+
// console resolution decides: `hs dev mode:cloud` / `hs mcp pair` stamp a
|
|
398
|
+
// hosted URL into ~/.heyspex/credentials.yaml, and a non-local resolution
|
|
399
|
+
// needs no dev server either. Only a localhost resolution keeps the old
|
|
400
|
+
// behavior of requiring (and probing for) a running local console.
|
|
401
|
+
const vaultRoot = process.env.HS_VAULT_ROOT || defaultVaultRoot();
|
|
402
|
+
let base = args.url;
|
|
403
|
+
if (!base) {
|
|
404
|
+
const resolved = resolveConsoleUrl({ repoRoot: vaultRoot });
|
|
405
|
+
if (!isLocalConsoleUrl(resolved)) {
|
|
406
|
+
base = resolved.replace(/\/+$/, '');
|
|
407
|
+
} else {
|
|
408
|
+
const status = await getDevServerStatus({ vaultRoot: process.env.HS_VAULT_ROOT });
|
|
409
|
+
if (!status.running) {
|
|
410
|
+
process.stderr.write(
|
|
411
|
+
'hs view: this machine points at a local console but no spex-console dev server is running.\n' +
|
|
412
|
+
' Start it first (npm run dev), then re-run this command.\n' +
|
|
413
|
+
` (Meant the hosted console? Run \`${hsCommand('dev mode:cloud')}\`, or pass --url https://…)\n`,
|
|
414
|
+
);
|
|
415
|
+
process.exitCode = 1;
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const consoleServer = (status.servers || []).find((s) => s.probe === 'console');
|
|
419
|
+
base = (consoleServer?.url || DEFAULT_CONSOLE_URL).replace(/\/+$/, '');
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (args.spec) {
|
|
424
|
+
const resolution = await resolveSpec(base, args.spec, vaultRoot);
|
|
425
|
+
if (!resolution.ok) {
|
|
426
|
+
if (resolution.kind === 'ambiguous') {
|
|
427
|
+
process.stderr.write(
|
|
428
|
+
`hs view: "${args.spec}" is ambiguous — it matches more than one spec:\n` +
|
|
429
|
+
resolution.candidates.map((c) => ` ${c}\n`).join('') +
|
|
430
|
+
'Pass the full basename to disambiguate.\n',
|
|
431
|
+
);
|
|
432
|
+
} else {
|
|
433
|
+
process.stderr.write(`hs view: no spec found for "${args.spec}".\n`);
|
|
434
|
+
if (resolution.suggestions.length) {
|
|
435
|
+
process.stderr.write(
|
|
436
|
+
'Did you mean:\n' + resolution.suggestions.map((s) => ` ${s}\n`).join(''),
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
process.exitCode = 1;
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// A fixed `?spec=` wins over any follow mode (both here and in the route),
|
|
446
|
+
// so only send `follow` when there is no fixed anchor to override it.
|
|
447
|
+
const params = new URLSearchParams();
|
|
448
|
+
if (args.spec) params.set('spec', args.spec);
|
|
449
|
+
else if (args.follow) params.set('follow', args.follow);
|
|
450
|
+
if (args.panel) params.set('panel', args.panel);
|
|
451
|
+
const search = params.toString();
|
|
452
|
+
const query = search ? `?${search}` : '';
|
|
453
|
+
const url = `${base}/solo/${encodeURIComponent(args.surface)}${query}`;
|
|
454
|
+
|
|
455
|
+
// Placement is best-effort: no work area (non-Windows, probe failure) means
|
|
456
|
+
// no position flags and today's behavior.
|
|
457
|
+
const side = resolvePlaceSide(args);
|
|
458
|
+
const placement =
|
|
459
|
+
side === 'none'
|
|
460
|
+
? null
|
|
461
|
+
: computeWindowPlacement({
|
|
462
|
+
workArea: getPrimaryWorkArea(),
|
|
463
|
+
side,
|
|
464
|
+
width: args.width,
|
|
465
|
+
// Full work-area height unless --height was passed.
|
|
466
|
+
height: args.heightExplicit ? args.height : undefined,
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
if (args.printOnly) {
|
|
470
|
+
const cmd = buildLaunchCommand(url, args, placement);
|
|
471
|
+
process.stdout.write(
|
|
472
|
+
`hs view (--print, nothing opened): place=${side}${placement ? '' : ' (no placement)'}\n` +
|
|
473
|
+
` ${cmd.file} ${cmd.args.join(' ')}\n` +
|
|
474
|
+
(placement ? ` agent window should take: ${agentSideFor(side)}\n` : ''),
|
|
475
|
+
);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
launchAppWindow(url, args, placement);
|
|
480
|
+
const size = placement
|
|
481
|
+
? `${placement.width}x${placement.height} at ${placement.x},${placement.y}`
|
|
482
|
+
: `${args.width}x${args.height}`;
|
|
483
|
+
process.stdout.write(
|
|
484
|
+
`hs view: opened ${url} in an app-mode ${args.browser === 'chrome' ? 'Chrome' : 'Edge'} window ` +
|
|
485
|
+
`(${size}). Close the window to close; re-run to reopen.\n`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Run only as the CLI entry point. Without this guard, importing the module —
|
|
490
|
+
// which is how `resolveWindowWidth` is tested — would spawn a browser window.
|
|
491
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
492
|
+
main().catch((err) => {
|
|
493
|
+
process.stderr.write(`hs view: ${err?.message || err}\n`);
|
|
494
|
+
process.exitCode = 1;
|
|
495
|
+
});
|
|
496
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/hs-worktree.mjs — `hs worktree list|alloc|release|reap|rebind`.
|
|
3
|
+
// Task: HS-V9XH (epic HS-03DO "Claim-bound worktree isolation for parallel agent
|
|
4
|
+
// sessions"). Thin shell: parse argv (scripts/lib/hs-worktree-args.mjs), then call
|
|
5
|
+
// dev-factory/lib/worktree-allocator.mjs's exported functions IN-PROCESS — never
|
|
6
|
+
// shells out to the allocator's own CLI mode, so there is exactly one process per
|
|
7
|
+
// invocation and JSON output never has to round-trip through a child's stdout.
|
|
8
|
+
//
|
|
9
|
+
// Exit codes:
|
|
10
|
+
// 0 ok
|
|
11
|
+
// 1 usage (unknown verb/flag — from the parser)
|
|
12
|
+
// 2 check-failed — ONLY `reap --expect-removed` that removed nothing
|
|
13
|
+
// 11 bad-arg (missing required flag / value-flag with no value — from the parser)
|
|
14
|
+
// 13 write-failed (the allocator threw on a git/fs write — alloc or rebind)
|
|
15
|
+
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
import { parseWorktreeArgs } from "./lib/hs-worktree-args.mjs";
|
|
20
|
+
import {
|
|
21
|
+
makeCtx,
|
|
22
|
+
DEFAULT_DEPS,
|
|
23
|
+
keyInfoFor,
|
|
24
|
+
worktreePathFor,
|
|
25
|
+
allocateForClaim,
|
|
26
|
+
markReleased,
|
|
27
|
+
rebind,
|
|
28
|
+
listWorktrees,
|
|
29
|
+
reapWorktrees,
|
|
30
|
+
} from "../dev-factory/lib/worktree-allocator.mjs";
|
|
31
|
+
|
|
32
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const REPO_ROOT = path.resolve(__dirname, "..");
|
|
34
|
+
|
|
35
|
+
const EXIT_OK = 0;
|
|
36
|
+
const EXIT_CHECK_FAILED = 2;
|
|
37
|
+
const EXIT_WRITE_FAILED = 13;
|
|
38
|
+
|
|
39
|
+
// ── human-output formatting ─────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
function formatAge(ms) {
|
|
42
|
+
if (ms == null || !Number.isFinite(ms) || ms < 0) return "-";
|
|
43
|
+
const s = Math.floor(ms / 1000);
|
|
44
|
+
if (s < 60) return `${s}s`;
|
|
45
|
+
const m = Math.floor(s / 60);
|
|
46
|
+
if (m < 60) return `${m}m`;
|
|
47
|
+
const h = Math.floor(m / 60);
|
|
48
|
+
if (h < 24) return `${h}h`;
|
|
49
|
+
const d = Math.floor(h / 24);
|
|
50
|
+
return `${d}d`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatPid(pid, alive) {
|
|
54
|
+
if (pid == null) return "-";
|
|
55
|
+
return `${pid}(${alive ? "alive" : "dead"})`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderTable(rows) {
|
|
59
|
+
const headers = ["PATH", "BRANCH", "KIND", "SPEC", "SESSION", "PID", "AGE", "DIRTY", "RELEASED"];
|
|
60
|
+
const data = rows.map((w) => [
|
|
61
|
+
w.path,
|
|
62
|
+
w.branch || "(detached)",
|
|
63
|
+
w.kind,
|
|
64
|
+
w.specId || "-",
|
|
65
|
+
w.sessionId || "-",
|
|
66
|
+
formatPid(w.pid, w.pidAlive),
|
|
67
|
+
formatAge(w.ageMs),
|
|
68
|
+
w.dirtyCount == null ? "?" : String(w.dirtyCount),
|
|
69
|
+
w.releasedAt ? "yes" : "-",
|
|
70
|
+
]);
|
|
71
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...data.map((r) => String(r[i]).length)));
|
|
72
|
+
const line = (cols) => cols.map((c, i) => String(c).padEnd(widths[i])).join(" ").trimEnd();
|
|
73
|
+
return [line(headers), ...data.map(line)];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── agent identity ──────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Session id default per task brief: env only, never the MCP's fuller
|
|
80
|
+
* detectAgentIdentity precedence (Cursor/CI vars, etc) — a --session flag
|
|
81
|
+
* always wins.
|
|
82
|
+
*/
|
|
83
|
+
function resolveSessionId(flags) {
|
|
84
|
+
return (
|
|
85
|
+
flags.session
|
|
86
|
+
|| process.env.CLAUDE_CODE_SESSION_ID
|
|
87
|
+
|| process.env.HEYSPEX_SESSION_ID
|
|
88
|
+
|| "cli"
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Agent default per task brief: the same detection the MCP uses
|
|
94
|
+
* (vendor/heyspex-mcp/mcp-config.mjs detectAgentIdentity) when it is
|
|
95
|
+
* trivially importable, else HEYSPEX_AGENT env or "cli". A --agent flag
|
|
96
|
+
* always wins over both.
|
|
97
|
+
*/
|
|
98
|
+
async function resolveAgent(flags, sessionId) {
|
|
99
|
+
if (flags.agent) return flags.agent;
|
|
100
|
+
try {
|
|
101
|
+
const mod = await import("../vendor/heyspex-mcp/mcp-config.mjs");
|
|
102
|
+
if (typeof mod.detectAgentIdentity === "function") {
|
|
103
|
+
const identity = mod.detectAgentIdentity(undefined, sessionId);
|
|
104
|
+
if (identity?.agent) return identity.agent;
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// Not importable in this install shape (e.g. a published package without
|
|
108
|
+
// vendor/heyspex-mcp) — fall back below.
|
|
109
|
+
}
|
|
110
|
+
return process.env.HEYSPEX_AGENT || "cli";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── main ─────────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
async function main() {
|
|
116
|
+
const parsed = parseWorktreeArgs(process.argv.slice(2));
|
|
117
|
+
if (!parsed.ok) {
|
|
118
|
+
process.stderr.write(`hs worktree: ${parsed.message}\n`);
|
|
119
|
+
process.exitCode = parsed.code;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const { verb, flags } = parsed;
|
|
123
|
+
const ctx = makeCtx({ repo: REPO_ROOT, worktreeRoot: flags.root || undefined });
|
|
124
|
+
|
|
125
|
+
const emit = (obj, lines) => {
|
|
126
|
+
if (flags.json) process.stdout.write(`${JSON.stringify(obj, null, 2)}\n`);
|
|
127
|
+
else for (const l of lines) process.stdout.write(`${l}\n`);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (verb === "list") {
|
|
131
|
+
const worktrees = listWorktrees(ctx);
|
|
132
|
+
emit({ worktrees }, renderTable(worktrees));
|
|
133
|
+
process.exitCode = EXIT_OK;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (verb === "alloc") {
|
|
138
|
+
const info = keyInfoFor(flags.spec);
|
|
139
|
+
if (flags.dryRun) {
|
|
140
|
+
const wtPath = worktreePathFor(ctx, flags.spec);
|
|
141
|
+
const baseSha = (DEFAULT_DEPS.git(ctx, ctx.repo, "rev-parse", "HEAD").stdout || "").trim() || null;
|
|
142
|
+
emit(
|
|
143
|
+
{ dryRun: true, path: wtPath, branch: info.branch, baseSha },
|
|
144
|
+
[`would allocate ${wtPath} on ${info.branch} from ${baseSha ?? "(unknown)"}`],
|
|
145
|
+
);
|
|
146
|
+
process.exitCode = EXIT_OK;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const sessionId = resolveSessionId(flags);
|
|
150
|
+
const agent = await resolveAgent(flags, sessionId);
|
|
151
|
+
try {
|
|
152
|
+
const r = allocateForClaim(ctx, { specId: flags.spec, sessionId, agent, pid: process.pid });
|
|
153
|
+
emit(r, [`${r.created ? "allocated" : "re-attached"} ${r.path} on ${r.branch}`]);
|
|
154
|
+
process.exitCode = EXIT_OK;
|
|
155
|
+
} catch (e) {
|
|
156
|
+
process.stderr.write(`hs worktree alloc: ${e.message}\n`);
|
|
157
|
+
process.exitCode = EXIT_WRITE_FAILED;
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (verb === "release") {
|
|
163
|
+
const r = markReleased(ctx, { specId: flags.spec });
|
|
164
|
+
emit(r, [
|
|
165
|
+
r.found
|
|
166
|
+
? `${r.alreadyReleased ? "already released" : "released"} ${worktreePathFor(ctx, flags.spec)}`
|
|
167
|
+
: `no worktree for ${flags.spec}`,
|
|
168
|
+
]);
|
|
169
|
+
process.exitCode = EXIT_OK;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (verb === "rebind") {
|
|
174
|
+
try {
|
|
175
|
+
const r = rebind(ctx, { fromSpecId: flags.spec, toSpecId: flags.to });
|
|
176
|
+
emit(r, [r.found ? `rebound ${flags.spec} -> ${flags.to} on ${r.branch}` : `no worktree for ${flags.spec}`]);
|
|
177
|
+
process.exitCode = EXIT_OK;
|
|
178
|
+
} catch (e) {
|
|
179
|
+
process.stderr.write(`hs worktree rebind: ${e.message}\n`);
|
|
180
|
+
process.exitCode = EXIT_WRITE_FAILED;
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// reap
|
|
186
|
+
const r = await reapWorktrees(ctx, {
|
|
187
|
+
dryRun: flags.dryRun,
|
|
188
|
+
includeForeign: flags.includeForeign,
|
|
189
|
+
yes: flags.yes,
|
|
190
|
+
});
|
|
191
|
+
emit(r, [
|
|
192
|
+
`${r.dryRun ? "would remove" : "removed"} ${r.removed.length}: ${r.removed.join(", ") || "(none)"}`,
|
|
193
|
+
...r.skipped.map((s) => ` skipped ${s.path} - ${s.reason}`),
|
|
194
|
+
]);
|
|
195
|
+
process.exitCode = flags.expectRemoved && r.removed.length === 0 ? EXIT_CHECK_FAILED : EXIT_OK;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
main().catch((err) => {
|
|
199
|
+
process.stderr.write(`hs worktree: ${err?.stack ?? err}\n`);
|
|
200
|
+
process.exitCode = 1;
|
|
201
|
+
});
|