heyspex 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +14 -0
- package/README.md +21 -0
- package/bin/hs.mjs +1089 -104
- package/bin/spexcode.mjs +262 -0
- package/dev-factory/engine/worktree.mjs +740 -0
- package/dev-factory/lib/code-repo-root.mjs +67 -0
- package/dev-factory/lib/context-budget.mjs +84 -0
- package/dev-factory/lib/epic-detect.mjs +190 -0
- package/dev-factory/lib/fleet-queue-file.mjs +846 -0
- package/dev-factory/lib/fleet-queue.mjs +426 -0
- package/dev-factory/lib/fleet-state.mjs +14 -18
- package/dev-factory/lib/model-catalog.mjs +338 -0
- package/dev-factory/lib/seat-context.mjs +199 -0
- package/dev-factory/lib/spec-io.mjs +93 -0
- package/dev-factory/lib/sufficiency-score.mjs +8 -2
- package/dev-factory/lib/worktree-allocator.mjs +744 -0
- package/dev-factory/model-catalog.json +222 -0
- package/dist/cli/hs-check.mjs +1127 -103
- package/dist/cli/hs-claim.mjs +981 -63
- package/dist/cli/hs-connect.mjs +10059 -1729
- package/dist/cli/hs-defer.mjs +5507 -3072
- package/dist/cli/hs-dev.mjs +576 -0
- package/dist/cli/hs-env.mjs +416 -302
- package/dist/cli/hs-index-rebuild.mjs +7334 -0
- package/dist/cli/hs-index-reconcile.mjs +8138 -0
- package/dist/cli/hs-init.mjs +2840 -149
- package/dist/cli/hs-install.mjs +7214 -594
- package/dist/cli/hs-join.mjs +10640 -2421
- package/dist/cli/hs-vault-pull.mjs +8157 -6370
- package/dist/cli/hs-vault-push.mjs +6433 -0
- package/dist/cli/hs-vault.mjs +4029 -1555
- package/dist/cli/hs-workspace.mjs +2702 -538
- package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
- package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
- package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
- package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
- package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
- package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
- package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
- package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
- package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
- package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -0
- package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
- package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
- package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
- package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
- package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -0
- package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
- package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
- package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
- package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
- package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
- package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
- package/dist/cli-templates/harness-commands/cursor/hs-ground.md +33 -0
- package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
- package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
- package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
- package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
- package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
- package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-view.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
- package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
- package/docs/copilot-plugin-install.md +162 -0
- package/docs/driving-heyspex-from-copilot.md +165 -0
- package/docs/mcp-client-config.md +259 -6
- package/package.json +249 -124
- package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
- package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
- package/plugins/heyspex/.mcp.json +12 -0
- package/plugins/heyspex/README.md +309 -0
- package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
- package/plugins/heyspex/bin/run-hook.mjs +80 -0
- package/plugins/heyspex/hooks/cursor-hooks.json +308 -0
- package/plugins/heyspex/hooks/hooks.json +418 -0
- package/plugins/heyspex/mcp.json +13 -0
- package/scripts/check-mcp-freshness.mjs +17 -9
- package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
- package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
- package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
- package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -0
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +356 -0
- package/scripts/claude-hooks/_run.mjs +413 -0
- package/scripts/claude-hooks/_transcript-text.mjs +60 -0
- package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
- package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
- package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
- package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
- package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
- package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
- package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
- package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
- package/scripts/claude-hooks/claim-liveness-shared.mjs +577 -0
- package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
- package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
- package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
- package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
- package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
- package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
- package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
- package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
- package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
- package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
- package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
- package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
- package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
- package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
- package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
- package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
- package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
- package/scripts/claude-hooks/inject-communication-style.mjs +86 -0
- package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
- package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
- package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
- package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
- package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -0
- package/scripts/claude-hooks/log-session-activity.mjs +341 -0
- package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
- package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
- package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
- package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
- package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
- package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
- package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
- package/scripts/claude-hooks/research-method-guard.mjs +398 -0
- package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
- package/scripts/claude-hooks/session-end.mjs +451 -0
- package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
- package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
- package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
- package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
- package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
- package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
- package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
- package/scripts/claude-hooks/wip-checkpoint.mjs +302 -0
- package/scripts/cursor-hooks/manifest.mjs +281 -0
- package/scripts/dev-server-status.mjs +34 -13
- package/scripts/dev-server-stop.mjs +313 -0
- package/scripts/heyspex-mode.mjs +288 -0
- package/scripts/hs-auto-resolve.mjs +240 -0
- package/scripts/hs-auto-source.mjs +398 -0
- package/scripts/hs-auto.mjs +134 -0
- package/scripts/hs-capture-cli.mjs +326 -0
- package/scripts/hs-classifier.mjs +136 -0
- package/scripts/hs-companion.mjs +88 -0
- package/scripts/hs-gateway.mjs +115 -0
- package/scripts/hs-mode.mjs +125 -0
- package/scripts/hs-next.mjs +441 -0
- package/scripts/hs-settings.mjs +338 -0
- package/scripts/hs-view.mjs +496 -0
- package/scripts/hs-worktree.mjs +201 -0
- package/scripts/install-claude-code-hooks.mjs +272 -0
- package/scripts/install-cursor-hooks.mjs +471 -0
- package/scripts/lib/agent-env-markers.mjs +38 -0
- package/scripts/lib/console-url-resolver.mjs +199 -61
- package/scripts/lib/customer-kit.mjs +1203 -0
- package/scripts/lib/dev-mode.mjs +249 -32
- package/scripts/lib/directive-obligations.mjs +349 -0
- package/scripts/lib/heyspex-home.mjs +72 -0
- package/scripts/lib/hook-dispositions.mjs +210 -0
- package/scripts/lib/hook-vault-header.mjs +30 -0
- package/scripts/lib/hs-command.mjs +277 -0
- package/scripts/lib/hs-companion-settings.mjs +472 -0
- package/scripts/lib/hs-source-branch.mjs +196 -0
- package/scripts/lib/hs-source-commit.mjs +59 -5
- package/scripts/lib/hs-source-identity.mjs +526 -0
- package/scripts/lib/hs-source-log.mjs +13 -22
- package/scripts/lib/hs-source-preflight.mjs +18 -4
- package/scripts/lib/hs-source-push.mjs +386 -307
- package/scripts/lib/hs-worktree-args.mjs +115 -0
- package/scripts/lib/shared-path-helpers.mjs +134 -0
- package/scripts/lib/standalone-doctor.mjs +540 -3
- package/scripts/lib/subagent-git-lock.mjs +109 -0
- package/scripts/lib/template-pin.mjs +34 -0
- package/scripts/lib/window-placement.mjs +109 -0
- package/scripts/provenance-lib.mjs +91 -0
- package/scripts/stamp-provenance-on-edit.mjs +222 -0
- package/scripts/wip-ledger-health.mjs +653 -0
- package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
- package/spexharness/src/adapters/audit.mjs +197 -0
- package/spexharness/src/adapters/claude-code.mjs +449 -0
- package/spexharness/src/adapters/copilot.mjs +236 -0
- package/spexharness/src/adapters/detect-harness.mjs +34 -0
- package/spexharness/src/chat/tui/ansi.mjs +448 -0
- package/spexharness/src/chat/tui/palette.mjs +22 -0
- package/spexharness/src/chat/tui/select.mjs +208 -0
- package/spexharness/src/config-root.mjs +54 -0
- package/spexharness/src/context-budget.mjs +141 -0
- package/spexharness/src/dev-server-control.mjs +389 -0
- package/spexharness/src/doctrine/behaviour.mjs +371 -0
- package/spexharness/src/doctrine/corpus.mjs +733 -0
- package/spexharness/src/doctrine/inject.mjs +338 -0
- package/spexharness/src/doctrine/merge.mjs +184 -0
- package/spexharness/src/doctrine/sources.mjs +252 -0
- package/spexharness/src/doctrine/synthesize.mjs +401 -0
- package/spexharness/src/doctrine/vault-source.mjs +574 -0
- package/spexharness/src/doctrine/writer.mjs +176 -0
- package/spexharness/src/events.mjs +84 -0
- package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
- package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
- package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
- package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
- package/spexharness/src/gates/deep-research-cost-gate.mjs +275 -0
- package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
- package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
- package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
- package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
- package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
- package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
- package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
- package/spexharness/src/gates/git-identity-guard.mjs +61 -0
- package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
- package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
- package/spexharness/src/gates/git-stash-gate.mjs +159 -0
- package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
- package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
- package/spexharness/src/gates/inject-vault-context.mjs +239 -0
- package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
- package/spexharness/src/gates/plan-review-gate.mjs +233 -0
- package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
- package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
- package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
- package/spexharness/src/gates/settings-write-gate.mjs +104 -0
- package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
- package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
- package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
- package/spexharness/src/git-reflog-audit.mjs +294 -0
- package/spexharness/src/intent-classifier.mjs +196 -0
- package/spexharness/src/intent-directive.mjs +626 -0
- package/spexharness/src/profile/learn.mjs +551 -0
- package/spexharness/src/profile/proposals.mjs +357 -0
- package/spexharness/src/profile/vault-inclusion.mjs +386 -0
- package/spexharness/src/redact.mjs +151 -0
- package/spexharness/src/rule-table.mjs +849 -0
- package/spexharness/src/seat-context.mjs +122 -0
- package/spexharness/src/session-bootstrap.mjs +512 -0
- package/spexharness/src/session-record.mjs +815 -0
- package/spexharness/src/token-inventory.mjs +207 -0
- package/vendor/heyspex-mcp/account-switch.mjs +96 -0
- package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
- package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
- package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
- package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
- package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +77 -8
- package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
- package/vendor/heyspex-mcp/capture-console-client.mjs +117 -3
- package/vendor/heyspex-mcp/capture-focus.mjs +1061 -31
- package/vendor/heyspex-mcp/capture-outbox.mjs +15 -5
- package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
- package/vendor/heyspex-mcp/capture-tools.mjs +1264 -0
- package/vendor/heyspex-mcp/claim-tools.mjs +1147 -0
- package/vendor/heyspex-mcp/cli-verbs.mjs +2074 -0
- package/vendor/heyspex-mcp/cli.mjs +207 -15524
- package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
- package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +602 -0
- package/vendor/heyspex-mcp/config-seed.yaml +15 -0
- package/vendor/heyspex-mcp/config-settings.mjs +290 -7
- package/vendor/heyspex-mcp/console-fetch.mjs +822 -17
- package/vendor/heyspex-mcp/console-transport.mjs +172 -0
- package/vendor/heyspex-mcp/credentials.mjs +585 -0
- package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
- package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
- package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
- package/vendor/heyspex-mcp/design-tools.mjs +4 -1
- package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
- package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
- package/vendor/heyspex-mcp/dev-tools.mjs +131 -0
- package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
- package/vendor/heyspex-mcp/events-tools.mjs +207 -0
- package/vendor/heyspex-mcp/filing-tools.mjs +723 -0
- package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +71 -15
- package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
- package/vendor/heyspex-mcp/folder-trust-store.mjs +60 -0
- package/vendor/heyspex-mcp/http-transport.mjs +302 -0
- package/vendor/heyspex-mcp/list-tools.mjs +257 -0
- package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
- package/vendor/heyspex-mcp/mcp-config.mjs +107 -0
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
- package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
- package/vendor/heyspex-mcp/occupied-keys.mjs +142 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +3377 -0
- package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
- package/vendor/heyspex-mcp/package-root.mjs +32 -0
- package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
- package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
- package/vendor/heyspex-mcp/plan-push.mjs +908 -22
- package/vendor/heyspex-mcp/plan-review.mjs +124 -30
- package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
- package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -970
- package/vendor/heyspex-mcp/promote-capture.mjs +388 -69
- package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
- package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
- package/vendor/heyspex-mcp/promote-research.mjs +666 -53
- package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
- package/vendor/heyspex-mcp/prompts.mjs +1460 -0
- package/vendor/heyspex-mcp/read-path.mjs +191 -15
- package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
- package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
- package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
- package/vendor/heyspex-mcp/register-spec.mjs +266 -54
- package/vendor/heyspex-mcp/request-context.mjs +83 -0
- package/vendor/heyspex-mcp/research-focus.mjs +207 -4
- package/vendor/heyspex-mcp/research-handlers.mjs +625 -0
- package/vendor/heyspex-mcp/research-tools.mjs +4 -4
- package/vendor/heyspex-mcp/response-budget.mjs +348 -0
- package/vendor/heyspex-mcp/review-focus.mjs +25 -1
- package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
- package/vendor/heyspex-mcp/search-specs.mjs +280 -0
- package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
- package/vendor/heyspex-mcp/session-owner.mjs +109 -0
- package/vendor/heyspex-mcp/session-tools.mjs +162 -0
- package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
- package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
- package/vendor/heyspex-mcp/spec-edit-tools.mjs +453 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
- package/vendor/heyspex-mcp/split-research.mjs +304 -56
- package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +150 -7
- package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
- package/vendor/heyspex-mcp/summary-write-tools.mjs +228 -0
- package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +123 -1
- package/vendor/heyspex-mcp/tool-catalog.mjs +2819 -0
- package/vendor/heyspex-mcp/tool-dispatch.mjs +657 -0
- package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
- package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
- package/vendor/heyspex-mcp/vault-io.mjs +720 -0
- package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +554 -68
- package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
- package/vendor/heyspex-mcp/whoami.mjs +92 -0
- package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write outbox — "degrade, don't die" for vault WRITES in cloud reliance.
|
|
3
|
+
*
|
|
4
|
+
* Per [[heyspex-is-online-only-no-offline]] (2026-08-20 amendment): when the
|
|
5
|
+
* console is unreachable (status 0 / timeout) or answers 5xx, a spec write is
|
|
6
|
+
* queued durably and replayed on the next successful console call, with a
|
|
7
|
+
* truthful receipt ("Not written to the console yet; queued…") — never a lie
|
|
8
|
+
* that it landed, never a failed workflow.
|
|
9
|
+
*
|
|
10
|
+
* Modelled on capture-outbox.mjs (stash -> list -> replay -> remove, quarantine
|
|
11
|
+
* for what the console structurally refuses, vault-root path guard HS-AI1T).
|
|
12
|
+
*
|
|
13
|
+
* What is queued: spec create / update / ordered batch (promotions) and the
|
|
14
|
+
* append-only control writes (report_progress, record_wip, handoff_record).
|
|
15
|
+
* What is NEVER queued: claims and status flips — the Neon control plane is the
|
|
16
|
+
* mutual-exclusion / transition authority, so those keep failing loudly. 4xx
|
|
17
|
+
* and 401/403 are never queued either (they fail identically on replay, or need
|
|
18
|
+
* `hs login`).
|
|
19
|
+
*
|
|
20
|
+
* Layout: `<vaultRoot>/.heyspex/write-outbox/<ISO>__<NN>__<op>.json`.
|
|
21
|
+
*
|
|
22
|
+
* Replay safety:
|
|
23
|
+
* - the STORED idempotency-key is replayed, never a fresh one ([[HS-2FQY]]);
|
|
24
|
+
* - a stored `create` that failed with a timeout / 5xx first looks the spec up
|
|
25
|
+
* BY NAME and skips the POST when it already landed (`landed_server_side`);
|
|
26
|
+
* - a stored `update` is refused (quarantined, `cloud_newer`) when the cloud
|
|
27
|
+
* copy changed after the pre-image the edit was based on.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
readdirSync,
|
|
32
|
+
readFileSync,
|
|
33
|
+
writeFileSync,
|
|
34
|
+
mkdirSync,
|
|
35
|
+
existsSync,
|
|
36
|
+
renameSync,
|
|
37
|
+
rmSync,
|
|
38
|
+
} from 'node:fs';
|
|
39
|
+
import { basename, join, isAbsolute } from 'node:path';
|
|
40
|
+
|
|
41
|
+
import { assertPathUnderRoot } from '../../scripts/lib/shared-path-helpers.mjs';
|
|
42
|
+
|
|
43
|
+
export const DEFERRED_RECEIPT =
|
|
44
|
+
'Not written to the console yet; queued to .heyspex/write-outbox and replayed on the next successful console call.';
|
|
45
|
+
|
|
46
|
+
/** Process-monotonic within-millisecond tiebreaker (see capture-outbox.mjs). */
|
|
47
|
+
let stashSeq = 0;
|
|
48
|
+
|
|
49
|
+
export function writeOutboxDir(vaultRoot) {
|
|
50
|
+
return join(vaultRoot, '.heyspex', 'write-outbox');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function writeOutboxQuarantineDir(vaultRoot) {
|
|
54
|
+
return join(vaultRoot, '.heyspex', 'write-outbox', 'quarantine');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Default vault root — set once by the tool dispatcher so deep writers
|
|
59
|
+
// (createSpecViaConsole & co.) that were never handed a vaultRoot can still
|
|
60
|
+
// stash. With no root known, writers keep their old throw-loudly behaviour.
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
let defaultVaultRoot = null;
|
|
63
|
+
|
|
64
|
+
export function setWriteOutboxRoot(vaultRoot) {
|
|
65
|
+
if (typeof vaultRoot === 'string' && vaultRoot.trim() && isAbsolute(vaultRoot)) {
|
|
66
|
+
defaultVaultRoot = vaultRoot;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getWriteOutboxRoot() {
|
|
71
|
+
return defaultVaultRoot;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Test helper. */
|
|
75
|
+
export function resetWriteOutboxRoot() {
|
|
76
|
+
defaultVaultRoot = null;
|
|
77
|
+
drainOnceDone = false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Classification
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* True for the OUTAGE class only: transport failure (status 0 / timeout / abort
|
|
86
|
+
* / network) or a 5xx answer. Sign-in problems, typed scope refusals, 401/403
|
|
87
|
+
* and every other 4xx are NOT outages — they never queue.
|
|
88
|
+
*/
|
|
89
|
+
export function isOutageClassResult(result) {
|
|
90
|
+
if (!result || result.ok) return false;
|
|
91
|
+
if (result.authProblem || result.refusal || result.urlMismatch) return false;
|
|
92
|
+
const status = typeof result.status === 'number' ? result.status : 0;
|
|
93
|
+
if (status === 401 || status === 403) return false;
|
|
94
|
+
if (status >= 400 && status < 500) return false;
|
|
95
|
+
if (status >= 500) return true;
|
|
96
|
+
// status 0 / missing: a genuine non-response.
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Stash / list / remove / quarantine
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Persist one write. `entry` carries
|
|
106
|
+
* `{ op, route, method, request_body, idempotency_key, acting_agent, session_id,
|
|
107
|
+
* failure: {status, errorKind, message}, pre_image_updated? , steps? }`.
|
|
108
|
+
* Returns `{ outbox_id, path, entry }`, or `null` when no vault root is usable
|
|
109
|
+
* or the write failed (callers then fall back to their loud error).
|
|
110
|
+
*/
|
|
111
|
+
export function stashWrite(vaultRoot, entry) {
|
|
112
|
+
try {
|
|
113
|
+
const dir = writeOutboxDir(vaultRoot);
|
|
114
|
+
assertPathUnderRoot(vaultRoot, dir, 'write-outbox write');
|
|
115
|
+
mkdirSync(dir, { recursive: true });
|
|
116
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
117
|
+
const seq = String(stashSeq++).padStart(6, '0');
|
|
118
|
+
const op = ['create', 'update', 'batch', 'control'].includes(entry.op) ? entry.op : 'control';
|
|
119
|
+
const outboxId = `${stamp}__${seq}__${op}`;
|
|
120
|
+
const record = {
|
|
121
|
+
op,
|
|
122
|
+
route: entry.route ?? null,
|
|
123
|
+
method: entry.method ?? null,
|
|
124
|
+
request_body: entry.request_body ?? null,
|
|
125
|
+
idempotency_key: entry.idempotency_key ?? null,
|
|
126
|
+
acting_agent: entry.acting_agent ?? null,
|
|
127
|
+
session_id: entry.session_id ?? null,
|
|
128
|
+
attempted_at: entry.attempted_at ?? new Date().toISOString(),
|
|
129
|
+
failure: entry.failure ?? null,
|
|
130
|
+
...(entry.pre_image_updated !== undefined ? { pre_image_updated: entry.pre_image_updated } : {}),
|
|
131
|
+
...(entry.steps ? { steps: entry.steps } : {}),
|
|
132
|
+
...(entry.completed_steps ? { completed_steps: entry.completed_steps } : {}),
|
|
133
|
+
...(entry.spec_id ? { spec_id: entry.spec_id } : {}),
|
|
134
|
+
};
|
|
135
|
+
const path = join(dir, `${outboxId}.json`);
|
|
136
|
+
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
|
|
137
|
+
// A fresh entry re-arms the once-per-process first-success drain.
|
|
138
|
+
drainOnceDone = false;
|
|
139
|
+
return { outbox_id: outboxId, path, entry: record };
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Oldest-first list of parsable entries. Unreadable files are skipped. */
|
|
146
|
+
export function listOutbox(vaultRoot) {
|
|
147
|
+
const dir = writeOutboxDir(vaultRoot);
|
|
148
|
+
if (!existsSync(dir)) return [];
|
|
149
|
+
const out = [];
|
|
150
|
+
for (const d of readdirSync(dir, { withFileTypes: true })) {
|
|
151
|
+
if (!d.isFile() || !d.name.endsWith('.json')) continue;
|
|
152
|
+
try {
|
|
153
|
+
const entry = JSON.parse(readFileSync(join(dir, d.name), 'utf8'));
|
|
154
|
+
out.push({ ...entry, outbox_id: d.name.replace(/\.json$/, ''), path: join(dir, d.name) });
|
|
155
|
+
} catch {
|
|
156
|
+
/* skip unreadable */
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return out.sort((a, b) => (a.outbox_id < b.outbox_id ? -1 : a.outbox_id > b.outbox_id ? 1 : 0));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function outboxCount(vaultRoot) {
|
|
163
|
+
try {
|
|
164
|
+
return listOutbox(vaultRoot).length;
|
|
165
|
+
} catch {
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function removeOutboxEntry(path) {
|
|
171
|
+
try {
|
|
172
|
+
rmSync(path);
|
|
173
|
+
return true;
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Move an entry to quarantine/ with a `<name>.reason.json` sidecar. */
|
|
180
|
+
export function quarantineOutboxEntry(vaultRoot, entryPath, metadata = {}) {
|
|
181
|
+
try {
|
|
182
|
+
const qdir = writeOutboxQuarantineDir(vaultRoot);
|
|
183
|
+
assertPathUnderRoot(vaultRoot, qdir, 'write-outbox write');
|
|
184
|
+
mkdirSync(qdir, { recursive: true });
|
|
185
|
+
const name = basename(entryPath);
|
|
186
|
+
renameSync(entryPath, join(qdir, name));
|
|
187
|
+
writeFileSync(
|
|
188
|
+
join(qdir, `${name}.reason.json`),
|
|
189
|
+
`${JSON.stringify(
|
|
190
|
+
{
|
|
191
|
+
reason: metadata.reason || 'console_refused',
|
|
192
|
+
status: metadata.status ?? null,
|
|
193
|
+
consoleMessage: metadata.consoleMessage ?? null,
|
|
194
|
+
quarantined_at: new Date().toISOString(),
|
|
195
|
+
},
|
|
196
|
+
null,
|
|
197
|
+
2,
|
|
198
|
+
)}\n`,
|
|
199
|
+
'utf8',
|
|
200
|
+
);
|
|
201
|
+
return true;
|
|
202
|
+
} catch {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function listQuarantinedOutbox(vaultRoot) {
|
|
208
|
+
const dir = writeOutboxQuarantineDir(vaultRoot);
|
|
209
|
+
if (!existsSync(dir)) return [];
|
|
210
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
211
|
+
.filter((d) => d.isFile() && d.name.endsWith('.json') && !d.name.endsWith('.reason.json'))
|
|
212
|
+
.map((d) => d.name)
|
|
213
|
+
.sort()
|
|
214
|
+
.map((f) => {
|
|
215
|
+
let reason = null;
|
|
216
|
+
try {
|
|
217
|
+
reason = JSON.parse(readFileSync(join(dir, `${f}.reason.json`), 'utf8'));
|
|
218
|
+
} catch {
|
|
219
|
+
/* sidecar may be absent */
|
|
220
|
+
}
|
|
221
|
+
return { file: f, path: join(dir, f), reason };
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Deferred response
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
/** The truthful tool response for a queued write. Never `isError`. */
|
|
230
|
+
export function buildDeferredPayload(stashed, vaultRoot, extra = {}) {
|
|
231
|
+
return {
|
|
232
|
+
status: 'deferred',
|
|
233
|
+
outbox_id: stashed.outbox_id,
|
|
234
|
+
pending_sync: outboxCount(vaultRoot),
|
|
235
|
+
receipt: DEFERRED_RECEIPT,
|
|
236
|
+
...extra,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** MCP tool envelope for {@link buildDeferredPayload}. */
|
|
241
|
+
export function deferredToolResponse(payload) {
|
|
242
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Result descriptor for a failed consoleFetch, as stored in `failure`. */
|
|
246
|
+
export function failureOf(result, fallbackMessage = null) {
|
|
247
|
+
return {
|
|
248
|
+
status: typeof result?.status === 'number' ? result.status : 0,
|
|
249
|
+
errorKind: result?.errorKind ?? null,
|
|
250
|
+
message: String(result?.error ?? fallbackMessage ?? '').slice(0, 500) || null,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Stash a single write described by a failed consoleFetch result, when (and only
|
|
256
|
+
* when) it is outage class and a vault root is known. Returns the stash record
|
|
257
|
+
* or null (caller then surfaces its original loud error).
|
|
258
|
+
*/
|
|
259
|
+
export function stashIfOutage(result, entry, vaultRoot = defaultVaultRoot) {
|
|
260
|
+
if (!vaultRoot || !isOutageClassResult(result)) return null;
|
|
261
|
+
return stashWrite(vaultRoot, { ...entry, failure: failureOf(result) });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Drain
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
let draining = false;
|
|
269
|
+
let drainOnceDone = false;
|
|
270
|
+
|
|
271
|
+
const sameName = (a, b) =>
|
|
272
|
+
typeof a === 'string' && typeof b === 'string' && a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
273
|
+
|
|
274
|
+
/** Find an already-landed spec by exact name (case-insensitive) within its type. */
|
|
275
|
+
async function findSpecByName(consoleFetch, type, name) {
|
|
276
|
+
let after = null;
|
|
277
|
+
for (let i = 0; i < 20; i += 1) {
|
|
278
|
+
const params = new URLSearchParams();
|
|
279
|
+
if (type) params.set('type', type);
|
|
280
|
+
params.set('limit', '500');
|
|
281
|
+
if (after) params.set('after', after);
|
|
282
|
+
const res = await consoleFetch(`/api/specs?${params.toString()}`);
|
|
283
|
+
if (!res?.ok) return { error: res };
|
|
284
|
+
const rows = Array.isArray(res.body?.specs) ? res.body.specs : [];
|
|
285
|
+
const hit = rows.find((r) => sameName(r?.name, name) || sameName(r?.title, name));
|
|
286
|
+
if (hit) return { hit };
|
|
287
|
+
const next = typeof res.body?.next_cursor === 'string' && res.body.next_cursor ? res.body.next_cursor : null;
|
|
288
|
+
if (!next || next === after || rows.length === 0) break;
|
|
289
|
+
after = next;
|
|
290
|
+
}
|
|
291
|
+
return { hit: null };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function basenameOf(spec) {
|
|
295
|
+
const rel = spec?.relPath ?? spec?.path ?? null;
|
|
296
|
+
if (rel) return String(rel).split(/[\\/]/).pop().replace(/\.md$/i, '');
|
|
297
|
+
return spec?.basename ?? spec?.id ?? null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const needsLandingCheck = (failure) =>
|
|
301
|
+
failure?.errorKind === 'timeout' || failure?.errorKind === 'abort' || (failure?.status ?? 0) >= 500;
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Replay one request. Returns
|
|
305
|
+
* `{ outcome: 'ok'|'skipped'|'quarantine'|'stop', result?, reason?, spec? }`.
|
|
306
|
+
* `outcome: 'stop'` means outage / sign-in: leave the entry (and everything
|
|
307
|
+
* after it) for the next successful call.
|
|
308
|
+
*/
|
|
309
|
+
async function replayRequest(consoleFetch, step, parentOp) {
|
|
310
|
+
if (parentOp === 'create' && needsLandingCheck(step.failure ?? null)) {
|
|
311
|
+
const found = await findSpecByName(consoleFetch, step.request_body?.type, step.request_body?.name);
|
|
312
|
+
if (found.error) {
|
|
313
|
+
return isOutageClassResult(found.error) || found.error.status === 401 || found.error.status === 403
|
|
314
|
+
? { outcome: 'stop', result: found.error }
|
|
315
|
+
: { outcome: 'quarantine', reason: 'console_refused', result: found.error };
|
|
316
|
+
}
|
|
317
|
+
if (found.hit) return { outcome: 'skipped', landed_server_side: true, spec: found.hit };
|
|
318
|
+
}
|
|
319
|
+
if (parentOp === 'update' && step.pre_image_updated) {
|
|
320
|
+
const cur = await consoleFetch(step.route);
|
|
321
|
+
if (!cur?.ok) {
|
|
322
|
+
return isOutageClassResult(cur) || cur?.status === 401 || cur?.status === 403
|
|
323
|
+
? { outcome: 'stop', result: cur }
|
|
324
|
+
: { outcome: 'quarantine', reason: 'console_refused', result: cur };
|
|
325
|
+
}
|
|
326
|
+
const cloudUpdated = cur.body?.spec?.updated ?? cur.body?.spec?.frontmatter?.updated ?? null;
|
|
327
|
+
const a = Date.parse(String(cloudUpdated));
|
|
328
|
+
const b = Date.parse(String(step.pre_image_updated));
|
|
329
|
+
if (Number.isFinite(a) && Number.isFinite(b) && a > b) {
|
|
330
|
+
return { outcome: 'quarantine', reason: 'cloud_newer', result: cur };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const headers = step.idempotency_key ? { 'idempotency-key': step.idempotency_key } : undefined;
|
|
334
|
+
const res = await consoleFetch(step.route, {
|
|
335
|
+
method: step.method || 'POST',
|
|
336
|
+
...(step.request_body !== null && step.request_body !== undefined ? { body: step.request_body } : {}),
|
|
337
|
+
...(headers ? { headers } : {}),
|
|
338
|
+
...(parentOp === 'create' ? { retryOnTimeout: false } : {}),
|
|
339
|
+
});
|
|
340
|
+
if (res?.ok) return { outcome: 'ok', result: res };
|
|
341
|
+
if (isOutageClassResult(res) || res?.status === 401 || res?.status === 403 || res?.authProblem) {
|
|
342
|
+
return { outcome: 'stop', result: res };
|
|
343
|
+
}
|
|
344
|
+
return { outcome: 'quarantine', reason: 'console_refused', result: res };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Replay every queued write, oldest-first, with the STORED idempotency keys.
|
|
349
|
+
* Never stashes (recursion-guarded). 4xx -> quarantine and keep going; 5xx /
|
|
350
|
+
* status 0 / sign-in problem -> stop and leave the rest queued.
|
|
351
|
+
*
|
|
352
|
+
* @returns {Promise<{drained: object[], quarantined: object[], stopped_at: object|null}>}
|
|
353
|
+
*/
|
|
354
|
+
export async function drainWriteOutbox({ vaultRoot, consoleFetch } = {}) {
|
|
355
|
+
const out = { drained: [], quarantined: [], stopped_at: null };
|
|
356
|
+
if (draining || !vaultRoot || typeof consoleFetch !== 'function') return out;
|
|
357
|
+
draining = true;
|
|
358
|
+
try {
|
|
359
|
+
for (const entry of listOutbox(vaultRoot)) {
|
|
360
|
+
const stopAt = (res, step = null) => {
|
|
361
|
+
out.stopped_at = {
|
|
362
|
+
outbox_id: entry.outbox_id,
|
|
363
|
+
status: res?.status ?? 0,
|
|
364
|
+
errorKind: res?.errorKind ?? null,
|
|
365
|
+
...(step ? { step_index: step } : {}),
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
const quarantine = (r, extra = {}) => {
|
|
369
|
+
quarantineOutboxEntry(vaultRoot, entry.path, {
|
|
370
|
+
reason: r.reason,
|
|
371
|
+
status: r.result?.status ?? null,
|
|
372
|
+
consoleMessage: r.result?.error ?? JSON.stringify(r.result?.body ?? {}).slice(0, 400),
|
|
373
|
+
});
|
|
374
|
+
out.quarantined.push({
|
|
375
|
+
outbox_id: entry.outbox_id,
|
|
376
|
+
reason: r.reason,
|
|
377
|
+
status: r.result?.status ?? null,
|
|
378
|
+
route: entry.route,
|
|
379
|
+
...extra,
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
if (entry.op === 'batch') {
|
|
384
|
+
const steps = Array.isArray(entry.steps) ? [...entry.steps] : [];
|
|
385
|
+
let stopped = false;
|
|
386
|
+
let done = 0;
|
|
387
|
+
while (steps.length > 0) {
|
|
388
|
+
const step = steps[0];
|
|
389
|
+
const r = await replayRequest(consoleFetch, { ...step, failure: step.failure ?? (done === 0 ? entry.failure : null) }, step.kind ?? 'batch');
|
|
390
|
+
if (r.outcome === 'stop') {
|
|
391
|
+
stopAt(r.result, done);
|
|
392
|
+
stopped = true;
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
if (r.outcome === 'quarantine') {
|
|
396
|
+
quarantine(r, { step_index: done });
|
|
397
|
+
stopped = 'quarantined';
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
const spec = r.spec ?? r.result?.body?.spec ?? null;
|
|
401
|
+
out.drained.push({
|
|
402
|
+
outbox_id: entry.outbox_id,
|
|
403
|
+
spec_id: spec?.id ?? step.spec_id ?? null,
|
|
404
|
+
basename: basenameOf(spec),
|
|
405
|
+
route: step.route,
|
|
406
|
+
...(r.landed_server_side ? { landed_server_side: true } : {}),
|
|
407
|
+
});
|
|
408
|
+
steps.shift();
|
|
409
|
+
done += 1;
|
|
410
|
+
if (steps.length > 0) {
|
|
411
|
+
// persist progress so a crash/outage never re-sends a landed step
|
|
412
|
+
try {
|
|
413
|
+
writeFileSync(entry.path, `${JSON.stringify({ ...stripRuntime(entry), steps }, null, 2)}\n`, 'utf8');
|
|
414
|
+
} catch {
|
|
415
|
+
/* best effort */
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (stopped === 'quarantined') continue;
|
|
420
|
+
if (stopped) break;
|
|
421
|
+
removeOutboxEntry(entry.path);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const r = await replayRequest(consoleFetch, entry, entry.op);
|
|
426
|
+
if (r.outcome === 'stop') {
|
|
427
|
+
stopAt(r.result);
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
if (r.outcome === 'quarantine') {
|
|
431
|
+
quarantine(r);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const spec = r.spec ?? r.result?.body?.spec ?? null;
|
|
435
|
+
removeOutboxEntry(entry.path);
|
|
436
|
+
out.drained.push({
|
|
437
|
+
outbox_id: entry.outbox_id,
|
|
438
|
+
spec_id: spec?.id ?? entry.spec_id ?? null,
|
|
439
|
+
basename: basenameOf(spec),
|
|
440
|
+
route: entry.route,
|
|
441
|
+
...(r.landed_server_side ? { landed_server_side: true } : {}),
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
} catch {
|
|
445
|
+
/* a drain never throws into a tool call */
|
|
446
|
+
} finally {
|
|
447
|
+
draining = false;
|
|
448
|
+
}
|
|
449
|
+
return out;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function stripRuntime(entry) {
|
|
453
|
+
const { outbox_id: _id, path: _p, ...rest } = entry;
|
|
454
|
+
return rest;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Drain and shape the result for a tool response: `{ drained, pending_sync }`
|
|
459
|
+
* fields only when there is something to say. Failure-safe.
|
|
460
|
+
*/
|
|
461
|
+
export async function drainAndSummarize(vaultRoot, consoleFetch) {
|
|
462
|
+
if (!vaultRoot) return {};
|
|
463
|
+
try {
|
|
464
|
+
if (outboxCount(vaultRoot) === 0) return {};
|
|
465
|
+
const d = await drainWriteOutbox({ vaultRoot, consoleFetch });
|
|
466
|
+
const pending = outboxCount(vaultRoot);
|
|
467
|
+
return {
|
|
468
|
+
...(d.drained.length ? { drained: d.drained } : {}),
|
|
469
|
+
...(d.quarantined.length ? { quarantined: d.quarantined } : {}),
|
|
470
|
+
...(pending ? { pending_sync: pending } : {}),
|
|
471
|
+
};
|
|
472
|
+
} catch {
|
|
473
|
+
return {};
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Once per process, after the first successful console call: drain in the
|
|
479
|
+
* background. Non-blocking and failure-safe.
|
|
480
|
+
*/
|
|
481
|
+
export function maybeDrainOnFirstSuccess(consoleFetch) {
|
|
482
|
+
if (drainOnceDone || !defaultVaultRoot) return;
|
|
483
|
+
drainOnceDone = true;
|
|
484
|
+
const root = defaultVaultRoot;
|
|
485
|
+
setImmediate(() => {
|
|
486
|
+
try {
|
|
487
|
+
if (outboxCount(root) === 0) return;
|
|
488
|
+
drainWriteOutbox({ vaultRoot: root, consoleFetch }).catch(() => {});
|
|
489
|
+
} catch {
|
|
490
|
+
/* swallow */
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Control-plane append writes (report_progress, record_wip, handoff_record):
|
|
497
|
+
* on outage class in cloud reliance, queue as `op: 'control'` and return the
|
|
498
|
+
* `deferred` tool response; otherwise return null so the caller keeps its loud
|
|
499
|
+
* error envelope. `isCloud` is an injected async predicate (the caller's
|
|
500
|
+
* `resolveReadTier() === 'neon'`), keeping this module free of read-path.
|
|
501
|
+
* Claims and status flips must NEVER be routed through here.
|
|
502
|
+
*/
|
|
503
|
+
export async function deferControlWrite(result, entry, { vaultRoot = defaultVaultRoot, isCloud = async () => true } = {}) {
|
|
504
|
+
if (!vaultRoot || !isOutageClassResult(result)) return null;
|
|
505
|
+
try {
|
|
506
|
+
if (!(await isCloud())) return null;
|
|
507
|
+
} catch {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
const stashed = stashWrite(vaultRoot, { ...entry, op: 'control', failure: failureOf(result) });
|
|
511
|
+
return stashed ? deferredToolResponse(buildDeferredPayload(stashed, vaultRoot)) : null;
|
|
512
|
+
}
|