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,348 @@
|
|
|
1
|
+
// Byte budget for every MCP READ tool
|
|
2
|
+
// ([[HS-4H8T-8KK5-TASK-every-mcp-read-tool-declares-a-byte]] under
|
|
3
|
+
// [[HS-49AL-8KK5-EPIC-hot-row-cache-and-bounded-agent-tools]]).
|
|
4
|
+
//
|
|
5
|
+
// Before this module no byte budget existed anywhere in the MCP server: memory
|
|
6
|
+
// recall bounded by COUNT and sliced, list tools that page
|
|
7
|
+
// ([[HS-E66W-8KK5-TASK-mcp-list-tools-page-with-a-cursor-and-a]], `list-tools.mjs`)
|
|
8
|
+
// fit their own 8 KB page, and everything else returned whatever it had —
|
|
9
|
+
// `heyspex_list_known_tags` once handed an agent 138 KB in one call
|
|
10
|
+
// ([[HS-7QM6-1Q1C-DIAG-heyspex-list-known-tags-response-busts-t]]).
|
|
11
|
+
//
|
|
12
|
+
// The contract:
|
|
13
|
+
// - every read-shaped catalog tool (name matching {@link READ_TOOL_NAME_PATTERN})
|
|
14
|
+
// declares `budget: { max_bytes }` in `tool-catalog.mjs`
|
|
15
|
+
// ({@link DEFAULT_TOOL_BUDGET_BYTES} unless the tool says otherwise);
|
|
16
|
+
// - `tool-dispatch.mjs` runs every declared budget through
|
|
17
|
+
// {@link applyToolBudget} on the way out, so a tool that pages natively
|
|
18
|
+
// (list-tools, known-tags) is merely re-checked, and a tool that does NOT
|
|
19
|
+
// page is bounded anyway;
|
|
20
|
+
// - {@link enforceByteBudget} measures `Buffer.byteLength(JSON.stringify(result))`
|
|
21
|
+
// — the whole MCP result envelope, a strict superset of the text the agent
|
|
22
|
+
// reads — and when over budget drops trailing items from the payload's list,
|
|
23
|
+
// re-points `next_cursor` at the FIRST DROPPED item, and sets
|
|
24
|
+
// `truncated: true`. It never returns more than `max_bytes`: a payload with
|
|
25
|
+
// no list (or whose single remaining item is itself too big) has its
|
|
26
|
+
// longest strings cut with a marker, and as the last resort the payload is
|
|
27
|
+
// replaced by a tiny `RESPONSE_OVER_BUDGET` envelope.
|
|
28
|
+
//
|
|
29
|
+
// The generic `next_cursor` is a BUDGET cursor: an opaque base64url token the
|
|
30
|
+
// dispatcher recognises on the way IN (`applyToolBudget` strips it, re-issues
|
|
31
|
+
// the tool's own `after`, and resumes from the first dropped item), so a tool
|
|
32
|
+
// with no native paging is still walkable to exhaustion one bounded page at a
|
|
33
|
+
// time, and a tool WITH native paging keeps its own cursor untouched (the
|
|
34
|
+
// budget cursor carries it inside).
|
|
35
|
+
|
|
36
|
+
/** 48 000 bytes ≈ 12k tokens at ~4 bytes/token — the default ceiling for one read-tool reply. */
|
|
37
|
+
export const DEFAULT_TOOL_BUDGET_BYTES = 48_000;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Read-shaped tool names: the verb segment (after any namespace segment such as
|
|
41
|
+
* `design_`) is one of list / get / recall / search / next / ground.
|
|
42
|
+
* `heyspex_get_procedure`, `heyspex_design_get_pattern`, `heyspex_list_known_tags`,
|
|
43
|
+
* `heyspex_next_on_deck`, `heyspex_ground_plan` all match; `heyspex_claim_spec`
|
|
44
|
+
* does not.
|
|
45
|
+
*/
|
|
46
|
+
export const READ_TOOL_NAME_PATTERN = /^heyspex_(?:[a-z0-9]+_)*?(?:list|get|recall|search|next|ground)_/;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Suffix-shaped reads (`heyspex_claim_list`, `heyspex_events_show`,
|
|
50
|
+
* `heyspex_settings_get`, …) — not in the task's prefix list, but every one of
|
|
51
|
+
* them returns rows or a record to an agent and so declares a budget too.
|
|
52
|
+
*/
|
|
53
|
+
export const READ_TOOL_SUFFIX_PATTERN = /_(?:list|show|get|load)$/;
|
|
54
|
+
|
|
55
|
+
export function isReadShapedToolName(name) {
|
|
56
|
+
return READ_TOOL_NAME_PATTERN.test(name) || READ_TOOL_SUFFIX_PATTERN.test(name);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const TRUNCATION_MARKER = '…[truncated: byte budget]';
|
|
60
|
+
|
|
61
|
+
/** The measure every budget is checked against — the whole MCP result envelope. */
|
|
62
|
+
export function measureResultBytes(result) {
|
|
63
|
+
return Buffer.byteLength(JSON.stringify(result), 'utf8');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Budget cursor ────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* `{ key, at, index, after }` → opaque base64url token.
|
|
70
|
+
* key — the payload field holding the list that was trimmed
|
|
71
|
+
* at — identity ({@link itemIdentity}) of the FIRST DROPPED item, or null
|
|
72
|
+
* index — absolute position of that item in the tool's untrimmed list
|
|
73
|
+
* after — the tool's OWN `after` argument for this page (null when none),
|
|
74
|
+
* re-issued verbatim when the walker comes back with this cursor
|
|
75
|
+
*/
|
|
76
|
+
export function encodeBudgetCursor({ key, at, index, after }) {
|
|
77
|
+
return Buffer.from(
|
|
78
|
+
JSON.stringify({ budget: 1, key: String(key), at: at == null ? null : String(at), index: Number(index) || 0, after: after ?? null }),
|
|
79
|
+
'utf8',
|
|
80
|
+
).toString('base64url');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Decode a budget cursor; `null` for anything that is not one (a tool's own cursor, garbage, non-strings). */
|
|
84
|
+
export function decodeBudgetCursor(cursor) {
|
|
85
|
+
if (typeof cursor !== 'string' || cursor.length === 0) return null;
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
|
|
88
|
+
if (parsed && typeof parsed === 'object' && parsed.budget === 1 && typeof parsed.key === 'string') {
|
|
89
|
+
return {
|
|
90
|
+
key: parsed.key,
|
|
91
|
+
at: typeof parsed.at === 'string' ? parsed.at : null,
|
|
92
|
+
index: Number.isFinite(parsed.index) && parsed.index >= 0 ? Math.floor(parsed.index) : 0,
|
|
93
|
+
after: typeof parsed.after === 'string' && parsed.after ? parsed.after : null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// not a budget cursor
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Stable identity for a list item — the field a resume can find it by again. Null when nothing fits. */
|
|
103
|
+
export function itemIdentity(item) {
|
|
104
|
+
if (item == null) return null;
|
|
105
|
+
if (typeof item !== 'object') return String(item);
|
|
106
|
+
for (const field of ['id', 'name', 'tag', 'key', 'path', 'spec_id']) {
|
|
107
|
+
const v = item[field];
|
|
108
|
+
if (typeof v === 'string' && v) return v;
|
|
109
|
+
if (typeof v === 'number') return String(v);
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The top-level array field carrying the most bytes — the list a trim should shorten. */
|
|
115
|
+
export function pickListKey(payload) {
|
|
116
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null;
|
|
117
|
+
let best = null;
|
|
118
|
+
let bestBytes = -1;
|
|
119
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
120
|
+
if (!Array.isArray(value) || value.length === 0) continue;
|
|
121
|
+
const bytes = Buffer.byteLength(JSON.stringify(value), 'utf8');
|
|
122
|
+
if (bytes > bestBytes) {
|
|
123
|
+
best = key;
|
|
124
|
+
bestBytes = bytes;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return best;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parsePayload(result) {
|
|
131
|
+
const block = result?.content?.[0];
|
|
132
|
+
if (!block || block.type !== 'text' || typeof block.text !== 'string') return { block: null, payload: null };
|
|
133
|
+
try {
|
|
134
|
+
const payload = JSON.parse(block.text);
|
|
135
|
+
return { block, payload: payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : null };
|
|
136
|
+
} catch {
|
|
137
|
+
return { block, payload: null };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function withPayload(result, block, payload, pretty) {
|
|
142
|
+
const text = pretty ? JSON.stringify(payload, null, 2) : JSON.stringify(payload);
|
|
143
|
+
return { ...result, content: [{ ...block, text }, ...result.content.slice(1)] };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isPretty(text) {
|
|
147
|
+
return /^\s*[{[]\s*\n/.test(text);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Resume a tool's untrimmed result from a budget cursor: drop every item before
|
|
152
|
+
* the first-dropped item the cursor names. Returns the sliced result plus the
|
|
153
|
+
* absolute `offset` the slice started at (so the NEXT budget cursor's `index`
|
|
154
|
+
* stays absolute). A cursor whose list key does not exist in the payload
|
|
155
|
+
* resumes nothing (offset 0) rather than failing — the walker still terminates
|
|
156
|
+
* because the page is re-fitted below.
|
|
157
|
+
*/
|
|
158
|
+
export function resumeFromBudgetCursor(result, cursor) {
|
|
159
|
+
const { block, payload } = parsePayload(result);
|
|
160
|
+
if (!block || !payload || !Array.isArray(payload[cursor.key])) return { result, offset: 0 };
|
|
161
|
+
const list = payload[cursor.key];
|
|
162
|
+
let offset = -1;
|
|
163
|
+
if (cursor.at != null) offset = list.findIndex((item) => itemIdentity(item) === cursor.at);
|
|
164
|
+
if (offset === -1) offset = Math.min(cursor.index, list.length);
|
|
165
|
+
const next = { ...payload, [cursor.key]: list.slice(offset) };
|
|
166
|
+
return { result: withPayload(result, block, next, isPretty(block.text)), offset };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Enforcement ──────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
/** Largest `n` in `[lo, hi]` for which `fits(n)` holds (`fits` is monotone: smaller n ⇒ fewer bytes). */
|
|
172
|
+
function largestFitting(lo, hi, fits) {
|
|
173
|
+
let best = lo - 1;
|
|
174
|
+
while (lo <= hi) {
|
|
175
|
+
const mid = (lo + hi) >>> 1;
|
|
176
|
+
if (fits(mid)) {
|
|
177
|
+
best = mid;
|
|
178
|
+
lo = mid + 1;
|
|
179
|
+
} else {
|
|
180
|
+
hi = mid - 1;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return best;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Walk a payload for its string leaves (depth-limited) — `[path, length]`, longest first. */
|
|
187
|
+
function longestStrings(value, path = [], depth = 0, out = []) {
|
|
188
|
+
if (depth > 4 || value == null) return out;
|
|
189
|
+
if (typeof value === 'string') {
|
|
190
|
+
out.push({ path, length: value.length });
|
|
191
|
+
} else if (Array.isArray(value)) {
|
|
192
|
+
value.forEach((v, i) => longestStrings(v, [...path, i], depth + 1, out));
|
|
193
|
+
} else if (typeof value === 'object') {
|
|
194
|
+
for (const [k, v] of Object.entries(value)) longestStrings(v, [...path, k], depth + 1, out);
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function getAt(obj, path) {
|
|
200
|
+
return path.reduce((acc, k) => (acc == null ? undefined : acc[k]), obj);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function setAt(obj, path, value) {
|
|
204
|
+
const clone = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
205
|
+
if (path.length === 1) {
|
|
206
|
+
clone[path[0]] = value;
|
|
207
|
+
return clone;
|
|
208
|
+
}
|
|
209
|
+
clone[path[0]] = setAt(obj[path[0]], path.slice(1), value);
|
|
210
|
+
return clone;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function overBudgetEnvelope(result, { tool, bytes, maxBytes }) {
|
|
214
|
+
const text = JSON.stringify({
|
|
215
|
+
error: 'RESPONSE_OVER_BUDGET',
|
|
216
|
+
tool: tool ?? null,
|
|
217
|
+
truncated: true,
|
|
218
|
+
bytes,
|
|
219
|
+
max_bytes: maxBytes,
|
|
220
|
+
message: `The tool reply was ${bytes} bytes against a ${maxBytes}-byte budget and could not be trimmed to fit. Narrow the call (filters, a smaller limit, compact mode) and retry.`,
|
|
221
|
+
});
|
|
222
|
+
return { ...(result?.isError ? { isError: true } : {}), content: [{ type: 'text', text }] };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Bound one MCP tool result to `maxBytes` (measured on the whole envelope).
|
|
227
|
+
*
|
|
228
|
+
* @param result the MCP result `{ content: [{ type: 'text', text }], isError? }`
|
|
229
|
+
* @param options.maxBytes the budget ({@link DEFAULT_TOOL_BUDGET_BYTES} when omitted)
|
|
230
|
+
* @param options.listKey the payload field to trim; auto-detected ({@link pickListKey}) when omitted
|
|
231
|
+
* @param options.tool tool name, recorded in the trim envelope
|
|
232
|
+
* @param options.after the tool's own `after` arg for this page — carried inside the budget cursor
|
|
233
|
+
* @param options.indexOffset absolute index of the payload list's first item (a resumed page)
|
|
234
|
+
* @returns {{ result: object, bytes: number, truncated: boolean, dropped: number }}
|
|
235
|
+
*/
|
|
236
|
+
export function enforceByteBudget(result, options = {}) {
|
|
237
|
+
const maxBytes = Number.isFinite(options.maxBytes) && options.maxBytes > 0 ? options.maxBytes : DEFAULT_TOOL_BUDGET_BYTES;
|
|
238
|
+
const tool = options.tool ?? null;
|
|
239
|
+
if (!result || !Array.isArray(result.content)) {
|
|
240
|
+
return { result, bytes: measureResultBytes(result ?? null), truncated: false, dropped: 0 };
|
|
241
|
+
}
|
|
242
|
+
let bytes = measureResultBytes(result);
|
|
243
|
+
if (bytes <= maxBytes) return { result, bytes, truncated: false, dropped: 0 };
|
|
244
|
+
|
|
245
|
+
const { block, payload } = parsePayload(result);
|
|
246
|
+
let dropped = 0;
|
|
247
|
+
|
|
248
|
+
if (block && payload) {
|
|
249
|
+
const pretty = isPretty(block.text);
|
|
250
|
+
let current = payload;
|
|
251
|
+
|
|
252
|
+
// 1. Drop trailing list items; the cursor points at the first dropped one.
|
|
253
|
+
const key = options.listKey && Array.isArray(payload[options.listKey]) ? options.listKey : pickListKey(payload);
|
|
254
|
+
if (key && Array.isArray(current[key]) && current[key].length > 1) {
|
|
255
|
+
const list = current[key];
|
|
256
|
+
const offset = Number(options.indexOffset) || 0;
|
|
257
|
+
const shaped = (n) => ({
|
|
258
|
+
...current,
|
|
259
|
+
[key]: list.slice(0, n),
|
|
260
|
+
next_cursor: encodeBudgetCursor({ key, at: itemIdentity(list[n]), index: offset + n, after: options.after ?? null }),
|
|
261
|
+
truncated: true,
|
|
262
|
+
budget: { max_bytes: maxBytes, list_key: key, returned: n, dropped: list.length - n },
|
|
263
|
+
});
|
|
264
|
+
const fits = (n) => measureResultBytes(withPayload(result, block, shaped(n), pretty)) <= maxBytes;
|
|
265
|
+
// Never an empty page with a cursor: at least one item stays, and if that
|
|
266
|
+
// lone item still does not fit the string cut below handles it.
|
|
267
|
+
const keep = Math.max(1, largestFitting(1, list.length - 1, fits));
|
|
268
|
+
current = shaped(keep);
|
|
269
|
+
dropped = list.length - keep;
|
|
270
|
+
bytes = measureResultBytes(withPayload(result, block, current, pretty));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 2. Still over: cut the longest strings, longest first, until it fits.
|
|
274
|
+
if (bytes > maxBytes) {
|
|
275
|
+
current = { ...current, truncated: true, budget: { ...(current.budget ?? {}), max_bytes: maxBytes, strings_cut: 0 } };
|
|
276
|
+
let guard = 0;
|
|
277
|
+
while (bytes > maxBytes && guard < 64) {
|
|
278
|
+
guard += 1;
|
|
279
|
+
const candidates = longestStrings(current).filter((c) => c.length > TRUNCATION_MARKER.length + 8 && c.path[0] !== 'next_cursor');
|
|
280
|
+
if (candidates.length === 0) break;
|
|
281
|
+
candidates.sort((a, b) => b.length - a.length);
|
|
282
|
+
const target = candidates[0];
|
|
283
|
+
const value = getAt(current, target.path);
|
|
284
|
+
const overshoot = bytes - maxBytes;
|
|
285
|
+
// Cut generously: the overshoot plus the marker, but never below 8 chars of the original.
|
|
286
|
+
const keepChars = Math.max(8, value.length - overshoot - TRUNCATION_MARKER.length);
|
|
287
|
+
current = setAt(current, target.path, value.slice(0, keepChars) + TRUNCATION_MARKER);
|
|
288
|
+
current.budget = { ...current.budget, strings_cut: (current.budget.strings_cut ?? 0) + 1 };
|
|
289
|
+
bytes = measureResultBytes(withPayload(result, block, current, pretty));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (bytes <= maxBytes) {
|
|
294
|
+
return { result: withPayload(result, block, current, pretty), bytes, truncated: true, dropped };
|
|
295
|
+
}
|
|
296
|
+
} else if (block) {
|
|
297
|
+
// 3. Non-JSON text: cut the text itself to fit.
|
|
298
|
+
const text = block.text;
|
|
299
|
+
const fitsText = (n) => measureResultBytes({ ...result, content: [{ ...block, text: text.slice(0, n) + TRUNCATION_MARKER }, ...result.content.slice(1)] }) <= maxBytes;
|
|
300
|
+
const keep = largestFitting(0, text.length, fitsText);
|
|
301
|
+
if (keep >= 0) {
|
|
302
|
+
const cut = { ...result, content: [{ ...block, text: text.slice(0, keep) + TRUNCATION_MARKER }, ...result.content.slice(1)] };
|
|
303
|
+
return { result: cut, bytes: measureResultBytes(cut), truncated: true, dropped: 0 };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 4. Last resort — the envelope is a few hundred bytes and always fits.
|
|
308
|
+
const envelope = overBudgetEnvelope(result, { tool, bytes, maxBytes });
|
|
309
|
+
return { result: envelope, bytes: measureResultBytes(envelope), truncated: true, dropped };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The dispatcher's hook: run `invoke(args)` for a tool that declares `budget`,
|
|
314
|
+
* resuming from a budget cursor on the way in and enforcing the budget on the
|
|
315
|
+
* way out. A tool without a budget is invoked untouched. `invoke` may return a
|
|
316
|
+
* Promise (several handlers are async) — the result is unwrapped either way.
|
|
317
|
+
*/
|
|
318
|
+
export function applyToolBudget(name, args, invoke, budget) {
|
|
319
|
+
const maxBytes = Number(budget?.max_bytes);
|
|
320
|
+
if (!Number.isFinite(maxBytes) || maxBytes <= 0) return invoke(args);
|
|
321
|
+
|
|
322
|
+
let effectiveArgs = args ?? {};
|
|
323
|
+
let resume = null;
|
|
324
|
+
if (typeof effectiveArgs.after === 'string') {
|
|
325
|
+
resume = decodeBudgetCursor(effectiveArgs.after);
|
|
326
|
+
if (resume) {
|
|
327
|
+
effectiveArgs = { ...effectiveArgs };
|
|
328
|
+
if (resume.after) effectiveArgs.after = resume.after;
|
|
329
|
+
else delete effectiveArgs.after;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const finish = (raw) => {
|
|
334
|
+
let result = raw;
|
|
335
|
+
let indexOffset = 0;
|
|
336
|
+
if (resume) ({ result, offset: indexOffset } = resumeFromBudgetCursor(result, resume));
|
|
337
|
+
return enforceByteBudget(result, {
|
|
338
|
+
maxBytes,
|
|
339
|
+
listKey: budget.list_key,
|
|
340
|
+
tool: name,
|
|
341
|
+
after: typeof effectiveArgs.after === 'string' ? effectiveArgs.after : null,
|
|
342
|
+
indexOffset,
|
|
343
|
+
}).result;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const raw = invoke(effectiveArgs);
|
|
347
|
+
return raw && typeof raw.then === 'function' ? raw.then(finish) : finish(raw);
|
|
348
|
+
}
|
|
@@ -5,6 +5,23 @@
|
|
|
5
5
|
* Mirrors `.heyspex/focus.yaml` (build pipeline) and `.heyspex/research-focus.yaml`
|
|
6
6
|
* (research pipeline). Honored unconditionally when present, same semantics as
|
|
7
7
|
* [[focus-yaml-always-wins-in-hs-next]].
|
|
8
|
+
*
|
|
9
|
+
* CLOUD WRITE MODE ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]):
|
|
10
|
+
* this module is deliberately NOT routed through the console, and that is a
|
|
11
|
+
* decision rather than an omission. The pin it writes is LOCAL MACHINE STATE,
|
|
12
|
+
* not vault content — it lives in the repo's dot-directory, never under
|
|
13
|
+
* `specs/`, carries no id/type/frontmatter contract, and is not indexed by
|
|
14
|
+
* anything. It is therefore not a split brain the way a stray `specs/*.md`
|
|
15
|
+
* would be: a pin is per-workstation by design, and two machines are SUPPOSED
|
|
16
|
+
* to be able to hold different ones. HS-KR16 §Scope made exactly this call for
|
|
17
|
+
* the promote-research review-gate markers in the same dot-directory.
|
|
18
|
+
*
|
|
19
|
+
* The corollary: because it is not vault content, it also has no spec `type`
|
|
20
|
+
* and therefore no `POST /api/specs` route it could be filed through — routing
|
|
21
|
+
* it would mean inventing a type, which is the thing
|
|
22
|
+
* `CLOUD_MODE_UNSUPPORTED_WRITE` exists to refuse. Keeping the disk write is
|
|
23
|
+
* the correct behaviour in BOTH modes; it is not degraded and does not need to
|
|
24
|
+
* be reported.
|
|
8
25
|
*/
|
|
9
26
|
|
|
10
27
|
import {
|
|
@@ -14,8 +31,10 @@ import {
|
|
|
14
31
|
mkdirSync,
|
|
15
32
|
rmSync,
|
|
16
33
|
} from 'node:fs';
|
|
17
|
-
import { join } from 'node:path';
|
|
34
|
+
import { join, isAbsolute, resolve, sep } from 'node:path';
|
|
18
35
|
import { parseSimpleFocusYaml } from './capture-focus.mjs';
|
|
36
|
+
import { assertPathUnderRoot } from '../../scripts/lib/shared-path-helpers.mjs';
|
|
37
|
+
|
|
19
38
|
|
|
20
39
|
export function reviewFocusPath(vaultRoot) {
|
|
21
40
|
return join(vaultRoot, '.heyspex', 'review-focus.yaml');
|
|
@@ -36,6 +55,11 @@ export function writeReviewFocus(vaultRoot, { specId, reason }) {
|
|
|
36
55
|
return { error: 'MISSING_SPEC_ID', message: 'spec_id is required' };
|
|
37
56
|
}
|
|
38
57
|
const dir = join(vaultRoot, '.heyspex');
|
|
58
|
+
try {
|
|
59
|
+
assertPathUnderRoot(vaultRoot, dir, 'review-focus write');
|
|
60
|
+
} catch (err) {
|
|
61
|
+
return { error: 'BAD_VAULT_ROOT', message: err.message };
|
|
62
|
+
}
|
|
39
63
|
mkdirSync(dir, { recursive: true });
|
|
40
64
|
const lines = [
|
|
41
65
|
'# HeySpex review-on-deck — MCP /hs-review ([[HS-TASK-RVDC-heyspex-review-on-deck-mcp-and-hs-review]])',
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Per-vault default roadmap resolution. The MCP used to hardcode the HeySpex
|
|
2
|
+
// platform vault's roadmap id ('HS-ROADMAP-PRODUCT'), so every customer vault
|
|
3
|
+
// (whose roadmap has a different id, e.g. the `hs init` starter
|
|
4
|
+
// 'HS-START-ROADMAP') failed `heyspex_next_on_deck` with ROADMAP_NOT_FOUND.
|
|
5
|
+
//
|
|
6
|
+
// Precedence (explicit `roadmap_id` arg is applied by callers, above this):
|
|
7
|
+
// HEYSPEX_ROADMAP_ID env > the vault's own `type: roadmap` spec >
|
|
8
|
+
// 'HS-ROADMAP-PRODUCT' (last resort).
|
|
9
|
+
// Resolved per call from the vault root of THAT call: one MCP process can
|
|
10
|
+
// serve several vaults, so nothing here is cached.
|
|
11
|
+
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
14
|
+
|
|
15
|
+
export const PLATFORM_ROADMAP_ID = 'HS-ROADMAP-PRODUCT';
|
|
16
|
+
|
|
17
|
+
const SCAN_DIRS = ['specs/roadmaps', 'specs/roadmap', 'specs'];
|
|
18
|
+
|
|
19
|
+
function headFields(file) {
|
|
20
|
+
let text;
|
|
21
|
+
try {
|
|
22
|
+
text = readFileSync(file, 'utf8');
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
27
|
+
if (!m) return null;
|
|
28
|
+
const grab = (k) => {
|
|
29
|
+
const r = new RegExp('^' + k + ':[ \t]*(.+?)[ \t\r]*$', 'm').exec(m[1]);
|
|
30
|
+
return r ? r[1].replace(/^["']|["']$/g, '') : null;
|
|
31
|
+
};
|
|
32
|
+
return { type: grab('type'), id: grab('id') };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** All `type: roadmap` spec ids found on disk under the vault's specs/. */
|
|
36
|
+
export function listVaultRoadmapIds(vaultRoot) {
|
|
37
|
+
const ids = new Set();
|
|
38
|
+
if (!vaultRoot) return [];
|
|
39
|
+
for (const dir of SCAN_DIRS) {
|
|
40
|
+
const abs = join(vaultRoot, dir);
|
|
41
|
+
if (!existsSync(abs)) continue;
|
|
42
|
+
let names;
|
|
43
|
+
try {
|
|
44
|
+
names = readdirSync(abs);
|
|
45
|
+
} catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (const n of names) {
|
|
49
|
+
if (!n.endsWith('.md')) continue;
|
|
50
|
+
const f = headFields(join(abs, n));
|
|
51
|
+
if (f && f.type === 'roadmap' && f.id) ids.add(f.id);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [...ids].sort();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @returns {{ id: string, source: 'env'|'vault'|'vault-multiple'|'fallback', candidates?: string[] }}
|
|
59
|
+
*/
|
|
60
|
+
export function resolveDefaultRoadmap(vaultRoot, env = process.env) {
|
|
61
|
+
const fromEnv = env.HEYSPEX_ROADMAP_ID?.trim();
|
|
62
|
+
if (fromEnv) return { id: fromEnv, source: 'env' };
|
|
63
|
+
const ids = listVaultRoadmapIds(vaultRoot);
|
|
64
|
+
if (ids.length === 1) return { id: ids[0], source: 'vault' };
|
|
65
|
+
if (ids.length > 1) {
|
|
66
|
+
const id = ids.includes(PLATFORM_ROADMAP_ID) ? PLATFORM_ROADMAP_ID : ids[0];
|
|
67
|
+
return { id, source: 'vault-multiple', candidates: ids };
|
|
68
|
+
}
|
|
69
|
+
return { id: PLATFORM_ROADMAP_ID, source: 'fallback' };
|
|
70
|
+
}
|