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
|
@@ -669,7 +669,20 @@ function buildTerminalLeafClaimTarget(node, alignmentState, breadcrumb, level =
|
|
|
669
669
|
* `{ kind: 'has_claimable', ref, child }` when a non-terminal child exists so
|
|
670
670
|
* the caller can recurse without re-walking.
|
|
671
671
|
*/
|
|
672
|
-
|
|
672
|
+
/**
|
|
673
|
+
* `excludeIds` (lower-cased spec ids, optional): tasks another agent is
|
|
674
|
+
* actively holding ([[HS-3C9M-A4TX-CAP]]). A held child is passed over for the
|
|
675
|
+
* next launchable sibling so a parallel agent gets fresh work; when every
|
|
676
|
+
* launchable child is held the rollup says so (`all_held`) instead of
|
|
677
|
+
* pretending the epic is review-blocked or closed.
|
|
678
|
+
*/
|
|
679
|
+
export function classifyEpicChildRollup(
|
|
680
|
+
epic,
|
|
681
|
+
loadRef,
|
|
682
|
+
schedulerOrderIds = [],
|
|
683
|
+
reviewLevel = 'task',
|
|
684
|
+
excludeIds = null,
|
|
685
|
+
) {
|
|
673
686
|
const fm = epic?.frontmatter || {};
|
|
674
687
|
const childRefs = rankRefsBySchedulerOrder(
|
|
675
688
|
Array.isArray(fm.children) ? fm.children.map(wikilinkTarget).filter(Boolean) : [],
|
|
@@ -679,6 +692,7 @@ export function classifyEpicChildRollup(epic, loadRef, schedulerOrderIds = [], r
|
|
|
679
692
|
|
|
680
693
|
let loadedCount = 0;
|
|
681
694
|
let inReviewCount = 0;
|
|
695
|
+
let heldCount = 0;
|
|
682
696
|
const statuses = [];
|
|
683
697
|
for (const ref of childRefs) {
|
|
684
698
|
if (typeof loadRef !== 'function') break;
|
|
@@ -688,11 +702,17 @@ export function classifyEpicChildRollup(epic, loadRef, schedulerOrderIds = [], r
|
|
|
688
702
|
const status = specStatus(child);
|
|
689
703
|
statuses.push(status);
|
|
690
704
|
if (!CLAIM_TERMINAL_STATUSES.has(status)) {
|
|
705
|
+
const childId = String(specId(child) || ref).toLowerCase();
|
|
706
|
+
if (excludeIds && excludeIds.has(childId)) {
|
|
707
|
+
heldCount += 1;
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
691
710
|
return { kind: 'has_claimable', ref, child };
|
|
692
711
|
}
|
|
693
712
|
if (REVIEW_AWAITING_STATUSES.has(status)) inReviewCount += 1;
|
|
694
713
|
}
|
|
695
714
|
|
|
715
|
+
if (heldCount > 0) return { kind: 'all_held', held: heldCount };
|
|
696
716
|
if (loadedCount === 0) return { kind: 'no_children' };
|
|
697
717
|
const rollupKind = classifyParentRollup(statuses, 'task', reviewLevel);
|
|
698
718
|
if (rollupKind === 'reviewer_blocked') {
|
|
@@ -752,6 +772,7 @@ export function computeClaimTarget(
|
|
|
752
772
|
loadRef,
|
|
753
773
|
schedulerOrderIds = [],
|
|
754
774
|
rollupOpts,
|
|
775
|
+
excludeIds = null,
|
|
755
776
|
) {
|
|
756
777
|
if (!spec) return null;
|
|
757
778
|
void vaultRoot;
|
|
@@ -768,6 +789,7 @@ export function computeClaimTarget(
|
|
|
768
789
|
new Set(),
|
|
769
790
|
schedulerOrderIds,
|
|
770
791
|
reviewLevel,
|
|
792
|
+
excludeIds,
|
|
771
793
|
);
|
|
772
794
|
}
|
|
773
795
|
|
|
@@ -779,6 +801,7 @@ function resolveClaimTargetRecursive(
|
|
|
779
801
|
visited,
|
|
780
802
|
schedulerOrderIds = [],
|
|
781
803
|
reviewLevel = 'task',
|
|
804
|
+
excludeIds = null,
|
|
782
805
|
) {
|
|
783
806
|
if (!spec) return null;
|
|
784
807
|
const id = specId(spec);
|
|
@@ -800,7 +823,17 @@ function resolveClaimTargetRecursive(
|
|
|
800
823
|
}
|
|
801
824
|
|
|
802
825
|
if (type === 'epic') {
|
|
803
|
-
const rollup = classifyEpicChildRollup(
|
|
826
|
+
const rollup = classifyEpicChildRollup(
|
|
827
|
+
spec,
|
|
828
|
+
loadRef,
|
|
829
|
+
schedulerOrderIds,
|
|
830
|
+
reviewLevel,
|
|
831
|
+
excludeIds,
|
|
832
|
+
);
|
|
833
|
+
// Every launchable child is held by another agent: no target here, so a
|
|
834
|
+
// milestone walk moves on to its next epic (and a root epic yields null —
|
|
835
|
+
// the caller falls back to the held task with a "held by" note).
|
|
836
|
+
if (rollup.kind === 'all_held') return null;
|
|
804
837
|
if (rollup.kind === 'has_claimable') {
|
|
805
838
|
const child = rollup.child;
|
|
806
839
|
const childBreadcrumb = [...breadcrumb, specId(child)].filter(Boolean);
|
|
@@ -812,6 +845,7 @@ function resolveClaimTargetRecursive(
|
|
|
812
845
|
visited,
|
|
813
846
|
schedulerOrderIds,
|
|
814
847
|
reviewLevel,
|
|
848
|
+
excludeIds,
|
|
815
849
|
);
|
|
816
850
|
if (childTarget) return childTarget;
|
|
817
851
|
}
|
|
@@ -844,6 +878,7 @@ function resolveClaimTargetRecursive(
|
|
|
844
878
|
visited,
|
|
845
879
|
schedulerOrderIds,
|
|
846
880
|
reviewLevel,
|
|
881
|
+
excludeIds,
|
|
847
882
|
);
|
|
848
883
|
if (childTarget) return childTarget;
|
|
849
884
|
}
|
|
@@ -23,7 +23,10 @@
|
|
|
23
23
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
24
24
|
import { join } from 'node:path';
|
|
25
25
|
|
|
26
|
+
import { resolveConsoleDeps } from './breakdown-source.mjs';
|
|
27
|
+
import { listSpecsViaConsole } from './cloud-vault-writes.mjs';
|
|
26
28
|
import { getFrontmatterBlock, getScalarField } from './promotion-helpers.mjs';
|
|
29
|
+
import { readViaSelector } from './read-path.mjs';
|
|
27
30
|
|
|
28
31
|
/**
|
|
29
32
|
* Statuses a candidate must NOT be in to count as covering the area. Mirrors the
|
|
@@ -136,12 +139,28 @@ function scanDir(vaultRoot, dirParts, type) {
|
|
|
136
139
|
* attach a bug fix to the wrong epic.
|
|
137
140
|
*/
|
|
138
141
|
export function scanExistingCoverage(vaultRoot, source, opts = {}) {
|
|
142
|
+
const prepared = prepareCoverageScan(source, opts);
|
|
143
|
+
if (!prepared) return { milestones: [], epics: [], source_terms: [] };
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
source_terms: prepared.source_terms,
|
|
147
|
+
milestones: prepared.rank(scanDir(vaultRoot, ['specs', 'milestones'], 'milestone')),
|
|
148
|
+
epics: prepared.rank(scanDir(vaultRoot, ['specs', 'epics'], 'epic')),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The source-side half of {@link scanExistingCoverage} — its term set and its
|
|
154
|
+
* ranker — lifted out so BOTH read tiers rank identically. Returns `null` for
|
|
155
|
+
* the "source carries no subject terms" early-out. Pure; no I/O.
|
|
156
|
+
*/
|
|
157
|
+
function prepareCoverageScan(source, opts = {}) {
|
|
139
158
|
const minOverlap = Number.isFinite(opts.minOverlap) ? opts.minOverlap : 2;
|
|
140
159
|
const limit = Number.isFinite(opts.limit) ? opts.limit : 5;
|
|
141
160
|
|
|
142
161
|
const block = frontmatterOf(source?.raw ?? '');
|
|
143
162
|
const sourceTerms = new Set(terms(subjectText(block, source?.basename)));
|
|
144
|
-
if (sourceTerms.size === 0) return
|
|
163
|
+
if (sourceTerms.size === 0) return null;
|
|
145
164
|
|
|
146
165
|
const rank = (candidates) =>
|
|
147
166
|
candidates
|
|
@@ -162,13 +181,109 @@ export function scanExistingCoverage(vaultRoot, source, opts = {}) {
|
|
|
162
181
|
.sort((a, b) => b.overlap - a.overlap || a.basename.localeCompare(b.basename))
|
|
163
182
|
.slice(0, limit);
|
|
164
183
|
|
|
184
|
+
return { rank, source_terms: [...sourceTerms].sort() };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
188
|
+
// Cloud (`neon`) read tier ([[HS-T6GS-MCPC-EPIC]]).
|
|
189
|
+
//
|
|
190
|
+
// `scanDir` above `readdirSync`es `specs/milestones` and `specs/epics`. In the
|
|
191
|
+
// `neon` tier there may be no vault checkout at all, so that scan is an ENOENT,
|
|
192
|
+
// not a slow path. The cloud twin is `GET /api/specs?type=milestone|epic`
|
|
193
|
+
// (`listSpecsViaConsole`, which follows `next_cursor` so a caller that used to
|
|
194
|
+
// see every FILE still sees every ROW).
|
|
195
|
+
//
|
|
196
|
+
// `scanExistingCoverage` itself is untouched and stays synchronous — it IS the
|
|
197
|
+
// `diskRead` arm, so `promote-diagnose.mjs`'s existing call site keeps working
|
|
198
|
+
// unchanged. The tier is resolved ONCE by the owning handler and threaded in as
|
|
199
|
+
// `deps.tier`, defaulting to `'disk'`.
|
|
200
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* `GET /api/specs` summary rows carry `name`, `tags` and `milestone`, but
|
|
204
|
+
* `SpecSummary` has NO `summary` field. The disk scan's subject text is
|
|
205
|
+
* `name + tags + summary`, so cloud-tier ranking sees a slightly smaller term
|
|
206
|
+
* set. Reported as a warning on the result rather than swallowed — a thinner
|
|
207
|
+
* signal that looks identical to the full one is how a "no coverage found"
|
|
208
|
+
* answer mints a milestone the vault already has.
|
|
209
|
+
*/
|
|
210
|
+
export const COVERAGE_CLOUD_FIDELITY_WARNING =
|
|
211
|
+
'cloud-tier coverage ranking omits each candidate\'s `summary:` text — GET /api/specs summary rows carry name + tags + milestone but no summary field, so overlap is computed from a smaller term set than the disk scan uses';
|
|
212
|
+
|
|
213
|
+
/** One `GET /api/specs` row → the candidate shape {@link scanDir} produces. */
|
|
214
|
+
function candidateFromRow(row, type) {
|
|
215
|
+
const basename = row?.basename ?? row?.id ?? null;
|
|
216
|
+
if (!basename) return null;
|
|
217
|
+
const status = String(row?.status ?? 'open').trim().toLowerCase();
|
|
218
|
+
if (!COVERAGE_ELIGIBLE_STATUSES.has(status)) return null;
|
|
219
|
+
if (row?.archived === true) return null;
|
|
220
|
+
if (row?.canonical === false) return null;
|
|
221
|
+
const name = row?.name ?? basename;
|
|
222
|
+
const tags = Array.isArray(row?.tags) ? row.tags.join(' ') : '';
|
|
165
223
|
return {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
224
|
+
type,
|
|
225
|
+
id: row?.id ?? basename,
|
|
226
|
+
basename,
|
|
227
|
+
name,
|
|
228
|
+
status,
|
|
229
|
+
milestone: row?.milestone ?? null,
|
|
230
|
+
// `row.summary` is read opportunistically: absent from SpecSummary today,
|
|
231
|
+
// free fidelity the day the route projects it.
|
|
232
|
+
terms: new Set(terms([name, tags, row?.summary ?? ''].join(' '))),
|
|
169
233
|
};
|
|
170
234
|
}
|
|
171
235
|
|
|
236
|
+
/** List one entity type through the console and shape it into scan candidates. */
|
|
237
|
+
async function scanTypeViaConsole({ consoleFetch, consoleUrl }, type) {
|
|
238
|
+
const rows = await listSpecsViaConsole({ consoleFetch, consoleUrl, type });
|
|
239
|
+
return (rows ?? []).map((r) => candidateFromRow(r, type)).filter(Boolean);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Tier-aware {@link scanExistingCoverage}.
|
|
244
|
+
*
|
|
245
|
+
* @param {string} vaultRoot
|
|
246
|
+
* @param {{ raw?: string, basename?: string }} source
|
|
247
|
+
* @param {{ minOverlap?: number, limit?: number }} [opts]
|
|
248
|
+
* @param {{ tier?: 'disk'|'neon', consoleUrl?: string, consoleFetch?: Function }} [deps]
|
|
249
|
+
*
|
|
250
|
+
* `tier` defaults to `'disk'`, whose arm is `scanExistingCoverage(...)`
|
|
251
|
+
* verbatim — byte-identical, and no console call is made. A `neon`-tier
|
|
252
|
+
* console failure surfaces as `ConsoleUnreachableCloudModeError`
|
|
253
|
+
* (`CONSOLE_UNREACHABLE_CLOUD_MODE`); there is no silent disk fallback.
|
|
254
|
+
*/
|
|
255
|
+
export async function scanExistingCoverageViaTier(vaultRoot, source, opts = {}, deps = {}) {
|
|
256
|
+
const { tier = 'disk' } = deps;
|
|
257
|
+
const console_ = resolveConsoleDeps(tier, deps);
|
|
258
|
+
return readViaSelector({
|
|
259
|
+
tier,
|
|
260
|
+
diskRead: () => scanExistingCoverage(vaultRoot, source, opts),
|
|
261
|
+
neonRead: async () => {
|
|
262
|
+
const prepared = prepareCoverageScan(source, opts);
|
|
263
|
+
if (!prepared) {
|
|
264
|
+
return {
|
|
265
|
+
milestones: [],
|
|
266
|
+
epics: [],
|
|
267
|
+
source_terms: [],
|
|
268
|
+
cloud_read_warnings: [COVERAGE_CLOUD_FIDELITY_WARNING],
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const [milestones, epics] = await Promise.all([
|
|
272
|
+
scanTypeViaConsole(console_, 'milestone'),
|
|
273
|
+
scanTypeViaConsole(console_, 'epic'),
|
|
274
|
+
]);
|
|
275
|
+
return {
|
|
276
|
+
source_terms: prepared.source_terms,
|
|
277
|
+
milestones: prepared.rank(milestones),
|
|
278
|
+
epics: prepared.rank(epics),
|
|
279
|
+
cloud_read_warnings: [COVERAGE_CLOUD_FIDELITY_WARNING],
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
routeLabel: 'GET /api/specs?type=milestone|epic',
|
|
283
|
+
consoleUrl: console_.consoleUrl,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
172
287
|
/**
|
|
173
288
|
* Extract the source's own `## Fix` section, verbatim. Returns `''` when there is
|
|
174
289
|
* none — a write-up with no Fix section has not been investigated far enough to be
|
|
@@ -23,7 +23,12 @@
|
|
|
23
23
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
24
24
|
import { join } from 'node:path';
|
|
25
25
|
|
|
26
|
+
import { listSpecsViaConsole } from './cloud-vault-writes.mjs';
|
|
27
|
+
import { consoleFetch as realConsoleFetch } from './console-transport.mjs';
|
|
28
|
+
import { getConsoleUrl } from './mcp-config.mjs';
|
|
26
29
|
import { getFrontmatterBlock, getScalarField } from './promotion-helpers.mjs';
|
|
30
|
+
import { readViaSelector } from './read-path.mjs';
|
|
31
|
+
import { yamlScalar } from './yaml-scalar.mjs';
|
|
27
32
|
|
|
28
33
|
/**
|
|
29
34
|
* The kinds of document the breakdown stage accepts, and the folder each lives in.
|
|
@@ -234,3 +239,269 @@ export function readSourceFrontmatter(record) {
|
|
|
234
239
|
type: getScalarField(block, 'type') ?? null,
|
|
235
240
|
};
|
|
236
241
|
}
|
|
242
|
+
|
|
243
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
244
|
+
// Cloud (`neon`) read tier ([[HS-T6GS-MCPC-EPIC]]).
|
|
245
|
+
//
|
|
246
|
+
// Everything above `readdirSync`es `specs/research` and `specs/diagnoses`. In
|
|
247
|
+
// the `neon` tier there may be NO vault checkout at all, so the folder scan is
|
|
248
|
+
// not slow — it is an ENOENT. The cloud twin of a folder scan is
|
|
249
|
+
// `GET /api/specs?type=…` (`listSpecsViaConsole`, which follows `next_cursor`
|
|
250
|
+
// so a caller that used to see every FILE still sees every ROW).
|
|
251
|
+
//
|
|
252
|
+
// The sync exports above are UNCHANGED and keep working for every existing
|
|
253
|
+
// caller (`promote-diagnose.mjs`, `promote-research.mjs`) — they are the
|
|
254
|
+
// `diskRead` arm verbatim. Tier-aware callers use the async `*ViaTier`
|
|
255
|
+
// siblings, the same convention the rest of the server uses
|
|
256
|
+
// (`loadSpecByIdViaTier`, `handleRecordAlignmentViaTier`, …). The tier is
|
|
257
|
+
// resolved ONCE by the handler that owns the request and threaded in as
|
|
258
|
+
// `deps.tier`; nothing here calls `resolveReadTier()` itself, and `tier`
|
|
259
|
+
// defaults to `'disk'`.
|
|
260
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Console dependencies for a `neon`-tier read, injectable end to end so a test
|
|
264
|
+
* can drive a recorder and never open a socket. `getConsoleUrl()` is only
|
|
265
|
+
* CALLED on the cloud path (it walks the vault resolver), never on disk.
|
|
266
|
+
*/
|
|
267
|
+
export function resolveConsoleDeps(tier, deps = {}) {
|
|
268
|
+
if (tier !== 'neon') return { consoleFetch: null, consoleUrl: null };
|
|
269
|
+
return {
|
|
270
|
+
consoleFetch: deps.consoleFetch ?? realConsoleFetch,
|
|
271
|
+
consoleUrl: deps.consoleUrl ?? getConsoleUrl(),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Rebuild a `---`-delimited markdown document from the `{ frontmatter, body }`
|
|
277
|
+
* pair `GET /api/specs/:id` returns (`SpecDetail` carries no raw markdown).
|
|
278
|
+
*
|
|
279
|
+
* Every consumer of `record.raw` in this stage reads it through
|
|
280
|
+
* `getFrontmatterBlock` / `getScalarField` / a `## Heading` regex
|
|
281
|
+
* (`readSourceFrontmatter`, `scanExistingCoverage`, `countFixes`,
|
|
282
|
+
* `causeIsKnown`), so the reconstruction only has to satisfy those: one line
|
|
283
|
+
* per scalar, a block list per array, `yamlScalar` quoting — the SAME quoter
|
|
284
|
+
* every disk writer in this package uses, so a value round-trips through
|
|
285
|
+
* `getScalarField` identically whether it came from a file or from the wire.
|
|
286
|
+
* Embedded newlines in a scalar are folded to spaces (a single-line YAML
|
|
287
|
+
* scalar is the only shape `getScalarField` reads on disk either).
|
|
288
|
+
*/
|
|
289
|
+
export function stringifyCloudSpec(frontmatter = {}, body = '') {
|
|
290
|
+
const lines = ['---'];
|
|
291
|
+
for (const [key, value] of Object.entries(frontmatter ?? {})) {
|
|
292
|
+
if (value === null || value === undefined) continue;
|
|
293
|
+
if (Array.isArray(value)) {
|
|
294
|
+
lines.push(`${key}:`);
|
|
295
|
+
for (const item of value) lines.push(` - ${yamlScalar(item)}`);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (typeof value === 'object') continue; // no consumer here reads a nested map
|
|
299
|
+
lines.push(
|
|
300
|
+
`${key}: ${typeof value === 'string' ? yamlScalar(value.replace(/\r?\n/g, ' ')) : String(value)}`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
lines.push('---', '');
|
|
304
|
+
return `${lines.join('\n')}\n${String(body ?? '')}`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** `GET /api/specs/:id` → a record shaped exactly like the disk `loadRecord` one. */
|
|
308
|
+
async function fetchCloudRecord({ consoleFetch, consoleUrl }, kind, ref) {
|
|
309
|
+
const res = await consoleFetch(`/api/specs/${encodeURIComponent(ref)}`);
|
|
310
|
+
if (res?.status === 404) return null; // not_found / ambiguous_id — a real answer.
|
|
311
|
+
if (!res?.ok) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
`GET /api/specs/${ref} returned ${res?.status}: ${res?.error || JSON.stringify(res?.body ?? {})}`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
const spec = res.body?.spec;
|
|
317
|
+
if (!spec) return null;
|
|
318
|
+
const frontmatter = spec.frontmatter || {};
|
|
319
|
+
const basename = spec.basename ?? String(spec.relPath ?? ref).split(/[\\/]/).pop().replace(/\.md$/i, '');
|
|
320
|
+
return {
|
|
321
|
+
kind: kind ?? spec.type ?? null,
|
|
322
|
+
basename,
|
|
323
|
+
// No local file exists in cloud mode — say so rather than hand back a path
|
|
324
|
+
// that looks writable. `relPath` stays populated for error messages.
|
|
325
|
+
absPath: null,
|
|
326
|
+
relPath: spec.relPath ?? null,
|
|
327
|
+
raw: stringifyCloudSpec(frontmatter, spec.body ?? ''),
|
|
328
|
+
frontmatter,
|
|
329
|
+
body: spec.body ?? '',
|
|
330
|
+
cloud: true,
|
|
331
|
+
consoleUrl: consoleUrl ?? null,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* The four-step resolution ladder of {@link resolveWithinKind}, run against the
|
|
337
|
+
* console's spec listing instead of a directory listing. Same steps, same
|
|
338
|
+
* first-hit-wins order, same "report ambiguity rather than resolve it" rule.
|
|
339
|
+
*
|
|
340
|
+
* One fidelity note: step 3 matches the row's `id` only. `SpecSummary` carries
|
|
341
|
+
* no `file_id`, so a reference given as a bare `file_id:` that differs from
|
|
342
|
+
* `id:` falls through to step 4 (id-prefix) here where disk would have caught
|
|
343
|
+
* it at step 3. Both steps resolve the same document when they hit; the
|
|
344
|
+
* difference is only which step reports it.
|
|
345
|
+
*/
|
|
346
|
+
function resolveWithinKindFromRows(rows, ref) {
|
|
347
|
+
const named = (rows ?? []).filter((r) => typeof r?.basename === 'string' && r.basename);
|
|
348
|
+
|
|
349
|
+
const exact = named.filter((r) => r.basename === ref);
|
|
350
|
+
if (exact.length === 1) return { row: exact[0] };
|
|
351
|
+
|
|
352
|
+
const lowered = ref.toLowerCase();
|
|
353
|
+
const ciExact = named.filter((r) => r.basename.toLowerCase() === lowered);
|
|
354
|
+
if (ciExact.length === 1) return { row: ciExact[0] };
|
|
355
|
+
if (ciExact.length > 1) {
|
|
356
|
+
return { ambiguous: ciExact.map((r) => r.basename), step: 'case-insensitive basename' };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const idHits = named.filter((r) => r.id === ref);
|
|
360
|
+
if (idHits.length === 1) return { row: idHits[0] };
|
|
361
|
+
if (idHits.length > 1) {
|
|
362
|
+
return { ambiguous: idHits.map((r) => r.basename), step: 'frontmatter id' };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const prefixHits = named.filter((r) => r.basename.startsWith(`${ref}-`));
|
|
366
|
+
if (prefixHits.length === 1) return { row: prefixHits[0] };
|
|
367
|
+
if (prefixHits.length > 1) {
|
|
368
|
+
return { ambiguous: prefixHits.map((r) => r.basename), step: 'id prefix' };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return { miss: true };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Cloud twin of {@link resolveBreakdownSource}. Returns the SAME result shapes
|
|
376
|
+
* — `{ record }` or one of `INVALID_ARGS` / `SOURCE_ID_AMBIGUOUS` /
|
|
377
|
+
* `SOURCE_NOT_FOUND` / `SOURCE_TYPE_MISMATCH` — so a caller branches
|
|
378
|
+
* identically on either tier.
|
|
379
|
+
*/
|
|
380
|
+
async function resolveBreakdownSourceViaConsole(rawRef, opts, deps) {
|
|
381
|
+
const kinds = Array.isArray(opts.kinds) && opts.kinds.length > 0
|
|
382
|
+
? opts.kinds.filter((k) => k in BREAKDOWN_SOURCE_KINDS)
|
|
383
|
+
: Object.keys(BREAKDOWN_SOURCE_KINDS);
|
|
384
|
+
|
|
385
|
+
const ref = normalizeSourceRef(rawRef);
|
|
386
|
+
if (!ref) {
|
|
387
|
+
return { error: 'INVALID_ARGS', message: 'a source id or basename is required' };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Every known kind is searched, not only the requested ones — same reason as
|
|
391
|
+
// the disk path: a hit under a kind the caller did not ask for is the
|
|
392
|
+
// type-mismatch case, and reporting it as such beats a bare "not found".
|
|
393
|
+
const hits = [];
|
|
394
|
+
const ambiguities = [];
|
|
395
|
+
for (const kind of Object.keys(BREAKDOWN_SOURCE_KINDS)) {
|
|
396
|
+
const rows = await listSpecsViaConsole({
|
|
397
|
+
consoleFetch: deps.consoleFetch,
|
|
398
|
+
consoleUrl: deps.consoleUrl,
|
|
399
|
+
type: kind,
|
|
400
|
+
});
|
|
401
|
+
const found = resolveWithinKindFromRows(rows, ref);
|
|
402
|
+
if (found.row) hits.push({ kind, row: found.row });
|
|
403
|
+
else if (found.ambiguous) ambiguities.push({ kind, ...found });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (hits.length > 1) {
|
|
407
|
+
return {
|
|
408
|
+
error: 'SOURCE_ID_AMBIGUOUS',
|
|
409
|
+
message: `${rawRef} resolves to ${hits.length} documents across ${hits.map((h) => h.kind).join(' + ')} — pass the full basename instead`,
|
|
410
|
+
candidates: hits.map((h) => h.row.basename),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (hits.length === 0) {
|
|
415
|
+
if (ambiguities.length > 0) {
|
|
416
|
+
const first = ambiguities[0];
|
|
417
|
+
return {
|
|
418
|
+
error: 'SOURCE_ID_AMBIGUOUS',
|
|
419
|
+
message: `${rawRef} matches ${first.ambiguous.length} files under ${BREAKDOWN_SOURCE_KINDS[first.kind].dir.join('/')}/ by ${first.step} — pass the exact basename or the frontmatter id: instead`,
|
|
420
|
+
candidates: first.ambiguous,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
// The type listings only cover research + diagnoses. On disk a document of
|
|
424
|
+
// some OTHER type sitting in one of those folders is still found and
|
|
425
|
+
// reported as a type mismatch; here one direct id lookup buys the same
|
|
426
|
+
// answer, so "you passed a task to the breakdown stage" keeps reading as
|
|
427
|
+
// that instead of a misleading "not found".
|
|
428
|
+
const other = await fetchCloudRecord(deps, null, ref);
|
|
429
|
+
if (other?.kind && !(String(other.kind).toLowerCase() in BREAKDOWN_SOURCE_KINDS)) {
|
|
430
|
+
return {
|
|
431
|
+
error: 'SOURCE_TYPE_MISMATCH',
|
|
432
|
+
message: `${rawRef} resolved to ${other.relPath ?? other.basename}, which is \`type: ${String(other.kind).toLowerCase()}\` — the breakdown stage accepts a research doc or a bug write-up`,
|
|
433
|
+
resolved_basename: other.basename,
|
|
434
|
+
resolved_type: String(other.kind).toLowerCase(),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
error: 'SOURCE_NOT_FOUND',
|
|
439
|
+
message: `${rawRef} not found under ${kinds.map((k) => BREAKDOWN_SOURCE_KINDS[k].dir.join('/') + '/').join(' or ')} (tried exact basename, case-insensitive basename, frontmatter id:, and id-prefix match)`,
|
|
440
|
+
searched_kinds: kinds,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const { kind, row } = hits[0];
|
|
445
|
+
const record = await fetchCloudRecord(deps, kind, row.id ?? row.basename);
|
|
446
|
+
if (!record) {
|
|
447
|
+
return {
|
|
448
|
+
error: 'SOURCE_NOT_FOUND',
|
|
449
|
+
message: `${rawRef} was listed by GET /api/specs?type=${kind} as ${row.basename} but GET /api/specs/${row.id ?? row.basename} did not return it`,
|
|
450
|
+
searched_kinds: kinds,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// The document's own `type:` wins, exactly as on disk; the listing's `type`
|
|
455
|
+
// is the fallback when the document declares none.
|
|
456
|
+
const declared = String(record.frontmatter?.type ?? '').trim().toLowerCase();
|
|
457
|
+
if (declared) {
|
|
458
|
+
if (!(declared in BREAKDOWN_SOURCE_KINDS)) {
|
|
459
|
+
return {
|
|
460
|
+
error: 'SOURCE_TYPE_MISMATCH',
|
|
461
|
+
message: `${rawRef} resolved to ${record.relPath ?? record.basename}, which is \`type: ${declared}\` — the breakdown stage accepts a research doc or a bug write-up`,
|
|
462
|
+
resolved_basename: record.basename,
|
|
463
|
+
resolved_type: declared,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
record.kind = declared;
|
|
467
|
+
} else {
|
|
468
|
+
record.kind = kind;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (!kinds.includes(record.kind)) {
|
|
472
|
+
return {
|
|
473
|
+
error: 'SOURCE_TYPE_MISMATCH',
|
|
474
|
+
message: `${rawRef} resolved to ${record.relPath ?? record.basename}, a ${BREAKDOWN_SOURCE_KINDS[record.kind].label} — this call expects ${kinds.map((k) => BREAKDOWN_SOURCE_KINDS[k].label).join(' or ')}`,
|
|
475
|
+
resolved_basename: record.basename,
|
|
476
|
+
resolved_type: record.kind,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return { record };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Tier-aware {@link resolveBreakdownSource}.
|
|
485
|
+
*
|
|
486
|
+
* @param {string} vaultRoot
|
|
487
|
+
* @param {string} rawRef
|
|
488
|
+
* @param {{ kinds?: string[] }} [opts]
|
|
489
|
+
* @param {{ tier?: 'disk'|'neon', consoleUrl?: string, consoleFetch?: Function }} [deps]
|
|
490
|
+
*
|
|
491
|
+
* `tier` defaults to `'disk'`, whose arm is `resolveBreakdownSource(...)`
|
|
492
|
+
* verbatim — byte-identical, and no console call is made. A `neon`-tier
|
|
493
|
+
* console failure surfaces as `ConsoleUnreachableCloudModeError`
|
|
494
|
+
* (`CONSOLE_UNREACHABLE_CLOUD_MODE`), never as a silent fall back to a disk
|
|
495
|
+
* copy that may not exist.
|
|
496
|
+
*/
|
|
497
|
+
export async function resolveBreakdownSourceViaTier(vaultRoot, rawRef, opts = {}, deps = {}) {
|
|
498
|
+
const { tier = 'disk' } = deps;
|
|
499
|
+
const console_ = resolveConsoleDeps(tier, deps);
|
|
500
|
+
return readViaSelector({
|
|
501
|
+
tier,
|
|
502
|
+
diskRead: () => resolveBreakdownSource(vaultRoot, rawRef, opts),
|
|
503
|
+
neonRead: () => resolveBreakdownSourceViaConsole(rawRef, opts, console_),
|
|
504
|
+
routeLabel: 'GET /api/specs?type=research|diagnose',
|
|
505
|
+
consoleUrl: console_.consoleUrl,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
import { existsSync, readFileSync } from 'node:fs';
|
|
27
27
|
import { join } from 'node:path';
|
|
28
28
|
|
|
29
|
-
import {
|
|
29
|
+
import { allocateFileKey, parseSpecId } from './spec-id-lib.mjs';
|
|
30
30
|
import { listVaultPillars, resolvePillar } from './promote-research.mjs';
|
|
31
31
|
import {
|
|
32
32
|
appendChangeLogRow,
|
|
@@ -250,12 +250,32 @@ export function requireResolvableMilestone(vaultRoot, milestoneRef) {
|
|
|
250
250
|
* @returns `{ spawned, writes }` on success, or `{ error, message, ... }`. Nothing is
|
|
251
251
|
* written until every gate has passed; the commit itself rolls back as a unit.
|
|
252
252
|
*/
|
|
253
|
-
export function executeBreakdownWrites({ vaultRoot, source, sourceFrontmatter, ladder, placement, args = {}, deps = {} }) {
|
|
253
|
+
export function executeBreakdownWrites({ vaultRoot, source, sourceFrontmatter, ladder, placement, occupiedKeys, args = {}, deps = {} }) {
|
|
254
254
|
const now = (deps.clock ?? isoNow)();
|
|
255
255
|
const today = now.slice(0, 10);
|
|
256
256
|
const sourceBasename = source.basename;
|
|
257
257
|
const familyKey = inheritedFamilyKey(sourceFrontmatter, sourceBasename) ?? 'BFIX';
|
|
258
|
-
|
|
258
|
+
// TIER-AWARE OCCUPANCY, SUPPLIED BY THE CALLER ([[HS-FJWL]]). This used to be
|
|
259
|
+
// `collectOccupiedKeysEitherKind(vaultRoot)` inline — a `specs/**` walk that
|
|
260
|
+
// is complete on the disk tier and blind on the cloud tier, where a filed
|
|
261
|
+
// spec has a Neon row and no local `.md`. A file key is the unique per-file
|
|
262
|
+
// identity under Naming V3, so minting against a stale local clone re-issues
|
|
263
|
+
// a live id. The resolution is the CALLER's (promote-diagnose.mjs), which is
|
|
264
|
+
// async and can reach the console; this writer stays synchronous.
|
|
265
|
+
//
|
|
266
|
+
// No disk fallback when it is missing: a silent default is exactly the blind
|
|
267
|
+
// scan this fix removes, so an unsupplied set refuses the whole breakdown.
|
|
268
|
+
if (!(occupiedKeys instanceof Set)) {
|
|
269
|
+
return {
|
|
270
|
+
error: 'OCCUPANCY_NOT_RESOLVED',
|
|
271
|
+
message:
|
|
272
|
+
'executeBreakdownWrites requires a resolved `occupiedKeys` Set ([[HS-FJWL]]) — the ' +
|
|
273
|
+
'caller resolves it for the active read tier via collectOccupiedKeysEitherKindViaTier. ' +
|
|
274
|
+
'Refusing to fall back to a local vault scan, which is blind to cloud-only specs and ' +
|
|
275
|
+
'would mint a duplicate file key.',
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const occupied = new Set(occupiedKeys);
|
|
259
279
|
const mintKey = () => {
|
|
260
280
|
const key = allocateFileKey(occupied, { randomUnit: deps.randomUnit });
|
|
261
281
|
occupied.add(key);
|