heyspex 0.1.0 → 0.2.0
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/README.md +21 -0
- package/bin/hs.mjs +931 -98
- 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 +1085 -103
- package/dist/cli/hs-claim.mjs +936 -63
- package/dist/cli/hs-connect.mjs +9786 -1910
- package/dist/cli/hs-defer.mjs +5267 -3053
- package/dist/cli/hs-dev.mjs +564 -0
- package/dist/cli/hs-env.mjs +385 -299
- package/dist/cli/hs-index-rebuild.mjs +7129 -0
- package/dist/cli/hs-index-reconcile.mjs +7852 -0
- package/dist/cli/hs-init.mjs +777 -138
- package/dist/cli/hs-install.mjs +6583 -485
- package/dist/cli/hs-join.mjs +10137 -2368
- package/dist/cli/hs-vault-pull.mjs +4659 -5882
- package/dist/cli/hs-vault.mjs +3793 -1537
- package/dist/cli/hs-workspace.mjs +2550 -524
- 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 +28 -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 +26 -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 +31 -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 +30 -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/mcp-client-config.md +259 -6
- package/package.json +231 -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/bin/launch-mcp.mjs +95 -0
- package/plugins/heyspex/bin/run-hook.mjs +62 -0
- package/plugins/heyspex/hooks/cursor-hooks.json +227 -0
- package/plugins/heyspex/hooks/hooks.json +361 -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 +410 -0
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +351 -0
- package/scripts/claude-hooks/_run.mjs +120 -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-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 +575 -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/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 +66 -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 +247 -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 +449 -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/wip-checkpoint.mjs +298 -0
- package/scripts/cursor-hooks/manifest.mjs +235 -0
- package/scripts/dev-server-status.mjs +34 -13
- package/scripts/dev-server-stop.mjs +313 -0
- package/scripts/heyspex-mode.mjs +268 -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 +333 -0
- package/scripts/hs-worktree.mjs +201 -0
- package/scripts/install-claude-code-hooks.mjs +270 -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 +197 -60
- package/scripts/lib/customer-kit.mjs +886 -0
- package/scripts/lib/dev-mode.mjs +244 -28
- package/scripts/lib/heyspex-home.mjs +72 -0
- package/scripts/lib/hs-command.mjs +277 -0
- package/scripts/lib/hs-companion-settings.mjs +472 -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 +11 -21
- package/scripts/lib/hs-source-push.mjs +107 -58
- 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/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 +441 -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/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 +274 -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/git-reflog-audit.mjs +294 -0
- package/spexharness/src/intent-classifier.mjs +194 -0
- package/spexharness/src/intent-directive.mjs +541 -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 +819 -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 +95 -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 +46 -5
- package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
- package/vendor/heyspex-mcp/capture-console-client.mjs +80 -1
- package/vendor/heyspex-mcp/capture-focus.mjs +1035 -29
- package/vendor/heyspex-mcp/capture-outbox.mjs +5 -1
- package/vendor/heyspex-mcp/capture-tools.mjs +1219 -0
- package/vendor/heyspex-mcp/claim-tools.mjs +1124 -0
- package/vendor/heyspex-mcp/cli-verbs.mjs +2076 -0
- package/vendor/heyspex-mcp/cli.mjs +201 -15524
- package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +412 -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 +657 -16
- package/vendor/heyspex-mcp/console-transport.mjs +166 -0
- package/vendor/heyspex-mcp/credentials.mjs +584 -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 +124 -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 +688 -0
- package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +69 -14
- package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
- package/vendor/heyspex-mcp/folder-trust-store.mjs +59 -0
- package/vendor/heyspex-mcp/http-transport.mjs +300 -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 +94 -0
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
- package/vendor/heyspex-mcp/occupied-keys.mjs +141 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +3360 -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 +906 -21
- 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 +325 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +540 -351
- package/vendor/heyspex-mcp/promote-capture.mjs +386 -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 +660 -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 +29 -9
- 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 +59 -0
- package/vendor/heyspex-mcp/research-focus.mjs +207 -4
- package/vendor/heyspex-mcp/research-handlers.mjs +623 -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/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 +108 -0
- package/vendor/heyspex-mcp/session-tools.mjs +152 -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 +446 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
- package/vendor/heyspex-mcp/split-research.mjs +301 -56
- package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +132 -6
- package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
- package/vendor/heyspex-mcp/summary-write-tools.mjs +224 -0
- package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +108 -1
- package/vendor/heyspex-mcp/tool-catalog.mjs +2813 -0
- package/vendor/heyspex-mcp/tool-dispatch.mjs +639 -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 +724 -0
- package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +366 -68
- package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
- package/vendor/heyspex-mcp/whoami.mjs +91 -0
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from 'node:fs';
|
|
9
|
-
import { join, relative } from 'node:path';
|
|
9
|
+
import { dirname, join, relative } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
10
11
|
import { execFileSync } from 'node:child_process';
|
|
11
12
|
import { prepareVaultPlanContent } from './plan-ingest.mjs';
|
|
12
13
|
import { yamlScalar } from './yaml-scalar.mjs';
|
|
@@ -22,6 +23,36 @@ import {
|
|
|
22
23
|
formatSummaryFrontmatterField,
|
|
23
24
|
resolveAuthoredSummaryValue,
|
|
24
25
|
} from './summary-contract.mjs';
|
|
26
|
+
// The cloud (console) write path. Transport-free and side-effect-free, so it
|
|
27
|
+
// is safe to import statically; the actual transport (console-transport.mjs)
|
|
28
|
+
// and the mode resolver (read-path.mjs) are DYNAMIC-imported at call time in
|
|
29
|
+
// {@link resolveCloudDeps} / {@link resolveWriteModeForPlanPush} so a local-mode
|
|
30
|
+
// run — and every existing local-mode test — keeps exactly the module graph it
|
|
31
|
+
// had before cloud mode existed.
|
|
32
|
+
import { cloudWriteFailure, createSpecViaConsole } from './cloud-vault-writes.mjs';
|
|
33
|
+
|
|
34
|
+
// scripts/validate-vault-ids.mjs is a CODE-repo artifact that ships with this
|
|
35
|
+
// package — since the repo split (2026-08-31) the vault root no longer holds
|
|
36
|
+
// it, so resolve it relative to THIS file's checkout first and fall back to
|
|
37
|
+
// the vault root only for pre-split layouts where they were the same repo.
|
|
38
|
+
// `HS_PKG_ROOT` wins when set (same precedence as dev-tools.mjs's
|
|
39
|
+
// CHECKOUT_ROOT / plan-review.mjs's resolveCodeAuditRoots).
|
|
40
|
+
const CHECKOUT_ROOT = resolveCodeRoot({
|
|
41
|
+
packageRoot: dirname(dirname(dirname(fileURLToPath(import.meta.url)))),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve scripts/validate-vault-ids.mjs against the code checkout first,
|
|
46
|
+
* falling back to `vaultRoot` for pre-split layouts where the vault and the
|
|
47
|
+
* code repo were the same checkout. Returns `null` when it exists in neither
|
|
48
|
+
* (the pre-existing "skipped" behavior).
|
|
49
|
+
*/
|
|
50
|
+
function resolveValidateVaultIdsScript(vaultRoot) {
|
|
51
|
+
const local = join(CHECKOUT_ROOT, 'scripts', 'validate-vault-ids.mjs');
|
|
52
|
+
if (existsSync(local)) return local;
|
|
53
|
+
const legacy = join(vaultRoot, 'scripts', 'validate-vault-ids.mjs');
|
|
54
|
+
return existsSync(legacy) ? legacy : null;
|
|
55
|
+
}
|
|
25
56
|
|
|
26
57
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
58
|
// Milestone-parent resolution + bidirectional mirror-update (HS-TASK-T5PX)
|
|
@@ -218,20 +249,19 @@ const DESIGN_DECISIONS = {
|
|
|
218
249
|
};
|
|
219
250
|
|
|
220
251
|
/**
|
|
221
|
-
* Detect if plan content mentions web-app file paths
|
|
222
|
-
*
|
|
223
|
-
* uses the pre-rename paths).
|
|
252
|
+
* Detect if plan content mentions web-app file paths (`spex-console/` and
|
|
253
|
+
* component-shaped sources).
|
|
224
254
|
* @param {string} content - Plan markdown or epic body
|
|
225
255
|
* @returns {boolean}
|
|
226
256
|
*/
|
|
227
257
|
function detectConsolePaths(content) {
|
|
228
|
-
// Look for spex-console
|
|
258
|
+
// Look for spex-console file path patterns
|
|
229
259
|
const patterns = [
|
|
230
|
-
/
|
|
231
|
-
/
|
|
232
|
-
/
|
|
233
|
-
/
|
|
234
|
-
/
|
|
260
|
+
/spex-console\/src\//i,
|
|
261
|
+
/spex-console\/components\//i,
|
|
262
|
+
/spex-console\/app\//i,
|
|
263
|
+
/spex-console.*\.tsx/i,
|
|
264
|
+
/spex-console.*\.ts/i,
|
|
235
265
|
/\bsurface\b.*\b(tsx?|component)\b/i,
|
|
236
266
|
];
|
|
237
267
|
|
|
@@ -843,6 +873,7 @@ const VAULT_ID_MIN_SUFFIX_LENGTH = 4;
|
|
|
843
873
|
const KIND_TYPE_LABEL = { task: 'TASK', epic: 'EPIC' };
|
|
844
874
|
|
|
845
875
|
import { deriveSlug } from './promotion-helpers.mjs';
|
|
876
|
+
import { resolveCodeRoot } from '../../scripts/lib/shared-path-helpers.mjs';
|
|
846
877
|
export { deriveSlug };
|
|
847
878
|
|
|
848
879
|
function randomSuffix(length, charset = VAULT_ID_CHARSET) {
|
|
@@ -1181,7 +1212,22 @@ function buildOnDeckBanner(reason) {
|
|
|
1181
1212
|
// / `focus-history.jsonl` writer here anymore ([[no-fallbacks-by-default]]; the
|
|
1182
1213
|
// focus.yaml-fallback incident).
|
|
1183
1214
|
|
|
1184
|
-
|
|
1215
|
+
/**
|
|
1216
|
+
* `heyspex_push_plan`.
|
|
1217
|
+
*
|
|
1218
|
+
* WRITE MODE ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]):
|
|
1219
|
+
* `options.vaultWriteMode` selects where the epic lands and defaults to
|
|
1220
|
+
* `'local'`, so every pre-existing two-argument caller keeps the byte-identical
|
|
1221
|
+
* synchronous disk path below. `'cloud'` delegates to
|
|
1222
|
+
* {@link pushPlanViaConsole} and therefore returns a PROMISE — resolve the mode
|
|
1223
|
+
* once at the handler entry ({@link executePushPlanCloudAware}) and `await` the
|
|
1224
|
+
* result, which is correct for both modes (awaiting the sync local object is a
|
|
1225
|
+
* no-op).
|
|
1226
|
+
*/
|
|
1227
|
+
export function executePushPlan(vaultRoot, args, options = {}) {
|
|
1228
|
+
if ((options.vaultWriteMode ?? 'local') === 'cloud') {
|
|
1229
|
+
return pushPlanViaConsole(vaultRoot, args, options);
|
|
1230
|
+
}
|
|
1185
1231
|
const {
|
|
1186
1232
|
epic_name: epicName,
|
|
1187
1233
|
summary: epicSummaryArg,
|
|
@@ -1377,13 +1423,14 @@ ${epicBody}
|
|
|
1377
1423
|
|
|
1378
1424
|
let validateOk = null;
|
|
1379
1425
|
let validateMessage = null;
|
|
1380
|
-
const validateScript =
|
|
1381
|
-
if (
|
|
1426
|
+
const validateScript = resolveValidateVaultIdsScript(vaultRoot);
|
|
1427
|
+
if (validateScript) {
|
|
1382
1428
|
try {
|
|
1383
1429
|
execFileSync(process.execPath, [validateScript], {
|
|
1384
|
-
cwd:
|
|
1430
|
+
cwd: dirname(validateScript),
|
|
1385
1431
|
encoding: 'utf8',
|
|
1386
1432
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1433
|
+
env: { ...process.env, HEYSPEX_VALIDATE_ROOT: vaultRoot },
|
|
1387
1434
|
});
|
|
1388
1435
|
validateOk = true;
|
|
1389
1436
|
} catch (e) {
|
|
@@ -1392,7 +1439,7 @@ ${epicBody}
|
|
|
1392
1439
|
}
|
|
1393
1440
|
} else {
|
|
1394
1441
|
validateOk = null;
|
|
1395
|
-
validateMessage = 'skipped (scripts/validate-vault-ids.mjs not in vault root)';
|
|
1442
|
+
validateMessage = 'skipped (scripts/validate-vault-ids.mjs not found in the code root or vault root)';
|
|
1396
1443
|
}
|
|
1397
1444
|
|
|
1398
1445
|
// Reuse the pre-flight conflict detection result computed before file writes
|
|
@@ -1415,7 +1462,13 @@ ${epicBody}
|
|
|
1415
1462
|
spec_path: epicRelPath,
|
|
1416
1463
|
},
|
|
1417
1464
|
children: [],
|
|
1418
|
-
|
|
1465
|
+
// NOT a claim that the pin landed — this function is sync and cannot write to
|
|
1466
|
+
// Neon (see the note above). It reports the REQUEST (`on_deck`), and the async
|
|
1467
|
+
// caller (`handlePushPlan`) overwrites this with the measured result of
|
|
1468
|
+
// `writeFocusSelectionVendor`. `null` = "not attempted here / unknown", so a
|
|
1469
|
+
// path that somehow skips the caller cannot read as a successful pin
|
|
1470
|
+
// ([[HS-IOJW-5YDH-DIAG]] sweep idiom: report the result, never the request).
|
|
1471
|
+
focus_written: onDeck ? null : false,
|
|
1419
1472
|
focus_reason: computedFocusReason,
|
|
1420
1473
|
validate_vault_ids: validateOk,
|
|
1421
1474
|
validate_message: validateMessage,
|
|
@@ -1459,8 +1512,15 @@ ${epicBody}
|
|
|
1459
1512
|
/**
|
|
1460
1513
|
* Retroactively create an epic + child tasks for completed work.
|
|
1461
1514
|
* All specs are created with status: in_review and a review doc is auto-generated.
|
|
1515
|
+
*
|
|
1516
|
+
* Same write-mode contract as {@link executePushPlan}: `options.vaultWriteMode`
|
|
1517
|
+
* defaults to `'local'` (byte-identical synchronous disk path), `'cloud'`
|
|
1518
|
+
* delegates to {@link shipEpicViaConsole} and returns a Promise.
|
|
1462
1519
|
*/
|
|
1463
|
-
export function executeShipEpic(vaultRoot, args) {
|
|
1520
|
+
export function executeShipEpic(vaultRoot, args, options = {}) {
|
|
1521
|
+
if ((options.vaultWriteMode ?? 'local') === 'cloud') {
|
|
1522
|
+
return shipEpicViaConsole(vaultRoot, args, options);
|
|
1523
|
+
}
|
|
1464
1524
|
const {
|
|
1465
1525
|
epic_name: epicName,
|
|
1466
1526
|
summary = '',
|
|
@@ -1718,13 +1778,14 @@ _Space for reviewer comments._
|
|
|
1718
1778
|
// Validate vault IDs
|
|
1719
1779
|
let validateOk = null;
|
|
1720
1780
|
let validateMessage = null;
|
|
1721
|
-
const validateScript =
|
|
1722
|
-
if (
|
|
1781
|
+
const validateScript = resolveValidateVaultIdsScript(vaultRoot);
|
|
1782
|
+
if (validateScript) {
|
|
1723
1783
|
try {
|
|
1724
1784
|
execFileSync(process.execPath, [validateScript], {
|
|
1725
|
-
cwd:
|
|
1785
|
+
cwd: dirname(validateScript),
|
|
1726
1786
|
encoding: 'utf8',
|
|
1727
1787
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1788
|
+
env: { ...process.env, HEYSPEX_VALIDATE_ROOT: vaultRoot },
|
|
1728
1789
|
});
|
|
1729
1790
|
validateOk = true;
|
|
1730
1791
|
} catch (e) {
|
|
@@ -1733,7 +1794,7 @@ _Space for reviewer comments._
|
|
|
1733
1794
|
}
|
|
1734
1795
|
} else {
|
|
1735
1796
|
validateOk = null;
|
|
1736
|
-
validateMessage = 'skipped (scripts/validate-vault-ids.mjs not in vault root)';
|
|
1797
|
+
validateMessage = 'skipped (scripts/validate-vault-ids.mjs not found in the code root or vault root)';
|
|
1737
1798
|
}
|
|
1738
1799
|
|
|
1739
1800
|
return {
|
|
@@ -1824,3 +1885,827 @@ export function buildShipWorkflowMarkdown(options = {}) {
|
|
|
1824
1885
|
|
|
1825
1886
|
return lines.join('\n');
|
|
1826
1887
|
}
|
|
1888
|
+
|
|
1889
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1890
|
+
// CLOUD WRITE MODE — `resolveVaultWriteMode() === 'cloud'`
|
|
1891
|
+
// [[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]
|
|
1892
|
+
//
|
|
1893
|
+
// In cloud reliance the vault is NOT on this machine's disk: a `writeFileSync`
|
|
1894
|
+
// here is a split brain — a markdown file the console's index has never heard
|
|
1895
|
+
// of, possibly on a machine with no clone of the vault at all. So every write
|
|
1896
|
+
// `push_plan` / `backfill_epic` makes goes through the console instead
|
|
1897
|
+
// (`POST /api/specs`, via cloud-vault-writes.mjs), and every id/file-key mint
|
|
1898
|
+
// that used to SCAN THE VAULT ON DISK is skipped — the console mints the id,
|
|
1899
|
+
// the family and the filename server-side.
|
|
1900
|
+
//
|
|
1901
|
+
// The structural consequence is CREATE-FIRST / LINK-SECOND. On disk the epic
|
|
1902
|
+
// basename was computable before any write, so children could be named in the
|
|
1903
|
+
// epic's own frontmatter. In cloud mode the basename only exists once
|
|
1904
|
+
// `POST /api/specs` has answered, so the order is: create the epic -> read back
|
|
1905
|
+
// its basename -> create each task with `parent: "[[<epic basename>]]"` ->
|
|
1906
|
+
// create the review doc, which cites both. Nothing is hand-linked in the
|
|
1907
|
+
// reverse direction: `createSpec` back-links `children:` (task->epic) and
|
|
1908
|
+
// `epics:` (epic->milestone) itself at create time (KIND_LINKAGE in
|
|
1909
|
+
// spex-console's vault.ts), which is exactly what `appendEpicToMilestone` did
|
|
1910
|
+
// on disk.
|
|
1911
|
+
//
|
|
1912
|
+
// There is NO rollback (the console has no un-create), so each entry point
|
|
1913
|
+
// stops at the first failure and reports which step failed and which documents
|
|
1914
|
+
// already landed — never an implied clean abort.
|
|
1915
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1916
|
+
|
|
1917
|
+
/**
|
|
1918
|
+
* The one document these tools write that has NO console route, described once
|
|
1919
|
+
* so the degrade message and any report of it can never drift apart.
|
|
1920
|
+
*
|
|
1921
|
+
* The console's create route accepts only the types in its `TYPE_TO_FOLDER`
|
|
1922
|
+
* map (task, epic, milestone, diagnose, capture, planning-flow, memory-*,
|
|
1923
|
+
* protocol, workflow-recipe, research, review, pillar, agent, golden-case,
|
|
1924
|
+
* adr, draft, daily). A roadmap manifest is none of those, and its update is
|
|
1925
|
+
* not a spec create at all: `addEpicToRoadmapPhase` scans `specs/roadmaps/`
|
|
1926
|
+
* for the file whose frontmatter carries `id: <roadmapId>` and then performs
|
|
1927
|
+
* line-surgical YAML editing inside a `roadmap_manifest` phase block. Borrowing
|
|
1928
|
+
* some other `type` would mint a NEW document in the wrong folder rather than
|
|
1929
|
+
* amend the existing manifest, so in cloud mode the write is skipped and said
|
|
1930
|
+
* out loud.
|
|
1931
|
+
*/
|
|
1932
|
+
const CLOUD_UNSUPPORTED_ROADMAP_WRITE = Object.freeze({
|
|
1933
|
+
error: 'CLOUD_MODE_UNSUPPORTED_WRITE',
|
|
1934
|
+
document: 'specs/roadmaps/<manifest>.md',
|
|
1935
|
+
write: 'addEpicToRoadmapPhase — append the epic basename to a roadmap_manifest phase',
|
|
1936
|
+
why:
|
|
1937
|
+
'There is no console route for a roadmap manifest: POST /api/specs only accepts the types in TYPE_TO_FOLDER '
|
|
1938
|
+
+ '(roadmap is not one of them), and this is an in-place edit of an existing YAML phase block, not a spec create. '
|
|
1939
|
+
+ 'Writing it to local disk in cloud mode would be a split brain, and minting it under a borrowed type would '
|
|
1940
|
+
+ 'create a new document instead of amending the manifest.',
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1943
|
+
/**
|
|
1944
|
+
* Resolve the console transport + URL for a cloud-mode run.
|
|
1945
|
+
*
|
|
1946
|
+
* Both are DEPENDENCIES with real defaults: a test injects a recorder and never
|
|
1947
|
+
* opens a socket, while production gets `consoleFetch` from
|
|
1948
|
+
* ./console-transport.mjs and `getConsoleUrl()` from ./mcp-config.mjs. The
|
|
1949
|
+
* imports are dynamic so a local-mode run never loads the transport at all and
|
|
1950
|
+
* the pre-existing local tests keep exactly the module graph they had.
|
|
1951
|
+
*/
|
|
1952
|
+
async function resolveCloudDeps(options = {}) {
|
|
1953
|
+
let consoleFetch = options.consoleFetch;
|
|
1954
|
+
if (typeof consoleFetch !== 'function') {
|
|
1955
|
+
({ consoleFetch } = await import('./console-transport.mjs'));
|
|
1956
|
+
}
|
|
1957
|
+
let consoleUrl = options.consoleUrl ?? null;
|
|
1958
|
+
if (!consoleUrl) {
|
|
1959
|
+
try {
|
|
1960
|
+
const { getConsoleUrl } = await import('./mcp-config.mjs');
|
|
1961
|
+
consoleUrl = getConsoleUrl();
|
|
1962
|
+
} catch {
|
|
1963
|
+
consoleUrl = null;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
return { consoleFetch, consoleUrl, actingAgent: options.actingAgent ?? null };
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
/** `resolveVaultWriteMode()` with the same injectable seam the handlers use. */
|
|
1970
|
+
async function resolveWriteModeForPlanPush(options = {}) {
|
|
1971
|
+
if (options.vaultWriteMode === 'local' || options.vaultWriteMode === 'cloud') {
|
|
1972
|
+
return options.vaultWriteMode;
|
|
1973
|
+
}
|
|
1974
|
+
if (typeof options.resolveVaultWriteMode === 'function') {
|
|
1975
|
+
return (await options.resolveVaultWriteMode()) ?? 'local';
|
|
1976
|
+
}
|
|
1977
|
+
const { resolveVaultWriteMode } = await import('./read-path.mjs');
|
|
1978
|
+
return (await resolveVaultWriteMode()) ?? 'local';
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
/**
|
|
1982
|
+
* `heyspex_push_plan` HANDLER ENTRY: resolve the write mode ONCE, then run.
|
|
1983
|
+
* `deps` is the same object threaded down to {@link executePushPlan} — pass
|
|
1984
|
+
* `{ consoleFetch }` in tests, nothing in production.
|
|
1985
|
+
*/
|
|
1986
|
+
export async function executePushPlanCloudAware(vaultRoot, args, deps = {}) {
|
|
1987
|
+
const vaultWriteMode = await resolveWriteModeForPlanPush(deps);
|
|
1988
|
+
return executePushPlan(vaultRoot, args, { ...deps, vaultWriteMode });
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
/** `heyspex_backfill_epic` HANDLER ENTRY — see {@link executePushPlanCloudAware}. */
|
|
1992
|
+
export async function executeShipEpicCloudAware(vaultRoot, args, deps = {}) {
|
|
1993
|
+
const vaultWriteMode = await resolveWriteModeForPlanPush(deps);
|
|
1994
|
+
return executeShipEpic(vaultRoot, args, { ...deps, vaultWriteMode });
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
/**
|
|
1998
|
+
* `GET /api/specs/:id` — the cloud twin of reading a vault file off disk. The
|
|
1999
|
+
* route resolves an `id` OR a wikilink basename, which is exactly the two
|
|
2000
|
+
* shapes the disk readers here resolve. Returns `null` for a genuine 404 (the
|
|
2001
|
+
* spec is not in this caller's vault — the same "skip it" answer
|
|
2002
|
+
* `resolveWikilink` gives); every other failure throws the canonical
|
|
2003
|
+
* `CONSOLE_UNREACHABLE_CLOUD_MODE` error.
|
|
2004
|
+
*/
|
|
2005
|
+
async function readSpecViaConsole({ consoleFetch, consoleUrl, ref }) {
|
|
2006
|
+
const routeLabel = `GET /api/specs/${ref}`;
|
|
2007
|
+
if (typeof consoleFetch !== 'function') {
|
|
2008
|
+
throw cloudWriteFailure(routeLabel, consoleUrl, 'no consoleFetch dependency was injected');
|
|
2009
|
+
}
|
|
2010
|
+
let result;
|
|
2011
|
+
try {
|
|
2012
|
+
result = await consoleFetch(`/api/specs/${encodeURIComponent(ref)}`);
|
|
2013
|
+
} catch (err) {
|
|
2014
|
+
throw cloudWriteFailure(routeLabel, consoleUrl, String(err?.message ?? err));
|
|
2015
|
+
}
|
|
2016
|
+
if (result?.status === 404) return null;
|
|
2017
|
+
if (!result?.ok) {
|
|
2018
|
+
throw cloudWriteFailure(
|
|
2019
|
+
routeLabel,
|
|
2020
|
+
result?.url ?? consoleUrl,
|
|
2021
|
+
`${result?.status ?? '?'}${result?.error ? ` ${result.error}` : ''}`,
|
|
2022
|
+
{ status: result?.status ?? null, console_response: result?.body ?? null },
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
2025
|
+
return result.body?.spec ?? null;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
/**
|
|
2029
|
+
* Cloud twin of {@link resolveMilestoneArg}. The shape checks (missing /
|
|
2030
|
+
* not-an-HS-MIL) are pure and stay identical; only the EXISTENCE check moves
|
|
2031
|
+
* from `existsSync(specs/milestones/<b>.md)` to the console. Also returns the
|
|
2032
|
+
* milestone's `pillar:` so the epic can inherit it (the disk path reads the
|
|
2033
|
+
* same field via {@link resolveMilestonePillar}).
|
|
2034
|
+
*
|
|
2035
|
+
* `family:` is deliberately NOT returned: in cloud mode `createSpec` inherits
|
|
2036
|
+
* the family from the resolved structural parent itself, so minting or echoing
|
|
2037
|
+
* one here would only give the server a chance to disagree with itself.
|
|
2038
|
+
*/
|
|
2039
|
+
async function resolveMilestoneArgViaConsole(raw, { consoleFetch, consoleUrl }) {
|
|
2040
|
+
if (!raw?.trim?.()) {
|
|
2041
|
+
return {
|
|
2042
|
+
error: 'MISSING_MILESTONE',
|
|
2043
|
+
message:
|
|
2044
|
+
'milestone is required — wikilink to parent HS-MIL-* (e.g. "[[HS-MIL-HMNJ-status-and-roadmap-surface-refactor]]"). See [[HS-TASK-T5PX]].',
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
const basename = normalizeMilestoneBasename(raw);
|
|
2048
|
+
if (!basename) {
|
|
2049
|
+
return {
|
|
2050
|
+
error: 'INVALID_MILESTONE',
|
|
2051
|
+
message: `milestone is not a valid HS-MIL wikilink: ${raw}`,
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
const spec = await readSpecViaConsole({ consoleFetch, consoleUrl, ref: basename });
|
|
2055
|
+
if (!spec) {
|
|
2056
|
+
return {
|
|
2057
|
+
error: 'MILESTONE_NOT_FOUND',
|
|
2058
|
+
message: `milestone does not exist in the console vault: ${basename} (cloud write mode — checked via GET /api/specs/${basename}, not local disk)`,
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
const resolvedBasename =
|
|
2062
|
+
typeof spec.basename === 'string' && spec.basename ? spec.basename : basename;
|
|
2063
|
+
return {
|
|
2064
|
+
basename: resolvedBasename,
|
|
2065
|
+
wikilink: `[[${resolvedBasename}]]`,
|
|
2066
|
+
pillar: normalizePillarRef(spec.frontmatter?.pillar),
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
/**
|
|
2071
|
+
* Cloud twin of {@link detectSpecConflicts}. Same claim extraction and the same
|
|
2072
|
+
* comparison — only the per-link body read moves from `resolveWikilink` +
|
|
2073
|
+
* `readFileSync` to `GET /api/specs/:id`. A link the console cannot serve is
|
|
2074
|
+
* SKIPPED and named in `unreachable`, mirroring the disk path's "skip files
|
|
2075
|
+
* that can't be read" rather than failing a pre-flight advisory.
|
|
2076
|
+
*/
|
|
2077
|
+
async function detectSpecConflictsViaConsole(epicContent, relatedBasenames, deps) {
|
|
2078
|
+
const epicClaims = extractBehavioralClaims(extractConflictCheckContent(epicContent), 'epic');
|
|
2079
|
+
if (epicClaims.length === 0) {
|
|
2080
|
+
return { conflicts: [], linked_specs_checked: [], unreachable: [] };
|
|
2081
|
+
}
|
|
2082
|
+
const conflicts = [];
|
|
2083
|
+
const linkedSpecsChecked = [];
|
|
2084
|
+
const unreachable = [];
|
|
2085
|
+
for (const basename of relatedBasenames) {
|
|
2086
|
+
let spec;
|
|
2087
|
+
try {
|
|
2088
|
+
spec = await readSpecViaConsole({ ...deps, ref: basename });
|
|
2089
|
+
} catch (err) {
|
|
2090
|
+
unreachable.push({ basename, message: String(err?.message ?? err) });
|
|
2091
|
+
continue;
|
|
2092
|
+
}
|
|
2093
|
+
if (!spec) continue;
|
|
2094
|
+
linkedSpecsChecked.push(basename);
|
|
2095
|
+
conflicts.push(
|
|
2096
|
+
...findConflicts(epicClaims, extractBehavioralClaims(String(spec.body ?? ''), basename)),
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
return { conflicts, linked_specs_checked: linkedSpecsChecked, unreachable };
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
/** The `related:` wikilink array both cloud writers build (user links + defaults). */
|
|
2103
|
+
function normalizeRelatedWikilinks(related, defaults = []) {
|
|
2104
|
+
const list = (Array.isArray(related) ? related : []).map(
|
|
2105
|
+
(r) => `[[${String(r).replace(/^\[\[|\]\]$/g, '')}]]`,
|
|
2106
|
+
);
|
|
2107
|
+
return [...new Set([...list, ...defaults])];
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
/** The `landed` block every cloud result carries — the twin of research-handlers'. */
|
|
2111
|
+
function cloudLandedBlock({ consoleUrl, relPath, consoleWrite = null, vaultRoot = null }) {
|
|
2112
|
+
return {
|
|
2113
|
+
tier: 'cloud',
|
|
2114
|
+
reported_by: consoleWrite ? 'console' : 'inferred_from_console_url',
|
|
2115
|
+
console_url: consoleUrl ?? null,
|
|
2116
|
+
rel_path: relPath ?? null,
|
|
2117
|
+
abs_path: null,
|
|
2118
|
+
on_disk: false,
|
|
2119
|
+
in_local_vault: false,
|
|
2120
|
+
vault_root: vaultRoot ?? null,
|
|
2121
|
+
console_write: consoleWrite,
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
/**
|
|
2126
|
+
* The one failure envelope both cloud entry points return. Names the step that
|
|
2127
|
+
* failed AND everything that already landed — there is no rollback, so a caller
|
|
2128
|
+
* must be able to say what state the vault is actually in.
|
|
2129
|
+
*/
|
|
2130
|
+
function cloudWriteFailureResult(err, { step, landed = [], extra = {} }) {
|
|
2131
|
+
return {
|
|
2132
|
+
error: 'CONSOLE_UNREACHABLE_CLOUD_MODE',
|
|
2133
|
+
success: false,
|
|
2134
|
+
write_mode: 'cloud',
|
|
2135
|
+
failed_step: step,
|
|
2136
|
+
message: String(err?.message ?? err),
|
|
2137
|
+
console_url: err?.console_url ?? null,
|
|
2138
|
+
...(err?.status != null ? { status: err.status } : {}),
|
|
2139
|
+
...(err?.console_response ? { console_response: err.console_response } : {}),
|
|
2140
|
+
...(err?.hint ? { hint: err.hint } : {}),
|
|
2141
|
+
already_landed: landed,
|
|
2142
|
+
rollback:
|
|
2143
|
+
'none — the console has no un-create; the documents in `already_landed` are real and must not be re-created on a retry',
|
|
2144
|
+
...extra,
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
/** In cloud mode the vault is not on this machine, so the id validator cannot run. */
|
|
2149
|
+
const CLOUD_VALIDATE_SKIPPED =
|
|
2150
|
+
'skipped (cloud write mode — scripts/validate-vault-ids.mjs scans the vault on disk and this machine may hold no clone; the console validates ids server-side at create time)';
|
|
2151
|
+
|
|
2152
|
+
/**
|
|
2153
|
+
* `push_plan` in cloud write mode. Mirrors {@link executePushPlan}'s result
|
|
2154
|
+
* shape so callers read ONE shape in both modes, minus everything that only
|
|
2155
|
+
* exists on disk (the `file_id`/`family` mints, the milestone mirror-update,
|
|
2156
|
+
* the id-validator subprocess).
|
|
2157
|
+
*/
|
|
2158
|
+
async function pushPlanViaConsole(vaultRoot, args, options = {}) {
|
|
2159
|
+
const {
|
|
2160
|
+
epic_name: epicName,
|
|
2161
|
+
summary: epicSummaryArg,
|
|
2162
|
+
plan_markdown: planMarkdown = '',
|
|
2163
|
+
implementation_prompt: implementationPrompt,
|
|
2164
|
+
source_plan_paths: sourcePlanPaths = [],
|
|
2165
|
+
children = [],
|
|
2166
|
+
on_deck: onDeck = false,
|
|
2167
|
+
focus_reason: focusReason,
|
|
2168
|
+
kind = 'feature',
|
|
2169
|
+
effort = 3,
|
|
2170
|
+
tags = [],
|
|
2171
|
+
related = [],
|
|
2172
|
+
skip_conflict_check: skipConflictCheck = false,
|
|
2173
|
+
milestone,
|
|
2174
|
+
// Cloud-only: POST /api/specs HARD-REQUIRES `pillar:` on an epic (route.ts
|
|
2175
|
+
// `pillar_required`), which the disk path never emitted because nothing on
|
|
2176
|
+
// disk enforced it. Honoured only here, so the local branch stays
|
|
2177
|
+
// byte-identical.
|
|
2178
|
+
pillar,
|
|
2179
|
+
} = args;
|
|
2180
|
+
|
|
2181
|
+
if (!epicName?.trim()) {
|
|
2182
|
+
return { error: 'MISSING_EPIC_NAME', message: 'epic_name is required' };
|
|
2183
|
+
}
|
|
2184
|
+
const epicSummary = resolveAuthoredSummaryValue(epicSummaryArg);
|
|
2185
|
+
if (!epicSummary) {
|
|
2186
|
+
return buildSummaryRequiredMcpError('epic');
|
|
2187
|
+
}
|
|
2188
|
+
if (Array.isArray(children) && children.some((c) => c?.name?.trim())) {
|
|
2189
|
+
return {
|
|
2190
|
+
error: 'CHILDREN_NOT_SUPPORTED',
|
|
2191
|
+
message:
|
|
2192
|
+
'heyspex_push_plan files the epic only (`children: []`). File tasks just-in-time from the What\'s Next `/hs-next` breakdown via `heyspex_file_spec` with `summary` — not via push_plan ([[spec-hierarchy]] §empty-state grain-split).',
|
|
2193
|
+
procedure: 'spec-hierarchy',
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
const { consoleFetch, consoleUrl, actingAgent } = await resolveCloudDeps(options);
|
|
2198
|
+
|
|
2199
|
+
let milestoneResolved;
|
|
2200
|
+
try {
|
|
2201
|
+
milestoneResolved = await resolveMilestoneArgViaConsole(milestone, {
|
|
2202
|
+
consoleFetch,
|
|
2203
|
+
consoleUrl,
|
|
2204
|
+
});
|
|
2205
|
+
} catch (err) {
|
|
2206
|
+
return cloudWriteFailureResult(err, { step: 'resolve_milestone' });
|
|
2207
|
+
}
|
|
2208
|
+
if (milestoneResolved.error) return milestoneResolved;
|
|
2209
|
+
|
|
2210
|
+
const resolvedPillar = normalizePillarRef(pillar) ?? milestoneResolved.pillar ?? null;
|
|
2211
|
+
if (!resolvedPillar) {
|
|
2212
|
+
return {
|
|
2213
|
+
error: 'PILLAR_REQUIRED',
|
|
2214
|
+
message:
|
|
2215
|
+
`Cannot resolve a pillar for this epic: parent milestone "${milestoneResolved.basename}" has no \`pillar:\` and none was passed. ` +
|
|
2216
|
+
'Cloud write mode files the epic through POST /api/specs, which hard-blocks an epic with no resolvable `pillar:`. ' +
|
|
2217
|
+
'Pass `pillar: "[[HS-PILR-<slug>]]"`, or assign the milestone a pillar first. ' +
|
|
2218
|
+
'Use heyspex_suggest_pillar to pick one against each pillar\'s written charter — never by name ([[pillar-assignment-protocol]]).',
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
// Reads LOCAL plan source files (Cursor `*.plan.md` etc.) into the epic body.
|
|
2223
|
+
// Those are this machine's own files, not vault content, and this makes no
|
|
2224
|
+
// vault write — so it is unchanged in cloud mode.
|
|
2225
|
+
const prepared = prepareVaultPlanContent(vaultRoot, {
|
|
2226
|
+
plan_markdown: planMarkdown,
|
|
2227
|
+
implementation_prompt: implementationPrompt,
|
|
2228
|
+
source_plan_paths: sourcePlanPaths,
|
|
2229
|
+
});
|
|
2230
|
+
|
|
2231
|
+
let epicBody =
|
|
2232
|
+
prepared.plan_markdown.trim() || `# ${epicName}\n\n## Goal\n\n_Describe outcome._\n`;
|
|
2233
|
+
const preparedPrompt = prepared.implementation_prompt;
|
|
2234
|
+
if (onDeck) {
|
|
2235
|
+
epicBody = `${buildOnDeckBanner(focusReason)}\n${epicBody}`;
|
|
2236
|
+
if (preparedPrompt?.trim()) {
|
|
2237
|
+
epicBody += buildNextStepSection(preparedPrompt);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
const userRelatedLinks = Array.isArray(related) ? related : [];
|
|
2242
|
+
const uniqueRelated = normalizeRelatedWikilinks(userRelatedLinks, [
|
|
2243
|
+
'[[plan-ingest-agent-workflow]]',
|
|
2244
|
+
'[[vault-id-assignment-protocol]]',
|
|
2245
|
+
]);
|
|
2246
|
+
|
|
2247
|
+
// Pre-flight conflict detection, BEFORE anything is created — same blocking
|
|
2248
|
+
// contract as the disk path (an on_deck push with conflicts writes nothing).
|
|
2249
|
+
let preflightConflicts = [];
|
|
2250
|
+
let preflightLinkedSpecs = [];
|
|
2251
|
+
let preflightUnreachable = [];
|
|
2252
|
+
if (!skipConflictCheck && userRelatedLinks.length > 0) {
|
|
2253
|
+
const fullEpicContent = [epicBody, preparedPrompt || ''].join('\n');
|
|
2254
|
+
const relatedBasenames = userRelatedLinks.map((r) => String(r).replace(/^\[\[|\]\]$/g, ''));
|
|
2255
|
+
const conflictResult = await detectSpecConflictsViaConsole(fullEpicContent, relatedBasenames, {
|
|
2256
|
+
consoleFetch,
|
|
2257
|
+
consoleUrl,
|
|
2258
|
+
});
|
|
2259
|
+
preflightConflicts = conflictResult.conflicts;
|
|
2260
|
+
preflightLinkedSpecs = conflictResult.linked_specs_checked;
|
|
2261
|
+
preflightUnreachable = conflictResult.unreachable;
|
|
2262
|
+
|
|
2263
|
+
if (onDeck && preflightConflicts.length > 0) {
|
|
2264
|
+
return {
|
|
2265
|
+
error: 'CONFLICTS_DETECTED',
|
|
2266
|
+
success: false,
|
|
2267
|
+
on_deck: true,
|
|
2268
|
+
blocked: true,
|
|
2269
|
+
write_mode: 'cloud',
|
|
2270
|
+
message: `${preflightConflicts.length} behavioral conflict(s) detected against linked spec(s). Resolve them or pass skip_conflict_check: true to force the push.`,
|
|
2271
|
+
conflicts: preflightConflicts,
|
|
2272
|
+
linked_specs_checked: preflightLinkedSpecs,
|
|
2273
|
+
has_conflicts: true,
|
|
2274
|
+
next_steps: [
|
|
2275
|
+
'Review the conflicts below — each shows a behavioral claim in this plan that contradicts a linked spec.',
|
|
2276
|
+
'Resolution options:',
|
|
2277
|
+
' • Edit the NEXT STEP / acceptance criteria in plan_markdown to match the linked spec, then re-run heyspex_push_plan.',
|
|
2278
|
+
' • Edit the linked spec (its Interaction model section) to match this plan, then re-run heyspex_push_plan.',
|
|
2279
|
+
' • Pass skip_conflict_check: true to force the push (a conflict_override row can be recorded later via the console Dismiss-with-reason action).',
|
|
2280
|
+
],
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
// No `id`/`file_id`/`family`/`created`/`updated`: every one of those is
|
|
2286
|
+
// server-minted (cloud-vault-writes.mjs SERVER_MINTED_FIELDS), and `family`
|
|
2287
|
+
// is inherited from the milestone by `createSpec` itself.
|
|
2288
|
+
const epicFrontmatter = {
|
|
2289
|
+
kind,
|
|
2290
|
+
effort,
|
|
2291
|
+
requires_user_testing: false,
|
|
2292
|
+
status: onDeck ? 'in_progress' : 'open',
|
|
2293
|
+
milestone: milestoneResolved.wikilink,
|
|
2294
|
+
pillar: `[[${resolvedPillar}]]`,
|
|
2295
|
+
tags: tags.length > 0 ? [...tags] : ['epic'],
|
|
2296
|
+
// Left empty ON PURPOSE — push_plan never files children, and the console
|
|
2297
|
+
// back-links any later task into this array at THAT task's create time.
|
|
2298
|
+
children: [],
|
|
2299
|
+
related: uniqueRelated,
|
|
2300
|
+
};
|
|
2301
|
+
const epicMarkdown = `${epicBody}
|
|
2302
|
+
|
|
2303
|
+
## Change log
|
|
2304
|
+
|
|
2305
|
+
| Date | Author | Change |
|
|
2306
|
+
|------|--------|--------|
|
|
2307
|
+
| ${todayDate()} | pwher (with agent via heyspex_push_plan) | Epic created via hs-push${onDeck ? '-next' : ''}. |
|
|
2308
|
+
`;
|
|
2309
|
+
|
|
2310
|
+
let createdEpic;
|
|
2311
|
+
try {
|
|
2312
|
+
createdEpic = await createSpecViaConsole({
|
|
2313
|
+
consoleFetch,
|
|
2314
|
+
consoleUrl,
|
|
2315
|
+
type: 'epic',
|
|
2316
|
+
name: epicName,
|
|
2317
|
+
summary: epicSummary,
|
|
2318
|
+
frontmatter: epicFrontmatter,
|
|
2319
|
+
body: epicMarkdown,
|
|
2320
|
+
actingAgent,
|
|
2321
|
+
});
|
|
2322
|
+
} catch (err) {
|
|
2323
|
+
return cloudWriteFailureResult(err, { step: 'create_epic' });
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
const computedFocusReason = onDeck ? focusReason || `On deck: ${epicName} (hs-push-next)` : null;
|
|
2327
|
+
const designDecisions = getApplicableDesignDecisions(
|
|
2328
|
+
[epicBody, preparedPrompt || ''].join('\n'),
|
|
2329
|
+
);
|
|
2330
|
+
|
|
2331
|
+
const result = {
|
|
2332
|
+
success: true,
|
|
2333
|
+
write_mode: 'cloud',
|
|
2334
|
+
on_deck: onDeck,
|
|
2335
|
+
epic: {
|
|
2336
|
+
id: createdEpic.specId,
|
|
2337
|
+
basename: createdEpic.basename,
|
|
2338
|
+
spec_path: createdEpic.relPath,
|
|
2339
|
+
},
|
|
2340
|
+
children: [],
|
|
2341
|
+
landed: cloudLandedBlock({
|
|
2342
|
+
consoleUrl: createdEpic.consoleUrl ?? consoleUrl,
|
|
2343
|
+
relPath: createdEpic.relPath,
|
|
2344
|
+
consoleWrite: createdEpic.consoleWrite,
|
|
2345
|
+
vaultRoot,
|
|
2346
|
+
}),
|
|
2347
|
+
// The console back-links `epics:` on the milestone at create time
|
|
2348
|
+
// (KIND_LINKAGE.epic.parentArray), which is what appendEpicToMilestone did
|
|
2349
|
+
// on disk — so this tool does NOT hand-write the parent array in cloud mode.
|
|
2350
|
+
milestone_backlink: {
|
|
2351
|
+
milestone: milestoneResolved.basename,
|
|
2352
|
+
by: 'console (createSpec KIND_LINKAGE.epic.parentArray = "epics")',
|
|
2353
|
+
},
|
|
2354
|
+
focus_written: onDeck ? null : false,
|
|
2355
|
+
focus_reason: computedFocusReason,
|
|
2356
|
+
validate_vault_ids: null,
|
|
2357
|
+
validate_message: CLOUD_VALIDATE_SKIPPED,
|
|
2358
|
+
conflicts: preflightConflicts,
|
|
2359
|
+
linked_specs_checked: preflightLinkedSpecs,
|
|
2360
|
+
has_conflicts: preflightConflicts.length > 0,
|
|
2361
|
+
design_decisions: designDecisions,
|
|
2362
|
+
has_design_decisions: designDecisions.length > 0,
|
|
2363
|
+
plans_ingested: prepared.plans_ingested,
|
|
2364
|
+
ingested_plans: prepared.ingested_plans,
|
|
2365
|
+
missing_plans: prepared.missing_plans,
|
|
2366
|
+
...(Array.isArray(createdEpic.warnings) && createdEpic.warnings.length > 0
|
|
2367
|
+
? { console_warnings: createdEpic.warnings }
|
|
2368
|
+
: {}),
|
|
2369
|
+
...(preflightUnreachable.length > 0
|
|
2370
|
+
? { conflict_check_unreachable: preflightUnreachable }
|
|
2371
|
+
: {}),
|
|
2372
|
+
next_steps: [
|
|
2373
|
+
'heyspex_validate_linkage',
|
|
2374
|
+
onDeck
|
|
2375
|
+
? 'heyspex_next_on_deck or /hs-next — break into tasks via the breakdown brief (heyspex_file_spec with summary)'
|
|
2376
|
+
: 'Optional: heyspex_push_plan on_deck after review',
|
|
2377
|
+
'heyspex_register_spec if epic should appear in roadmap manifest',
|
|
2378
|
+
'Record the WIP ledger (heyspex_record_wip) for the epic if this is the new focus (next-session-handoff.md retired — HS-EPIC-HNDR)',
|
|
2379
|
+
],
|
|
2380
|
+
};
|
|
2381
|
+
|
|
2382
|
+
if (result.conflicts.length > 0) {
|
|
2383
|
+
result.next_steps.unshift(
|
|
2384
|
+
`⚠️ ${result.conflicts.length} spec conflict(s) detected — review before implementation`,
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
if (designDecisions.length > 0) {
|
|
2388
|
+
result.next_steps.unshift(
|
|
2389
|
+
`📐 ${designDecisions.length} design decision(s) apply — review before implementation`,
|
|
2390
|
+
);
|
|
2391
|
+
}
|
|
2392
|
+
return result;
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
/**
|
|
2396
|
+
* `backfill_epic` in cloud write mode — the CREATE-FIRST / LINK-SECOND flow.
|
|
2397
|
+
*
|
|
2398
|
+
* Order is load-bearing: the epic must exist before a task can name it as
|
|
2399
|
+
* `parent:`, and the review doc must come last because it cites both the epic
|
|
2400
|
+
* and every task by basename — and in cloud mode a basename is only knowable
|
|
2401
|
+
* once the console has answered.
|
|
2402
|
+
*
|
|
2403
|
+
* The `children:` array is NOT hand-written: `createSpec` back-links each task
|
|
2404
|
+
* into its epic at the task's own create time. The roadmap manifest has no
|
|
2405
|
+
* console route at all and degrades explicitly (see
|
|
2406
|
+
* {@link CLOUD_UNSUPPORTED_ROADMAP_WRITE}).
|
|
2407
|
+
*/
|
|
2408
|
+
async function shipEpicViaConsole(vaultRoot, args, options = {}) {
|
|
2409
|
+
const {
|
|
2410
|
+
epic_name: epicName,
|
|
2411
|
+
summary = '',
|
|
2412
|
+
plan_markdown: planMarkdown = '',
|
|
2413
|
+
children = [],
|
|
2414
|
+
files_changed: filesChanged = [],
|
|
2415
|
+
kind = 'feature',
|
|
2416
|
+
effort = 3,
|
|
2417
|
+
tags = [],
|
|
2418
|
+
related = [],
|
|
2419
|
+
roadmap_id: roadmapId = 'HS-ROADMAP-PRODUCT',
|
|
2420
|
+
roadmap_phase: roadmapPhase = 'P0',
|
|
2421
|
+
skip_roadmap_registration: skipRoadmapRegistration = false,
|
|
2422
|
+
milestone,
|
|
2423
|
+
pillar,
|
|
2424
|
+
} = args;
|
|
2425
|
+
|
|
2426
|
+
if (!epicName?.trim()) {
|
|
2427
|
+
return { error: 'MISSING_EPIC_NAME', message: 'epic_name is required' };
|
|
2428
|
+
}
|
|
2429
|
+
const epicSummary = resolveAuthoredSummaryValue(summary);
|
|
2430
|
+
if (!epicSummary) {
|
|
2431
|
+
return buildSummaryRequiredMcpError('epic');
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
const { consoleFetch, consoleUrl, actingAgent } = await resolveCloudDeps(options);
|
|
2435
|
+
|
|
2436
|
+
let milestoneResolved;
|
|
2437
|
+
try {
|
|
2438
|
+
milestoneResolved = await resolveMilestoneArgViaConsole(milestone, {
|
|
2439
|
+
consoleFetch,
|
|
2440
|
+
consoleUrl,
|
|
2441
|
+
});
|
|
2442
|
+
} catch (err) {
|
|
2443
|
+
return cloudWriteFailureResult(err, { step: 'resolve_milestone' });
|
|
2444
|
+
}
|
|
2445
|
+
if (milestoneResolved.error) return milestoneResolved;
|
|
2446
|
+
|
|
2447
|
+
const resolvedPillar = normalizePillarRef(pillar) ?? milestoneResolved.pillar ?? null;
|
|
2448
|
+
if (!resolvedPillar) {
|
|
2449
|
+
return {
|
|
2450
|
+
error: 'PILLAR_REQUIRED',
|
|
2451
|
+
message:
|
|
2452
|
+
`Cannot resolve a pillar for this epic: parent milestone "${milestoneResolved.basename}" has no \`pillar:\` and none was passed. ` +
|
|
2453
|
+
'Pass `pillar: "[[HS-PILR-<slug>]]"`, or assign the milestone a pillar first. ' +
|
|
2454
|
+
'Use heyspex_suggest_pillar to pick one against each pillar\'s written charter — never by name ([[pillar-assignment-protocol]]).',
|
|
2455
|
+
};
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
// Validate EVERY child before creating anything: a summary-less child must
|
|
2459
|
+
// not surface after half the vault has already been written.
|
|
2460
|
+
const plannedChildren = [];
|
|
2461
|
+
for (const child of children) {
|
|
2462
|
+
if (!child?.name?.trim()) continue;
|
|
2463
|
+
const childSummary = resolveAuthoredSummaryValue(child.summary);
|
|
2464
|
+
if (!childSummary) {
|
|
2465
|
+
return {
|
|
2466
|
+
...buildSummaryRequiredMcpError('task'),
|
|
2467
|
+
message: `Child task "${child.name}" requires a plain-language \`summary\` when backfilled — pass \`summary\` on each child per spec-summary-authoring.`,
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
plannedChildren.push({ name: child.name, summary: childSummary });
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
const relatedWikilinks = normalizeRelatedWikilinks(related);
|
|
2474
|
+
const epicBody = planMarkdown.trim() || `# ${epicName}\n\n${epicSummary}\n`;
|
|
2475
|
+
const epicFrontmatter = {
|
|
2476
|
+
kind,
|
|
2477
|
+
effort,
|
|
2478
|
+
requires_user_testing: false,
|
|
2479
|
+
status: 'in_review',
|
|
2480
|
+
milestone: milestoneResolved.wikilink,
|
|
2481
|
+
pillar: `[[${resolvedPillar}]]`,
|
|
2482
|
+
tags: tags.length > 0 ? [...tags] : ['epic'],
|
|
2483
|
+
// Empty at create time BY CONSTRUCTION — the task basenames do not exist
|
|
2484
|
+
// yet. Each task back-links itself into this array when the console creates
|
|
2485
|
+
// it (KIND_LINKAGE.task.parentArray = "children").
|
|
2486
|
+
children: [],
|
|
2487
|
+
related: relatedWikilinks,
|
|
2488
|
+
};
|
|
2489
|
+
|
|
2490
|
+
const landed = [];
|
|
2491
|
+
|
|
2492
|
+
// ── Step 1: the epic (create-first) ────────────────────────────────────────
|
|
2493
|
+
let createdEpic;
|
|
2494
|
+
try {
|
|
2495
|
+
createdEpic = await createSpecViaConsole({
|
|
2496
|
+
consoleFetch,
|
|
2497
|
+
consoleUrl,
|
|
2498
|
+
type: 'epic',
|
|
2499
|
+
name: epicName,
|
|
2500
|
+
summary: epicSummary,
|
|
2501
|
+
frontmatter: epicFrontmatter,
|
|
2502
|
+
body: `${epicBody}
|
|
2503
|
+
|
|
2504
|
+
## Change log
|
|
2505
|
+
|
|
2506
|
+
| Date | Author | Change |
|
|
2507
|
+
|------|--------|--------|
|
|
2508
|
+
| ${todayDate()} | pwher (with agent via heyspex_backfill_epic) | Epic created retroactively, backfilled to review. |
|
|
2509
|
+
`,
|
|
2510
|
+
actingAgent,
|
|
2511
|
+
});
|
|
2512
|
+
} catch (err) {
|
|
2513
|
+
return cloudWriteFailureResult(err, { step: 'create_epic', landed });
|
|
2514
|
+
}
|
|
2515
|
+
const epicBasename = createdEpic.basename;
|
|
2516
|
+
landed.push({
|
|
2517
|
+
step: 'create_epic',
|
|
2518
|
+
type: 'epic',
|
|
2519
|
+
id: createdEpic.specId,
|
|
2520
|
+
basename: epicBasename,
|
|
2521
|
+
rel_path: createdEpic.relPath,
|
|
2522
|
+
});
|
|
2523
|
+
|
|
2524
|
+
// ── Step 2: the child tasks (link-second — the parent is now knowable) ─────
|
|
2525
|
+
const childSpecs = [];
|
|
2526
|
+
const childWikilinks = [];
|
|
2527
|
+
for (const child of plannedChildren) {
|
|
2528
|
+
let createdTask;
|
|
2529
|
+
try {
|
|
2530
|
+
createdTask = await createSpecViaConsole({
|
|
2531
|
+
consoleFetch,
|
|
2532
|
+
consoleUrl,
|
|
2533
|
+
type: 'task',
|
|
2534
|
+
name: child.name,
|
|
2535
|
+
summary: child.summary,
|
|
2536
|
+
frontmatter: {
|
|
2537
|
+
kind,
|
|
2538
|
+
effort,
|
|
2539
|
+
requires_user_testing: false,
|
|
2540
|
+
status: 'in_review',
|
|
2541
|
+
tags: ['shipped'],
|
|
2542
|
+
parent: `[[${epicBasename}]]`,
|
|
2543
|
+
},
|
|
2544
|
+
body: `# ${child.name}
|
|
2545
|
+
|
|
2546
|
+
_Completed as part of [[${epicBasename}]]._
|
|
2547
|
+
|
|
2548
|
+
## Change log
|
|
2549
|
+
|
|
2550
|
+
| Date | Author | Change |
|
|
2551
|
+
|------|--------|--------|
|
|
2552
|
+
| ${todayDate()} | pwher (with agent via heyspex_backfill_epic) | Task backfilled to review as part of [[${epicBasename}]]. |
|
|
2553
|
+
`,
|
|
2554
|
+
actingAgent,
|
|
2555
|
+
});
|
|
2556
|
+
} catch (err) {
|
|
2557
|
+
return cloudWriteFailureResult(err, {
|
|
2558
|
+
step: 'create_task',
|
|
2559
|
+
landed,
|
|
2560
|
+
extra: { failed_task: child.name },
|
|
2561
|
+
});
|
|
2562
|
+
}
|
|
2563
|
+
landed.push({
|
|
2564
|
+
step: 'create_task',
|
|
2565
|
+
type: 'task',
|
|
2566
|
+
id: createdTask.specId,
|
|
2567
|
+
basename: createdTask.basename,
|
|
2568
|
+
rel_path: createdTask.relPath,
|
|
2569
|
+
});
|
|
2570
|
+
childWikilinks.push(`[[${createdTask.basename}]]`);
|
|
2571
|
+
childSpecs.push({
|
|
2572
|
+
id: createdTask.specId,
|
|
2573
|
+
basename: createdTask.basename,
|
|
2574
|
+
relPath: createdTask.relPath,
|
|
2575
|
+
name: child.name,
|
|
2576
|
+
summary: child.summary,
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
// ── Step 3: the review doc (cites the epic AND every task — so, last) ─────
|
|
2581
|
+
const filesTable =
|
|
2582
|
+
filesChanged.length > 0
|
|
2583
|
+
? filesChanged.map((f) => `| ${f} | Modified |`).join('\n')
|
|
2584
|
+
: '| _No files listed_ | - |';
|
|
2585
|
+
const reviewBody = `# Review: ${epicName}
|
|
2586
|
+
|
|
2587
|
+
## Pre-Review Checklist
|
|
2588
|
+
|
|
2589
|
+
- [ ] Code changes reviewed
|
|
2590
|
+
- [ ] Tests passing (if applicable)
|
|
2591
|
+
- [ ] No regressions introduced
|
|
2592
|
+
- [ ] Documentation updated (if applicable)
|
|
2593
|
+
|
|
2594
|
+
## Files Changed
|
|
2595
|
+
|
|
2596
|
+
| File | Change Type |
|
|
2597
|
+
|------|-------------|
|
|
2598
|
+
${filesTable}
|
|
2599
|
+
|
|
2600
|
+
## Summary
|
|
2601
|
+
|
|
2602
|
+
${epicSummary}
|
|
2603
|
+
|
|
2604
|
+
## Child Tasks
|
|
2605
|
+
|
|
2606
|
+
${childWikilinks.length > 0 ? childWikilinks.map((c) => `- ${c}`).join('\n') : '_No child tasks._'}
|
|
2607
|
+
|
|
2608
|
+
## Notes
|
|
2609
|
+
|
|
2610
|
+
_Space for reviewer comments._
|
|
2611
|
+
`;
|
|
2612
|
+
let createdReview;
|
|
2613
|
+
try {
|
|
2614
|
+
createdReview = await createSpecViaConsole({
|
|
2615
|
+
consoleFetch,
|
|
2616
|
+
consoleUrl,
|
|
2617
|
+
type: 'review',
|
|
2618
|
+
name: `Review: ${epicName}`,
|
|
2619
|
+
summary: epicSummary,
|
|
2620
|
+
frontmatter: {
|
|
2621
|
+
// `parent:` is REQUIRED for a review by the console's linkage rules
|
|
2622
|
+
// (KIND_LINKAGE.review.parentRequired); `for:` preserves the field the
|
|
2623
|
+
// on-disk `<epic>.review.md` carried so existing readers still resolve.
|
|
2624
|
+
parent: `[[${epicBasename}]]`,
|
|
2625
|
+
for: `[[${epicBasename}]]`,
|
|
2626
|
+
},
|
|
2627
|
+
body: reviewBody,
|
|
2628
|
+
actingAgent,
|
|
2629
|
+
});
|
|
2630
|
+
} catch (err) {
|
|
2631
|
+
return cloudWriteFailureResult(err, { step: 'create_review_doc', landed });
|
|
2632
|
+
}
|
|
2633
|
+
landed.push({
|
|
2634
|
+
step: 'create_review_doc',
|
|
2635
|
+
type: 'review',
|
|
2636
|
+
id: createdReview.specId,
|
|
2637
|
+
basename: createdReview.basename,
|
|
2638
|
+
rel_path: createdReview.relPath,
|
|
2639
|
+
});
|
|
2640
|
+
|
|
2641
|
+
// ── Step 4: the roadmap manifest — NO CLOUD PATH, degrade out loud ────────
|
|
2642
|
+
const roadmapRegistration = skipRoadmapRegistration
|
|
2643
|
+
? { success: true, message: 'Skipped (skip_roadmap_registration: true)' }
|
|
2644
|
+
: {
|
|
2645
|
+
success: false,
|
|
2646
|
+
...CLOUD_UNSUPPORTED_ROADMAP_WRITE,
|
|
2647
|
+
roadmap_id: roadmapId,
|
|
2648
|
+
roadmap_phase: roadmapPhase,
|
|
2649
|
+
epic_basename: epicBasename,
|
|
2650
|
+
message:
|
|
2651
|
+
`Roadmap registration was SKIPPED in cloud write mode: ${CLOUD_UNSUPPORTED_ROADMAP_WRITE.why} `
|
|
2652
|
+
+ 'Nothing was written to local disk and no substitute document was minted. '
|
|
2653
|
+
+ `To land it, add "${epicBasename}" to phase ${roadmapPhase} of ${roadmapId} by hand, or run this backfill in local write mode.`,
|
|
2654
|
+
};
|
|
2655
|
+
|
|
2656
|
+
return {
|
|
2657
|
+
success: true,
|
|
2658
|
+
write_mode: 'cloud',
|
|
2659
|
+
shipped_to_review: true,
|
|
2660
|
+
epic: {
|
|
2661
|
+
id: createdEpic.specId,
|
|
2662
|
+
basename: epicBasename,
|
|
2663
|
+
spec_path: createdEpic.relPath,
|
|
2664
|
+
},
|
|
2665
|
+
children: childSpecs,
|
|
2666
|
+
review_doc: createdReview.relPath,
|
|
2667
|
+
landed: cloudLandedBlock({
|
|
2668
|
+
consoleUrl: createdEpic.consoleUrl ?? consoleUrl,
|
|
2669
|
+
relPath: createdEpic.relPath,
|
|
2670
|
+
consoleWrite: createdEpic.consoleWrite,
|
|
2671
|
+
vaultRoot,
|
|
2672
|
+
}),
|
|
2673
|
+
console_writes: landed,
|
|
2674
|
+
children_backlink: {
|
|
2675
|
+
by: 'console (createSpec KIND_LINKAGE.task.parentArray = "children")',
|
|
2676
|
+
note: 'each task back-links itself into the epic `children:` array at its own create time — this tool does not hand-write the array in cloud mode',
|
|
2677
|
+
},
|
|
2678
|
+
milestone_backlink: {
|
|
2679
|
+
milestone: milestoneResolved.basename,
|
|
2680
|
+
by: 'console (createSpec KIND_LINKAGE.epic.parentArray = "epics")',
|
|
2681
|
+
},
|
|
2682
|
+
roadmap_registration: roadmapRegistration,
|
|
2683
|
+
...(skipRoadmapRegistration
|
|
2684
|
+
? {}
|
|
2685
|
+
: { cloud_mode_unsupported_writes: [CLOUD_UNSUPPORTED_ROADMAP_WRITE] }),
|
|
2686
|
+
validate_vault_ids: null,
|
|
2687
|
+
validate_message: CLOUD_VALIDATE_SKIPPED,
|
|
2688
|
+
next_steps: [
|
|
2689
|
+
roadmapRegistration.success
|
|
2690
|
+
? `Roadmap registration: ${roadmapRegistration.message}`
|
|
2691
|
+
: `⚠️ Roadmap registration has NO cloud path — add "${epicBasename}" to ${roadmapPhase} of ${roadmapId} by hand, or the epic will not appear in the kanban Review column`,
|
|
2692
|
+
'Human reviews the epic and child tasks',
|
|
2693
|
+
'Move to status: closed after verification',
|
|
2694
|
+
'Record the WIP ledger (heyspex_record_wip) for the epic if needed (next-session-handoff.md retired — HS-EPIC-HNDR)',
|
|
2695
|
+
],
|
|
2696
|
+
};
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
/** Internals exposed for tests only. */
|
|
2700
|
+
export const __testables = {
|
|
2701
|
+
CLOUD_UNSUPPORTED_ROADMAP_WRITE,
|
|
2702
|
+
CLOUD_VALIDATE_SKIPPED,
|
|
2703
|
+
detectSpecConflictsViaConsole,
|
|
2704
|
+
normalizeRelatedWikilinks,
|
|
2705
|
+
pushPlanViaConsole,
|
|
2706
|
+
readSpecViaConsole,
|
|
2707
|
+
resolveCloudDeps,
|
|
2708
|
+
resolveMilestoneArgViaConsole,
|
|
2709
|
+
resolveWriteModeForPlanPush,
|
|
2710
|
+
shipEpicViaConsole,
|
|
2711
|
+
};
|