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
|
@@ -36,9 +36,20 @@
|
|
|
36
36
|
*/
|
|
37
37
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
38
38
|
import { join } from 'node:path';
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
collectVaultSpecs,
|
|
41
|
+
validateLinkage,
|
|
42
|
+
buildSpecIndex,
|
|
43
|
+
resolveSpecRef,
|
|
44
|
+
refNamesSpec,
|
|
45
|
+
refsNameSameSpec,
|
|
46
|
+
} from './validate-linkage.mjs';
|
|
40
47
|
import { yamlScalar } from './yaml-scalar.mjs';
|
|
41
|
-
import { toCanonicalFamilyFirstId } from './spec-id-lib.mjs';
|
|
48
|
+
import { toCanonicalFamilyFirstId, lookupFileKeyOf, fileKeyFromBasename } from './spec-id-lib.mjs';
|
|
49
|
+
import { consoleFetch as realConsoleFetch } from './console-transport.mjs';
|
|
50
|
+
import { getConsoleUrl } from './mcp-config.mjs';
|
|
51
|
+
import { resolveVaultWriteMode } from './read-path.mjs';
|
|
52
|
+
import { splitRawSpec, updateSpecViaConsole } from './cloud-vault-writes.mjs';
|
|
42
53
|
|
|
43
54
|
const SUBJECT_TYPES = new Set(['milestone', 'epic', 'task']);
|
|
44
55
|
|
|
@@ -48,9 +59,15 @@ const SUBJECT_TYPES = new Set(['milestone', 'epic', 'task']);
|
|
|
48
59
|
// ---------------------------------------------------------------------------
|
|
49
60
|
|
|
50
61
|
// Canonical, ordering-independent core of a spec id (dual-read: a type-first id
|
|
51
|
-
// and its family-first form resolve to the SAME core
|
|
52
|
-
//
|
|
53
|
-
//
|
|
62
|
+
// and its family-first form resolve to the SAME core). [[HS-EPIC-DLE3]] /
|
|
63
|
+
// [[HS-EPIC-6SNQ]]. Falls back to the legacy anchored regex for unknown-type /
|
|
64
|
+
// hyphenated ids parseSpecId can't read.
|
|
65
|
+
//
|
|
66
|
+
// NOT an identity ([[HS-J6IE-K2EL-DIAG-heyspex-register-spec-resolves-a-task]]):
|
|
67
|
+
// under Naming V3 the core is family+TYPE, which every task in one family
|
|
68
|
+
// shares. It is used here ONLY for the SPEC_NOT_FOUND suggestion list; all
|
|
69
|
+
// resolution and list-membership goes through the shared file-key-first
|
|
70
|
+
// resolver in validate-linkage.mjs (resolveSpecRef / refNamesSpec).
|
|
54
71
|
function idCore(value) {
|
|
55
72
|
const canonical = toCanonicalFamilyFirstId(value);
|
|
56
73
|
if (canonical) return canonical.toUpperCase();
|
|
@@ -58,6 +75,14 @@ function idCore(value) {
|
|
|
58
75
|
return m ? m[1].toUpperCase() : null;
|
|
59
76
|
}
|
|
60
77
|
|
|
78
|
+
// File key a caller's ref carries (bare `HS-OCL4`, display `HS-OCL4-NYPR`, full
|
|
79
|
+
// era-3 basename, or an era-1/2 basename whose slug leads with the key).
|
|
80
|
+
function refFileKey(ref) {
|
|
81
|
+
if (typeof ref !== 'string' || !ref.trim()) return null;
|
|
82
|
+
const t = ref.trim().replace(/^hs-/i, 'HS-');
|
|
83
|
+
return lookupFileKeyOf(t) ?? fileKeyFromBasename(t) ?? null;
|
|
84
|
+
}
|
|
85
|
+
|
|
61
86
|
function stripWikilink(value) {
|
|
62
87
|
if (typeof value !== 'string') return null;
|
|
63
88
|
const inner = value.match(/\[\[([^\]]+)\]\]/);
|
|
@@ -104,8 +129,7 @@ function lineRefMatches(line, ref) {
|
|
|
104
129
|
const im = line.match(/^[ \t]+-[ \t]*(.+?)[ \t]*$/);
|
|
105
130
|
if (!im) return false;
|
|
106
131
|
const target = stripWikilink(im[1].replace(/^['"]|['"]$/g, '')) ?? '';
|
|
107
|
-
|
|
108
|
-
return target === stripWikilink(ref) || (!!refCore && idCore(target) === refCore);
|
|
132
|
+
return refsNameSameSpec(target, ref);
|
|
109
133
|
}
|
|
110
134
|
|
|
111
135
|
/**
|
|
@@ -218,6 +242,53 @@ function loadFile(vaultRoot, relFile) {
|
|
|
218
242
|
* @param {boolean} [opts.updateTimestamps=true]
|
|
219
243
|
*/
|
|
220
244
|
export function registerSpec(vaultRoot, opts = {}) {
|
|
245
|
+
const planned = planRegisterSpec(vaultRoot, opts);
|
|
246
|
+
if (!planned.__plan) return planned;
|
|
247
|
+
return finalize(planned.__plan);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Mode-aware entry point — the one a handler should call
|
|
252
|
+
* ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]).
|
|
253
|
+
*
|
|
254
|
+
* {@link registerSpec} stays SYNCHRONOUS and local-only on purpose: it is
|
|
255
|
+
* called from sync call sites in filing-tools.mjs, cli-verbs.mjs and
|
|
256
|
+
* promote-research.mjs, and async-ifying it cascades through all three (the
|
|
257
|
+
* same bounded deferral
|
|
258
|
+
* [[HS-M0MK-CPFP-TASK-migrate-capture-focus-yaml-to-neon-capture-focus-pin]]
|
|
259
|
+
* took for the vendor capture writers). This wrapper resolves the vault write
|
|
260
|
+
* mode ONCE and dispatches:
|
|
261
|
+
*
|
|
262
|
+
* - `local` — delegates straight to {@link registerSpec}; byte-identical.
|
|
263
|
+
* - `cloud` — plans exactly the same frontmatter surgery, then sends each
|
|
264
|
+
* touched file through `PUT /api/specs/:id` ({@link updateSpecViaConsole})
|
|
265
|
+
* instead of `writeFileSync`. Nothing is written to this machine's disk.
|
|
266
|
+
*
|
|
267
|
+
* `registerSpec` only ever EDITS existing vault files (it links an already-filed
|
|
268
|
+
* spec into its parent's back-reference list), so there is no create branch here
|
|
269
|
+
* — `createSpecViaConsole` belongs to the filing tools, not to this one.
|
|
270
|
+
*
|
|
271
|
+
* @param {object} [deps] Injection seam for tests: `consoleFetch`, `consoleUrl`,
|
|
272
|
+
* `resolveWriteMode`.
|
|
273
|
+
*/
|
|
274
|
+
export async function registerSpecViaMode(vaultRoot, opts = {}, deps = {}) {
|
|
275
|
+
const writeMode =
|
|
276
|
+
opts.writeMode ?? (await (deps.resolveWriteMode ?? resolveVaultWriteMode)()) ?? 'local';
|
|
277
|
+
if (writeMode !== 'cloud') return registerSpec(vaultRoot, opts);
|
|
278
|
+
const planned = planRegisterSpec(vaultRoot, opts);
|
|
279
|
+
if (!planned.__plan) return planned;
|
|
280
|
+
return finalizeViaConsole(planned.__plan, {
|
|
281
|
+
consoleFetch: deps.consoleFetch ?? realConsoleFetch,
|
|
282
|
+
consoleUrl: deps.consoleUrl ?? getConsoleUrl(),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Resolve + stage every edit without committing any of them. Returns either a
|
|
288
|
+
* failure result (the `err(...)` shape callers already read) or `{ __plan }` —
|
|
289
|
+
* everything {@link finalize} / {@link finalizeViaConsole} need.
|
|
290
|
+
*/
|
|
291
|
+
function planRegisterSpec(vaultRoot, opts = {}) {
|
|
221
292
|
const {
|
|
222
293
|
spec_id,
|
|
223
294
|
parent_id = null,
|
|
@@ -233,32 +304,45 @@ export function registerSpec(vaultRoot, opts = {}) {
|
|
|
233
304
|
}
|
|
234
305
|
|
|
235
306
|
const specs = collectVaultSpecs(vaultRoot);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (!
|
|
246
|
-
const
|
|
247
|
-
return
|
|
307
|
+
// Shared Naming V3 resolver ([[HS-J6IE-K2EL-DIAG-heyspex-register-spec-resolves-a-task]]):
|
|
308
|
+
// exact basename → exact id → UNIQUE file key → idCore soft match only for a
|
|
309
|
+
// key-less (era-1/2) ref and only when exactly one spec matches. A ref that
|
|
310
|
+
// names two specs is refused with AMBIGUOUS_SPEC_ID naming the candidates;
|
|
311
|
+
// the old first-wins family-core map re-parented an unrelated sibling.
|
|
312
|
+
const index = buildSpecIndex(specs);
|
|
313
|
+
const resolveRef = (ref) => resolveSpecRef(index, ref).spec;
|
|
314
|
+
const ambiguous = (ref, role) => {
|
|
315
|
+
const r = resolveSpecRef(index, ref);
|
|
316
|
+
if (!r.ambiguous) return null;
|
|
317
|
+
const names = r.candidates.map((c) => c.basename).join(', ');
|
|
318
|
+
return {
|
|
319
|
+
...err(
|
|
320
|
+
'AMBIGUOUS_SPEC_ID',
|
|
321
|
+
`${role} "${stripWikilink(ref) ?? ref}" names ${r.candidates.length} specs (${names}). ` +
|
|
322
|
+
'Under Naming V3 the file key (the token right after HS-) is the only unique key; a family key or ' +
|
|
323
|
+
'family+TYPE core is shared by every spec in the family. Pass the full basename or the bare file key ' +
|
|
324
|
+
'(HS-<fileKey>). NOTHING WAS WRITTEN.',
|
|
325
|
+
),
|
|
326
|
+
candidates: r.candidates,
|
|
327
|
+
matched_by: r.matched_by,
|
|
328
|
+
};
|
|
248
329
|
};
|
|
249
|
-
const listHasSpec = (list, target) =>
|
|
250
|
-
list.some((ref) => {
|
|
251
|
-
const t = stripWikilink(ref) ?? ref;
|
|
252
|
-
return t === target.basename || t === target.id || (!!target.idCore && idCore(t) === target.idCore);
|
|
253
|
-
});
|
|
330
|
+
const listHasSpec = (list, target) => list.some((ref) => refNamesSpec(ref, target));
|
|
254
331
|
|
|
332
|
+
const specAmbiguity = ambiguous(spec_id, 'spec_id');
|
|
333
|
+
if (specAmbiguity) return specAmbiguity;
|
|
255
334
|
const spec = resolveRef(spec_id);
|
|
256
335
|
if (!spec) {
|
|
257
336
|
const want = idCore(spec_id);
|
|
258
337
|
const suggestions = want
|
|
259
338
|
? specs.filter((s) => s.idCore && s.idCore.startsWith(want.slice(0, 7))).map((s) => s.id).slice(0, 3)
|
|
260
339
|
: [];
|
|
261
|
-
|
|
340
|
+
const key = refFileKey(String(spec_id));
|
|
341
|
+
const hint = key
|
|
342
|
+
? ` It carries file key ${key}, which no file under specs/ is stored under — a cloud-only spec with no local ` +
|
|
343
|
+
'copy? Pull the vault (or run in cloud write mode) before registering; the resolver never falls through to a sibling.'
|
|
344
|
+
: '';
|
|
345
|
+
return err('SPEC_NOT_FOUND', `Spec ${spec_id} does not exist in the vault.${hint}`, suggestions);
|
|
262
346
|
}
|
|
263
347
|
if (!SUBJECT_TYPES.has(spec.type)) {
|
|
264
348
|
return err(
|
|
@@ -275,7 +359,20 @@ export function registerSpec(vaultRoot, opts = {}) {
|
|
|
275
359
|
const { abs, raw } = loadFile(vaultRoot, record.file);
|
|
276
360
|
const block = getFrontmatter(raw);
|
|
277
361
|
if (block == null) throw new Error(`No frontmatter in ${record.file}`);
|
|
278
|
-
|
|
362
|
+
// `specId`/`type`/`file` ride along so the cloud commit can address this
|
|
363
|
+
// file by the id `PUT /api/specs/:id` takes, and can refuse the one
|
|
364
|
+
// document shape that cannot survive that route (see finalizeViaConsole).
|
|
365
|
+
const e = {
|
|
366
|
+
abs,
|
|
367
|
+
raw,
|
|
368
|
+
block,
|
|
369
|
+
eol: detectEol(raw),
|
|
370
|
+
actions: [],
|
|
371
|
+
touched: false,
|
|
372
|
+
specId: record.id ?? null,
|
|
373
|
+
type: record.type ?? null,
|
|
374
|
+
file: record.file,
|
|
375
|
+
};
|
|
279
376
|
edits.set(record.file, e);
|
|
280
377
|
return e;
|
|
281
378
|
};
|
|
@@ -285,6 +382,8 @@ export function registerSpec(vaultRoot, opts = {}) {
|
|
|
285
382
|
if (spec.type === 'milestone') {
|
|
286
383
|
// Target roadmap: explicit parent_id wins, else roadmap_id default.
|
|
287
384
|
const roadmapRef = parent_id || roadmap_id;
|
|
385
|
+
const roadmapAmbiguity = ambiguous(roadmapRef, 'roadmap');
|
|
386
|
+
if (roadmapAmbiguity) return roadmapAmbiguity;
|
|
288
387
|
const roadmap = resolveRef(roadmapRef);
|
|
289
388
|
if (!roadmap || !roadmap.manifest) {
|
|
290
389
|
return err('ROADMAP_NOT_FOUND', `Roadmap ${roadmapRef} not found (or it has no roadmap_manifest).`);
|
|
@@ -298,18 +397,20 @@ export function registerSpec(vaultRoot, opts = {}) {
|
|
|
298
397
|
e.touched = true;
|
|
299
398
|
changes.push({ file: roadmap.file, action: `Added ${spec.basename} to roadmap_manifest.milestones` });
|
|
300
399
|
}
|
|
301
|
-
return
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
400
|
+
return {
|
|
401
|
+
__plan: {
|
|
402
|
+
spec,
|
|
403
|
+
parent: { id: roadmap.id, type: 'roadmap' },
|
|
404
|
+
roadmap_id: roadmap.id,
|
|
405
|
+
already,
|
|
406
|
+
edits,
|
|
407
|
+
changes,
|
|
408
|
+
vaultRoot,
|
|
409
|
+
dryRun,
|
|
410
|
+
updateTimestamps,
|
|
411
|
+
now,
|
|
412
|
+
},
|
|
413
|
+
};
|
|
313
414
|
}
|
|
314
415
|
|
|
315
416
|
// epic → milestone (milestone:/epics:) or task → epic (parent:/children:)
|
|
@@ -325,6 +426,8 @@ export function registerSpec(vaultRoot, opts = {}) {
|
|
|
325
426
|
`${spec.id} is a ${spec.type} with no ${parentType} parent. Pass parent_id (an existing HS-${parentType === 'milestone' ? 'MIL' : 'EPIC'}-* id) to register it.`,
|
|
326
427
|
);
|
|
327
428
|
}
|
|
429
|
+
const parentAmbiguity = ambiguous(targetRef, 'parent_id');
|
|
430
|
+
if (parentAmbiguity) return parentAmbiguity;
|
|
328
431
|
const parent = resolveRef(targetRef);
|
|
329
432
|
if (!parent) {
|
|
330
433
|
return err('PARENT_NOT_FOUND', `Parent ${targetRef} not found in the vault.`);
|
|
@@ -337,11 +440,7 @@ export function registerSpec(vaultRoot, opts = {}) {
|
|
|
337
440
|
}
|
|
338
441
|
|
|
339
442
|
// Side 1: the child's own pointer field.
|
|
340
|
-
const fieldPointsAtParent =
|
|
341
|
-
existingParentRef &&
|
|
342
|
-
(stripWikilink(existingParentRef) === parent.basename ||
|
|
343
|
-
stripWikilink(existingParentRef) === parent.id ||
|
|
344
|
-
(!!parent.idCore && idCore(existingParentRef) === parent.idCore));
|
|
443
|
+
const fieldPointsAtParent = !!existingParentRef && refNamesSpec(existingParentRef, parent);
|
|
345
444
|
if (!fieldPointsAtParent) {
|
|
346
445
|
const e = stage(spec);
|
|
347
446
|
e.block = setScalar(e.block, childField, yamlScalar(`[[${parent.basename}]]`), e.eol);
|
|
@@ -361,17 +460,19 @@ export function registerSpec(vaultRoot, opts = {}) {
|
|
|
361
460
|
changes.push({ file: parent.file, action: `Added [[${spec.basename}]] to ${parent.id} ${parentListField}[]` });
|
|
362
461
|
}
|
|
363
462
|
|
|
364
|
-
return
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
463
|
+
return {
|
|
464
|
+
__plan: {
|
|
465
|
+
spec,
|
|
466
|
+
parent: { id: parent.id, type: parent.type },
|
|
467
|
+
already: fieldPointsAtParent && listAlready,
|
|
468
|
+
edits,
|
|
469
|
+
changes,
|
|
470
|
+
vaultRoot,
|
|
471
|
+
dryRun,
|
|
472
|
+
updateTimestamps,
|
|
473
|
+
now,
|
|
474
|
+
},
|
|
475
|
+
};
|
|
375
476
|
}
|
|
376
477
|
|
|
377
478
|
function positionErrorMessage(res) {
|
|
@@ -415,3 +516,114 @@ function finalize({ spec, parent, roadmap_id, already, edits, changes, vaultRoot
|
|
|
415
516
|
}
|
|
416
517
|
return result;
|
|
417
518
|
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Cloud-write-mode twin of {@link finalize}: the SAME staged frontmatter
|
|
522
|
+
* surgery, committed through `PUT /api/specs/:id` instead of `writeFileSync`.
|
|
523
|
+
*
|
|
524
|
+
* Each touched file's spliced markdown is split back into the
|
|
525
|
+
* `{ frontmatter, body }` pair the route takes ({@link splitRawSpec}), and
|
|
526
|
+
* addressed by the spec id the planner already resolved for that file (falling
|
|
527
|
+
* back to the document's own `id:`). A file with no cloud route degrades
|
|
528
|
+
* explicitly as `CLOUD_MODE_UNSUPPORTED_WRITE` rather than quietly landing on a
|
|
529
|
+
* disk the mode forbade.
|
|
530
|
+
*
|
|
531
|
+
* Writes run IN ORDER and stop at the FIRST failure, and nothing is rolled back
|
|
532
|
+
* — the console has no un-write. The failure result therefore names the step
|
|
533
|
+
* that failed AND the steps that already landed, so a caller reports the real
|
|
534
|
+
* half-linked state instead of implying a clean abort (the
|
|
535
|
+
* `commitWritesViaConsole` contract).
|
|
536
|
+
*
|
|
537
|
+
* `validation` is deliberately ABSENT: `validateLinkage` re-reads the vault from
|
|
538
|
+
* local disk, which in cloud mode still holds the PRE-edit bytes (or no clone at
|
|
539
|
+
* all), so running it would report a confidently wrong verdict. The reason rides
|
|
540
|
+
* in `validation_skipped`.
|
|
541
|
+
*/
|
|
542
|
+
async function finalizeViaConsole(
|
|
543
|
+
{ spec, parent, roadmap_id, already, edits, changes, dryRun, updateTimestamps, now },
|
|
544
|
+
{ consoleFetch, consoleUrl },
|
|
545
|
+
) {
|
|
546
|
+
const written = [];
|
|
547
|
+
if (!dryRun) {
|
|
548
|
+
for (const e of edits.values()) {
|
|
549
|
+
if (!e.touched) continue;
|
|
550
|
+
let block = e.block;
|
|
551
|
+
if (updateTimestamps) block = setScalar(block, 'updated', yamlScalar(now), e.eol);
|
|
552
|
+
const next = replaceFrontmatter(e.raw, block);
|
|
553
|
+
if (next == null) throw new Error(`Failed to splice frontmatter for ${e.abs}`);
|
|
554
|
+
// A ROADMAP has no safe cloud route today, and the failure would be
|
|
555
|
+
// silent DATA LOSS rather than an error: `PUT /api/specs/:id` takes a
|
|
556
|
+
// PARSED frontmatter object, and the parser that builds it
|
|
557
|
+
// (`parseFrontmatter` → `parseRoadmapManifest`, vault-io.mjs) still reads
|
|
558
|
+
// the retired v2 manifest shape — it returns `{ phases, initiatives,
|
|
559
|
+
// horizon }` and drops `milestones[]` entirely. Sending that object back
|
|
560
|
+
// would erase the roadmap's whole milestone list. The disk write this
|
|
561
|
+
// mode forbade is NOT used as a fallback; the caller is told which file
|
|
562
|
+
// and why.
|
|
563
|
+
if (e.type === 'roadmap' || /^[ \t]*roadmap_manifest:/m.test(block)) {
|
|
564
|
+
return {
|
|
565
|
+
...err(
|
|
566
|
+
'CLOUD_MODE_UNSUPPORTED_WRITE',
|
|
567
|
+
`register_spec cannot write ${e.file ?? e.abs} in cloud vault-write mode: it carries a `
|
|
568
|
+
+ '`roadmap_manifest:`, and the client-side frontmatter parser behind PUT /api/specs/:id still reads '
|
|
569
|
+
+ 'the retired v2 manifest shape (phases/initiatives/horizon) — round-tripping it would DROP the v3 '
|
|
570
|
+
+ '`milestones[]` list. NOTHING WAS WRITTEN. File the milestone through the console (which owns the '
|
|
571
|
+
+ 'manifest), or run this registration in local write mode.',
|
|
572
|
+
),
|
|
573
|
+
write_mode: 'cloud',
|
|
574
|
+
console_url: consoleUrl ?? null,
|
|
575
|
+
on_disk: false,
|
|
576
|
+
completed_writes: written,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
const { frontmatter, body } = splitRawSpec(next);
|
|
580
|
+
const specId = e.specId || (typeof frontmatter?.id === 'string' ? frontmatter.id.trim() : '');
|
|
581
|
+
if (!specId) {
|
|
582
|
+
return {
|
|
583
|
+
...err(
|
|
584
|
+
'CLOUD_MODE_UNSUPPORTED_WRITE',
|
|
585
|
+
`register_spec cannot write ${e.file ?? e.abs} in cloud vault-write mode: neither the staged spec `
|
|
586
|
+
+ "record nor the document's own frontmatter carries an `id:`, and PUT /api/specs/:id needs one. "
|
|
587
|
+
+ 'NOTHING WAS WRITTEN — the local disk write is deliberately NOT used as a fallback.',
|
|
588
|
+
),
|
|
589
|
+
write_mode: 'cloud',
|
|
590
|
+
console_url: consoleUrl ?? null,
|
|
591
|
+
on_disk: false,
|
|
592
|
+
completed_writes: written,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
const res = await updateSpecViaConsole({ consoleFetch, consoleUrl, specId, frontmatter, body });
|
|
597
|
+
written.push({ file: e.file ?? null, spec_id: res.specId, rel_path: res.relPath });
|
|
598
|
+
} catch (cause) {
|
|
599
|
+
return {
|
|
600
|
+
...err('CONSOLE_UNREACHABLE_CLOUD_MODE', String(cause?.message ?? cause)),
|
|
601
|
+
write_mode: 'cloud',
|
|
602
|
+
console_url: consoleUrl ?? null,
|
|
603
|
+
on_disk: false,
|
|
604
|
+
failed_write: { file: e.file ?? null, spec_id: specId },
|
|
605
|
+
completed_writes: written,
|
|
606
|
+
...(cause?.notFound ? { not_found: true } : {}),
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const result = {
|
|
613
|
+
success: true,
|
|
614
|
+
spec_id: spec.id,
|
|
615
|
+
type: spec.type,
|
|
616
|
+
parent,
|
|
617
|
+
already_registered: !!already && changes.length === 0,
|
|
618
|
+
changes,
|
|
619
|
+
dry_run: !!dryRun,
|
|
620
|
+
write_mode: 'cloud',
|
|
621
|
+
on_disk: false,
|
|
622
|
+
console_url: consoleUrl ?? null,
|
|
623
|
+
console_writes: written,
|
|
624
|
+
validation_skipped:
|
|
625
|
+
'cloud write mode — the local vault copy was not updated, so validateLinkage would re-read pre-edit bytes',
|
|
626
|
+
};
|
|
627
|
+
if (roadmap_id) result.roadmap_id = roadmap_id;
|
|
628
|
+
return result;
|
|
629
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* request-context.mjs — per-call overrides for the console transport.
|
|
3
|
+
*
|
|
4
|
+
* WHY: the stdio MCP server is one process per user, so the console URL and
|
|
5
|
+
* the outbound bearer are process-wide (`HEYSPEX_MCP_TOKEN` env, the
|
|
6
|
+
* credentials file, the shared console-url resolver). The remote endpoint
|
|
7
|
+
* (`POST /api/mcp` on the console, WP6) runs the SAME dispatch in-process on a
|
|
8
|
+
* shared serverless runtime, where one request's caller must never leak into
|
|
9
|
+
* another's — so `process.env` is off limits and the override has to travel
|
|
10
|
+
* with the async call chain instead.
|
|
11
|
+
*
|
|
12
|
+
* An `AsyncLocalStorage` store carries `{ consoleUrl, authHeader }` for the
|
|
13
|
+
* duration of one `runWithRequestContext(ctx, fn)` call, including every
|
|
14
|
+
* `await` beneath it. `console-transport.mjs` consults the two accessors when
|
|
15
|
+
* it binds `makeConsoleFetch`; with no active store they return `undefined`
|
|
16
|
+
* and the transport falls through to today's process-wide resolution, so the
|
|
17
|
+
* stdio server's behavior is unchanged byte-for-byte.
|
|
18
|
+
*/
|
|
19
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
20
|
+
|
|
21
|
+
/** @typedef {{ consoleUrl?: string, authHeader?: Record<string, string>, vaultId?: string }} RequestContext */
|
|
22
|
+
|
|
23
|
+
const storage = new AsyncLocalStorage();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Run `fn` with `ctx` as the active request context. Returns whatever `fn`
|
|
27
|
+
* returns (a Promise stays a Promise; the store follows its continuations).
|
|
28
|
+
*
|
|
29
|
+
* @template T
|
|
30
|
+
* @param {RequestContext} ctx
|
|
31
|
+
* @param {() => T} fn
|
|
32
|
+
* @returns {T}
|
|
33
|
+
*/
|
|
34
|
+
export function runWithRequestContext(ctx, fn) {
|
|
35
|
+
return storage.run(ctx ?? {}, fn);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The active request context, or `undefined` outside any `runWithRequestContext`. */
|
|
39
|
+
export function getRequestContext() {
|
|
40
|
+
return storage.getStore();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Console origin override for the active request, or `undefined` (use the shared resolver). */
|
|
44
|
+
export function contextConsoleUrl() {
|
|
45
|
+
const url = storage.getStore()?.consoleUrl;
|
|
46
|
+
return typeof url === 'string' && url.trim() ? url.replace(/\/+$/, '') : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Outbound auth header(s) for the active request, or `undefined` (use the
|
|
51
|
+
* process-wide `getAuthHeader`). An explicit empty object is honored — it
|
|
52
|
+
* means "this caller presented no credential; do not fall back to the
|
|
53
|
+
* server's own token", which is the correct fail-closed shape on a shared
|
|
54
|
+
* runtime.
|
|
55
|
+
*/
|
|
56
|
+
export function contextAuthHeader() {
|
|
57
|
+
const header = storage.getStore()?.authHeader;
|
|
58
|
+
return header && typeof header === 'object' ? header : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Outbound `x-heyspex-vault-id` for the active request, or `undefined` outside
|
|
63
|
+
* any request context (the stdio server then resolves the repo-bound vault
|
|
64
|
+
* from its cwd). INSIDE a context the answer is ALWAYS an object: the caller's
|
|
65
|
+
* own vault header, or `{}` when it sent none. It is never the server
|
|
66
|
+
* process's own vault (a shared serverless runtime has none), and never the
|
|
67
|
+
* account-wide active pointer: a bearer call that names no vault is refused by
|
|
68
|
+
* the console (`vault-scope-required`). Owner principles P1/P2/P3, 2026-10-16.
|
|
69
|
+
*/
|
|
70
|
+
export function contextVaultIdHeader() {
|
|
71
|
+
const store = storage.getStore();
|
|
72
|
+
if (!store) return undefined;
|
|
73
|
+
const id = typeof store.vaultId === 'string' ? store.vaultId.trim() : '';
|
|
74
|
+
return id ? { 'x-heyspex-vault-id': id } : {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Outbound `x-heyspex-account-id` for the active request: always `{}` inside a
|
|
79
|
+
* context (the forwarded credential IS the account), `undefined` outside.
|
|
80
|
+
*/
|
|
81
|
+
export function contextAccountIdHeader() {
|
|
82
|
+
return storage.getStore() ? {} : undefined;
|
|
83
|
+
}
|