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,602 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cloud-vault-writes.mjs — the WRITE-side twin of `read-path.mjs`.
|
|
4
|
+
*
|
|
5
|
+
* `read-path.mjs` decides `disk` vs `neon` for every vault READ the MCP
|
|
6
|
+
* server makes. This module is the single place every vault WRITE goes when
|
|
7
|
+
* `resolveVaultWriteMode()` says `cloud`: a create becomes
|
|
8
|
+
* `POST /api/specs` (the console mints the id, the family and the filename),
|
|
9
|
+
* an edit of an existing spec becomes `PUT /api/specs/:id`, and a listing
|
|
10
|
+
* needed to build either becomes `GET /api/specs?type=…`.
|
|
11
|
+
*
|
|
12
|
+
* Why this exists: a disk write in cloud mode is a SPLIT BRAIN — a markdown
|
|
13
|
+
* file on a machine the console's index has never heard of, on a machine that
|
|
14
|
+
* may not even hold a clone of the vault. `handleSaveResearch`
|
|
15
|
+
* (research-handlers.mjs) and `handleFileSpec` (filing-tools.mjs) already
|
|
16
|
+
* file through the console; this module gives every OTHER writer (the
|
|
17
|
+
* promotion helpers, plan-push, register-spec, spec_edit, capture-focus) the
|
|
18
|
+
* same two verbs without each re-deriving the request shape, the
|
|
19
|
+
* idempotency-key rule, or the failure envelope.
|
|
20
|
+
*
|
|
21
|
+
* Transport-free by design, like read-path.mjs: callers inject
|
|
22
|
+
* `consoleFetch`. The module never imports console-transport.mjs, so every
|
|
23
|
+
* test drives it with a recorder and never opens a socket.
|
|
24
|
+
*
|
|
25
|
+
* Failure contract: every failure throws {@link CloudWriteUnreachableError},
|
|
26
|
+
* whose `message` STARTS WITH `CONSOLE_UNREACHABLE_CLOUD_MODE` and names the
|
|
27
|
+
* route and the console — the same string shape `readViaSelector` produces on
|
|
28
|
+
* the read side, so one `catch` in a handler can render either. There is no
|
|
29
|
+
* disk fallback: a cloud-reliant session that cannot reach its console fails
|
|
30
|
+
* loudly rather than leaving bytes behind that nothing indexes.
|
|
31
|
+
*
|
|
32
|
+
* There is deliberately NO rollback. `commitWrites`'s disk transaction can
|
|
33
|
+
* restore a snapshot; the console has no "un-create". Instead
|
|
34
|
+
* {@link commitWritesViaConsole} stops at the FIRST failure and reports which
|
|
35
|
+
* step failed and which steps already landed, so the caller can say exactly
|
|
36
|
+
* what state the vault is in ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]] AC5).
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { randomUUID } from 'node:crypto';
|
|
40
|
+
import { walkSpecListPages } from './list-tools.mjs';
|
|
41
|
+
import { parseFrontmatter } from './vault-io.mjs';
|
|
42
|
+
import {
|
|
43
|
+
buildDeferredPayload,
|
|
44
|
+
drainAndSummarize,
|
|
45
|
+
getWriteOutboxRoot,
|
|
46
|
+
isOutageClassResult,
|
|
47
|
+
stashIfOutage,
|
|
48
|
+
stashWrite,
|
|
49
|
+
failureOf,
|
|
50
|
+
DEFERRED_RECEIPT,
|
|
51
|
+
} from './write-outbox.mjs';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Raised when a `cloud`-mode write fails. Mirrors
|
|
55
|
+
* `ConsoleUnreachableCloudModeError` (read-path.mjs) exactly — same `name`,
|
|
56
|
+
* same message prefix — so callers that already render one render both.
|
|
57
|
+
*/
|
|
58
|
+
export class CloudWriteUnreachableError extends Error {
|
|
59
|
+
constructor(message, details = {}) {
|
|
60
|
+
super(message);
|
|
61
|
+
this.name = 'CONSOLE_UNREACHABLE_CLOUD_MODE';
|
|
62
|
+
Object.assign(this, details);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Raised INSTEAD of a plain {@link CloudWriteUnreachableError} when the write hit
|
|
68
|
+
* an outage (status 0 / timeout / 5xx) and was queued to the write outbox
|
|
69
|
+
* ([[heyspex-is-online-only-no-offline]] 2026-08-20 amendment). It extends the
|
|
70
|
+
* unreachable error so every existing `catch` still works, but it carries
|
|
71
|
+
* `deferred: true` + the truthful receipt: the write did NOT land yet, and it is
|
|
72
|
+
* NOT lost. Callers that can return a tool response render
|
|
73
|
+
* {@link deferredPayloadFromError}; callers that need the new spec's id/basename
|
|
74
|
+
* (promotion chains) cannot continue and surface it as before.
|
|
75
|
+
*/
|
|
76
|
+
export class CloudWriteDeferredError extends CloudWriteUnreachableError {
|
|
77
|
+
constructor(message, details = {}) {
|
|
78
|
+
super(message, details);
|
|
79
|
+
this.deferred = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The `{ status: 'deferred', … }` tool payload for a deferred error, or null. */
|
|
84
|
+
export function deferredPayloadFromError(err) {
|
|
85
|
+
if (!err?.deferred || !err.outbox_id) return null;
|
|
86
|
+
return {
|
|
87
|
+
status: 'deferred',
|
|
88
|
+
outbox_id: err.outbox_id,
|
|
89
|
+
pending_sync: err.pending_sync ?? 1,
|
|
90
|
+
receipt: err.receipt ?? DEFERRED_RECEIPT,
|
|
91
|
+
...(err.completed_steps ? { completed_steps: err.completed_steps } : {}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Details carrying the replayable request for a no-stash caller (commitWritesViaConsole). */
|
|
96
|
+
function outageRequest(noStash, result, entry) {
|
|
97
|
+
return noStash && isOutageClassResult(result)
|
|
98
|
+
? { outage_request: { ...entry, failure: failureOf(result) } }
|
|
99
|
+
: {};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function deferredError(routeLabel, consoleUrl, stashed, vaultRoot, details = {}) {
|
|
103
|
+
const payload = buildDeferredPayload(stashed, vaultRoot);
|
|
104
|
+
return new CloudWriteDeferredError(
|
|
105
|
+
`CONSOLE_UNREACHABLE_CLOUD_MODE: route "${routeLabel}" failed against console ${consoleUrl ?? '<unknown console>'} — ${DEFERRED_RECEIPT} (outbox_id ${payload.outbox_id})`,
|
|
106
|
+
{ route: routeLabel, console_url: consoleUrl ?? null, ...details, outbox_id: payload.outbox_id, pending_sync: payload.pending_sync, receipt: payload.receipt },
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Build the canonical failure error for a route + console + detail. */
|
|
111
|
+
export function cloudWriteFailure(routeLabel, consoleUrl, detail, details = {}) {
|
|
112
|
+
return new CloudWriteUnreachableError(
|
|
113
|
+
`CONSOLE_UNREACHABLE_CLOUD_MODE: route "${routeLabel}" failed against console ${consoleUrl ?? '<unknown console>'} — ${detail}`,
|
|
114
|
+
{ route: routeLabel, console_url: consoleUrl ?? null, ...details },
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** One-line summary of a consoleFetch failure result, for the message above. */
|
|
119
|
+
function describeResult(result) {
|
|
120
|
+
if (!result) return 'no response';
|
|
121
|
+
const bodyText =
|
|
122
|
+
result.error ||
|
|
123
|
+
(result.body ? JSON.stringify(result.body).slice(0, 400) : '') ||
|
|
124
|
+
'';
|
|
125
|
+
return `${result.status ?? '?'}${bodyText ? ` ${bodyText}` : ''}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Fields the SERVER mints and a create request must therefore never carry.
|
|
130
|
+
* `id`/`file_id` are the file key itself (server-minted since 2026-07-10 —
|
|
131
|
+
* a locally minted key in a file-less vault collides with a cloud-side one);
|
|
132
|
+
* `created`/`updated`/`created_tz`/`updated_tz` are stamped by `createSpec`.
|
|
133
|
+
* `family` is NOT stripped: an explicit family is a legal, meaningful
|
|
134
|
+
* override (`createSpec` honours it over parent inheritance).
|
|
135
|
+
*/
|
|
136
|
+
export const SERVER_MINTED_FIELDS = Object.freeze([
|
|
137
|
+
'id',
|
|
138
|
+
'file_id',
|
|
139
|
+
'created',
|
|
140
|
+
'updated',
|
|
141
|
+
'created_tz',
|
|
142
|
+
'updated_tz',
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The status the console seeds on every non-ADR create (`createSpec` in
|
|
147
|
+
* spex-console/src/lib/vault.ts). Sending it on a POST asks for nothing.
|
|
148
|
+
*/
|
|
149
|
+
export const DEFAULT_INITIAL_STATUS = 'open';
|
|
150
|
+
|
|
151
|
+
/** True when `value` is the default initial status (case/whitespace-insensitive). */
|
|
152
|
+
export function isDefaultInitialStatus(value) {
|
|
153
|
+
return typeof value === 'string' && value.trim().toLowerCase() === DEFAULT_INITIAL_STATUS;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Copy of `frontmatter` with every server-minted field removed. */
|
|
157
|
+
export function stripServerMintedFields(frontmatter = {}) {
|
|
158
|
+
const out = {};
|
|
159
|
+
for (const [key, value] of Object.entries(frontmatter ?? {})) {
|
|
160
|
+
if (SERVER_MINTED_FIELDS.includes(key)) continue;
|
|
161
|
+
out[key] = value;
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Split a raw `.md` file (the shape every disk writer already builds) into the
|
|
168
|
+
* `{ frontmatter, body }` pair both console write routes take. Reuses
|
|
169
|
+
* `parseFrontmatter` from vault-io.mjs so the YAML dialect the vault writes is
|
|
170
|
+
* the YAML dialect this reads — no second parser to drift.
|
|
171
|
+
*/
|
|
172
|
+
export function splitRawSpec(raw) {
|
|
173
|
+
const { frontmatter, body } = parseFrontmatter(String(raw ?? ''));
|
|
174
|
+
return { frontmatter: frontmatter ?? {}, body: body ?? '' };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Basename (no extension) from a console-returned relPath. */
|
|
178
|
+
export function basenameFromRelPath(relPath) {
|
|
179
|
+
if (!relPath) return null;
|
|
180
|
+
return String(relPath)
|
|
181
|
+
.split(/[\\/]/)
|
|
182
|
+
.pop()
|
|
183
|
+
.replace(/\.md$/i, '');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* `POST /api/specs` — create one spec through the console.
|
|
188
|
+
*
|
|
189
|
+
* The request shape is `buildFileSpecPostBody`'s (filing-tools.mjs), down to
|
|
190
|
+
* the per-call `idempotency-key` header and `retryOnTimeout: false` (the
|
|
191
|
+
* [[HS-2FQY]] double-mint guard: a blind retry against a REMOTE console can
|
|
192
|
+
* land on a different serverless instance whose in-memory idempotency
|
|
193
|
+
* registry cannot replay the first create, minting a second id for one spec).
|
|
194
|
+
*
|
|
195
|
+
* Returns `{ specId, relPath, basename, spec, consoleWrite, warnings }`. The
|
|
196
|
+
* BASENAME is the load-bearing return value for promotion flows: every
|
|
197
|
+
* sibling write references the new document by `[[basename]]`, and in cloud
|
|
198
|
+
* mode that basename is only knowable after this call returns — which is why
|
|
199
|
+
* cloud-mode promotion is create-first, link-second.
|
|
200
|
+
*/
|
|
201
|
+
export async function createSpecViaConsole({
|
|
202
|
+
consoleFetch,
|
|
203
|
+
consoleUrl = null,
|
|
204
|
+
type,
|
|
205
|
+
name,
|
|
206
|
+
summary = null,
|
|
207
|
+
frontmatter = null,
|
|
208
|
+
body = null,
|
|
209
|
+
actingAgent = null,
|
|
210
|
+
idempotencyKey = null,
|
|
211
|
+
routeLabel = 'POST /api/specs',
|
|
212
|
+
vaultRoot = null,
|
|
213
|
+
noStash = false,
|
|
214
|
+
}) {
|
|
215
|
+
const outboxRoot = noStash ? null : vaultRoot ?? getWriteOutboxRoot();
|
|
216
|
+
if (typeof consoleFetch !== 'function') {
|
|
217
|
+
throw cloudWriteFailure(routeLabel, consoleUrl, 'no consoleFetch dependency was injected');
|
|
218
|
+
}
|
|
219
|
+
if (typeof type !== 'string' || !type.trim()) {
|
|
220
|
+
throw cloudWriteFailure(routeLabel, consoleUrl, 'a spec `type` is required to create through the console');
|
|
221
|
+
}
|
|
222
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
223
|
+
throw cloudWriteFailure(routeLabel, consoleUrl, 'a spec `name` is required to create through the console');
|
|
224
|
+
}
|
|
225
|
+
const cleanedFm = stripServerMintedFields(frontmatter ?? {});
|
|
226
|
+
// `name`/`type`/`summary` ride at the TOP level of the request (the route's
|
|
227
|
+
// contract); leaving duplicates inside `frontmatter` lets the two disagree.
|
|
228
|
+
delete cleanedFm.name;
|
|
229
|
+
delete cleanedFm.type;
|
|
230
|
+
// [[HS-ISZP]]: `status:` is owned by the status route — the create route
|
|
231
|
+
// refuses it out of band (`out_of_band_status_write`). The DEFAULT initial
|
|
232
|
+
// value is not a transition (the server seeds `open` itself), so it is
|
|
233
|
+
// dropped here for every disk-template-derived body (research stub,
|
|
234
|
+
// diagnose, epic/task stubs). Any OTHER value is deliberately left in so the
|
|
235
|
+
// server refuses it loudly rather than this client silently downgrading it.
|
|
236
|
+
if (isDefaultInitialStatus(cleanedFm.status)) delete cleanedFm.status;
|
|
237
|
+
const requestBody = {
|
|
238
|
+
type: type.trim(),
|
|
239
|
+
name: name.trim(),
|
|
240
|
+
...(summary ? { summary } : {}),
|
|
241
|
+
...(Object.keys(cleanedFm).length > 0 ? { frontmatter: cleanedFm } : {}),
|
|
242
|
+
...(typeof body === 'string' ? { body } : {}),
|
|
243
|
+
...(actingAgent ? { acting_agent: actingAgent } : {}),
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// One key per LOGICAL call: the stash below stores THIS key so a replay is the
|
|
247
|
+
// same create, never a second mint ([[HS-2FQY]]).
|
|
248
|
+
const createKey = idempotencyKey ?? randomUUID();
|
|
249
|
+
const createEntry = {
|
|
250
|
+
op: 'create',
|
|
251
|
+
kind: 'create',
|
|
252
|
+
route: '/api/specs',
|
|
253
|
+
method: 'POST',
|
|
254
|
+
request_body: requestBody,
|
|
255
|
+
idempotency_key: createKey,
|
|
256
|
+
acting_agent: actingAgent,
|
|
257
|
+
};
|
|
258
|
+
if (outboxRoot) await drainAndSummarize(outboxRoot, consoleFetch);
|
|
259
|
+
let result;
|
|
260
|
+
try {
|
|
261
|
+
result = await consoleFetch('/api/specs', {
|
|
262
|
+
method: 'POST',
|
|
263
|
+
body: requestBody,
|
|
264
|
+
headers: { 'idempotency-key': createKey },
|
|
265
|
+
retryOnTimeout: false,
|
|
266
|
+
});
|
|
267
|
+
} catch (err) {
|
|
268
|
+
if (outboxRoot) {
|
|
269
|
+
const stashed = stashWrite(outboxRoot, {
|
|
270
|
+
...createEntry,
|
|
271
|
+
failure: { status: 0, errorKind: 'network', message: String(err?.message ?? err).slice(0, 500) },
|
|
272
|
+
});
|
|
273
|
+
if (stashed) throw deferredError(routeLabel, consoleUrl, stashed, outboxRoot, { attempted: { type: requestBody.type, name: requestBody.name } });
|
|
274
|
+
}
|
|
275
|
+
throw cloudWriteFailure(routeLabel, consoleUrl, String(err?.message ?? err), {
|
|
276
|
+
attempted: { type: requestBody.type, name: requestBody.name },
|
|
277
|
+
...outageRequest(noStash, { ok: false, status: 0, errorKind: 'network', error: String(err?.message ?? err) }, createEntry),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (!result?.ok) {
|
|
281
|
+
const stashed = outboxRoot
|
|
282
|
+
? stashIfOutage(result, createEntry, outboxRoot)
|
|
283
|
+
: null;
|
|
284
|
+
if (stashed) {
|
|
285
|
+
throw deferredError(routeLabel, result?.url ?? consoleUrl, stashed, outboxRoot, {
|
|
286
|
+
status: result?.status ?? null,
|
|
287
|
+
attempted: { type: requestBody.type, name: requestBody.name },
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
throw cloudWriteFailure(routeLabel, result?.url ?? consoleUrl, describeResult(result), {
|
|
291
|
+
status: result?.status ?? null,
|
|
292
|
+
console_response: result?.body ?? null,
|
|
293
|
+
attempted: { type: requestBody.type, name: requestBody.name },
|
|
294
|
+
...outageRequest(noStash, result, createEntry),
|
|
295
|
+
...(result?.errorKind === 'timeout'
|
|
296
|
+
? {
|
|
297
|
+
hint:
|
|
298
|
+
'The create may still have completed server-side after this client timed out. '
|
|
299
|
+
+ 'Search for a spec with this exact name before retrying — a blind retry can mint a duplicate id ([[HS-2FQY]]).',
|
|
300
|
+
}
|
|
301
|
+
: {}),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
const spec = result.body?.spec ?? null;
|
|
305
|
+
const specId = typeof spec?.id === 'string' && spec.id ? spec.id : null;
|
|
306
|
+
const relPath = spec?.relPath ?? spec?.path ?? null;
|
|
307
|
+
if (!specId) {
|
|
308
|
+
throw cloudWriteFailure(
|
|
309
|
+
routeLabel,
|
|
310
|
+
result.url ?? consoleUrl,
|
|
311
|
+
`answered ${result.status} but carried no spec.id — treat the spec as UNCREATED and check the console before retrying`,
|
|
312
|
+
{ status: result.status, console_response: result.body ?? null },
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
specId,
|
|
317
|
+
relPath,
|
|
318
|
+
basename: basenameFromRelPath(relPath) ?? specId,
|
|
319
|
+
spec,
|
|
320
|
+
consoleWrite: result.body?.write ?? null,
|
|
321
|
+
warnings: Array.isArray(result.body?.warnings) ? result.body.warnings : [],
|
|
322
|
+
consoleUrl: result.url ?? consoleUrl,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* `PUT /api/specs/:id` — replace an existing spec's frontmatter + body.
|
|
328
|
+
*
|
|
329
|
+
* The route takes `{ frontmatter: object, body: string }` and answers 404 for
|
|
330
|
+
* an id it cannot resolve (a real answer: the spec is not in this caller's
|
|
331
|
+
* vault), which is surfaced here as a distinct `notFound: true` failure so a
|
|
332
|
+
* caller can tell "unreachable console" from "no such spec".
|
|
333
|
+
*/
|
|
334
|
+
export async function updateSpecViaConsole({
|
|
335
|
+
consoleFetch,
|
|
336
|
+
consoleUrl = null,
|
|
337
|
+
specId,
|
|
338
|
+
frontmatter,
|
|
339
|
+
body,
|
|
340
|
+
routeLabel = null,
|
|
341
|
+
vaultRoot = null,
|
|
342
|
+
noStash = false,
|
|
343
|
+
}) {
|
|
344
|
+
const route = routeLabel ?? `PUT /api/specs/${specId}`;
|
|
345
|
+
const outboxRoot = noStash ? null : vaultRoot ?? getWriteOutboxRoot();
|
|
346
|
+
if (typeof consoleFetch !== 'function') {
|
|
347
|
+
throw cloudWriteFailure(route, consoleUrl, 'no consoleFetch dependency was injected');
|
|
348
|
+
}
|
|
349
|
+
if (typeof specId !== 'string' || !specId.trim()) {
|
|
350
|
+
throw cloudWriteFailure(route, consoleUrl, 'a spec id is required to update through the console');
|
|
351
|
+
}
|
|
352
|
+
if (typeof frontmatter !== 'object' || frontmatter === null) {
|
|
353
|
+
throw cloudWriteFailure(route, consoleUrl, 'PUT /api/specs/:id requires a frontmatter object');
|
|
354
|
+
}
|
|
355
|
+
const putPath = `/api/specs/${encodeURIComponent(specId)}`;
|
|
356
|
+
const putBody = { frontmatter, body: typeof body === 'string' ? body : '' };
|
|
357
|
+
const updateEntry = {
|
|
358
|
+
op: 'update',
|
|
359
|
+
kind: 'update',
|
|
360
|
+
route: putPath,
|
|
361
|
+
method: 'PUT',
|
|
362
|
+
request_body: putBody,
|
|
363
|
+
spec_id: specId,
|
|
364
|
+
// The cloud copy this edit was based on; replay refuses (cloud_newer)
|
|
365
|
+
// when the cloud has moved on since.
|
|
366
|
+
pre_image_updated: frontmatter?.updated ?? null,
|
|
367
|
+
};
|
|
368
|
+
const stashUpdate = (result) => (outboxRoot ? stashIfOutage(result, updateEntry, outboxRoot) : null);
|
|
369
|
+
if (outboxRoot) await drainAndSummarize(outboxRoot, consoleFetch);
|
|
370
|
+
let result;
|
|
371
|
+
try {
|
|
372
|
+
result = await consoleFetch(putPath, { method: 'PUT', body: putBody });
|
|
373
|
+
} catch (err) {
|
|
374
|
+
const stashed = stashUpdate({ ok: false, status: 0, errorKind: 'network', error: String(err?.message ?? err) });
|
|
375
|
+
if (stashed) throw deferredError(route, consoleUrl, stashed, outboxRoot, { spec_id: specId });
|
|
376
|
+
throw cloudWriteFailure(route, consoleUrl, String(err?.message ?? err), {
|
|
377
|
+
spec_id: specId,
|
|
378
|
+
...outageRequest(noStash, { ok: false, status: 0, errorKind: 'network', error: String(err?.message ?? err) }, updateEntry),
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
if (!result?.ok) {
|
|
382
|
+
const stashed = stashUpdate(result);
|
|
383
|
+
if (stashed) throw deferredError(route, result?.url ?? consoleUrl, stashed, outboxRoot, { status: result?.status ?? null, spec_id: specId });
|
|
384
|
+
throw cloudWriteFailure(route, result?.url ?? consoleUrl, describeResult(result), {
|
|
385
|
+
status: result?.status ?? null,
|
|
386
|
+
console_response: result?.body ?? null,
|
|
387
|
+
spec_id: specId,
|
|
388
|
+
...outageRequest(noStash, result, updateEntry),
|
|
389
|
+
...(result?.status === 404 ? { notFound: true } : {}),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
const spec = result.body?.spec ?? null;
|
|
393
|
+
const relPath = spec?.relPath ?? spec?.path ?? null;
|
|
394
|
+
return {
|
|
395
|
+
specId: spec?.id ?? specId,
|
|
396
|
+
relPath,
|
|
397
|
+
basename: basenameFromRelPath(relPath) ?? spec?.id ?? specId,
|
|
398
|
+
spec,
|
|
399
|
+
consoleUrl: result.url ?? consoleUrl,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* `GET /api/specs?type=…` — the cloud twin of a `readdirSync(specs/<folder>)`
|
|
405
|
+
* scan. The keyset walk itself is {@link walkSpecListPages} (list-tools.mjs),
|
|
406
|
+
* the dependency-free primitive every other cloud listing in this package
|
|
407
|
+
* already uses — reused rather than re-implemented so there is exactly one
|
|
408
|
+
* place that knows the `next_cursor` contract. This wrapper only adds the
|
|
409
|
+
* `type`/`status`/`tag` filter and the shared failure envelope, so a listing
|
|
410
|
+
* that fails reads identically to a create or an edit that fails.
|
|
411
|
+
*
|
|
412
|
+
* Returns the raw summary rows (`{ id, basename, type, status, … }`) the route
|
|
413
|
+
* already emits — no reshaping, so callers keep whatever fields they read.
|
|
414
|
+
*/
|
|
415
|
+
export async function listSpecsViaConsole({
|
|
416
|
+
consoleFetch,
|
|
417
|
+
consoleUrl = null,
|
|
418
|
+
type = null,
|
|
419
|
+
status = null,
|
|
420
|
+
tag = null,
|
|
421
|
+
limit = 500,
|
|
422
|
+
maxPages = 50,
|
|
423
|
+
}) {
|
|
424
|
+
const routeLabel = `GET /api/specs${type ? `?type=${type}` : ''}`;
|
|
425
|
+
if (typeof consoleFetch !== 'function') {
|
|
426
|
+
throw cloudWriteFailure(routeLabel, consoleUrl, 'no consoleFetch dependency was injected');
|
|
427
|
+
}
|
|
428
|
+
const params = new URLSearchParams();
|
|
429
|
+
if (type) params.set('type', type);
|
|
430
|
+
if (status) params.set('status', status);
|
|
431
|
+
if (tag) params.set('tag', tag);
|
|
432
|
+
const query = params.toString();
|
|
433
|
+
try {
|
|
434
|
+
return await walkSpecListPages(consoleFetch, `/api/specs${query ? `?${query}` : ''}`, {
|
|
435
|
+
limit,
|
|
436
|
+
maxPages,
|
|
437
|
+
});
|
|
438
|
+
} catch (err) {
|
|
439
|
+
throw cloudWriteFailure(routeLabel, consoleUrl, String(err?.message ?? err));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* The cloud twin of `commitWrites` (promotion-helpers.mjs).
|
|
445
|
+
*
|
|
446
|
+
* Takes the SAME `{ kind: 'create' | 'update', absPath, content }` descriptors
|
|
447
|
+
* the disk path takes, so a caller switches paths with one `if` and never
|
|
448
|
+
* builds two different write plans. Cloud metadata that cannot be derived from
|
|
449
|
+
* the markdown may ride along on an optional `cloud` property:
|
|
450
|
+
* `{ cloud: { type, name, summary, specId } }`.
|
|
451
|
+
* When absent it is derived from the content's own frontmatter (`type:`,
|
|
452
|
+
* `name:`, `summary:`, `id:`) — which every vault document already carries.
|
|
453
|
+
*
|
|
454
|
+
* Writes run IN ORDER and stop at the first failure. Nothing is rolled back
|
|
455
|
+
* (the console has no un-create); the returned error names the failing step
|
|
456
|
+
* and lists the ones that already landed, so the caller reports the real state
|
|
457
|
+
* rather than implying a clean abort.
|
|
458
|
+
*
|
|
459
|
+
* Returns `{ ok: true, results }` where each result is
|
|
460
|
+
* `{ kind, absPath, specId, basename, relPath }`, or
|
|
461
|
+
* `{ ok: false, error, completed }` — the same `{ ok }`-shaped verdict
|
|
462
|
+
* `commitWrites` returns, so callers branch identically.
|
|
463
|
+
*/
|
|
464
|
+
export async function commitWritesViaConsole(writes, { consoleFetch, consoleUrl = null, actingAgent = null, vaultRoot = null } = {}) {
|
|
465
|
+
const results = [];
|
|
466
|
+
const outboxRoot = vaultRoot ?? getWriteOutboxRoot();
|
|
467
|
+
if (outboxRoot && typeof consoleFetch === 'function') await drainAndSummarize(outboxRoot, consoleFetch);
|
|
468
|
+
for (let i = 0; i < (writes ?? []).length; i++) {
|
|
469
|
+
const w = writes[i];
|
|
470
|
+
const { frontmatter, body } = splitRawSpec(w.content);
|
|
471
|
+
const meta = w.cloud ?? {};
|
|
472
|
+
try {
|
|
473
|
+
if (w.kind === 'create') {
|
|
474
|
+
const type = meta.type ?? frontmatter.type ?? null;
|
|
475
|
+
const name = meta.name ?? frontmatter.name ?? null;
|
|
476
|
+
const summary = meta.summary ?? frontmatter.summary ?? null;
|
|
477
|
+
const created = await createSpecViaConsole({
|
|
478
|
+
consoleFetch,
|
|
479
|
+
consoleUrl,
|
|
480
|
+
type,
|
|
481
|
+
name,
|
|
482
|
+
summary,
|
|
483
|
+
frontmatter,
|
|
484
|
+
body,
|
|
485
|
+
actingAgent: meta.actingAgent ?? actingAgent,
|
|
486
|
+
noStash: true,
|
|
487
|
+
});
|
|
488
|
+
results.push({
|
|
489
|
+
kind: 'create',
|
|
490
|
+
absPath: w.absPath ?? null,
|
|
491
|
+
specId: created.specId,
|
|
492
|
+
basename: created.basename,
|
|
493
|
+
relPath: created.relPath,
|
|
494
|
+
});
|
|
495
|
+
} else if (w.kind === 'update') {
|
|
496
|
+
const specId =
|
|
497
|
+
meta.specId ?? frontmatter.id ?? (w.absPath ? basenameFromRelPath(w.absPath) : null);
|
|
498
|
+
const updated = await updateSpecViaConsole({
|
|
499
|
+
consoleFetch,
|
|
500
|
+
consoleUrl,
|
|
501
|
+
specId,
|
|
502
|
+
frontmatter,
|
|
503
|
+
body,
|
|
504
|
+
noStash: true,
|
|
505
|
+
});
|
|
506
|
+
results.push({
|
|
507
|
+
kind: 'update',
|
|
508
|
+
absPath: w.absPath ?? null,
|
|
509
|
+
specId: updated.specId,
|
|
510
|
+
basename: updated.basename,
|
|
511
|
+
relPath: updated.relPath,
|
|
512
|
+
});
|
|
513
|
+
} else {
|
|
514
|
+
throw cloudWriteFailure(
|
|
515
|
+
'commitWritesViaConsole',
|
|
516
|
+
consoleUrl,
|
|
517
|
+
`unknown write kind "${w.kind}" (expected "create" | "update")`,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
} catch (err) {
|
|
521
|
+
err.failed_step = { index: i, kind: w.kind, abs_path: w.absPath ?? null };
|
|
522
|
+
err.completed_steps = results;
|
|
523
|
+
// Outage on step i: queue steps i..n-1 as ONE ordered batch (each step keeps
|
|
524
|
+
// its own idempotency key) so the remainder replays in order.
|
|
525
|
+
const failedReq = err.outage_request;
|
|
526
|
+
if (outboxRoot && failedReq) {
|
|
527
|
+
const steps = [failedReq];
|
|
528
|
+
for (let j = i + 1; j < writes.length; j++) {
|
|
529
|
+
const planned = planBatchStep(writes[j], actingAgent);
|
|
530
|
+
if (!planned) break;
|
|
531
|
+
steps.push(planned);
|
|
532
|
+
}
|
|
533
|
+
if (steps.length === writes.length - i) {
|
|
534
|
+
const stashed = stashWrite(outboxRoot, {
|
|
535
|
+
op: 'batch',
|
|
536
|
+
route: failedReq.route,
|
|
537
|
+
method: failedReq.method,
|
|
538
|
+
request_body: null,
|
|
539
|
+
acting_agent: actingAgent,
|
|
540
|
+
failure: failedReq.failure,
|
|
541
|
+
steps,
|
|
542
|
+
completed_steps: results,
|
|
543
|
+
});
|
|
544
|
+
if (stashed) {
|
|
545
|
+
const deferred = deferredError('commitWritesViaConsole', consoleUrl, stashed, outboxRoot, {
|
|
546
|
+
status: err.status ?? null,
|
|
547
|
+
failed_step: err.failed_step,
|
|
548
|
+
completed_steps: results,
|
|
549
|
+
});
|
|
550
|
+
return { ok: false, error: deferred, completed: results, deferred: true };
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return { ok: false, error: err, completed: results };
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return { ok: true, results };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** Build a replayable batch step for a write that was never attempted. */
|
|
561
|
+
function planBatchStep(w, actingAgent) {
|
|
562
|
+
const { frontmatter, body } = splitRawSpec(w.content);
|
|
563
|
+
const meta = w.cloud ?? {};
|
|
564
|
+
if (w.kind === 'create') {
|
|
565
|
+
const type = meta.type ?? frontmatter.type ?? null;
|
|
566
|
+
const name = meta.name ?? frontmatter.name ?? null;
|
|
567
|
+
if (!type || !name) return null;
|
|
568
|
+
const cleaned = stripServerMintedFields(frontmatter);
|
|
569
|
+
delete cleaned.name;
|
|
570
|
+
delete cleaned.type;
|
|
571
|
+
if (isDefaultInitialStatus(cleaned.status)) delete cleaned.status;
|
|
572
|
+
const summary = meta.summary ?? frontmatter.summary ?? null;
|
|
573
|
+
const agent = meta.actingAgent ?? actingAgent;
|
|
574
|
+
return {
|
|
575
|
+
kind: 'create',
|
|
576
|
+
route: '/api/specs',
|
|
577
|
+
method: 'POST',
|
|
578
|
+
request_body: {
|
|
579
|
+
type: String(type).trim(),
|
|
580
|
+
name: String(name).trim(),
|
|
581
|
+
...(summary ? { summary } : {}),
|
|
582
|
+
...(Object.keys(cleaned).length > 0 ? { frontmatter: cleaned } : {}),
|
|
583
|
+
body,
|
|
584
|
+
...(agent ? { acting_agent: agent } : {}),
|
|
585
|
+
},
|
|
586
|
+
idempotency_key: randomUUID(),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
if (w.kind === 'update') {
|
|
590
|
+
const specId = meta.specId ?? frontmatter.id ?? (w.absPath ? basenameFromRelPath(w.absPath) : null);
|
|
591
|
+
if (!specId) return null;
|
|
592
|
+
return {
|
|
593
|
+
kind: 'update',
|
|
594
|
+
route: `/api/specs/${encodeURIComponent(specId)}`,
|
|
595
|
+
method: 'PUT',
|
|
596
|
+
request_body: { frontmatter, body },
|
|
597
|
+
spec_id: specId,
|
|
598
|
+
pre_image_updated: frontmatter?.updated ?? null,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# .heyspex/config.yaml — HeySpex workflow settings for this vault.
|
|
2
|
+
# Seeded by `hs init` (ADR-0009 §1: the file lives at the VAULT root and is git-tracked).
|
|
3
|
+
# Field reference: specs/protocols/agent-settings-protocol.md. Change values with
|
|
4
|
+
# `hs settings set <key> <value>` or heyspex_settings_set — never by hand.
|
|
5
|
+
|
|
6
|
+
mvp_mode:
|
|
7
|
+
enabled: true
|
|
8
|
+
review_grain: epic
|
|
9
|
+
review_backlog_never_blocks: true
|
|
10
|
+
|
|
11
|
+
pricing_tier:
|
|
12
|
+
tier: free
|
|
13
|
+
|
|
14
|
+
agent_behaviors:
|
|
15
|
+
ask_forgiveness_not_permission: true
|