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
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
writeFileSync,
|
|
43
43
|
} from 'node:fs';
|
|
44
44
|
import { createHash } from 'node:crypto';
|
|
45
|
-
import { join } from 'node:path';
|
|
45
|
+
import { join, isAbsolute, resolve, sep } from 'node:path';
|
|
46
46
|
import { allocateRandomVaultId } from '../../scripts/vault-id-lib.mjs';
|
|
47
47
|
import { yamlScalar } from './yaml-scalar.mjs';
|
|
48
48
|
import {
|
|
@@ -72,7 +72,36 @@ import {
|
|
|
72
72
|
bumpUpdated,
|
|
73
73
|
appendChangeLogRow,
|
|
74
74
|
commitWrites,
|
|
75
|
+
fetchSpecRecordViaConsole,
|
|
76
|
+
resolveWriteModeFromDeps,
|
|
77
|
+
splitRawSpecForConsole,
|
|
78
|
+
cloudWriteFailureResult,
|
|
79
|
+
cloudModeUnsupportedWrite,
|
|
75
80
|
} from './promotion-helpers.mjs';
|
|
81
|
+
// Cloud-mode vault WRITES ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]).
|
|
82
|
+
// See the block comment at the head of `runExecutePhase` for the create-first,
|
|
83
|
+
// link-second reorder these three verbs make possible.
|
|
84
|
+
import {
|
|
85
|
+
createSpecViaConsole,
|
|
86
|
+
updateSpecViaConsole,
|
|
87
|
+
listSpecsViaConsole,
|
|
88
|
+
stripServerMintedFields,
|
|
89
|
+
} from './cloud-vault-writes.mjs';
|
|
90
|
+
// Cloud-tier read selector ([[HS-KR16-CWPX-TASK-promotion-reads-through-the-cloud-tier]]) —
|
|
91
|
+
// reused verbatim, never copied. See the module doc above `fetchSpecRecordViaConsole`
|
|
92
|
+
// in promotion-helpers.mjs for why the neon branch goes through an injected
|
|
93
|
+
// consoleFetch instead of read-path.mjs's own transport.
|
|
94
|
+
import { readViaSelector } from './read-path.mjs';
|
|
95
|
+
import { assertPathUnderRoot } from '../../scripts/lib/shared-path-helpers.mjs';
|
|
96
|
+
|
|
97
|
+
// Placeholders the CLOUD path feeds the shared content builders in place of a
|
|
98
|
+
// locally-minted id / file key / family key — stripped from the request by
|
|
99
|
+
// `stripServerMintedFields` (plus an explicit `delete` of an un-inheritable
|
|
100
|
+
// `family:`) before it ever reaches the console. Their only job is to keep the
|
|
101
|
+
// builders' templates well-formed so the BODY stored in the cloud vault is the
|
|
102
|
+
// same markdown the disk path writes.
|
|
103
|
+
const CLOUD_PENDING_ID = 'HS-PENDING';
|
|
104
|
+
const CLOUD_PENDING_KEY = 'PENDING';
|
|
76
105
|
|
|
77
106
|
/** Milestone statuses that allow attaching spawned epics without an override. */
|
|
78
107
|
export const MILESTONE_ATTACH_DEFAULT_ALLOWED_STATUSES = ['open'];
|
|
@@ -177,6 +206,45 @@ function findResearchByIdLocal(vaultRoot, researchId) {
|
|
|
177
206
|
}
|
|
178
207
|
}
|
|
179
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Cloud-tier sibling of {@link findResearchByIdLocal} (HS-KR16). Same
|
|
211
|
+
* `{ record }` / error-shaped contract; the neon branch fetches the by-id
|
|
212
|
+
* spec route directly and re-derives the same `RESEARCH_NOT_FOUND` /
|
|
213
|
+
* `RESEARCH_TYPE_MISMATCH` shapes the disk branch's `resolveBreakdownSource`
|
|
214
|
+
* call produces, since that shared resolver stays disk-only and untouched.
|
|
215
|
+
* `tier`/`consoleUrl`/`consoleFetch` are resolved ONCE per handler call
|
|
216
|
+
* (`handlePromoteResearch` in research-handlers.mjs) and threaded down here.
|
|
217
|
+
*/
|
|
218
|
+
async function findResearchByIdViaTier(vaultRoot, researchId, { tier, consoleUrl, consoleFetch }) {
|
|
219
|
+
return readViaSelector({
|
|
220
|
+
allowDiskFallback: false,
|
|
221
|
+
tier,
|
|
222
|
+
diskRead: () => findResearchByIdLocal(vaultRoot, researchId),
|
|
223
|
+
neonRead: async () => {
|
|
224
|
+
const record = await fetchSpecRecordViaConsole(consoleFetch, researchId);
|
|
225
|
+
if (!record) {
|
|
226
|
+
return {
|
|
227
|
+
error: 'RESEARCH_NOT_FOUND',
|
|
228
|
+
message: `research ${researchId} not found under specs/research/ (tried exact basename, frontmatter id:, and id-prefix match)`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const fm = getFrontmatterBlock(record.raw);
|
|
232
|
+
const type = fm ? getScalarField(fm.block, 'type') : null;
|
|
233
|
+
if (type && type !== 'research') {
|
|
234
|
+
return {
|
|
235
|
+
error: 'RESEARCH_TYPE_MISMATCH',
|
|
236
|
+
message: `research_id ${researchId} resolved to ${record.relPath ?? 'a file'}, which is \`type: ${type}\` — not a research doc`,
|
|
237
|
+
resolved_basename: record.basename,
|
|
238
|
+
resolved_type: type,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return { record };
|
|
242
|
+
},
|
|
243
|
+
routeLabel: 'GET /api/specs/[id]',
|
|
244
|
+
consoleUrl,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
180
248
|
function findMilestoneByBasename(vaultRoot, basename) {
|
|
181
249
|
const absPath = join(vaultRoot, 'specs', 'milestones', `${basename}.md`);
|
|
182
250
|
if (!existsSync(absPath)) return null;
|
|
@@ -201,6 +269,36 @@ function findMilestoneById(vaultRoot, milestoneId) {
|
|
|
201
269
|
return null;
|
|
202
270
|
}
|
|
203
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Cloud-tier sibling of {@link findMilestoneByBasename} (HS-KR16). Same
|
|
274
|
+
* bare-record-or-null contract as the disk loader.
|
|
275
|
+
*/
|
|
276
|
+
async function findMilestoneByBasenameViaTier(vaultRoot, basename, { tier, consoleUrl, consoleFetch }) {
|
|
277
|
+
return readViaSelector({
|
|
278
|
+
allowDiskFallback: false,
|
|
279
|
+
tier,
|
|
280
|
+
diskRead: () => findMilestoneByBasename(vaultRoot, basename),
|
|
281
|
+
neonRead: () => fetchSpecRecordViaConsole(consoleFetch, basename),
|
|
282
|
+
routeLabel: 'GET /api/specs/[id]',
|
|
283
|
+
consoleUrl,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Cloud-tier sibling of {@link findMilestoneById} (HS-KR16). Same
|
|
289
|
+
* bare-record-or-null contract as the disk loader.
|
|
290
|
+
*/
|
|
291
|
+
async function findMilestoneByIdViaTier(vaultRoot, milestoneId, { tier, consoleUrl, consoleFetch }) {
|
|
292
|
+
return readViaSelector({
|
|
293
|
+
allowDiskFallback: false,
|
|
294
|
+
tier,
|
|
295
|
+
diskRead: () => findMilestoneById(vaultRoot, milestoneId),
|
|
296
|
+
neonRead: () => fetchSpecRecordViaConsole(consoleFetch, milestoneId),
|
|
297
|
+
routeLabel: 'GET /api/specs/[id]',
|
|
298
|
+
consoleUrl,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
204
302
|
function findEpicByIdOrBasename(vaultRoot, idOrBasename) {
|
|
205
303
|
const dir = join(vaultRoot, 'specs', 'epics');
|
|
206
304
|
if (!existsSync(dir)) return null;
|
|
@@ -232,6 +330,22 @@ function findEpicByIdOrBasename(vaultRoot, idOrBasename) {
|
|
|
232
330
|
return null;
|
|
233
331
|
}
|
|
234
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Cloud-tier sibling of {@link findEpicByIdOrBasename} (HS-KR16) — the
|
|
335
|
+
* console's by-id route accepts either an id or a basename, matching the
|
|
336
|
+
* disk loader's own two-shape contract. Same bare-record-or-null return.
|
|
337
|
+
*/
|
|
338
|
+
async function findEpicByIdOrBasenameViaTier(vaultRoot, idOrBasename, { tier, consoleUrl, consoleFetch }) {
|
|
339
|
+
return readViaSelector({
|
|
340
|
+
allowDiskFallback: false,
|
|
341
|
+
tier,
|
|
342
|
+
diskRead: () => findEpicByIdOrBasename(vaultRoot, idOrBasename),
|
|
343
|
+
neonRead: () => fetchSpecRecordViaConsole(consoleFetch, idOrBasename),
|
|
344
|
+
routeLabel: 'GET /api/specs/[id]',
|
|
345
|
+
consoleUrl,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
235
349
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
236
350
|
// Pillar resolution (epic→pillar hard requirement, [[pillar-assignment-protocol]])
|
|
237
351
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -295,6 +409,51 @@ export function resolvePillar(vaultRoot, raw) {
|
|
|
295
409
|
return { ok: true, basename, frontmatterValue: `'[[${basename}]]'` };
|
|
296
410
|
}
|
|
297
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Cloud sibling of {@link listVaultPillars} ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]] AC3).
|
|
414
|
+
*
|
|
415
|
+
* The disk version is a `readdirSync(specs/pillars)` scan, which in a file-less
|
|
416
|
+
* vault returns `[]` — so a `pillar_required` refusal would list NO valid
|
|
417
|
+
* options and read as "there are no pillars" rather than "this machine holds
|
|
418
|
+
* no copy of them". `GET /api/specs?type=pillar` (`listSpecsViaConsole`, which
|
|
419
|
+
* follows the keyset cursor) is the route that answers the same question in
|
|
420
|
+
* cloud mode. Fails SOFT to `[]`, exactly like the disk version: this list only
|
|
421
|
+
* enriches an error message that is being returned either way, and turning a
|
|
422
|
+
* refusal into an unreachable-console error would hide the real cause.
|
|
423
|
+
*/
|
|
424
|
+
export async function listVaultPillarsViaWriteMode(vaultRoot, { writeMode, consoleFetch, consoleUrl }) {
|
|
425
|
+
if (writeMode !== 'cloud') return listVaultPillars(vaultRoot);
|
|
426
|
+
try {
|
|
427
|
+
const rows = await listSpecsViaConsole({ consoleFetch, consoleUrl, type: 'pillar' });
|
|
428
|
+
return rows.map((row) => ({
|
|
429
|
+
id: row.basename ?? row.id ?? null,
|
|
430
|
+
name: row.name ?? row.basename ?? row.id ?? null,
|
|
431
|
+
}));
|
|
432
|
+
} catch {
|
|
433
|
+
return [];
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Cloud-tier sibling of {@link resolvePillar} (HS-KR16).
|
|
439
|
+
*/
|
|
440
|
+
async function resolvePillarViaTier(vaultRoot, raw, { tier, consoleUrl, consoleFetch }) {
|
|
441
|
+
const basename = normalizePillarRef(raw);
|
|
442
|
+
if (!basename) return { ok: false };
|
|
443
|
+
return readViaSelector({
|
|
444
|
+
allowDiskFallback: false,
|
|
445
|
+
tier,
|
|
446
|
+
diskRead: () => resolvePillar(vaultRoot, raw),
|
|
447
|
+
neonRead: async () => {
|
|
448
|
+
const record = await fetchSpecRecordViaConsole(consoleFetch, basename);
|
|
449
|
+
if (!record) return { ok: false };
|
|
450
|
+
return { ok: true, basename, frontmatterValue: `'[[${basename}]]'` };
|
|
451
|
+
},
|
|
452
|
+
routeLabel: 'GET /api/specs/[id]',
|
|
453
|
+
consoleUrl,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
298
457
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
299
458
|
// AI proposer wiring
|
|
300
459
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -495,21 +654,32 @@ function validateResearchState(research) {
|
|
|
495
654
|
// validateBidirectionalEnhances (RESEARCH_ALREADY_PROMOTED).
|
|
496
655
|
const status = getScalarField(fm.block, 'status');
|
|
497
656
|
const enhances = getScalarField(fm.block, 'enhances');
|
|
498
|
-
|
|
657
|
+
// [[HS-8XJA-KOGH-DIAG-passing-enhances-to-heyspex-promote]]: `enhances` is
|
|
658
|
+
// ALSO a heyspex_promote_capture argument that writes this same field one
|
|
659
|
+
// stage earlier, before any epic has actually been spawned — forging the
|
|
660
|
+
// "already promoted" signal ahead of the event it's supposed to record. A
|
|
661
|
+
// GENUINE promote-research execution always writes `spawned_epics` and/or
|
|
662
|
+
// `enhances_epics` in the SAME commit as `enhances` (buildUpdatedResearchContent
|
|
663
|
+
// below) — so `enhances` set with BOTH of those empty can only mean it was
|
|
664
|
+
// pre-set at capture-promote time, never backed by a real promotion.
|
|
665
|
+
const spawnedEpics = parseListFieldBasenames(fm.block, 'spawned_epics') ?? [];
|
|
666
|
+
const enhancesEpics = parseListFieldBasenames(fm.block, 'enhances_epics') ?? [];
|
|
667
|
+
const enhancesBacked = spawnedEpics.length > 0 || enhancesEpics.length > 0;
|
|
668
|
+
return { fm, status, enhances, enhancesBacked };
|
|
499
669
|
}
|
|
500
670
|
|
|
501
|
-
function resolveTargetMilestone(vaultRoot, args,
|
|
671
|
+
async function resolveTargetMilestone(vaultRoot, args, { tier, consoleUrl, consoleFetch }) {
|
|
502
672
|
if (args.target_milestone === 'NEW') {
|
|
503
673
|
return { mode: 'new', name: args.new_milestone_name.trim() };
|
|
504
674
|
}
|
|
505
675
|
// Accept either bare HS-MIL-XXXX, the full basename, or wikilink wrapping.
|
|
506
676
|
const basenameCandidate = normalizeMilestoneBasename(args.target_milestone);
|
|
507
677
|
if (basenameCandidate) {
|
|
508
|
-
const ms =
|
|
678
|
+
const ms = await findMilestoneByBasenameViaTier(vaultRoot, basenameCandidate, { tier, consoleUrl, consoleFetch });
|
|
509
679
|
if (ms) return { mode: 'existing', milestone: ms };
|
|
510
680
|
}
|
|
511
681
|
if (parseSpecId(args.target_milestone)?.typeLabel === 'MIL') {
|
|
512
|
-
const ms =
|
|
682
|
+
const ms = await findMilestoneByIdViaTier(vaultRoot, args.target_milestone, { tier, consoleUrl, consoleFetch });
|
|
513
683
|
if (ms) return { mode: 'existing', milestone: ms };
|
|
514
684
|
}
|
|
515
685
|
return {
|
|
@@ -652,6 +822,7 @@ async function runProposalPhase(args, ctx, deps) {
|
|
|
652
822
|
/** Minimum length for an explanation, matching attach_override_reason above. */
|
|
653
823
|
const EDIT_NOTE_MIN_LENGTH = 40;
|
|
654
824
|
|
|
825
|
+
|
|
655
826
|
/** Marker path for a research doc's proposal turn. Keyed on the research id — the
|
|
656
827
|
* one value stable across both turns, since the human is expected to EDIT the
|
|
657
828
|
* stubs in between (so hashing the stub content, as the plan gate does, would
|
|
@@ -672,6 +843,7 @@ function normalizeStubName(name) {
|
|
|
672
843
|
export function writeStubReviewMarker(vaultRoot, researchId, proposal = {}) {
|
|
673
844
|
try {
|
|
674
845
|
const file = stubReviewMarkerPath(vaultRoot, researchId);
|
|
846
|
+
assertPathUnderRoot(vaultRoot, join(file, '..'), 'promote-research write');
|
|
675
847
|
mkdirSync(join(file, '..'), { recursive: true });
|
|
676
848
|
writeFileSync(
|
|
677
849
|
file,
|
|
@@ -815,6 +987,62 @@ function resolveOwnerIntent(vaultRoot, researchRaw) {
|
|
|
815
987
|
}
|
|
816
988
|
}
|
|
817
989
|
|
|
990
|
+
/**
|
|
991
|
+
* Cloud-tier sibling of {@link resolveOwnerIntent} (HS-KR16) — same
|
|
992
|
+
* best-effort, never-throws contract (a missing/unreachable source capture
|
|
993
|
+
* just means no owner-intent enrichment, not a hard failure). Only the
|
|
994
|
+
* per-basename capture read moves through the tier selector; the
|
|
995
|
+
* sources_captures/related parsing stays identical to the disk version.
|
|
996
|
+
*/
|
|
997
|
+
async function resolveOwnerIntentViaTier(vaultRoot, researchRaw, { tier, consoleUrl, consoleFetch }) {
|
|
998
|
+
try {
|
|
999
|
+
const fm = getFrontmatterBlock(researchRaw);
|
|
1000
|
+
if (!fm) return null;
|
|
1001
|
+
let basenames = parseListFieldBasenames(fm.block, 'sources_captures') ?? [];
|
|
1002
|
+
if (basenames.length === 0) {
|
|
1003
|
+
basenames = (parseListFieldBasenames(fm.block, 'related') ?? []).filter((b) =>
|
|
1004
|
+
/(^|-)CAP(-|$)/i.test(b),
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
if (basenames.length === 0) return null;
|
|
1008
|
+
for (const basename of basenames) {
|
|
1009
|
+
let raw;
|
|
1010
|
+
try {
|
|
1011
|
+
raw = await readViaSelector({
|
|
1012
|
+
allowDiskFallback: false,
|
|
1013
|
+
tier,
|
|
1014
|
+
diskRead: () => {
|
|
1015
|
+
const capturesDir = join(vaultRoot, 'specs', 'captures');
|
|
1016
|
+
let abs = join(capturesDir, `${basename}.md`);
|
|
1017
|
+
if (!existsSync(abs)) {
|
|
1018
|
+
const byPrefix = findFileByPrefix(capturesDir, basename);
|
|
1019
|
+
if (!byPrefix) return null;
|
|
1020
|
+
abs = join(capturesDir, byPrefix);
|
|
1021
|
+
}
|
|
1022
|
+
return readFileSync(abs, 'utf8');
|
|
1023
|
+
},
|
|
1024
|
+
neonRead: async () => {
|
|
1025
|
+
const record = await fetchSpecRecordViaConsole(consoleFetch, basename);
|
|
1026
|
+
return record ? record.raw : null;
|
|
1027
|
+
},
|
|
1028
|
+
routeLabel: 'GET /api/specs/[id]',
|
|
1029
|
+
consoleUrl,
|
|
1030
|
+
});
|
|
1031
|
+
} catch {
|
|
1032
|
+
continue; // a genuine console failure here is soft — skip, try the next basename.
|
|
1033
|
+
}
|
|
1034
|
+
if (!raw) continue;
|
|
1035
|
+
const body = getFrontmatterBlock(raw)?.body ?? raw;
|
|
1036
|
+
const m = body.match(/#\s*Core Idea\s*\n+([\s\S]*?)(?=\n#{1,6}\s|$)/i);
|
|
1037
|
+
const coreIdea = m ? m[1].trim() : '';
|
|
1038
|
+
if (coreIdea) return { basename, coreIdea };
|
|
1039
|
+
}
|
|
1040
|
+
return null;
|
|
1041
|
+
} catch {
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
818
1046
|
/**
|
|
819
1047
|
* Pull the research doc's own `## JTBD (job-to-be-done)` section body, so
|
|
820
1048
|
* spawned milestone/epic stubs can re-derive their altitude's slice of the
|
|
@@ -1018,6 +1246,38 @@ async function runExecutePhase(args, ctx, deps) {
|
|
|
1018
1246
|
const now = (deps?.clock ?? isoNow)();
|
|
1019
1247
|
const today = now.slice(0, 10);
|
|
1020
1248
|
const author = 'promote-research (agent)';
|
|
1249
|
+
// Same already-resolved tier this call's executePromoteResearch entry point
|
|
1250
|
+
// read from deps.tier ([[HS-KR16-CWPX-TASK-promotion-reads-through-the-cloud-tier]])
|
|
1251
|
+
// — read again here, never re-resolved via resolveReadTier().
|
|
1252
|
+
const tierDeps = { tier: deps?.tier ?? 'disk', consoleUrl: deps?.consoleUrl, consoleFetch: deps?.consoleFetch };
|
|
1253
|
+
// ── Vault WRITE mode ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]) ──
|
|
1254
|
+
//
|
|
1255
|
+
// Resolved ONCE by the caller (`resolveVaultWriteMode()`) and threaded in on
|
|
1256
|
+
// `deps`; absent — every pre-existing caller and test — it is `local`, so the
|
|
1257
|
+
// disk plan-then-`commitWrites` path below is byte-identical to before.
|
|
1258
|
+
//
|
|
1259
|
+
// CREATE FIRST, LINK SECOND. On disk this flow mints every filename itself,
|
|
1260
|
+
// so it can stage all four write groups and commit them as one transaction.
|
|
1261
|
+
// In cloud mode the console mints the id, the family and the filename, and
|
|
1262
|
+
// the dependencies run strictly one way:
|
|
1263
|
+
//
|
|
1264
|
+
// milestone ← links to: research basename (known up front)
|
|
1265
|
+
// epic ← links to: MILESTONE basename (only after its POST)
|
|
1266
|
+
// epic ← links to: SIBLING epic basenames (only after their POSTs)
|
|
1267
|
+
// milestone ← links to: EPIC basenames (`epics:`) (only after their POSTs)
|
|
1268
|
+
// research ← links to: MILESTONE + EPIC basenames (only after their POSTs)
|
|
1269
|
+
//
|
|
1270
|
+
// so the cloud order is: POST milestone → POST every epic (with only the
|
|
1271
|
+
// deps already resolvable) → PUT epics that depend on a sibling → PUT the
|
|
1272
|
+
// milestone's `epics:` mirror → PUT the research doc. Every wikilink written
|
|
1273
|
+
// in a PUT body therefore names a basename a POST actually returned.
|
|
1274
|
+
const writeMode = resolveWriteModeFromDeps(deps);
|
|
1275
|
+
const cloudDeps = { consoleFetch: deps?.consoleFetch, consoleUrl: deps?.consoleUrl };
|
|
1276
|
+
const cloudCompleted = [];
|
|
1277
|
+
const recordCloud = (entry) => {
|
|
1278
|
+
cloudCompleted.push(entry);
|
|
1279
|
+
return entry;
|
|
1280
|
+
};
|
|
1021
1281
|
|
|
1022
1282
|
// Family inheritance ([[HS-GZM9-DIAG-G419]], mirrors promote-capture.mjs /
|
|
1023
1283
|
// split-research.mjs): the spawned milestone AND every spawned epic inherit
|
|
@@ -1041,23 +1301,39 @@ async function runExecutePhase(args, ctx, deps) {
|
|
|
1041
1301
|
let existingMilestoneEpics = [];
|
|
1042
1302
|
|
|
1043
1303
|
if (ctx.targetResolution.mode === 'new') {
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
inheritedFamilyKey ??
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1304
|
+
if (writeMode === 'cloud') {
|
|
1305
|
+
// NOTHING is minted locally in cloud mode (AC3): `allocateFileKey`,
|
|
1306
|
+
// `collectOccupiedKeysEitherKind` and `allocateMilestoneId` all derive
|
|
1307
|
+
// their answer from a vault SCAN, and in a file-less vault that scan is
|
|
1308
|
+
// empty — a key drawn against an empty occupied set can collide with one
|
|
1309
|
+
// the cloud vault already holds. `createSpec` mints the file key, the
|
|
1310
|
+
// family and the filename server-side. An INHERITED family is still
|
|
1311
|
+
// passed through: it is read off the source research doc, not minted.
|
|
1312
|
+
milestoneFileKey = null;
|
|
1313
|
+
milestoneFamilyKey = inheritedFamilyKey ?? null;
|
|
1314
|
+
milestoneId = null;
|
|
1315
|
+
milestoneBasename = null;
|
|
1316
|
+
milestoneAbsPath = null;
|
|
1317
|
+
milestoneRelPath = null;
|
|
1318
|
+
} else {
|
|
1319
|
+
// Per-file FILE KEY (owner ruling 2026-07-10) — filename + `file_id:` SSOT,
|
|
1320
|
+
// and (V3) the anchor for `id:` itself.
|
|
1321
|
+
milestoneFileKey = allocateFileKey(collectOccupiedKeysEitherKind(ctx.vaultRoot), {
|
|
1322
|
+
randomUnit: deps?.randomUnit,
|
|
1323
|
+
});
|
|
1324
|
+
// Family: inherit from the research doc; the legacy allocator's
|
|
1325
|
+
// `HS-<family>-MIL` draw is only the fallback family source (never blank —
|
|
1326
|
+
// last resort is the fresh file key itself, matching the old chain).
|
|
1327
|
+
milestoneFamilyKey =
|
|
1328
|
+
inheritedFamilyKey ??
|
|
1329
|
+
parseSpecId(allocateMilestoneId(ctx.vaultRoot, { randomUnit: deps?.randomUnit }))?.familyKey ??
|
|
1330
|
+
milestoneFileKey;
|
|
1331
|
+
milestoneId = `HS-${milestoneFileKey}`;
|
|
1332
|
+
const slug = deriveSlug(ctx.targetResolution.name);
|
|
1333
|
+
milestoneBasename = `${milestoneId}-${milestoneFamilyKey}-MIL-${slug}`;
|
|
1334
|
+
milestoneAbsPath = join(ctx.vaultRoot, 'specs', 'milestones', `${milestoneBasename}.md`);
|
|
1335
|
+
milestoneRelPath = join('specs', 'milestones', `${milestoneBasename}.md`);
|
|
1336
|
+
}
|
|
1061
1337
|
milestoneCreated = true;
|
|
1062
1338
|
} else {
|
|
1063
1339
|
const ms = ctx.targetResolution.milestone;
|
|
@@ -1076,7 +1352,7 @@ async function runExecutePhase(args, ctx, deps) {
|
|
|
1076
1352
|
const augmentEpics = [];
|
|
1077
1353
|
if (args.intent === 'augment' || args.intent === 'both') {
|
|
1078
1354
|
for (const epicRef of args.augment_epic_ids) {
|
|
1079
|
-
const found =
|
|
1355
|
+
const found = await findEpicByIdOrBasenameViaTier(ctx.vaultRoot, epicRef, tierDeps);
|
|
1080
1356
|
if (!found) {
|
|
1081
1357
|
return {
|
|
1082
1358
|
error: 'AUGMENT_EPIC_NOT_FOUND',
|
|
@@ -1106,10 +1382,14 @@ async function runExecutePhase(args, ctx, deps) {
|
|
|
1106
1382
|
|
|
1107
1383
|
const spawnedEpicSpecs = [];
|
|
1108
1384
|
if (acceptedStubs.length > 0) {
|
|
1109
|
-
|
|
1385
|
+
// Both occupied sets are DISK SCANS — skipped entirely in cloud mode,
|
|
1386
|
+
// where no key is minted client-side (AC3).
|
|
1387
|
+
const { epics: occupiedEpics } =
|
|
1388
|
+
writeMode === 'cloud' ? { epics: new Set() } : collectOccupiedVaultIds(ctx.vaultRoot);
|
|
1110
1389
|
// One occupied set across the whole loop — files are not on disk yet, so a
|
|
1111
1390
|
// fresh vault walk per stub would let two stubs draw the same file key.
|
|
1112
|
-
const occupiedFileKeys =
|
|
1391
|
+
const occupiedFileKeys =
|
|
1392
|
+
writeMode === 'cloud' ? new Set() : collectOccupiedKeysEitherKind(ctx.vaultRoot); // union: file+family keys (HS-SJOT)
|
|
1113
1393
|
for (const stub of acceptedStubs) {
|
|
1114
1394
|
if (!stub?.name?.trim()) {
|
|
1115
1395
|
return {
|
|
@@ -1117,23 +1397,32 @@ async function runExecutePhase(args, ctx, deps) {
|
|
|
1117
1397
|
message: 'every accepted stub must have a non-empty name',
|
|
1118
1398
|
};
|
|
1119
1399
|
}
|
|
1120
|
-
|
|
1121
|
-
// and (V3) the anchor for `id:` itself.
|
|
1122
|
-
const fileKey = allocateFileKey(occupiedFileKeys, { randomUnit: deps?.randomUnit });
|
|
1123
|
-
occupiedFileKeys.add(fileKey);
|
|
1124
|
-
// Family: inherit from the research doc; the legacy allocator's
|
|
1125
|
-
// `HS-<family>-EPIC` draw is only the fallback family source.
|
|
1400
|
+
let fileKey = null;
|
|
1126
1401
|
let epicFamilyKey = inheritedFamilyKey;
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1402
|
+
let epicId = null;
|
|
1403
|
+
let basename = null;
|
|
1404
|
+
if (writeMode === 'cloud') {
|
|
1405
|
+
// Basename/id stay null until this stub's own POST answers; the
|
|
1406
|
+
// inherited family (read off the research doc, never minted) is the
|
|
1407
|
+
// only naming input that survives into the request.
|
|
1408
|
+
epicFamilyKey = inheritedFamilyKey ?? null;
|
|
1409
|
+
} else {
|
|
1410
|
+
// Per-file FILE KEY (owner ruling 2026-07-10) — filename + `file_id:` SSOT,
|
|
1411
|
+
// and (V3) the anchor for `id:` itself.
|
|
1412
|
+
fileKey = allocateFileKey(occupiedFileKeys, { randomUnit: deps?.randomUnit });
|
|
1413
|
+
occupiedFileKeys.add(fileKey);
|
|
1414
|
+
// Family: inherit from the research doc; the legacy allocator's
|
|
1415
|
+
// `HS-<family>-EPIC` draw is only the fallback family source.
|
|
1416
|
+
if (!epicFamilyKey) {
|
|
1417
|
+
const allocatedEpicFamilyId = allocateRandomVaultId('epic', occupiedEpics, {
|
|
1418
|
+
randomUnit: deps?.randomUnit,
|
|
1419
|
+
});
|
|
1420
|
+
occupiedEpics.add(allocatedEpicFamilyId);
|
|
1421
|
+
epicFamilyKey = parseSpecId(allocatedEpicFamilyId)?.familyKey ?? fileKey;
|
|
1422
|
+
}
|
|
1423
|
+
epicId = `HS-${fileKey}`;
|
|
1424
|
+
basename = `${epicId}-${epicFamilyKey}-EPIC-${deriveSlug(stub.name)}`;
|
|
1133
1425
|
}
|
|
1134
|
-
const epicId = `HS-${fileKey}`;
|
|
1135
|
-
const slug = deriveSlug(stub.name);
|
|
1136
|
-
const basename = `${epicId}-${epicFamilyKey}-EPIC-${slug}`;
|
|
1137
1426
|
spawnedEpicSpecs.push({
|
|
1138
1427
|
id: epicId,
|
|
1139
1428
|
fileKey,
|
|
@@ -1147,8 +1436,8 @@ async function runExecutePhase(args, ctx, deps) {
|
|
|
1147
1436
|
// Per-stub pillar override; falls back to the batch-default `args.pillar`
|
|
1148
1437
|
// in the gate below (stub value wins — [[pillar-assignment-protocol]]).
|
|
1149
1438
|
pillar: typeof stub.pillar === 'string' ? stub.pillar : undefined,
|
|
1150
|
-
relPath: join('specs', 'epics', `${basename}.md`),
|
|
1151
|
-
absPath: join(ctx.vaultRoot, 'specs', 'epics', `${basename}.md`),
|
|
1439
|
+
relPath: basename ? join('specs', 'epics', `${basename}.md`) : null,
|
|
1440
|
+
absPath: basename ? join(ctx.vaultRoot, 'specs', 'epics', `${basename}.md`) : null,
|
|
1152
1441
|
});
|
|
1153
1442
|
}
|
|
1154
1443
|
}
|
|
@@ -1161,20 +1450,32 @@ async function runExecutePhase(args, ctx, deps) {
|
|
|
1161
1450
|
// each entry that matches a sibling stub's `name` with that sibling's
|
|
1162
1451
|
// freshly-minted basename; else resolve against an existing vault epic by
|
|
1163
1452
|
// id/basename; else DROP it (warn) rather than write an unresolvable link.
|
|
1453
|
+
//
|
|
1454
|
+
// In CLOUD mode a sibling's basename does not exist yet either (the
|
|
1455
|
+
// console mints it at POST time), so the sibling half of this resolution
|
|
1456
|
+
// is DEFERRED: the entry is parked on `spec.pendingSiblingDeps` and folded
|
|
1457
|
+
// in by a follow-up PUT once every sibling POST has answered. The
|
|
1458
|
+
// externally-resolved half is identical in both modes.
|
|
1164
1459
|
const depWarnings = [];
|
|
1165
1460
|
if (spawnedEpicSpecs.length > 0) {
|
|
1461
|
+
const siblingNames = new Set(spawnedEpicSpecs.map((s) => s.name.trim().toLowerCase()));
|
|
1166
1462
|
const siblingNameToBasename = new Map();
|
|
1167
1463
|
for (const s of spawnedEpicSpecs) {
|
|
1168
1464
|
siblingNameToBasename.set(s.name.trim().toLowerCase(), s.basename);
|
|
1169
1465
|
}
|
|
1170
1466
|
for (const spec of spawnedEpicSpecs) {
|
|
1171
1467
|
const resolved = [];
|
|
1468
|
+
const pendingSiblings = [];
|
|
1172
1469
|
for (const raw of spec.depends_on) {
|
|
1173
1470
|
const d = typeof raw === 'string' ? raw.trim() : '';
|
|
1174
1471
|
if (!d) continue;
|
|
1472
|
+
if (writeMode === 'cloud' && siblingNames.has(d.toLowerCase())) {
|
|
1473
|
+
pendingSiblings.push(d.toLowerCase());
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1175
1476
|
let target = siblingNameToBasename.get(d.toLowerCase());
|
|
1176
1477
|
if (!target) {
|
|
1177
|
-
const found =
|
|
1478
|
+
const found = await findEpicByIdOrBasenameViaTier(ctx.vaultRoot, d, tierDeps);
|
|
1178
1479
|
if (found) target = found.basename;
|
|
1179
1480
|
}
|
|
1180
1481
|
if (target) {
|
|
@@ -1184,6 +1485,7 @@ async function runExecutePhase(args, ctx, deps) {
|
|
|
1184
1485
|
}
|
|
1185
1486
|
}
|
|
1186
1487
|
spec.depends_on = resolved;
|
|
1488
|
+
spec.pendingSiblingDeps = pendingSiblings;
|
|
1187
1489
|
}
|
|
1188
1490
|
}
|
|
1189
1491
|
|
|
@@ -1195,14 +1497,19 @@ async function runExecutePhase(args, ctx, deps) {
|
|
|
1195
1497
|
if (spawnedEpicSpecs.length > 0) {
|
|
1196
1498
|
for (const spec of spawnedEpicSpecs) {
|
|
1197
1499
|
const pillarRef = spec.pillar ?? args.pillar;
|
|
1198
|
-
const resolved =
|
|
1500
|
+
const resolved = await resolvePillarViaTier(ctx.vaultRoot, pillarRef, tierDeps);
|
|
1199
1501
|
if (!resolved.ok) {
|
|
1200
1502
|
return {
|
|
1201
1503
|
error: 'pillar_required',
|
|
1202
1504
|
message: pillarRef
|
|
1203
1505
|
? `epic "${spec.name}" pillar \`${pillarRef}\` does not resolve to a pillar under specs/pillars/ — pick an existing pillar or author a new one first`
|
|
1204
1506
|
: `epic "${spec.name}" requires a \`pillar:\` wikilink to an HS-PILR theme — pass a top-level \`pillar\` batch default or a per-stub \`pillar\`; pick an existing pillar or author a new one first`,
|
|
1205
|
-
|
|
1507
|
+
// AC3: in cloud mode this list comes from `GET /api/specs?type=pillar`,
|
|
1508
|
+
// not from a `readdirSync(specs/pillars)` that would be empty here.
|
|
1509
|
+
existing_pillars: await listVaultPillarsViaWriteMode(ctx.vaultRoot, {
|
|
1510
|
+
writeMode,
|
|
1511
|
+
...cloudDeps,
|
|
1512
|
+
}),
|
|
1206
1513
|
create_pillar: {
|
|
1207
1514
|
tool: 'heyspex_file_spec',
|
|
1208
1515
|
call: { type: 'pillar', name: '<theme name>' },
|
|
@@ -1220,11 +1527,295 @@ async function runExecutePhase(args, ctx, deps) {
|
|
|
1220
1527
|
// The owner's own words from the originating capture, carried onto every spec
|
|
1221
1528
|
// this promotion creates so intent survives the hop into the plan graph.
|
|
1222
1529
|
// Null when no capture resolves — stubs then render exactly as before.
|
|
1223
|
-
const ownerIntent =
|
|
1530
|
+
const ownerIntent = await resolveOwnerIntentViaTier(ctx.vaultRoot, ctx.research.raw, tierDeps);
|
|
1224
1531
|
// HS-M1MW: seed spawned stubs' JTBD sections from the research doc's own
|
|
1225
1532
|
// JTBD section, when present.
|
|
1226
1533
|
const researchJtbd = extractResearchJtbd(ctx.research.raw);
|
|
1227
1534
|
|
|
1535
|
+
// ── CLOUD PATH ────────────────────────────────────────────────────────────
|
|
1536
|
+
// Returns early; everything below this block is the untouched disk
|
|
1537
|
+
// plan-then-`commitWrites` path.
|
|
1538
|
+
if (writeMode === 'cloud') {
|
|
1539
|
+
/** Frontmatter/body the console actually stored, falling back to ours. */
|
|
1540
|
+
const storedOf = (created, fallbackRaw) => {
|
|
1541
|
+
const fallback = splitRawSpecForConsole(fallbackRaw);
|
|
1542
|
+
return {
|
|
1543
|
+
frontmatter: created?.spec?.frontmatter ?? fallback.frontmatter,
|
|
1544
|
+
body: typeof created?.spec?.body === 'string' ? created.spec.body : fallback.body,
|
|
1545
|
+
};
|
|
1546
|
+
};
|
|
1547
|
+
|
|
1548
|
+
// ── 1. POST the milestone FIRST — every epic's `parent:`/`milestone:` and
|
|
1549
|
+
// the research's `enhances:` name the basename it returns.
|
|
1550
|
+
let milestoneStored = null;
|
|
1551
|
+
if (milestoneCreated) {
|
|
1552
|
+
const newContent = buildMilestoneStubContent(
|
|
1553
|
+
CLOUD_PENDING_ID,
|
|
1554
|
+
CLOUD_PENDING_KEY,
|
|
1555
|
+
milestoneFamilyKey ?? CLOUD_PENDING_KEY,
|
|
1556
|
+
ctx.targetResolution.name,
|
|
1557
|
+
ctx.research.basename,
|
|
1558
|
+
now,
|
|
1559
|
+
ownerIntent,
|
|
1560
|
+
researchJtbd,
|
|
1561
|
+
);
|
|
1562
|
+
const { frontmatter, body } = splitRawSpecForConsole(newContent);
|
|
1563
|
+
const cleaned = stripServerMintedFields(frontmatter);
|
|
1564
|
+
if (!milestoneFamilyKey) delete cleaned.family;
|
|
1565
|
+
let created;
|
|
1566
|
+
try {
|
|
1567
|
+
created = await createSpecViaConsole({
|
|
1568
|
+
...cloudDeps,
|
|
1569
|
+
type: 'milestone',
|
|
1570
|
+
name: ctx.targetResolution.name,
|
|
1571
|
+
frontmatter: cleaned,
|
|
1572
|
+
body,
|
|
1573
|
+
actingAgent: deps?.actingAgent ?? null,
|
|
1574
|
+
});
|
|
1575
|
+
} catch (err) {
|
|
1576
|
+
return cloudWriteFailureResult(err, {
|
|
1577
|
+
step: { kind: 'create', type: 'milestone', name: ctx.targetResolution.name },
|
|
1578
|
+
completed: cloudCompleted,
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
milestoneId = created.specId;
|
|
1582
|
+
milestoneBasename = created.basename;
|
|
1583
|
+
milestoneRelPath = created.relPath;
|
|
1584
|
+
milestoneStored = storedOf(created, newContent);
|
|
1585
|
+
recordCloud({
|
|
1586
|
+
kind: 'create',
|
|
1587
|
+
type: 'milestone',
|
|
1588
|
+
spec_id: created.specId,
|
|
1589
|
+
basename: created.basename,
|
|
1590
|
+
rel_path: created.relPath,
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// ── 2. POST every epic, now that the milestone basename is real.
|
|
1595
|
+
for (const spec of spawnedEpicSpecs) {
|
|
1596
|
+
const content = buildEpicContent(
|
|
1597
|
+
{
|
|
1598
|
+
...spec,
|
|
1599
|
+
id: CLOUD_PENDING_ID,
|
|
1600
|
+
fileKey: CLOUD_PENDING_KEY,
|
|
1601
|
+
familyKey: spec.familyKey ?? CLOUD_PENDING_KEY,
|
|
1602
|
+
},
|
|
1603
|
+
milestoneBasename,
|
|
1604
|
+
ctx.research.basename,
|
|
1605
|
+
now,
|
|
1606
|
+
ownerIntent,
|
|
1607
|
+
researchJtbd,
|
|
1608
|
+
);
|
|
1609
|
+
const { frontmatter, body } = splitRawSpecForConsole(content);
|
|
1610
|
+
const cleaned = stripServerMintedFields(frontmatter);
|
|
1611
|
+
if (!spec.familyKey) delete cleaned.family;
|
|
1612
|
+
let created;
|
|
1613
|
+
try {
|
|
1614
|
+
created = await createSpecViaConsole({
|
|
1615
|
+
...cloudDeps,
|
|
1616
|
+
type: 'epic',
|
|
1617
|
+
name: spec.name,
|
|
1618
|
+
frontmatter: cleaned,
|
|
1619
|
+
body,
|
|
1620
|
+
actingAgent: deps?.actingAgent ?? null,
|
|
1621
|
+
});
|
|
1622
|
+
} catch (err) {
|
|
1623
|
+
return cloudWriteFailureResult(err, {
|
|
1624
|
+
step: { kind: 'create', type: 'epic', name: spec.name },
|
|
1625
|
+
completed: cloudCompleted,
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
spec.id = created.specId;
|
|
1629
|
+
spec.basename = created.basename;
|
|
1630
|
+
spec.relPath = created.relPath;
|
|
1631
|
+
spec.cloudStored = storedOf(created, content);
|
|
1632
|
+
recordCloud({
|
|
1633
|
+
kind: 'create',
|
|
1634
|
+
type: 'epic',
|
|
1635
|
+
spec_id: created.specId,
|
|
1636
|
+
basename: created.basename,
|
|
1637
|
+
rel_path: created.relPath,
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// ── 3. PUT the epics whose `depends_on` names a SIBLING: that basename
|
|
1642
|
+
// only became knowable in step 2.
|
|
1643
|
+
const siblingBasenameByName = new Map(
|
|
1644
|
+
spawnedEpicSpecs.map((s) => [s.name.trim().toLowerCase(), s.basename]),
|
|
1645
|
+
);
|
|
1646
|
+
for (const spec of spawnedEpicSpecs) {
|
|
1647
|
+
if (!spec.pendingSiblingDeps?.length) continue;
|
|
1648
|
+
const siblingBasenames = [];
|
|
1649
|
+
for (const name of spec.pendingSiblingDeps) {
|
|
1650
|
+
const b = siblingBasenameByName.get(name);
|
|
1651
|
+
if (b) siblingBasenames.push(b);
|
|
1652
|
+
else depWarnings.push({ epic: spec.name, unresolved: name });
|
|
1653
|
+
}
|
|
1654
|
+
if (siblingBasenames.length === 0) continue;
|
|
1655
|
+
spec.depends_on = [...spec.depends_on, ...siblingBasenames];
|
|
1656
|
+
const fm = { ...spec.cloudStored.frontmatter };
|
|
1657
|
+
fm.depends_on = spec.depends_on.map((d) => `[[${d}]]`);
|
|
1658
|
+
try {
|
|
1659
|
+
const res = await updateSpecViaConsole({
|
|
1660
|
+
...cloudDeps,
|
|
1661
|
+
specId: spec.id,
|
|
1662
|
+
frontmatter: fm,
|
|
1663
|
+
body: spec.cloudStored.body,
|
|
1664
|
+
});
|
|
1665
|
+
recordCloud({
|
|
1666
|
+
kind: 'update',
|
|
1667
|
+
type: 'epic.depends_on',
|
|
1668
|
+
spec_id: res.specId,
|
|
1669
|
+
basename: res.basename ?? spec.basename,
|
|
1670
|
+
rel_path: res.relPath,
|
|
1671
|
+
});
|
|
1672
|
+
} catch (err) {
|
|
1673
|
+
return cloudWriteFailureResult(err, {
|
|
1674
|
+
step: { kind: 'update', type: 'epic.depends_on', basename: spec.basename },
|
|
1675
|
+
completed: cloudCompleted,
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// ── 4. PUT the milestone's `epics:` mirror (the cloud twin of the
|
|
1681
|
+
// post-commit `appendEpicToMilestone` line surgery) + `informed_by:`
|
|
1682
|
+
// on an EXISTING milestone. Both reference basenames from step 2.
|
|
1683
|
+
const spawnedBasenames = spawnedEpicSpecs.map((s) => s.basename);
|
|
1684
|
+
if (milestoneCreated) {
|
|
1685
|
+
if (spawnedBasenames.length > 0) {
|
|
1686
|
+
const fm = { ...milestoneStored.frontmatter };
|
|
1687
|
+
fm.epics = spawnedBasenames.map((b) => `[[${b}]]`);
|
|
1688
|
+
try {
|
|
1689
|
+
const res = await updateSpecViaConsole({
|
|
1690
|
+
...cloudDeps,
|
|
1691
|
+
specId: milestoneId,
|
|
1692
|
+
frontmatter: fm,
|
|
1693
|
+
body: milestoneStored.body,
|
|
1694
|
+
});
|
|
1695
|
+
recordCloud({
|
|
1696
|
+
kind: 'update',
|
|
1697
|
+
type: 'milestone.epics',
|
|
1698
|
+
spec_id: res.specId,
|
|
1699
|
+
basename: res.basename ?? milestoneBasename,
|
|
1700
|
+
rel_path: res.relPath,
|
|
1701
|
+
});
|
|
1702
|
+
} catch (err) {
|
|
1703
|
+
return cloudWriteFailureResult(err, {
|
|
1704
|
+
step: { kind: 'update', type: 'milestone.epics', basename: milestoneBasename },
|
|
1705
|
+
completed: cloudCompleted,
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
} else {
|
|
1710
|
+
const msRaw = ctx.targetResolution.milestone.raw;
|
|
1711
|
+
let updatedRaw = buildUpdatedMilestoneContent(msRaw, {
|
|
1712
|
+
researchBasename: ctx.research.basename,
|
|
1713
|
+
now,
|
|
1714
|
+
});
|
|
1715
|
+
const fmBlock = getFrontmatterBlock(updatedRaw);
|
|
1716
|
+
if (fmBlock && spawnedBasenames.length > 0) {
|
|
1717
|
+
let block = fmBlock.block;
|
|
1718
|
+
for (const b of spawnedBasenames) block = appendToListField(block, 'epics', b).block;
|
|
1719
|
+
updatedRaw = `${fmBlock.open}${block}${fmBlock.close}${fmBlock.body}`;
|
|
1720
|
+
}
|
|
1721
|
+
if (updatedRaw !== msRaw) {
|
|
1722
|
+
const { frontmatter, body } = splitRawSpecForConsole(updatedRaw);
|
|
1723
|
+
try {
|
|
1724
|
+
const res = await updateSpecViaConsole({
|
|
1725
|
+
...cloudDeps,
|
|
1726
|
+
specId: milestoneId,
|
|
1727
|
+
frontmatter,
|
|
1728
|
+
body,
|
|
1729
|
+
});
|
|
1730
|
+
recordCloud({
|
|
1731
|
+
kind: 'update',
|
|
1732
|
+
type: 'milestone',
|
|
1733
|
+
spec_id: res.specId,
|
|
1734
|
+
basename: res.basename ?? milestoneBasename,
|
|
1735
|
+
rel_path: res.relPath,
|
|
1736
|
+
});
|
|
1737
|
+
} catch (err) {
|
|
1738
|
+
return cloudWriteFailureResult(err, {
|
|
1739
|
+
step: { kind: 'update', type: 'milestone', basename: milestoneBasename },
|
|
1740
|
+
completed: cloudCompleted,
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// ── 5. PUT the research doc LAST: `enhances:` + `spawned_epics:` +
|
|
1747
|
+
// `enhances_epics:` all name basenames a POST returned above.
|
|
1748
|
+
const researchContentCloud = buildUpdatedResearchContent(ctx.research.raw, {
|
|
1749
|
+
milestoneBasename,
|
|
1750
|
+
spawnedEpicBasenames: spawnedBasenames,
|
|
1751
|
+
augmentedEpicBasenames: augmentEpics.map((e) => e.basename),
|
|
1752
|
+
now,
|
|
1753
|
+
today,
|
|
1754
|
+
author,
|
|
1755
|
+
});
|
|
1756
|
+
{
|
|
1757
|
+
const { frontmatter, body } = splitRawSpecForConsole(researchContentCloud);
|
|
1758
|
+
try {
|
|
1759
|
+
const res = await updateSpecViaConsole({
|
|
1760
|
+
...cloudDeps,
|
|
1761
|
+
specId: ctx.research.frontmatter?.id ?? args.research_id,
|
|
1762
|
+
frontmatter,
|
|
1763
|
+
body,
|
|
1764
|
+
});
|
|
1765
|
+
recordCloud({
|
|
1766
|
+
kind: 'update',
|
|
1767
|
+
type: 'research',
|
|
1768
|
+
spec_id: res.specId,
|
|
1769
|
+
basename: res.basename ?? ctx.research.basename,
|
|
1770
|
+
rel_path: res.relPath,
|
|
1771
|
+
});
|
|
1772
|
+
} catch (err) {
|
|
1773
|
+
return cloudWriteFailureResult(err, {
|
|
1774
|
+
step: { kind: 'update', type: 'research', basename: ctx.research.basename },
|
|
1775
|
+
completed: cloudCompleted,
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// ── 6. Roadmap manifest append — EXPLICIT DEGRADE. `registerSpec` edits
|
|
1781
|
+
// `roadmap_manifest.milestones` inside a roadmap doc, and `roadmap`
|
|
1782
|
+
// is not one of the console's creatable/updatable spec types (its
|
|
1783
|
+
// TYPE_TO_FOLDER has no entry for it), so there is no route to make
|
|
1784
|
+
// this write through. Reported, never silently skipped.
|
|
1785
|
+
const roadmapRegistrationCloud = milestoneCreated
|
|
1786
|
+
? {
|
|
1787
|
+
ok: false,
|
|
1788
|
+
...cloudModeUnsupportedWrite(
|
|
1789
|
+
`${args.roadmap_id || 'HS-ROADMAP-PRODUCT'} (roadmap_manifest.milestones)`,
|
|
1790
|
+
'a roadmap doc is not one of the spec types the console can create or update (no TYPE_TO_FOLDER entry for "roadmap"), so there is no POST/PUT route for the manifest append',
|
|
1791
|
+
{ milestone_basename: milestoneBasename },
|
|
1792
|
+
),
|
|
1793
|
+
}
|
|
1794
|
+
: undefined;
|
|
1795
|
+
|
|
1796
|
+
return {
|
|
1797
|
+
phase: 'executed',
|
|
1798
|
+
write_mode: 'cloud',
|
|
1799
|
+
milestone_id: milestoneId,
|
|
1800
|
+
milestone_basename: milestoneBasename,
|
|
1801
|
+
milestone_path: milestoneRelPath,
|
|
1802
|
+
milestone_created: milestoneCreated,
|
|
1803
|
+
spawned_epics: spawnedEpicSpecs.map((s) => ({
|
|
1804
|
+
id: s.id,
|
|
1805
|
+
basename: s.basename,
|
|
1806
|
+
path: s.relPath,
|
|
1807
|
+
})),
|
|
1808
|
+
augmented_epics: augmentEpics.map((e) => e.basename),
|
|
1809
|
+
research_id: ctx.research.frontmatter?.id ?? args.research_id,
|
|
1810
|
+
research_path: ctx.research.relPath,
|
|
1811
|
+
research_status_after: ctx.researchStatus ?? 'open',
|
|
1812
|
+
writes: cloudCompleted.map((w) => ({ kind: w.kind, path: w.rel_path ?? null, spec_id: w.spec_id })),
|
|
1813
|
+
human_review_required: false,
|
|
1814
|
+
...(depWarnings.length > 0 ? { depends_on_warnings: depWarnings } : {}),
|
|
1815
|
+
...(roadmapRegistrationCloud ? { roadmap_registration: roadmapRegistrationCloud } : {}),
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1228
1819
|
// 1. Milestone (create-if-new, else update later via line-surgical helper).
|
|
1229
1820
|
// Family inheritance (HS-TASK-QQHP defect #1) is resolved once, up top,
|
|
1230
1821
|
// where the era-3 basenames/ids are composed.
|
|
@@ -1401,10 +1992,20 @@ export async function executePromoteResearch(vaultRoot, args = {}, deps = {}) {
|
|
|
1401
1992
|
const validationError = validateInputShape(args);
|
|
1402
1993
|
if (validationError) return validationError;
|
|
1403
1994
|
|
|
1995
|
+
// Cloud-tier reads ([[HS-KR16-CWPX-TASK-promotion-reads-through-the-cloud-tier]]):
|
|
1996
|
+
// `deps.tier` is resolved ONCE by the caller (`handlePromoteResearch` in
|
|
1997
|
+
// research-handlers.mjs, via `resolveReadTier()`) and threaded down —
|
|
1998
|
+
// never re-resolved here. Absent (every pre-existing caller/test) defaults
|
|
1999
|
+
// to `disk`, so disk-tier behavior is byte-identical to before this task.
|
|
2000
|
+
const tier = deps.tier ?? 'disk';
|
|
2001
|
+
const consoleUrl = deps.consoleUrl;
|
|
2002
|
+
const consoleFetchDep = deps.consoleFetch;
|
|
2003
|
+
const tierDeps = { tier, consoleUrl, consoleFetch: consoleFetchDep };
|
|
2004
|
+
|
|
1404
2005
|
// 1. Load research file. Resolution happens FIRST (exact basename → .md /
|
|
1405
2006
|
// wikilink-stripped → frontmatter id: → unambiguous prefix); the "is this
|
|
1406
2007
|
// really research?" check reads the resolved file's `type:`.
|
|
1407
|
-
const resolved =
|
|
2008
|
+
const resolved = await findResearchByIdViaTier(vaultRoot, args.research_id, tierDeps);
|
|
1408
2009
|
if (!resolved || resolved.error) {
|
|
1409
2010
|
return (
|
|
1410
2011
|
resolved ?? {
|
|
@@ -1451,7 +2052,7 @@ export async function executePromoteResearch(vaultRoot, args = {}, deps = {}) {
|
|
|
1451
2052
|
}
|
|
1452
2053
|
|
|
1453
2054
|
// 2. Resolve target milestone.
|
|
1454
|
-
const targetResolution = resolveTargetMilestone(vaultRoot, args);
|
|
2055
|
+
const targetResolution = await resolveTargetMilestone(vaultRoot, args, tierDeps);
|
|
1455
2056
|
if (targetResolution.error) return targetResolution;
|
|
1456
2057
|
|
|
1457
2058
|
if (targetResolution.mode === 'existing') {
|
|
@@ -1476,12 +2077,17 @@ export async function executePromoteResearch(vaultRoot, args = {}, deps = {}) {
|
|
|
1476
2077
|
);
|
|
1477
2078
|
if (attachPolicy.error) return attachPolicy;
|
|
1478
2079
|
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
2080
|
+
// [[HS-8XJA-KOGH-DIAG]]: an `enhances` set with no spawned_epics/enhances_epics
|
|
2081
|
+
// behind it was never actually promoted (see validateResearchState) — treat
|
|
2082
|
+
// it as unset rather than blocking on a signal the capture-promote step
|
|
2083
|
+
// forged one stage early. buildUpdatedResearchContent below overwrites
|
|
2084
|
+
// `enhances` unconditionally on a successful run, so this self-heals the
|
|
2085
|
+
// stray pointer instead of requiring a hand-edit to clear it.
|
|
2086
|
+
const conflict = researchState.enhancesBacked
|
|
2087
|
+
? validateBidirectionalEnhances(researchState.enhances, targetResolution.milestone.basename)
|
|
2088
|
+
: null;
|
|
1483
2089
|
if (conflict) return conflict;
|
|
1484
|
-
} else if (researchState.enhances) {
|
|
2090
|
+
} else if (researchState.enhances && researchState.enhancesBacked) {
|
|
1485
2091
|
// Already enhances a milestone but target is NEW — refuse, the human should
|
|
1486
2092
|
// either split the research or pick the existing milestone.
|
|
1487
2093
|
return {
|
|
@@ -1538,6 +2144,13 @@ export const __testables = {
|
|
|
1538
2144
|
validateInputShape,
|
|
1539
2145
|
normalizeResearchRef,
|
|
1540
2146
|
findResearchByIdLocal,
|
|
2147
|
+
findResearchByIdViaTier,
|
|
2148
|
+
findMilestoneByBasenameViaTier,
|
|
2149
|
+
findMilestoneByIdViaTier,
|
|
2150
|
+
findEpicByIdOrBasenameViaTier,
|
|
2151
|
+
resolvePillarViaTier,
|
|
2152
|
+
resolveOwnerIntentViaTier,
|
|
2153
|
+
resolveTargetMilestone,
|
|
1541
2154
|
validateExistingMilestoneAttachable,
|
|
1542
2155
|
MILESTONE_ATTACH_DEFAULT_ALLOWED_STATUSES,
|
|
1543
2156
|
resolvePillar,
|