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,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* search-specs.mjs — `heyspex_search_specs`, the vault's text search for agents
|
|
3
|
+
* ([[HS-YFLS]] fix 2).
|
|
4
|
+
*
|
|
5
|
+
* THE DEFECT THIS CLOSES
|
|
6
|
+
* ----------------------
|
|
7
|
+
* The bug-filing protocol tells an agent to "search specs/diagnoses/ before
|
|
8
|
+
* filing" — and no MCP tool searched specs by text. In PROD mode the vault is
|
|
9
|
+
* CLOUD-primary and the local mirror is incomplete (537 files on disk against
|
|
10
|
+
* 559 in the system of record), so a `grep` over the checkout is not a search
|
|
11
|
+
* of the vault: it answers "not on this disk", which is not the same claim.
|
|
12
|
+
* `heyspex_resolve_focus_intent` is the nearest thing and it is built for a
|
|
13
|
+
* different job — given the AC2 symptom sentence it returns 20 specs matched on
|
|
14
|
+
* a broad `security` tag and neither of the two that actually describe it.
|
|
15
|
+
*
|
|
16
|
+
* WHAT THIS IS
|
|
17
|
+
* ------------
|
|
18
|
+
* A duplicate check. It reads the CLOUD vault through `GET /api/search` — the
|
|
19
|
+
* system of record, never local disk — asks a handful of bounded angles
|
|
20
|
+
* ({@link ./symptom-query.mjs}, see that module for why one query cannot work),
|
|
21
|
+
* merges the windows, and returns enough per row to JUDGE a duplicate without a
|
|
22
|
+
* second call: id, name, type, status, a marked-up snippet, and which angles
|
|
23
|
+
* matched.
|
|
24
|
+
*
|
|
25
|
+
* A NEGATIVE MUST MEAN "NOT THERE", NEVER "COULD NOT LOOK" ([[HS-YFLS]] AC3)
|
|
26
|
+
* -------------------------------------------------------------------------
|
|
27
|
+
* Every way of not finding something is reported as a DIFFERENT thing:
|
|
28
|
+
*
|
|
29
|
+
* result: "no_match" — the corpus WAS searched and holds nothing. `searched`
|
|
30
|
+
* is true and `queries_tried` lists exactly what was
|
|
31
|
+
* asked. This is the only shape an agent may act on by
|
|
32
|
+
* filing a new spec.
|
|
33
|
+
* result: "no_query" — nothing searchable was extracted from the input.
|
|
34
|
+
* isError envelopes — CONSOLE_UNREACHABLE / CONSOLE_NOT_SIGNED_IN /
|
|
35
|
+
* VAULT_SCOPE_UNRESOLVED (a transient 503, retry) /
|
|
36
|
+
* VAULT_SCOPE_UNAUTHORIZED (the caller's vault could
|
|
37
|
+
* not be resolved, so the endpoint returned an empty
|
|
38
|
+
* page that means nothing about the corpus) /
|
|
39
|
+
* SEARCH_FAILED / SEARCH_RESPONSE_UNRECOGNIZED.
|
|
40
|
+
*
|
|
41
|
+
* An angle that fails fails the WHOLE call. A partial answer is indistinguishable
|
|
42
|
+
* from a complete one at the call site, and a silently partial duplicate check is
|
|
43
|
+
* exactly how the duplicate gets filed.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { consoleFetch as defaultConsoleFetch, controlOk } from './console-transport.mjs';
|
|
47
|
+
import { buildSymptomQueries } from './symptom-query.mjs';
|
|
48
|
+
|
|
49
|
+
/** Rows requested per angle. Bounded: this tool never walks the vault. */
|
|
50
|
+
const ANGLE_LIMIT = 20;
|
|
51
|
+
|
|
52
|
+
/** Default / maximum rows returned to the agent. */
|
|
53
|
+
const DEFAULT_TOP_K = 10;
|
|
54
|
+
const MAX_TOP_K = 30;
|
|
55
|
+
|
|
56
|
+
/** Snippets are for judging a duplicate, not for reading the spec. */
|
|
57
|
+
const MAX_SNIPPET_CHARS = 320;
|
|
58
|
+
|
|
59
|
+
function errorResult(payload) {
|
|
60
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Classify a non-OK `/api/search` response. Each code sends the agent somewhere
|
|
65
|
+
* different, which is the entire point of not collapsing them into "no results".
|
|
66
|
+
*/
|
|
67
|
+
function searchFailure(res, query) {
|
|
68
|
+
const status = res?.status ?? 0;
|
|
69
|
+
const code =
|
|
70
|
+
status === 0
|
|
71
|
+
? 'CONSOLE_UNREACHABLE'
|
|
72
|
+
: status === 401
|
|
73
|
+
? 'CONSOLE_NOT_SIGNED_IN'
|
|
74
|
+
: status === 503
|
|
75
|
+
? 'VAULT_SCOPE_UNRESOLVED'
|
|
76
|
+
: 'SEARCH_FAILED';
|
|
77
|
+
const message =
|
|
78
|
+
code === 'CONSOLE_UNREACHABLE'
|
|
79
|
+
? 'The console did not answer, so the vault was NOT searched. Check heyspex_dev_server_status (or console reachability in cloud mode) — do NOT read this as "no matching spec".'
|
|
80
|
+
: code === 'CONSOLE_NOT_SIGNED_IN'
|
|
81
|
+
? 'The console refused this caller (401), so the vault was NOT searched. Re-pair the MCP bearer — do NOT read this as "no matching spec".'
|
|
82
|
+
: code === 'VAULT_SCOPE_UNRESOLVED'
|
|
83
|
+
? 'This console instance has not resolved its active vault yet (transient). Retry shortly — the vault was NOT searched.'
|
|
84
|
+
: `GET /api/search returned ${status}, so the vault was NOT searched.`;
|
|
85
|
+
return errorResult({
|
|
86
|
+
error: code,
|
|
87
|
+
message,
|
|
88
|
+
searched: false,
|
|
89
|
+
status,
|
|
90
|
+
url: res?.url ?? null,
|
|
91
|
+
attempted_query: query ?? null,
|
|
92
|
+
console_response: res?.body ?? null,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** `<mark>`s are kept — they show WHERE the match landed — but the row is capped. */
|
|
97
|
+
function normalizeSnippet(value) {
|
|
98
|
+
if (typeof value !== 'string') return '';
|
|
99
|
+
const flat = value.replace(/\s+/g, ' ').trim();
|
|
100
|
+
return flat.length > MAX_SNIPPET_CHARS ? `${flat.slice(0, MAX_SNIPPET_CHARS)}…` : flat;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeRow(row) {
|
|
104
|
+
const id = typeof row?.id === 'string' ? row.id : null;
|
|
105
|
+
if (!id) return null;
|
|
106
|
+
const rank = Number(row?.rank);
|
|
107
|
+
return {
|
|
108
|
+
id,
|
|
109
|
+
name: String(row?.name ?? id),
|
|
110
|
+
type: String(row?.type ?? 'unknown'),
|
|
111
|
+
status: String(row?.status ?? 'unknown').toLowerCase(),
|
|
112
|
+
rank: Number.isFinite(rank) && rank > 0 ? rank : 0,
|
|
113
|
+
snippet: normalizeSnippet(row?.snippet),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* One bounded page for one angle. Returns `{ rows, mode }` on success, or a
|
|
119
|
+
* ready-to-return error envelope under `{ failure }` — never an empty page
|
|
120
|
+
* standing in for a failure.
|
|
121
|
+
*/
|
|
122
|
+
async function fetchAngle(fetchImpl, { q, limit, type, status }) {
|
|
123
|
+
const params = new URLSearchParams();
|
|
124
|
+
params.set('q', q);
|
|
125
|
+
params.set('limit', String(limit));
|
|
126
|
+
params.set('scope', 'body');
|
|
127
|
+
if (type) params.set('type', type);
|
|
128
|
+
if (status) params.set('status', status);
|
|
129
|
+
|
|
130
|
+
let res;
|
|
131
|
+
try {
|
|
132
|
+
res = await fetchImpl(`/api/search?${params.toString()}`);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
return {
|
|
135
|
+
failure: errorResult({
|
|
136
|
+
error: 'CONSOLE_UNREACHABLE',
|
|
137
|
+
message: `The console search endpoint threw (${err?.message ?? err}), so the vault was NOT searched. Do NOT read this as "no matching spec".`,
|
|
138
|
+
searched: false,
|
|
139
|
+
attempted_query: q,
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (!res || !res.ok) return { failure: searchFailure(res, q) };
|
|
144
|
+
|
|
145
|
+
const body = res.body && typeof res.body === 'object' ? res.body : {};
|
|
146
|
+
// The route answers a caller whose vault scope it could not authorize with a
|
|
147
|
+
// 200 and an EMPTY page carrying `vaultScope.authorized: false`. That page is
|
|
148
|
+
// a permission verdict, not a statement about the corpus — the one shape that
|
|
149
|
+
// would otherwise read as a clean "no duplicate exists".
|
|
150
|
+
if (body.vaultScope && body.vaultScope.authorized === false) {
|
|
151
|
+
return {
|
|
152
|
+
failure: errorResult({
|
|
153
|
+
error: 'VAULT_SCOPE_UNAUTHORIZED',
|
|
154
|
+
message: `The console could not resolve a vault for this caller (${body.vaultScope.reason ?? 'unknown'}), so the empty page it returned says nothing about the vault. Do NOT read this as "no matching spec".`,
|
|
155
|
+
searched: false,
|
|
156
|
+
reason: body.vaultScope.reason ?? null,
|
|
157
|
+
attempted_query: q,
|
|
158
|
+
}),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const rows = Array.isArray(body.rows) ? body.rows : Array.isArray(body.results) ? body.results : null;
|
|
162
|
+
// `mode` ("fts" | "bm25" | "trgm" | "memory") is the endpoint's proof that it
|
|
163
|
+
// actually ran a query. Its absence alongside no rows is an unrecognized
|
|
164
|
+
// shape, and an unrecognized shape is never a negative result.
|
|
165
|
+
if (rows === null || typeof body.mode !== 'string') {
|
|
166
|
+
return {
|
|
167
|
+
failure: errorResult({
|
|
168
|
+
error: 'SEARCH_RESPONSE_UNRECOGNIZED',
|
|
169
|
+
message: 'GET /api/search answered 200 with a body this tool cannot read as a search result (no `rows`/`mode`), so the vault was NOT searched.',
|
|
170
|
+
searched: false,
|
|
171
|
+
attempted_query: q,
|
|
172
|
+
console_response: body,
|
|
173
|
+
}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return { rows: rows.map(normalizeRow).filter(Boolean), mode: body.mode };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* `heyspex_search_specs` — find specs in the CLOUD vault by symptom text.
|
|
181
|
+
*
|
|
182
|
+
* args: { query, top_k?, type?, status? }
|
|
183
|
+
* deps: { fetchImpl } (tests inject; production uses the shared consoleFetch)
|
|
184
|
+
*/
|
|
185
|
+
export async function handleSearchSpecs(args = {}, { fetchImpl = defaultConsoleFetch } = {}) {
|
|
186
|
+
const query = typeof args.query === 'string' ? args.query.trim() : '';
|
|
187
|
+
if (!query) {
|
|
188
|
+
return errorResult({
|
|
189
|
+
error: 'SEARCH_QUERY_REQUIRED',
|
|
190
|
+
message: '`query` is required — pass the symptom sentence (or the concept) you would otherwise grep for.',
|
|
191
|
+
searched: false,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const topKRaw = Number(args.top_k);
|
|
195
|
+
const topK = Number.isFinite(topKRaw) && topKRaw > 0 ? Math.min(MAX_TOP_K, Math.trunc(topKRaw)) : DEFAULT_TOP_K;
|
|
196
|
+
const type = typeof args.type === 'string' && args.type.trim() ? args.type.trim() : undefined;
|
|
197
|
+
const status = typeof args.status === 'string' && args.status.trim() ? args.status.trim() : undefined;
|
|
198
|
+
|
|
199
|
+
const { terms, queries } = buildSymptomQueries(query);
|
|
200
|
+
if (queries.length === 0) {
|
|
201
|
+
return controlOk({
|
|
202
|
+
query,
|
|
203
|
+
terms,
|
|
204
|
+
queries_tried: [],
|
|
205
|
+
searched: false,
|
|
206
|
+
result: 'no_query',
|
|
207
|
+
match_count: 0,
|
|
208
|
+
matches: [],
|
|
209
|
+
message: 'No searchable term survived normalization (the input was punctuation or stopwords only) — the vault was NOT searched. Rephrase with the concrete nouns from the symptom.',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** id → merged candidate. */
|
|
214
|
+
const byId = new Map();
|
|
215
|
+
const queriesTried = [];
|
|
216
|
+
let mode = null;
|
|
217
|
+
|
|
218
|
+
for (const angle of queries) {
|
|
219
|
+
// eslint-disable-next-line no-await-in-loop -- a handful of bounded angles, deliberately sequential so a failure stops the walk.
|
|
220
|
+
const page = await fetchAngle(fetchImpl, { q: angle.q, limit: ANGLE_LIMIT, type, status });
|
|
221
|
+
if (page.failure) return page.failure;
|
|
222
|
+
mode = mode ?? page.mode;
|
|
223
|
+
queriesTried.push({ label: angle.label, q: angle.q, hits: page.rows.length, mode: page.mode });
|
|
224
|
+
page.rows.forEach((row, position) => {
|
|
225
|
+
const existing = byId.get(row.id);
|
|
226
|
+
if (!existing) {
|
|
227
|
+
byId.set(row.id, { ...row, matched_by: [angle.label], best_position: position });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
// Keep the strongest evidence each angle produced for this spec.
|
|
231
|
+
existing.rank = Math.max(existing.rank, row.rank);
|
|
232
|
+
existing.best_position = Math.min(existing.best_position, position);
|
|
233
|
+
if (!existing.snippet && row.snippet) existing.snippet = row.snippet;
|
|
234
|
+
if (!existing.matched_by.includes(angle.label)) existing.matched_by.push(angle.label);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Agreement across angles first (a spec the quoted concept AND the loose term
|
|
239
|
+
// window both surfaced is the likeliest duplicate), then the endpoint's own
|
|
240
|
+
// `ts_rank_cd`, then the order it handed rows back — which is the only signal
|
|
241
|
+
// left on the disk read tier, where every rank is 0.
|
|
242
|
+
const matches = [...byId.values()]
|
|
243
|
+
.sort(
|
|
244
|
+
(a, b) =>
|
|
245
|
+
b.matched_by.length - a.matched_by.length ||
|
|
246
|
+
b.rank - a.rank ||
|
|
247
|
+
a.best_position - b.best_position ||
|
|
248
|
+
a.id.localeCompare(b.id),
|
|
249
|
+
)
|
|
250
|
+
.slice(0, topK)
|
|
251
|
+
.map(({ best_position: _position, ...row }) => row);
|
|
252
|
+
|
|
253
|
+
if (matches.length === 0) {
|
|
254
|
+
return controlOk({
|
|
255
|
+
query,
|
|
256
|
+
terms,
|
|
257
|
+
queries_tried: queriesTried,
|
|
258
|
+
searched: true,
|
|
259
|
+
corpus: 'cloud-vault (GET /api/search — the system of record, not local disk)',
|
|
260
|
+
result: 'no_match',
|
|
261
|
+
match_count: 0,
|
|
262
|
+
matches: [],
|
|
263
|
+
mode,
|
|
264
|
+
message: `Searched the vault on ${queriesTried.length} angle(s) and found NOTHING. This is an explicit negative: the corpus was read and holds no spec matching these terms — it is safe to file.`,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return controlOk({
|
|
269
|
+
query,
|
|
270
|
+
terms,
|
|
271
|
+
queries_tried: queriesTried,
|
|
272
|
+
searched: true,
|
|
273
|
+
corpus: 'cloud-vault (GET /api/search — the system of record, not local disk)',
|
|
274
|
+
result: 'match',
|
|
275
|
+
match_count: matches.length,
|
|
276
|
+
mode,
|
|
277
|
+
matches,
|
|
278
|
+
message: 'Read each match before filing: a spec that already describes this symptom must be commented on or reopened, not duplicated.',
|
|
279
|
+
});
|
|
280
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// vendor/heyspex-mcp/seat-block-ledger.mjs
|
|
2
|
+
//
|
|
3
|
+
// Per-session "has the DEV-FACTORY SEAT banner already gone out this session"
|
|
4
|
+
// decision for the two vendor surfaces that used to each keep their OWN flag:
|
|
5
|
+
// - tool-dispatch.mjs handleInitialize() → surface 'mcp_instructions'
|
|
6
|
+
// - cli.mjs maybePrependSeatBanner() → surface 'preflight' (when the
|
|
7
|
+
// first tool called is heyspex_preflight) or 'tool_result' otherwise
|
|
8
|
+
//
|
|
9
|
+
// Before this module, `initialize` always appended the banner to `instructions`
|
|
10
|
+
// AND, independently, the first tools/call response ALSO got the banner
|
|
11
|
+
// prepended (a separate `seatBannerSeenSessions` Set tracked only THAT surface) —
|
|
12
|
+
// so a caller could see "DEV-FACTORY SEAT: ..." twice in one session. That is
|
|
13
|
+
// exactly the bug spexharness/src/bootstrap-injection.mjs's engine contract
|
|
14
|
+
// exists to close for every surface (its own header calls out wiring
|
|
15
|
+
// vendor/heyspex-mcp as the deferred follow-up — HS-8U8X).
|
|
16
|
+
//
|
|
17
|
+
// spexharness/ is dev-only tooling: it is deliberately NOT part of the published
|
|
18
|
+
// `heyspex` npm package (see dev-factory-bridge.mjs for the identical open-core
|
|
19
|
+
// concern with dev-factory/spexcrawl/). scripts/verify-mcp-package.mjs's
|
|
20
|
+
// `closure` check fails the build if the server's STATIC import graph reaches a
|
|
21
|
+
// file the package doesn't ship, so the engine contract is reached the same way
|
|
22
|
+
// spexcrawl is: a memoized `await import()`, never a static import. A
|
|
23
|
+
// published install (no spexharness/ tree on disk) gets the fallback ledger
|
|
24
|
+
// below instead — same "at most one surface, across the whole session" contract,
|
|
25
|
+
// just without a shared spexharness bootstrap ledger to defer to.
|
|
26
|
+
|
|
27
|
+
/** @type {Promise<typeof import('../../spexharness/src/bootstrap-injection.mjs')>|undefined} */
|
|
28
|
+
let enginePromise;
|
|
29
|
+
|
|
30
|
+
function loadEngine() {
|
|
31
|
+
if (!enginePromise) {
|
|
32
|
+
enginePromise = import('../../spexharness/src/bootstrap-injection.mjs').catch(() => null);
|
|
33
|
+
}
|
|
34
|
+
return enginePromise;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Local, dependency-free ledger with the exact `shouldIncludeSeatBlock` /
|
|
39
|
+
* `markSeatBlockSent` shape bootstrap-injection.mjs's `withSeatBlockLedger`
|
|
40
|
+
* exposes — used when the engine module is not reachable (a published install,
|
|
41
|
+
* or any import failure). "No bootstrap yet" degrades to "emit once, guarded",
|
|
42
|
+
* never to "emit unguarded".
|
|
43
|
+
*/
|
|
44
|
+
function createFallbackLedger() {
|
|
45
|
+
const sent = new Set();
|
|
46
|
+
return {
|
|
47
|
+
shouldIncludeSeatBlock(surface) {
|
|
48
|
+
if (sent.has(surface)) return false;
|
|
49
|
+
return sent.size === 0;
|
|
50
|
+
},
|
|
51
|
+
markSeatBlockSent(surface) {
|
|
52
|
+
sent.add(surface);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function fallbackSeatBlockForSurface(ledger, surface, seatLine) {
|
|
58
|
+
if (!ledger.shouldIncludeSeatBlock(surface)) return null;
|
|
59
|
+
ledger.markSeatBlockSent(surface);
|
|
60
|
+
return seatLine ?? null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** One ledger controller per session id — two sessions sharing this process (e.g. two
|
|
64
|
+
* fleet workers spawned back-to-back) never see each other's sends. */
|
|
65
|
+
const controllersBySession = new Map();
|
|
66
|
+
|
|
67
|
+
async function getSeatBlockController(sessionId) {
|
|
68
|
+
const key = sessionId || 'local';
|
|
69
|
+
const existing = controllersBySession.get(key);
|
|
70
|
+
if (existing) return existing;
|
|
71
|
+
|
|
72
|
+
const engine = await loadEngine();
|
|
73
|
+
const ledger = engine?.withSeatBlockLedger ? engine.withSeatBlockLedger({}) : createFallbackLedger();
|
|
74
|
+
const forSurface = engine?.seatBlockForSurface
|
|
75
|
+
? (surface, seatLine) => engine.seatBlockForSurface(ledger, surface, seatLine)
|
|
76
|
+
: (surface, seatLine) => fallbackSeatBlockForSurface(ledger, surface, seatLine);
|
|
77
|
+
|
|
78
|
+
const controller = { forSurface };
|
|
79
|
+
controllersBySession.set(key, controller);
|
|
80
|
+
return controller;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Decide whether `surface` should emit the seat banner for `sessionId` this
|
|
85
|
+
* session — sourced from the engine contract's per-session ledger when
|
|
86
|
+
* spexharness/ is reachable (memoized per session id), else a local fallback
|
|
87
|
+
* with identical "exactly once, across every surface" semantics. Returns the
|
|
88
|
+
* banner text when `surface` is cleared to send it, else `null`. Never throws;
|
|
89
|
+
* a falsy `seatLine` (not enabled / unknown caller) short-circuits to `null`
|
|
90
|
+
* WITHOUT touching the ledger, so an ineligible caller never "uses up" the
|
|
91
|
+
* session's one send.
|
|
92
|
+
*
|
|
93
|
+
* @param {string|null|undefined} sessionId
|
|
94
|
+
* @param {string} surface one of bootstrap-injection.mjs's SEAT_BLOCK_SURFACES
|
|
95
|
+
* ('mcp_instructions', 'preflight', 'tool_result', 'bootstrap').
|
|
96
|
+
* @param {string|null|undefined} seatLine
|
|
97
|
+
* @returns {Promise<string|null>}
|
|
98
|
+
*/
|
|
99
|
+
export async function seatBlockForSessionSurface(sessionId, surface, seatLine) {
|
|
100
|
+
if (!seatLine) return null;
|
|
101
|
+
try {
|
|
102
|
+
const controller = await getSeatBlockController(sessionId);
|
|
103
|
+
return controller.forSurface(surface, seatLine);
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Test-only: forget every per-session ledger and the memoized engine probe. */
|
|
110
|
+
export function resetSeatBlockLedgerForTests() {
|
|
111
|
+
controllersBySession.clear();
|
|
112
|
+
enginePromise = undefined;
|
|
113
|
+
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { readFileSync } from "node:fs";
|
|
21
21
|
import path from "node:path";
|
|
22
|
-
import {
|
|
22
|
+
import { PACKAGE_ROOT } from "./package-root.mjs";
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Read and parse <vaultRoot>/dev-factory/seats.json.
|
|
@@ -169,11 +169,7 @@ function selfTest() {
|
|
|
169
169
|
};
|
|
170
170
|
|
|
171
171
|
// Repo root is two dirs up from this file (vendor/heyspex-mcp/ → repo root).
|
|
172
|
-
const repoRoot =
|
|
173
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
174
|
-
"..",
|
|
175
|
-
"..",
|
|
176
|
-
);
|
|
172
|
+
const repoRoot = PACKAGE_ROOT;
|
|
177
173
|
|
|
178
174
|
// 1. Real registry smoke-read.
|
|
179
175
|
const real = readSeatsRegistry(repoRoot);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// session-owner.mjs — the ONE place the MCP server learns which account owns
|
|
2
|
+
// the documents it files ([[HS-WE87-WW9R-TASK-thread-the-gated-caller-through-every]],
|
|
3
|
+
// epic [[HS-P1AW-WW9R-EPIC-identity-threading-and-the-end-of-the]] — invariant 1).
|
|
4
|
+
//
|
|
5
|
+
// Until this module the three frontmatter writers (`capture-focus.mjs`,
|
|
6
|
+
// `research-focus.mjs`, `defer-spec.mjs`) each stamped the HeySpex owner's
|
|
7
|
+
// handle as a literal — on a customer machine that put the platform owner's
|
|
8
|
+
// name on the customer's own documents. They now ask here, and when nothing
|
|
9
|
+
// resolves the `owner:` key is OMITTED rather than guessed.
|
|
10
|
+
//
|
|
11
|
+
// Resolution order (no network — this runs inside synchronous writers and
|
|
12
|
+
// under `node --test`):
|
|
13
|
+
// 1. `HS_ACCOUNT_ID` env — the same override the CLI's account reader honours.
|
|
14
|
+
// 2. the credentials store's current account (`credentials.mjs`
|
|
15
|
+
// `readCurrentAccountId`, [[HS-UA9S-WW9R-TASK-store-credentials-per-account-login]]):
|
|
16
|
+
// `current_account_id` of `~/.heyspex/credentials.yaml`, set by `hs login` /
|
|
17
|
+
// `hs mcp pair` from the account GET /api/mcp/session confirmed for the
|
|
18
|
+
// stored bearer and moved by `hs login --switch-account` / `hs logout`.
|
|
19
|
+
// An existing single-account (v1) file has no such key: its account is
|
|
20
|
+
// the `~/.heyspex/config.yaml` `account_id:` the earlier `hs login` wrote
|
|
21
|
+
// ([[HS-630L-WW9R-TASK-one-strict-cli-account-reader-hs-login]]) — read
|
|
22
|
+
// for that case only, never written any more (one account-id store).
|
|
23
|
+
// 3. null — the writer omits `owner:`.
|
|
24
|
+
//
|
|
25
|
+
// This is ALSO the CLI's account reader: `resolveConfiguredAccountId` in
|
|
26
|
+
// spex-console/src/lib/cli/vault-workspace-shared.ts and `readConfiguredAccountId`
|
|
27
|
+
// in spex-console/src/lib/cli/install-identity.ts both delegate to
|
|
28
|
+
// `resolveSessionOwnerUncached`, so one precedence serves `hs vault`, `hs connect`,
|
|
29
|
+
// `hs install` and every MCP frontmatter writer. Its bearer twin is
|
|
30
|
+
// `./credentials.mjs` (HS-HL0D).
|
|
31
|
+
//
|
|
32
|
+
// Cached per process once resolved from the real environment (the MCP server
|
|
33
|
+
// is spawned once per session; pairing changes restart it). Callers inject
|
|
34
|
+
// `env` / `homeDir` / `readFile` for tests, which bypasses the cache.
|
|
35
|
+
|
|
36
|
+
import { heyspexHome } from '../../scripts/lib/heyspex-home.mjs';
|
|
37
|
+
import { readFileSync } from 'node:fs';
|
|
38
|
+
import { homedir } from 'node:os';
|
|
39
|
+
import { join } from 'node:path';
|
|
40
|
+
|
|
41
|
+
import { readCurrentAccountId } from './credentials.mjs';
|
|
42
|
+
|
|
43
|
+
const ACCOUNT_ID_LINE_RE = /^account_id:\s*["']?([^"'\r\n#]+)/m;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read the paired account id from `~/.heyspex/config.yaml`, or null.
|
|
47
|
+
* @param {{ homeDir?: string, readFile?: (p: string) => string }} [opts]
|
|
48
|
+
*/
|
|
49
|
+
export function readConfigAccountId({ homeDir = heyspexHome(), readFile } = {}) {
|
|
50
|
+
const read = readFile ?? ((p) => readFileSync(p, 'utf8'));
|
|
51
|
+
try {
|
|
52
|
+
const raw = read(join(homeDir, '.heyspex', 'config.yaml'));
|
|
53
|
+
const match = String(raw ?? '').match(ACCOUNT_ID_LINE_RE);
|
|
54
|
+
const value = match?.[1]?.trim() ?? '';
|
|
55
|
+
return value || null;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve the session owner without caching — the pure core.
|
|
63
|
+
* @param {{ env?: NodeJS.ProcessEnv | Record<string, string | undefined>, homeDir?: string, readFile?: (p: string) => string }} [opts]
|
|
64
|
+
* @returns {string | null}
|
|
65
|
+
*/
|
|
66
|
+
export function resolveSessionOwnerUncached({ env = process.env, homeDir = heyspexHome(), readFile } = {}) {
|
|
67
|
+
const fromEnv = typeof env?.HS_ACCOUNT_ID === 'string' ? env.HS_ACCOUNT_ID.trim() : '';
|
|
68
|
+
if (fromEnv) return fromEnv;
|
|
69
|
+
return readCurrentAccountId({ homeDir, readFile });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let cached = null;
|
|
73
|
+
let cacheFilled = false;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The account every MCP-authored document is stamped with, or null when the
|
|
77
|
+
* machine has no resolvable account (the writers then OMIT `owner:`).
|
|
78
|
+
*
|
|
79
|
+
* The real-environment answer is memoised per process; any injected option
|
|
80
|
+
* bypasses the cache so tests stay hermetic.
|
|
81
|
+
* @param {{ env?: NodeJS.ProcessEnv | Record<string, string | undefined>, homeDir?: string, readFile?: (p: string) => string }} [opts]
|
|
82
|
+
* @returns {string | null}
|
|
83
|
+
*/
|
|
84
|
+
export function resolveSessionOwner(opts) {
|
|
85
|
+
if (opts && Object.keys(opts).length > 0) return resolveSessionOwnerUncached(opts);
|
|
86
|
+
if (!cacheFilled) {
|
|
87
|
+
cached = resolveSessionOwnerUncached();
|
|
88
|
+
cacheFilled = true;
|
|
89
|
+
}
|
|
90
|
+
return cached;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Test seam: forget the per-process answer. */
|
|
94
|
+
export function __resetSessionOwnerCacheForTests() {
|
|
95
|
+
cached = null;
|
|
96
|
+
cacheFilled = false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The `owner:` frontmatter entry for an object-shaped frontmatter — `{ owner }`
|
|
101
|
+
* when an account resolved, `{}` when none did (the key is omitted, never
|
|
102
|
+
* guessed). `explicit` (an `args.owner` the caller supplied) wins.
|
|
103
|
+
* @param {{ explicit?: unknown, resolve?: () => string | null }} [opts]
|
|
104
|
+
*/
|
|
105
|
+
export function ownerFrontmatterEntry({ explicit, resolve = resolveSessionOwner } = {}) {
|
|
106
|
+
const chosen =
|
|
107
|
+
typeof explicit === 'string' && explicit.trim() ? explicit.trim() : (resolve() ?? null);
|
|
108
|
+
return chosen ? { owner: chosen } : {};
|
|
109
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Session hand-off and agent identity tools: heyspex_handoff_record, handoff_load,
|
|
2
|
+
// import_agent. Extracted from cli.mjs under
|
|
3
|
+
// [[HS-BLNF-6W61-DIAG-cli-mjs-has-grown-into-a-13-665-line]] (no behavior change).
|
|
4
|
+
|
|
5
|
+
import { consoleFetch } from './console-transport.mjs';
|
|
6
|
+
import { consoleErrorCode, consoleErrorMessage } from './console-fetch.mjs';
|
|
7
|
+
import { detectAgentIdentity } from './mcp-config.mjs';
|
|
8
|
+
import { resolveReadTier } from './read-path.mjs';
|
|
9
|
+
import { deferControlWrite } from './write-outbox.mjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Import a community agent role via console `POST /api/agents/import` — the MCP
|
|
13
|
+
* face of agent import ([[HS-TASK-Z666]]). Same core as `hs agent import`; no
|
|
14
|
+
* field-mapping logic duplicated in JS.
|
|
15
|
+
*/
|
|
16
|
+
export async function handleImportAgent(args = {}) {
|
|
17
|
+
if (!args.collection || typeof args.collection !== 'string') {
|
|
18
|
+
return {
|
|
19
|
+
isError: true,
|
|
20
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: 'collection is required' }) }],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (!args.role || typeof args.role !== 'string') {
|
|
24
|
+
return {
|
|
25
|
+
isError: true,
|
|
26
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: 'role is required' }) }],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const body = {
|
|
30
|
+
collection: args.collection,
|
|
31
|
+
role: args.role,
|
|
32
|
+
...(args.dry_run === true || args.dry_run === 'true' ? { dry_run: true } : {}),
|
|
33
|
+
};
|
|
34
|
+
const result = await consoleFetch('/api/agents/import', { method: 'POST', body });
|
|
35
|
+
if (!result.ok) {
|
|
36
|
+
const payload = {
|
|
37
|
+
error: consoleErrorCode(result, 'IMPORT_AGENT_FAILED'),
|
|
38
|
+
message: consoleErrorMessage(result, `POST /api/agents/import returned ${result.status}`),
|
|
39
|
+
status: result.status,
|
|
40
|
+
url: result.url,
|
|
41
|
+
console_response: result.body,
|
|
42
|
+
attempted: body,
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
isError: true,
|
|
46
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const responseBody = result.body ?? {};
|
|
50
|
+
const payload = {
|
|
51
|
+
spec_id: responseBody.spec_id ?? null,
|
|
52
|
+
path: responseBody.path ?? null,
|
|
53
|
+
warnings: Array.isArray(responseBody.warnings) ? responseBody.warnings : [],
|
|
54
|
+
...(responseBody.dry_run ? { dry_run: true } : {}),
|
|
55
|
+
console_response: responseBody,
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Upsert the current resume context for this agent/lane into the Neon control
|
|
64
|
+
* plane — [[HS-EPIC-NCPS]] handoff_context / [[HS-EPIC-CLMW]] `hs handoff record`.
|
|
65
|
+
*
|
|
66
|
+
* Self-purging overwrite-in-place (PUT /api/control/handoff-context). The lane
|
|
67
|
+
* defaults to this agent's session id so each session is its own lane. Coordi-
|
|
68
|
+
* nation only — the endpoint rejects durable-knowledge fields.
|
|
69
|
+
*/
|
|
70
|
+
export async function handleHandoffRecord(args = {}) {
|
|
71
|
+
const identity = detectAgentIdentity(args.agent, args.session_id);
|
|
72
|
+
const laneKey =
|
|
73
|
+
typeof args.lane === 'string' && args.lane.trim()
|
|
74
|
+
? args.lane.trim()
|
|
75
|
+
: identity.sessionId;
|
|
76
|
+
const body = {
|
|
77
|
+
lane_key: laneKey,
|
|
78
|
+
agent: identity.agent,
|
|
79
|
+
...(typeof args.spec_id === 'string' ? { spec_id: args.spec_id } : {}),
|
|
80
|
+
...(typeof args.phase === 'string' ? { phase: args.phase } : {}),
|
|
81
|
+
...(typeof args.estimated_time === 'string' ? { estimated_time: args.estimated_time } : {}),
|
|
82
|
+
...(Array.isArray(args.blockers) ? { blockers: args.blockers.map(String) } : {}),
|
|
83
|
+
...(Array.isArray(args.links) ? { links: args.links.map(String) } : {}),
|
|
84
|
+
};
|
|
85
|
+
const result = await consoleFetch('/api/control/handoff-context', {
|
|
86
|
+
method: 'PUT',
|
|
87
|
+
body,
|
|
88
|
+
});
|
|
89
|
+
if (!result.ok) {
|
|
90
|
+
const deferred = await deferControlWrite(result, {
|
|
91
|
+
route: '/api/control/handoff-context',
|
|
92
|
+
method: 'PUT',
|
|
93
|
+
request_body: body,
|
|
94
|
+
acting_agent: identity.agent,
|
|
95
|
+
session_id: identity.sessionId,
|
|
96
|
+
}, { isCloud: async () => (await resolveReadTier()) === 'neon' });
|
|
97
|
+
if (deferred) return deferred;
|
|
98
|
+
const payload = {
|
|
99
|
+
error: consoleErrorCode(result, 'HANDOFF_RECORD_FAILED'),
|
|
100
|
+
message: consoleErrorMessage(result, `Handoff-context endpoint returned ${result.status}`),
|
|
101
|
+
status: result.status,
|
|
102
|
+
url: result.url,
|
|
103
|
+
console_response: result.body,
|
|
104
|
+
attempted: { lane: laneKey },
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
isError: true,
|
|
108
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
content: [{
|
|
113
|
+
type: 'text',
|
|
114
|
+
text: JSON.stringify({ lane: laneKey, console_response: result.body }, null, 2),
|
|
115
|
+
}],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Read the current resume context for a lane from the Neon control plane —
|
|
121
|
+
* [[HS-EPIC-NCPS]] handoff_context / [[HS-EPIC-CLMW]] `hs handoff load`.
|
|
122
|
+
*
|
|
123
|
+
* Defaults to this agent's own lane (its session); `all: true` lists every
|
|
124
|
+
* active lane (parallel-agent off-on-deck). Self-purging — no per-session
|
|
125
|
+
* history; expired rows read as absent.
|
|
126
|
+
*/
|
|
127
|
+
export async function handleHandoffLoad(args = {}) {
|
|
128
|
+
const identity = detectAgentIdentity(args.agent, args.session_id);
|
|
129
|
+
const wantAll = args.all === true;
|
|
130
|
+
const laneKey =
|
|
131
|
+
typeof args.lane === 'string' && args.lane.trim()
|
|
132
|
+
? args.lane.trim()
|
|
133
|
+
: identity.sessionId;
|
|
134
|
+
const path = wantAll
|
|
135
|
+
? '/api/control/handoff-context'
|
|
136
|
+
: `/api/control/handoff-context?lane=${encodeURIComponent(laneKey)}`;
|
|
137
|
+
const result = await consoleFetch(path);
|
|
138
|
+
if (!result.ok) {
|
|
139
|
+
const payload = {
|
|
140
|
+
error: consoleErrorCode(result, 'HANDOFF_LOAD_FAILED'),
|
|
141
|
+
message: consoleErrorMessage(result, `Handoff-context endpoint returned ${result.status}`),
|
|
142
|
+
status: result.status,
|
|
143
|
+
url: result.url,
|
|
144
|
+
console_response: result.body,
|
|
145
|
+
attempted: wantAll ? { all: true } : { lane: laneKey },
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
isError: true,
|
|
149
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
content: [{
|
|
154
|
+
type: 'text',
|
|
155
|
+
text: JSON.stringify(
|
|
156
|
+
wantAll ? result.body : { lane: laneKey, ...result.body },
|
|
157
|
+
null,
|
|
158
|
+
2,
|
|
159
|
+
),
|
|
160
|
+
}],
|
|
161
|
+
};
|
|
162
|
+
}
|