supipowers 1.5.3 → 2.0.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 +14 -8
- package/bin/install.mjs +20 -5
- package/bin/install.ts +95 -0
- package/package.json +8 -4
- package/skills/context-mode/SKILL.md +17 -10
- package/skills/harness/SKILL.md +94 -0
- package/skills/ui-design/SKILL.md +63 -0
- package/skills/ui-design/sub-agent-templates/component-builder.md +29 -0
- package/skills/ui-design/sub-agent-templates/design-critic.md +46 -0
- package/skills/ui-design/sub-agent-templates/pencil/component-builder.md +29 -0
- package/skills/ui-design/sub-agent-templates/pencil/design-critic.md +42 -0
- package/skills/ui-design/sub-agent-templates/pencil/section-assembler.md +27 -0
- package/skills/ui-design/sub-agent-templates/section-assembler.md +27 -0
- package/skills/ultraplan-discover/SKILL.md +96 -0
- package/skills/ultraplan-intake/SKILL.md +89 -0
- package/skills/ultraplan-research/SKILL.md +129 -0
- package/skills/ultraplan-review/SKILL.md +86 -0
- package/skills/ultraplan-review-scope/SKILL.md +111 -0
- package/skills/ultraplan-review-structure/SKILL.md +120 -0
- package/skills/ultraplan-review-tdd/SKILL.md +142 -0
- package/skills/ultraplan-scout/SKILL.md +110 -0
- package/skills/ultraplan-synthesize/SKILL.md +124 -0
- package/src/{quality/ai-session.ts → ai/final-message.ts} +27 -0
- package/src/ai/schema-text.ts +129 -0
- package/src/ai/structured-output.ts +274 -0
- package/src/ai/template.ts +27 -0
- package/src/bootstrap.ts +63 -28
- package/src/commands/agents.ts +131 -42
- package/src/commands/ai-review.ts +251 -30
- package/src/commands/clear.ts +434 -0
- package/src/commands/commit.ts +1 -0
- package/src/commands/config.ts +242 -44
- package/src/commands/context.ts +55 -28
- package/src/commands/doctor.ts +234 -6
- package/src/commands/fix-pr.ts +306 -131
- package/src/commands/generate.ts +111 -21
- package/src/commands/memory.ts +192 -0
- package/src/commands/model-picker.ts +28 -21
- package/src/commands/model.ts +18 -8
- package/src/commands/optimize-context.ts +408 -29
- package/src/commands/plan.ts +2 -0
- package/src/commands/qa.ts +312 -137
- package/src/commands/release.ts +259 -76
- package/src/commands/review.ts +293 -59
- package/src/commands/status.ts +200 -13
- package/src/commands/supi.ts +3 -35
- package/src/commands/ui-design.ts +394 -0
- package/src/commands/ultraplan.ts +1518 -0
- package/src/commands/update.ts +86 -0
- package/src/config/defaults.ts +62 -0
- package/src/config/loader.ts +448 -60
- package/src/config/schema.ts +108 -2
- package/src/context/optimizer.ts +25 -33
- package/src/context/rule-renderer.ts +223 -0
- package/src/context/savings.ts +258 -0
- package/src/context/startup-check.ts +380 -0
- package/src/context/startup-optimizer.ts +355 -0
- package/src/context/tokenignore.ts +146 -0
- package/src/context-mode/cache-handle.ts +49 -0
- package/src/context-mode/cache-preview.ts +71 -0
- package/src/context-mode/cache-store.ts +738 -0
- package/src/context-mode/compressor.ts +131 -26
- package/src/context-mode/dedup.ts +108 -0
- package/src/context-mode/detector.ts +35 -4
- package/src/context-mode/event-extractor.ts +14 -12
- package/src/context-mode/event-store.ts +91 -36
- package/src/context-mode/hooks.ts +798 -56
- package/src/context-mode/knowledge/store.ts +255 -11
- package/src/context-mode/memory-store.ts +325 -0
- package/src/context-mode/metrics-recorder.ts +158 -0
- package/src/context-mode/metrics-store.ts +765 -0
- package/src/context-mode/model.ts +24 -0
- package/src/context-mode/processor-keys.ts +29 -0
- package/src/context-mode/processors/build.ts +66 -0
- package/src/context-mode/processors/docker.ts +57 -0
- package/src/context-mode/processors/git.ts +111 -0
- package/src/context-mode/processors/json.ts +112 -0
- package/src/context-mode/processors/k8s.ts +67 -0
- package/src/context-mode/processors/lint.ts +67 -0
- package/src/context-mode/processors/log.ts +86 -0
- package/src/context-mode/processors/registry.ts +116 -0
- package/src/context-mode/processors/test-runner.ts +102 -0
- package/src/context-mode/processors/types.ts +20 -0
- package/src/context-mode/repomap.ts +400 -0
- package/src/context-mode/routing.ts +97 -24
- package/src/context-mode/sandbox/runners.ts +5 -1
- package/src/context-mode/snapshot-builder.ts +106 -11
- package/src/context-mode/source-hash.ts +173 -0
- package/src/context-mode/tool-name.ts +11 -0
- package/src/context-mode/tools.ts +654 -22
- package/src/context-mode/web/fetcher.ts +31 -12
- package/src/debug/logger.ts +2 -1
- package/src/deps/registry.ts +1 -1
- package/src/discipline/failure-summarizer.ts +170 -0
- package/src/discipline/failure-taxonomy.ts +131 -0
- package/src/discipline/workflow-invariants.ts +125 -0
- package/src/discovery/index.ts +31 -0
- package/src/discovery/lsp.ts +87 -0
- package/src/discovery/rank.ts +144 -0
- package/src/discovery/sources.ts +89 -0
- package/src/discovery/workflow.ts +87 -0
- package/src/docs/contracts.ts +39 -0
- package/src/docs/drift.ts +117 -87
- package/src/fix-pr/assessment.ts +200 -0
- package/src/fix-pr/contracts.ts +47 -0
- package/src/fix-pr/fetch-comments.ts +80 -0
- package/src/fix-pr/prompt-builder.ts +58 -40
- package/src/fix-pr/scripts/exec.ts +34 -0
- package/src/fix-pr/scripts/trigger-review.ts +106 -0
- package/src/fix-pr/scripts/wait-and-check.ts +108 -0
- package/src/fix-pr/types.ts +4 -0
- package/src/git/branch-finish.ts +5 -0
- package/src/git/commit-contract.ts +83 -0
- package/src/git/commit.ts +121 -184
- package/src/git/status.ts +62 -8
- package/src/harness/anti_slop/architecture-parser.ts +210 -0
- package/src/harness/anti_slop/backend-factory.ts +30 -0
- package/src/harness/anti_slop/backend.ts +140 -0
- package/src/harness/anti_slop/desloppify-adapter.ts +319 -0
- package/src/harness/anti_slop/fallow-adapter.ts +305 -0
- package/src/harness/anti_slop/installer.ts +227 -0
- package/src/harness/anti_slop/queue.ts +216 -0
- package/src/harness/anti_slop/recommend.ts +84 -0
- package/src/harness/anti_slop/score.ts +180 -0
- package/src/harness/anti_slop/synthetic-edit-test.ts +128 -0
- package/src/harness/artifacts/agents-md.ts +88 -0
- package/src/harness/artifacts/checks-wiring.ts +57 -0
- package/src/harness/artifacts/docs-tree.ts +79 -0
- package/src/harness/artifacts/lint-configs.ts +136 -0
- package/src/harness/artifacts/review-agents.ts +67 -0
- package/src/harness/bare-entry.ts +108 -0
- package/src/harness/command.ts +1010 -0
- package/src/harness/default-agents/design.md +23 -0
- package/src/harness/default-agents/discover.md +18 -0
- package/src/harness/default-agents/implement.md +24 -0
- package/src/harness/default-agents/plan.md +19 -0
- package/src/harness/default-agents/research.md +21 -0
- package/src/harness/default-agents/validate.md +22 -0
- package/src/harness/gc/reporter.ts +28 -0
- package/src/harness/gc/runner.ts +136 -0
- package/src/harness/hooks/layer-context-inject.ts +155 -0
- package/src/harness/hooks/post-session-sweep.ts +130 -0
- package/src/harness/hooks/pre-edit-dupe-probe.ts +224 -0
- package/src/harness/hooks/register.ts +118 -0
- package/src/harness/model.ts +117 -0
- package/src/harness/pipeline.ts +348 -0
- package/src/harness/project-paths.ts +235 -0
- package/src/harness/stage-runner.ts +107 -0
- package/src/harness/stages/design.ts +386 -0
- package/src/harness/stages/discover.ts +454 -0
- package/src/harness/stages/implement.ts +162 -0
- package/src/harness/stages/plan.ts +335 -0
- package/src/harness/stages/research.ts +263 -0
- package/src/harness/stages/validate.ts +684 -0
- package/src/harness/storage.ts +467 -0
- package/src/harness/tools.ts +426 -0
- package/src/lsp/bridge.ts +56 -95
- package/src/lsp/capabilities.ts +108 -0
- package/src/lsp/contracts.ts +35 -0
- package/src/lsp/detector.ts +8 -12
- package/src/markdown-frontmatter.ts +68 -0
- package/src/mempalace/bridge.ts +129 -0
- package/src/mempalace/config.ts +75 -0
- package/src/mempalace/format.ts +163 -0
- package/src/mempalace/hooks.ts +370 -0
- package/src/mempalace/installer-helper.ts +194 -0
- package/src/mempalace/python/mempalace_bridge.py +440 -0
- package/src/mempalace/runtime.ts +565 -0
- package/src/mempalace/schema.ts +264 -0
- package/src/mempalace/session-summary.ts +198 -0
- package/src/mempalace/tool.ts +186 -0
- package/src/mempalace/uv.ts +256 -0
- package/src/migrate/runner.ts +354 -0
- package/src/planning/approval-flow.ts +206 -9
- package/src/planning/plan-writer-prompt.ts +4 -3
- package/src/planning/planning-ask-tool.ts +39 -0
- package/src/planning/render-markdown.ts +74 -0
- package/src/planning/spec.ts +42 -0
- package/src/planning/system-prompt.ts +11 -8
- package/src/planning/validate.ts +84 -0
- package/src/platform/omp.ts +15 -2
- package/src/platform/system-prompt.ts +37 -0
- package/src/platform/test-utils.ts +3 -0
- package/src/platform/types.ts +6 -1
- package/src/qa/config.ts +12 -6
- package/src/qa/detect-app-type.ts +13 -6
- package/src/qa/matrix.ts +12 -6
- package/src/qa/prompt-builder.ts +28 -30
- package/src/qa/scripts/dev-server-utils.ts +72 -0
- package/src/qa/scripts/run-e2e-tests.ts +226 -0
- package/src/qa/scripts/start-dev-server.ts +138 -0
- package/src/qa/scripts/stop-dev-server.ts +77 -0
- package/src/qa/session.ts +13 -7
- package/src/quality/ai-setup.ts +27 -25
- package/src/quality/contracts.ts +34 -0
- package/src/quality/gates/ai-review.ts +20 -58
- package/src/quality/gates/command.ts +249 -46
- package/src/quality/review-gates.ts +18 -2
- package/src/quality/runner.ts +63 -22
- package/src/quality/schemas.ts +37 -2
- package/src/quality/setup.ts +96 -16
- package/src/release/changelog.ts +1 -1
- package/src/release/channels/custom.ts +13 -3
- package/src/release/channels/types.ts +5 -0
- package/src/release/contracts.ts +90 -0
- package/src/release/executor.ts +122 -45
- package/src/release/prompt.ts +18 -2
- package/src/release/targets.ts +86 -0
- package/src/release/version.ts +96 -71
- package/src/review/agent-loader.ts +221 -109
- package/src/review/fixer.ts +10 -6
- package/src/review/multi-agent-runner.ts +114 -13
- package/src/review/output.ts +12 -139
- package/src/review/runner.ts +12 -6
- package/src/review/scope.ts +144 -24
- package/src/review/types.ts +1 -20
- package/src/review/validator.ts +12 -6
- package/src/storage/fix-pr-sessions.ts +21 -14
- package/src/storage/plans.ts +14 -5
- package/src/storage/qa-sessions.ts +25 -19
- package/src/storage/reliability-metrics.ts +180 -0
- package/src/storage/reports.ts +8 -7
- package/src/storage/review-sessions.ts +55 -20
- package/src/tool-catalog/active-tool-controller.ts +164 -0
- package/src/tool-catalog/active-tool-planner.ts +212 -0
- package/src/tool-catalog/tool-groups.ts +102 -0
- package/src/types.ts +1399 -5
- package/src/ui-design/backend-adapter.ts +78 -0
- package/src/ui-design/backends/local-html.ts +82 -0
- package/src/ui-design/backends/pencil-mcp.ts +111 -0
- package/src/ui-design/components-scanner.ts +124 -0
- package/src/ui-design/config.ts +55 -0
- package/src/ui-design/pen-scanner.ts +95 -0
- package/src/ui-design/pen-selector.ts +72 -0
- package/src/ui-design/prompt-builder.ts +73 -0
- package/src/ui-design/scanner.ts +136 -0
- package/src/ui-design/session.ts +974 -0
- package/src/ui-design/system-prompt.ts +312 -0
- package/src/ui-design/tokens-scanner.ts +181 -0
- package/src/ui-design/types.ts +96 -0
- package/src/ultraplan/agent-catalog.ts +522 -0
- package/src/ultraplan/authoring/agent-catalog.ts +310 -0
- package/src/ultraplan/authoring/authoring-tools.ts +552 -0
- package/src/ultraplan/authoring/command-handlers.ts +339 -0
- package/src/ultraplan/authoring/markdown.ts +510 -0
- package/src/ultraplan/authoring/model.ts +162 -0
- package/src/ultraplan/authoring/pipeline.ts +319 -0
- package/src/ultraplan/authoring/stage-runner.ts +141 -0
- package/src/ultraplan/authoring/stages/approve.ts +249 -0
- package/src/ultraplan/authoring/stages/discover.ts +289 -0
- package/src/ultraplan/authoring/stages/intake.ts +203 -0
- package/src/ultraplan/authoring/stages/research.ts +399 -0
- package/src/ultraplan/authoring/stages/review.ts +333 -0
- package/src/ultraplan/authoring/stages/scout.ts +188 -0
- package/src/ultraplan/authoring/stages/synthesize.ts +348 -0
- package/src/ultraplan/authoring/storage.ts +594 -0
- package/src/ultraplan/authoring/synth-gate.ts +165 -0
- package/src/ultraplan/authoring-draft.ts +653 -0
- package/src/ultraplan/authoring-persist.ts +180 -0
- package/src/ultraplan/authoring-tool.ts +608 -0
- package/src/ultraplan/authoring-wizard.ts +587 -0
- package/src/ultraplan/batch/merge.ts +98 -0
- package/src/ultraplan/batch/planner.ts +150 -0
- package/src/ultraplan/batch/presenter.ts +97 -0
- package/src/ultraplan/batch/storage.ts +420 -0
- package/src/ultraplan/batch/supervisor.ts +317 -0
- package/src/ultraplan/batch/worker.ts +26 -0
- package/src/ultraplan/batch/worktree.ts +110 -0
- package/src/ultraplan/contracts.ts +1593 -0
- package/src/ultraplan/default-agents/authoring/discoverer.md +12 -0
- package/src/ultraplan/default-agents/authoring/intake.md +12 -0
- package/src/ultraplan/default-agents/authoring/planner.md +12 -0
- package/src/ultraplan/default-agents/authoring/researcher.md +12 -0
- package/src/ultraplan/default-agents/authoring/scope-checker.md +12 -0
- package/src/ultraplan/default-agents/authoring/scout.md +12 -0
- package/src/ultraplan/default-agents/authoring/structure-checker.md +12 -0
- package/src/ultraplan/default-agents/authoring/tdd-checker.md +12 -0
- package/src/ultraplan/default-agents/backend-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/backend-executor.md +10 -0
- package/src/ultraplan/default-agents/backend-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/backend-tester.md +10 -0
- package/src/ultraplan/default-agents/frontend-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/frontend-executor.md +10 -0
- package/src/ultraplan/default-agents/frontend-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/frontend-tester.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-executor.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-tester.md +10 -0
- package/src/ultraplan/execution/contract.ts +71 -0
- package/src/ultraplan/execution/policy.ts +217 -0
- package/src/ultraplan/execution/runtime-tools.ts +107 -0
- package/src/ultraplan/execution/session-runner.ts +281 -0
- package/src/ultraplan/next-router.ts +85 -0
- package/src/ultraplan/presenter.ts +359 -0
- package/src/ultraplan/project-paths.ts +342 -0
- package/src/ultraplan/runtime/active-execution.ts +72 -0
- package/src/ultraplan/runtime/apply-mutation.ts +416 -0
- package/src/ultraplan/runtime/blockers.ts +243 -0
- package/src/ultraplan/runtime/hook-bridge.ts +486 -0
- package/src/ultraplan/runtime/launch-context.ts +207 -0
- package/src/ultraplan/runtime/migration.ts +524 -0
- package/src/ultraplan/runtime/normalize.ts +281 -0
- package/src/ultraplan/runtime/proof.ts +260 -0
- package/src/ultraplan/runtime/reducer.ts +416 -0
- package/src/ultraplan/runtime/repair.ts +251 -0
- package/src/ultraplan/runtime/tracker-storage.ts +368 -0
- package/src/ultraplan/session-selection.ts +291 -0
- package/src/ultraplan/storage.ts +374 -0
- package/src/utils/editor.ts +38 -0
- package/src/utils/executable.ts +80 -0
- package/src/utils/paths.ts +1 -20
- package/src/utils/shell.ts +31 -0
- package/src/visual/companion.ts +2 -1
- package/src/visual/scripts/frame-template.html +60 -0
- package/src/visual/scripts/index.js +59 -13
- package/src/visual/scripts/package.json +3 -0
- package/src/visual/start-server.ts +2 -1
- package/src/workspace/git-scope.ts +64 -0
- package/src/workspace/locks.ts +23 -0
- package/src/workspace/package-manager.ts +117 -0
- package/src/workspace/path-mapping.ts +75 -0
- package/src/workspace/project-slug.ts +92 -0
- package/src/workspace/repo-root.ts +137 -0
- package/src/workspace/selector.ts +115 -0
- package/src/workspace/state-paths.ts +118 -0
- package/src/workspace/targets.ts +313 -0
- package/src/fix-pr/scripts/diff-comments.sh +0 -33
- package/src/fix-pr/scripts/fetch-pr-comments.sh +0 -25
- package/src/fix-pr/scripts/trigger-review.sh +0 -36
- package/src/fix-pr/scripts/wait-and-check.sh +0 -37
- package/src/qa/scripts/detect-app-type.sh +0 -68
- package/src/qa/scripts/discover-routes.sh +0 -143
- package/src/qa/scripts/run-e2e-tests.sh +0 -131
- package/src/qa/scripts/start-dev-server.sh +0 -46
- package/src/qa/scripts/stop-dev-server.sh +0 -36
- package/src/review/prompts/fix-output-schema.md +0 -18
- package/src/review/prompts/review-output-schema.md +0 -38
- package/src/review/template.ts +0 -15
- /package/src/{review → ai}/prompts/invalid-output-retry.md +0 -0
|
@@ -0,0 +1,1518 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type {
|
|
5
|
+
UltraPlanBatchActiveRunLease,
|
|
6
|
+
UltraPlanBatchNode,
|
|
7
|
+
UltraPlanBatchRun,
|
|
8
|
+
UltraPlanManifest,
|
|
9
|
+
UltraPlanSessionSummary,
|
|
10
|
+
} from "../types.js";
|
|
11
|
+
import type { Platform } from "../platform/types.js";
|
|
12
|
+
import { notifyError, notifyInfo, notifyWarning } from "../notifications/renderer.js";
|
|
13
|
+
import {
|
|
14
|
+
buildUltraPlanPickerOptions,
|
|
15
|
+
renderUltraPlanRecommendationStatusLine,
|
|
16
|
+
renderUltraPlanRecommendationSummary,
|
|
17
|
+
renderUltraPlanRunOutcome,
|
|
18
|
+
renderUltraPlanStatus,
|
|
19
|
+
} from "../ultraplan/presenter.js";
|
|
20
|
+
import {
|
|
21
|
+
renderUltraPlanBatchNodeSummary,
|
|
22
|
+
renderUltraPlanBatchSummary,
|
|
23
|
+
} from "../ultraplan/batch/presenter.js";
|
|
24
|
+
import {
|
|
25
|
+
rankUltraPlanVisibleSessions,
|
|
26
|
+
type UltraPlanSessionRecommendation,
|
|
27
|
+
} from "../ultraplan/next-router.js";
|
|
28
|
+
import {
|
|
29
|
+
getUltraPlanIdleReasonLabel,
|
|
30
|
+
resolveUltraPlanCurrentCursor,
|
|
31
|
+
resolveUltraPlanSessionBucket,
|
|
32
|
+
type UltraPlanVisibleSession,
|
|
33
|
+
} from "../ultraplan/session-selection.js";
|
|
34
|
+
import { ULTRAPLAN_AUTHORED_JSON_FILENAME } from "../ultraplan/project-paths.js";
|
|
35
|
+
import {
|
|
36
|
+
loadUltraPlanAuthoredArtifact,
|
|
37
|
+
loadUltraPlanIndex,
|
|
38
|
+
loadUltraPlanManifest,
|
|
39
|
+
loadUltraPlanSessionSummary,
|
|
40
|
+
} from "../ultraplan/storage.js";
|
|
41
|
+
import {
|
|
42
|
+
acquireUltraPlanBatchActiveRunLease,
|
|
43
|
+
appendUltraPlanBatchJournalEvent,
|
|
44
|
+
loadUltraPlanActiveBatchRun,
|
|
45
|
+
loadUltraPlanBatchActiveRunLease,
|
|
46
|
+
loadUltraPlanBatchRun,
|
|
47
|
+
releaseUltraPlanBatchActiveRunLease,
|
|
48
|
+
saveUltraPlanBatchRun,
|
|
49
|
+
} from "../ultraplan/batch/storage.js";
|
|
50
|
+
import { mergeUltraPlanBatchWorktree } from "../ultraplan/batch/merge.js";
|
|
51
|
+
import { runUltraPlanBatchWorker } from "../ultraplan/batch/worker.js";
|
|
52
|
+
import { prepareUltraPlanBatchWorktree } from "../ultraplan/batch/worktree.js";
|
|
53
|
+
import { computeUltraPlanBatchEligibleFrontier } from "../ultraplan/batch/planner.js";
|
|
54
|
+
import {
|
|
55
|
+
abandonUltraPlanBatchNode,
|
|
56
|
+
abandonUltraPlanBatchRun,
|
|
57
|
+
resumeUltraPlanBatchSupervisor,
|
|
58
|
+
runUltraPlanBatchSupervisor,
|
|
59
|
+
type UltraPlanBatchSupervisorDeps,
|
|
60
|
+
} from "../ultraplan/batch/supervisor.js";
|
|
61
|
+
import { resolveSessionMigration } from "../ultraplan/runtime/migration.js";
|
|
62
|
+
import { runUltraPlanSession } from "../ultraplan/execution/session-runner.js";
|
|
63
|
+
import { detectBaseBranch } from "../git/base-branch.js";
|
|
64
|
+
import { resolveRepoIdentityRootFromFs, resolveRepoRoot } from "../workspace/repo-root.js";
|
|
65
|
+
import {
|
|
66
|
+
driveAuthoringPipeline,
|
|
67
|
+
handleBareEntry as handleAuthoringBareEntry,
|
|
68
|
+
handlePlan,
|
|
69
|
+
handleResume,
|
|
70
|
+
handleStageSubcommand,
|
|
71
|
+
} from "../ultraplan/authoring/command-handlers.js";
|
|
72
|
+
|
|
73
|
+
const SUBCOMMANDS = [
|
|
74
|
+
{ name: "plan", description: "Start a new multi-stage authoring pipeline (default flow)" },
|
|
75
|
+
{ name: "discover", description: "Run/advance the discover stage of an authoring session" },
|
|
76
|
+
{ name: "research", description: "Run/advance the research stage of an authoring session" },
|
|
77
|
+
{ name: "synthesize", description: "Run/advance the synthesize stage of an authoring session" },
|
|
78
|
+
{ name: "review", description: "Run/advance the review stage of an authoring session" },
|
|
79
|
+
{ name: "approve", description: "Promote an approved draft to the canonical session" },
|
|
80
|
+
{ name: "resume", description: "Resume an in-flight authoring session" },
|
|
81
|
+
{ name: "quick", description: "Legacy single-shot authoring (deprecated; removed next release)" },
|
|
82
|
+
{ name: "run", description: "Run a session or start/resume a batch" },
|
|
83
|
+
{ name: "status", description: "Inspect status for an existing ultraplan session" },
|
|
84
|
+
{ name: "next", description: "Recommend the next ultraplan session to run" },
|
|
85
|
+
] as const;
|
|
86
|
+
|
|
87
|
+
type UltraPlanSubcommand = (typeof SUBCOMMANDS)[number]["name"];
|
|
88
|
+
|
|
89
|
+
const ULTRAPLAN_RECOMMENDATION_SURFACE_KEY = "supi-ultraplan-next";
|
|
90
|
+
|
|
91
|
+
type VisibleSessionLoadFailure = {
|
|
92
|
+
sessionId: string;
|
|
93
|
+
message: string;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
type VisibleSessionsLoadResult =
|
|
97
|
+
| { kind: "ok"; sessions: UltraPlanVisibleSession[]; failures: VisibleSessionLoadFailure[] }
|
|
98
|
+
| { kind: "missing-index"; message: string }
|
|
99
|
+
| { kind: "invalid-index"; message: string };
|
|
100
|
+
|
|
101
|
+
type SessionPickerSelection = {
|
|
102
|
+
session: UltraPlanVisibleSession;
|
|
103
|
+
recommendation: UltraPlanSessionRecommendation | null;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
type SessionPickerState = {
|
|
107
|
+
orderedSessions: UltraPlanVisibleSession[];
|
|
108
|
+
recommendations: ReadonlyMap<string, UltraPlanSessionRecommendation>;
|
|
109
|
+
topRecommendation: UltraPlanSessionRecommendation | null;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
function parseUltraplanSubcommand(args?: string): UltraPlanSubcommand | null {
|
|
113
|
+
const first = args?.trim().split(/\s+/)[0]?.toLowerCase();
|
|
114
|
+
if (!first) return null;
|
|
115
|
+
return SUBCOMMANDS.some((subcommand) => subcommand.name === first) ? (first as UltraPlanSubcommand) : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildCursorManifest(summary: UltraPlanSessionSummary): UltraPlanManifest {
|
|
119
|
+
return {
|
|
120
|
+
sessionId: summary.sessionId,
|
|
121
|
+
projectName: summary.projectName,
|
|
122
|
+
title: summary.title,
|
|
123
|
+
authored: {
|
|
124
|
+
json: ULTRAPLAN_AUTHORED_JSON_FILENAME,
|
|
125
|
+
},
|
|
126
|
+
state: summary.state,
|
|
127
|
+
cursor: summary.cursor,
|
|
128
|
+
lastCompleted: summary.lastCompleted,
|
|
129
|
+
progress: summary.progress,
|
|
130
|
+
stacks: summary.stacks,
|
|
131
|
+
blocker: summary.blocker,
|
|
132
|
+
reviews: summary.reviews,
|
|
133
|
+
createdAt: summary.createdAt,
|
|
134
|
+
updatedAt: summary.updatedAt,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function formatVisibleSessionFailure(
|
|
139
|
+
sessionId: string,
|
|
140
|
+
error: { message: string; details?: string[] },
|
|
141
|
+
): VisibleSessionLoadFailure {
|
|
142
|
+
const detailLines = error.details?.length ? `\n${error.details.join("\n")}` : "";
|
|
143
|
+
return {
|
|
144
|
+
sessionId,
|
|
145
|
+
message: `${sessionId}: ${error.message}${detailLines}`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
function loadVisibleSessions(
|
|
151
|
+
platform: Platform,
|
|
152
|
+
cwd: string,
|
|
153
|
+
options?: { includeDone?: boolean },
|
|
154
|
+
): VisibleSessionsLoadResult {
|
|
155
|
+
const index = loadUltraPlanIndex(platform.paths, cwd);
|
|
156
|
+
if (!index.ok) {
|
|
157
|
+
return index.error.kind === "missing"
|
|
158
|
+
? { kind: "missing-index", message: index.error.message }
|
|
159
|
+
: { kind: "invalid-index", message: index.error.message };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const includeDone = options?.includeDone ?? false;
|
|
163
|
+
const sessions: UltraPlanVisibleSession[] = [];
|
|
164
|
+
const failures: VisibleSessionLoadFailure[] = [];
|
|
165
|
+
const nowIso = new Date().toISOString();
|
|
166
|
+
|
|
167
|
+
for (const entry of index.value.sessions) {
|
|
168
|
+
const migration = resolveSessionMigration({
|
|
169
|
+
paths: platform.paths,
|
|
170
|
+
cwd,
|
|
171
|
+
sessionId: entry.sessionId,
|
|
172
|
+
nowIso,
|
|
173
|
+
});
|
|
174
|
+
if (migration.kind === "blocked") {
|
|
175
|
+
failures.push(formatVisibleSessionFailure(entry.sessionId, {
|
|
176
|
+
message: migration.blocker.message,
|
|
177
|
+
details: [
|
|
178
|
+
`blocker: ${migration.blocker.code}`,
|
|
179
|
+
`recovery: ${migration.blocker.recoveryMode}`,
|
|
180
|
+
`next action: ${migration.blocker.nextAction}`,
|
|
181
|
+
],
|
|
182
|
+
}));
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (migration.kind === "skip") {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const summary = loadUltraPlanSessionSummary(platform.paths, cwd, entry.sessionId);
|
|
190
|
+
if (!summary.ok) {
|
|
191
|
+
failures.push(formatVisibleSessionFailure(entry.sessionId, summary.error));
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const authored = loadUltraPlanAuthoredArtifact(platform.paths, cwd, entry.sessionId);
|
|
196
|
+
if (!authored.ok) {
|
|
197
|
+
failures.push(formatVisibleSessionFailure(entry.sessionId, authored.error));
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const resolved = resolveUltraPlanCurrentCursor(buildCursorManifest(summary.value), authored.value);
|
|
202
|
+
const session: UltraPlanVisibleSession = {
|
|
203
|
+
...summary.value,
|
|
204
|
+
cursor: resolved.cursor,
|
|
205
|
+
bucket: resolveUltraPlanSessionBucket(summary.value, resolved),
|
|
206
|
+
idleReasonLabel: getUltraPlanIdleReasonLabel(summary.value),
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (includeDone || session.bucket !== "done") {
|
|
210
|
+
sessions.push(session);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { kind: "ok", sessions, failures };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildRankedIncompleteSessions(sessions: UltraPlanVisibleSession[]): {
|
|
218
|
+
sessions: UltraPlanVisibleSession[];
|
|
219
|
+
recommendations: ReadonlyMap<string, UltraPlanSessionRecommendation>;
|
|
220
|
+
} {
|
|
221
|
+
const ranked = rankUltraPlanVisibleSessions(sessions.filter((session) => session.bucket !== "done"));
|
|
222
|
+
return {
|
|
223
|
+
sessions: ranked.map((recommendation) => recommendation.session),
|
|
224
|
+
recommendations: new Map(
|
|
225
|
+
ranked.map((recommendation) => [recommendation.session.sessionId, recommendation] as const),
|
|
226
|
+
),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function sortDoneSessions(sessions: UltraPlanVisibleSession[]): UltraPlanVisibleSession[] {
|
|
231
|
+
return [...sessions].sort((left, right) =>
|
|
232
|
+
left.title.localeCompare(right.title) || left.sessionId.localeCompare(right.sessionId));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function projectRecommendationStatus(
|
|
236
|
+
ctx: any,
|
|
237
|
+
recommendation: UltraPlanSessionRecommendation | null,
|
|
238
|
+
): void {
|
|
239
|
+
const content = recommendation ? renderUltraPlanRecommendationStatusLine(recommendation) : undefined;
|
|
240
|
+
try {
|
|
241
|
+
ctx.ui?.setStatus?.(ULTRAPLAN_RECOMMENDATION_SURFACE_KEY, content);
|
|
242
|
+
} catch {
|
|
243
|
+
// Projection is advisory only; command behavior must continue unchanged.
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
ctx.ui?.setWidget?.(ULTRAPLAN_RECOMMENDATION_SURFACE_KEY, content);
|
|
247
|
+
} catch {
|
|
248
|
+
// Projection is advisory only; command behavior must continue unchanged.
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function loadSessionPickerState(
|
|
253
|
+
platform: Platform,
|
|
254
|
+
ctx: any,
|
|
255
|
+
options?: { includeDone?: boolean },
|
|
256
|
+
): SessionPickerState | null {
|
|
257
|
+
const loaded = loadVisibleSessions(platform, ctx.cwd, options);
|
|
258
|
+
if (loaded.kind === "missing-index") {
|
|
259
|
+
projectRecommendationStatus(ctx, null);
|
|
260
|
+
notifyWarning(ctx, "Ultraplan session index is missing", "The resumable session index is unavailable. Rebuild the index or create a new ultraplan session.");
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (loaded.kind === "invalid-index") {
|
|
265
|
+
projectRecommendationStatus(ctx, null);
|
|
266
|
+
notifyError(ctx, "Ultraplan session index is invalid", loaded.message);
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (loaded.failures.length > 0) {
|
|
271
|
+
notifyWarning(
|
|
272
|
+
ctx,
|
|
273
|
+
"Skipped invalid ultraplan sessions",
|
|
274
|
+
loaded.failures.map((failure) => failure.message).join("\n"),
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const rankedIncomplete = buildRankedIncompleteSessions(loaded.sessions);
|
|
279
|
+
const orderedSessions = options?.includeDone
|
|
280
|
+
? [
|
|
281
|
+
...rankedIncomplete.sessions,
|
|
282
|
+
...sortDoneSessions(loaded.sessions.filter((session) => session.bucket === "done")),
|
|
283
|
+
]
|
|
284
|
+
: rankedIncomplete.sessions;
|
|
285
|
+
const topRecommendation = rankedIncomplete.sessions.length > 0
|
|
286
|
+
? rankedIncomplete.recommendations.get(rankedIncomplete.sessions[0].sessionId) ?? null
|
|
287
|
+
: null;
|
|
288
|
+
projectRecommendationStatus(ctx, topRecommendation);
|
|
289
|
+
if (orderedSessions.length === 0) {
|
|
290
|
+
notifyInfo(
|
|
291
|
+
ctx,
|
|
292
|
+
options?.includeDone ? "No ultraplan sessions" : "No incomplete ultraplan sessions",
|
|
293
|
+
loaded.failures.length > 0
|
|
294
|
+
? "Fix the skipped session artifacts or create a new ultraplan session."
|
|
295
|
+
: options?.includeDone
|
|
296
|
+
? "Create a new ultraplan session in a later phase."
|
|
297
|
+
: "Run authoring in a later phase to create one.",
|
|
298
|
+
);
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
orderedSessions,
|
|
304
|
+
recommendations: rankedIncomplete.recommendations,
|
|
305
|
+
topRecommendation,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function selectSession(
|
|
310
|
+
platform: Platform,
|
|
311
|
+
ctx: any,
|
|
312
|
+
options?: { includeDone?: boolean },
|
|
313
|
+
): Promise<SessionPickerSelection | null> {
|
|
314
|
+
const state = loadSessionPickerState(platform, ctx, options);
|
|
315
|
+
if (!state) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const optionsList = buildUltraPlanPickerOptions(state.orderedSessions, state.recommendations);
|
|
320
|
+
const entries = optionsList.map((option, index) => {
|
|
321
|
+
const session = state.orderedSessions[index];
|
|
322
|
+
const display = `${option.label} — ${option.description}`;
|
|
323
|
+
return [display, { session, recommendation: state.recommendations.get(session.sessionId) ?? null }] as const;
|
|
324
|
+
});
|
|
325
|
+
const displayToSelection = new Map(entries);
|
|
326
|
+
const displayOptions = entries.map(([display]) => display);
|
|
327
|
+
|
|
328
|
+
const selected = await ctx.ui.select("Ultraplan sessions", displayOptions, {
|
|
329
|
+
helpText: "Pick a session · Esc to cancel",
|
|
330
|
+
});
|
|
331
|
+
if (!selected) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return displayToSelection.get(selected) ?? null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function presentSelectedSession(platform: Platform, ctx: any, session: UltraPlanVisibleSession, mode: "run" | "status"): Promise<void> {
|
|
339
|
+
const manifest = loadUltraPlanManifest(platform.paths, ctx.cwd, session.sessionId);
|
|
340
|
+
if (!manifest.ok) {
|
|
341
|
+
notifyError(ctx, "Ultraplan manifest is invalid", manifest.error.message);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const authored = loadUltraPlanAuthoredArtifact(platform.paths, ctx.cwd, session.sessionId);
|
|
346
|
+
if (!authored.ok) {
|
|
347
|
+
notifyError(ctx, "Ultraplan authored.json is invalid", authored.error.message);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const resolved = resolveUltraPlanCurrentCursor(manifest.value, authored.value);
|
|
352
|
+
const statusText = renderUltraPlanStatus(session, authored.value, resolved);
|
|
353
|
+
|
|
354
|
+
notifyInfo(
|
|
355
|
+
ctx,
|
|
356
|
+
mode === "run" ? "Ultraplan session" : "Ultraplan status",
|
|
357
|
+
statusText,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function runSelectedSession(platform: Platform, ctx: any, session: UltraPlanVisibleSession): Promise<void> {
|
|
362
|
+
const outcome = await runUltraPlanSession({
|
|
363
|
+
platform,
|
|
364
|
+
cwd: ctx.cwd,
|
|
365
|
+
sessionId: session.sessionId,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
notifyInfo(
|
|
369
|
+
ctx,
|
|
370
|
+
outcome.kind === "completed" ? "Ultraplan complete" : "Ultraplan paused",
|
|
371
|
+
renderUltraPlanRunOutcome(outcome),
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
type UltraPlanRunBatchState =
|
|
376
|
+
| { kind: "single-or-batch" }
|
|
377
|
+
| { kind: "resume-batch"; run: UltraPlanBatchRun; lease: UltraPlanBatchActiveRunLease }
|
|
378
|
+
| { kind: "invalid-run"; message: string };
|
|
379
|
+
|
|
380
|
+
const BATCH_LEASE_DURATION_MS = 5 * 60 * 1000;
|
|
381
|
+
const BATCH_LEASE_RENEWAL_INTERVAL_MS = Math.max(1_000, Math.floor(BATCH_LEASE_DURATION_MS / 3));
|
|
382
|
+
const BATCH_GIT_TIMEOUT_MS = 120_000;
|
|
383
|
+
|
|
384
|
+
function resolveUltraPlanRunBatchState(
|
|
385
|
+
input: { paths: Platform["paths"]; cwd: string },
|
|
386
|
+
): UltraPlanRunBatchState {
|
|
387
|
+
const lease = loadUltraPlanBatchActiveRunLease(input.paths, input.cwd);
|
|
388
|
+
if (!lease.ok) {
|
|
389
|
+
return { kind: "invalid-run", message: `invalid-run: ${lease.error.message}` };
|
|
390
|
+
}
|
|
391
|
+
if (lease.value === null) {
|
|
392
|
+
return { kind: "single-or-batch" };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const run = loadUltraPlanActiveBatchRun(input.paths, input.cwd);
|
|
396
|
+
if (!run.ok || run.value === null) {
|
|
397
|
+
return {
|
|
398
|
+
kind: "invalid-run",
|
|
399
|
+
message: `invalid-run: ${run.ok ? "active batch run is missing" : run.error.message}` ,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
if (run.value.state === "complete" || run.value.state === "abandoned") {
|
|
403
|
+
return {
|
|
404
|
+
kind: "invalid-run",
|
|
405
|
+
message: `invalid-run: active-run.json points at terminal batch ${run.value.runId}` ,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return { kind: "resume-batch", run: run.value, lease: lease.value };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function deriveBatchWaves(nodes: UltraPlanBatchNode[]): UltraPlanBatchRun["waves"] {
|
|
413
|
+
return [...new Set(nodes.map((node) => node.waveIndex))]
|
|
414
|
+
.sort((left, right) => left - right)
|
|
415
|
+
.map((waveIndex) => ({
|
|
416
|
+
waveIndex,
|
|
417
|
+
sessionIds: nodes.filter((node) => node.waveIndex === waveIndex).map((node) => node.sessionId),
|
|
418
|
+
}));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function cloneBatchRun(run: UltraPlanBatchRun): UltraPlanBatchRun {
|
|
422
|
+
return {
|
|
423
|
+
...run,
|
|
424
|
+
nodes: run.nodes.map((node) => ({ ...node })),
|
|
425
|
+
waves: run.waves.map((wave) => ({ ...wave, sessionIds: [...wave.sessionIds] })),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function buildBatchRun(
|
|
430
|
+
input: { paths: Platform["paths"]; cwd: string; sessionIds: string[]; maxParallelism: number },
|
|
431
|
+
): UltraPlanBatchRun {
|
|
432
|
+
const nowIso = new Date().toISOString();
|
|
433
|
+
const runId = `batch-${Math.random().toString(36).slice(2, 10)}`;
|
|
434
|
+
const nodes = input.sessionIds.map((sessionId, index) => ({
|
|
435
|
+
nodeId: `node-${index + 1}` ,
|
|
436
|
+
sessionId,
|
|
437
|
+
title: sessionId,
|
|
438
|
+
waveIndex: 0,
|
|
439
|
+
dependencies: [],
|
|
440
|
+
state: "pending" as const,
|
|
441
|
+
blockerKind: null,
|
|
442
|
+
blockerSummary: null,
|
|
443
|
+
resumeRequestedAt: null,
|
|
444
|
+
branchName: null,
|
|
445
|
+
worktreePath: null,
|
|
446
|
+
updatedAt: nowIso,
|
|
447
|
+
}));
|
|
448
|
+
return {
|
|
449
|
+
runId,
|
|
450
|
+
projectRoot: input.cwd,
|
|
451
|
+
baseBranch: "main",
|
|
452
|
+
baseHead: "sha-base",
|
|
453
|
+
currentBaseHead: "sha-base",
|
|
454
|
+
createdAt: nowIso,
|
|
455
|
+
updatedAt: nowIso,
|
|
456
|
+
state: "paused",
|
|
457
|
+
maxParallelism: input.maxParallelism,
|
|
458
|
+
batchBlockerCode: null,
|
|
459
|
+
batchBlockerSummary: null,
|
|
460
|
+
batchResumeRequestedAt: null,
|
|
461
|
+
supervisorWorktreePath: input.cwd,
|
|
462
|
+
waves: deriveBatchWaves(nodes),
|
|
463
|
+
nodes,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function persistPlannedBatchRun(
|
|
468
|
+
input: { paths: Platform["paths"]; cwd: string; run: UltraPlanBatchRun },
|
|
469
|
+
): UltraPlanBatchRun {
|
|
470
|
+
saveBatchRunOrThrow(input.paths, input.cwd, input.run);
|
|
471
|
+
return input.run;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function buildLiveBatchRun(
|
|
475
|
+
input: { platform: Platform; cwd: string; sessionIds: string[]; maxParallelism: number },
|
|
476
|
+
): Promise<UltraPlanBatchRun> {
|
|
477
|
+
const repoRoot = await resolveRepoRoot(input.platform, input.cwd);
|
|
478
|
+
const baseBranch = await detectBaseBranch((cmd, args) => input.platform.exec(cmd, args, { cwd: repoRoot }));
|
|
479
|
+
const baseHead = await readGitHead(input.platform, repoRoot);
|
|
480
|
+
const nowIso = new Date().toISOString();
|
|
481
|
+
const runId = `batch-${Math.random().toString(36).slice(2, 10)}`;
|
|
482
|
+
const nodes = input.sessionIds.map((sessionId, index) => {
|
|
483
|
+
const summary = loadUltraPlanSessionSummary(input.platform.paths, input.cwd, sessionId);
|
|
484
|
+
if (!summary.ok) {
|
|
485
|
+
throw new Error(summary.error.message);
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
nodeId: `node-${index + 1}` ,
|
|
489
|
+
sessionId,
|
|
490
|
+
title: summary.value.title,
|
|
491
|
+
waveIndex: 0,
|
|
492
|
+
dependencies: [],
|
|
493
|
+
state: "pending" as const,
|
|
494
|
+
blockerKind: null,
|
|
495
|
+
blockerSummary: null,
|
|
496
|
+
resumeRequestedAt: null,
|
|
497
|
+
branchName: null,
|
|
498
|
+
worktreePath: null,
|
|
499
|
+
updatedAt: nowIso,
|
|
500
|
+
};
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
runId,
|
|
505
|
+
projectRoot: repoRoot,
|
|
506
|
+
baseBranch,
|
|
507
|
+
baseHead,
|
|
508
|
+
currentBaseHead: baseHead,
|
|
509
|
+
createdAt: nowIso,
|
|
510
|
+
updatedAt: nowIso,
|
|
511
|
+
state: "paused",
|
|
512
|
+
maxParallelism: input.maxParallelism,
|
|
513
|
+
batchBlockerCode: null,
|
|
514
|
+
batchBlockerSummary: null,
|
|
515
|
+
batchResumeRequestedAt: null,
|
|
516
|
+
supervisorWorktreePath: repoRoot,
|
|
517
|
+
waves: deriveBatchWaves(nodes),
|
|
518
|
+
nodes,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function readGitHead(platform: Platform, cwd: string): Promise<string> {
|
|
523
|
+
const result = await platform.exec("git", ["rev-parse", "HEAD"], { cwd, timeout: BATCH_GIT_TIMEOUT_MS });
|
|
524
|
+
const head = firstNonEmpty(result.stdout);
|
|
525
|
+
if (result.code !== 0 || !head) {
|
|
526
|
+
throw new Error(firstNonEmpty(result.stderr, result.stdout) ?? `Unable to resolve git HEAD for ${cwd}`);
|
|
527
|
+
}
|
|
528
|
+
return head;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function saveBatchRunOrThrow(paths: Platform["paths"], cwd: string, run: UltraPlanBatchRun): void {
|
|
532
|
+
run.waves = deriveBatchWaves(run.nodes);
|
|
533
|
+
const saved = saveUltraPlanBatchRun(paths, cwd, run);
|
|
534
|
+
if (!saved.ok) {
|
|
535
|
+
throw new Error(saved.error.message);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
function appendBatchJournalEventOrThrow(
|
|
541
|
+
paths: Platform["paths"],
|
|
542
|
+
cwd: string,
|
|
543
|
+
runId: string,
|
|
544
|
+
event: Parameters<typeof appendUltraPlanBatchJournalEvent>[3],
|
|
545
|
+
): void {
|
|
546
|
+
const appended = appendUltraPlanBatchJournalEvent(paths, cwd, runId, event);
|
|
547
|
+
if (!appended.ok) {
|
|
548
|
+
throw new Error(appended.error.message);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function makeBatchLease(runId: string, ownerSessionId: string, nowIso: string): UltraPlanBatchActiveRunLease {
|
|
553
|
+
return {
|
|
554
|
+
runId,
|
|
555
|
+
ownerSessionId,
|
|
556
|
+
leaseAcquiredAt: nowIso,
|
|
557
|
+
leaseExpiresAt: new Date(Date.parse(nowIso) + BATCH_LEASE_DURATION_MS).toISOString(),
|
|
558
|
+
updatedAt: nowIso,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
type BatchLeaseRenewalController = {
|
|
563
|
+
stop(): void;
|
|
564
|
+
assertHealthy(): void;
|
|
565
|
+
runWithRenewal<T>(work: Promise<T>): Promise<T>;
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
function acquireBatchLeaseOrThrow(
|
|
569
|
+
input: { paths: Platform["paths"]; cwd: string; runId: string; ownerSessionId: string; nowIso: string },
|
|
570
|
+
): UltraPlanBatchActiveRunLease {
|
|
571
|
+
const lease = makeBatchLease(input.runId, input.ownerSessionId, input.nowIso);
|
|
572
|
+
const acquired = acquireUltraPlanBatchActiveRunLease(input.paths, input.cwd, lease, { nowIso: input.nowIso });
|
|
573
|
+
if (!acquired.ok) {
|
|
574
|
+
throw new Error(acquired.error.message);
|
|
575
|
+
}
|
|
576
|
+
return acquired.value;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function startBatchLeaseRenewal(
|
|
580
|
+
input: { paths: Platform["paths"]; cwd: string; runId: string; ownerSessionId: string },
|
|
581
|
+
): BatchLeaseRenewalController {
|
|
582
|
+
let renewalError: Error | null = null;
|
|
583
|
+
let rejectFailure: ((error: Error) => void) | null = null;
|
|
584
|
+
const failure = new Promise<never>((_, reject) => { rejectFailure = reject; });
|
|
585
|
+
failure.catch(() => undefined);
|
|
586
|
+
|
|
587
|
+
function renew(): void {
|
|
588
|
+
acquireBatchLeaseOrThrow({
|
|
589
|
+
...input,
|
|
590
|
+
nowIso: new Date().toISOString(),
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const timer = setInterval(() => {
|
|
595
|
+
try {
|
|
596
|
+
renew();
|
|
597
|
+
} catch (error) {
|
|
598
|
+
renewalError = error instanceof Error ? error : new Error("UltraPlan batch lease renewal failed.");
|
|
599
|
+
clearInterval(timer);
|
|
600
|
+
rejectFailure?.(renewalError);
|
|
601
|
+
}
|
|
602
|
+
}, BATCH_LEASE_RENEWAL_INTERVAL_MS);
|
|
603
|
+
(timer as { unref?: () => void }).unref?.();
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
stop() {
|
|
607
|
+
clearInterval(timer);
|
|
608
|
+
},
|
|
609
|
+
assertHealthy() {
|
|
610
|
+
if (renewalError) {
|
|
611
|
+
throw renewalError;
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
async runWithRenewal<T>(work: Promise<T>): Promise<T> {
|
|
615
|
+
const result = await Promise.race([work, failure]);
|
|
616
|
+
this.assertHealthy();
|
|
617
|
+
return result;
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function makeBatchOwnerSessionId(): string {
|
|
623
|
+
return `ultraplan-batch-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function isLiveBatchLease(lease: UltraPlanBatchActiveRunLease, nowIso: string): boolean {
|
|
627
|
+
if (!lease.ownerSessionId || !lease.leaseAcquiredAt || !lease.leaseExpiresAt) {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
const expiresAt = Date.parse(lease.leaseExpiresAt);
|
|
631
|
+
const now = Date.parse(nowIso);
|
|
632
|
+
return Number.isFinite(expiresAt) && Number.isFinite(now) && expiresAt > now;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function mapBatchNodeStateToJournalType(
|
|
636
|
+
state: UltraPlanBatchNode["state"],
|
|
637
|
+
): Parameters<typeof appendUltraPlanBatchJournalEvent>[3]["type"] | null {
|
|
638
|
+
switch (state) {
|
|
639
|
+
case "preparing":
|
|
640
|
+
return "node-preparing";
|
|
641
|
+
case "running":
|
|
642
|
+
return "node-running";
|
|
643
|
+
case "paused":
|
|
644
|
+
return "node-paused";
|
|
645
|
+
case "blocked":
|
|
646
|
+
return "node-blocked";
|
|
647
|
+
case "awaiting-user":
|
|
648
|
+
return "node-awaiting-user";
|
|
649
|
+
case "merge-pending":
|
|
650
|
+
return "node-merge-pending";
|
|
651
|
+
case "merged":
|
|
652
|
+
return "node-merged";
|
|
653
|
+
case "abandoned":
|
|
654
|
+
return "node-abandoned";
|
|
655
|
+
default:
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function appendBatchNodeTransitionEvents(
|
|
661
|
+
paths: Platform["paths"],
|
|
662
|
+
cwd: string,
|
|
663
|
+
previous: UltraPlanBatchRun,
|
|
664
|
+
next: UltraPlanBatchRun,
|
|
665
|
+
): void {
|
|
666
|
+
const previousNodes = new Map(previous.nodes.map((node) => [node.sessionId, node] as const));
|
|
667
|
+
for (const node of next.nodes) {
|
|
668
|
+
const previousNode = previousNodes.get(node.sessionId);
|
|
669
|
+
const eventType = mapBatchNodeStateToJournalType(node.state);
|
|
670
|
+
if (eventType && previousNode?.state !== node.state) {
|
|
671
|
+
appendBatchJournalEventOrThrow(paths, cwd, next.runId, {
|
|
672
|
+
runId: next.runId,
|
|
673
|
+
sessionId: node.sessionId,
|
|
674
|
+
type: eventType,
|
|
675
|
+
recordedAt: next.updatedAt,
|
|
676
|
+
summary: renderUltraPlanBatchNodeSummary(node, next),
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
if (node.state === "merged" && node.worktreePath !== null && previousNode?.worktreePath !== node.worktreePath) {
|
|
680
|
+
appendBatchJournalEventOrThrow(paths, cwd, next.runId, {
|
|
681
|
+
runId: next.runId,
|
|
682
|
+
sessionId: node.sessionId,
|
|
683
|
+
type: "cleanup-warning",
|
|
684
|
+
recordedAt: next.updatedAt,
|
|
685
|
+
summary: `Merged ${node.sessionId} but kept worktree ${node.worktreePath} for manual cleanup.`,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
async function ensureBatchNodeWorktree(
|
|
692
|
+
platform: Platform,
|
|
693
|
+
run: UltraPlanBatchRun,
|
|
694
|
+
node: UltraPlanBatchNode,
|
|
695
|
+
): Promise<{ kind: "ready"; branchName: string; worktreePath: string } | { kind: "blocked"; summary: string }> {
|
|
696
|
+
const preparation = prepareUltraPlanBatchWorktree({
|
|
697
|
+
repoRoot: run.projectRoot,
|
|
698
|
+
runId: run.runId,
|
|
699
|
+
sessionId: node.sessionId,
|
|
700
|
+
globalWorktreesRoot: platform.paths.global("worktrees"),
|
|
701
|
+
deps: { readBranchName: readGitBranchNameSync },
|
|
702
|
+
});
|
|
703
|
+
if (preparation.kind === "blocked") {
|
|
704
|
+
return { kind: "blocked", summary: preparation.summary };
|
|
705
|
+
}
|
|
706
|
+
if (preparation.kind === "reused") {
|
|
707
|
+
return { kind: "ready", branchName: preparation.branchName, worktreePath: preparation.worktreePath };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const branchExists = await gitBranchExists(platform, run.projectRoot, preparation.branchName);
|
|
711
|
+
const args = branchExists
|
|
712
|
+
? ["worktree", "add", preparation.worktreePath, preparation.branchName]
|
|
713
|
+
: ["worktree", "add", "-b", preparation.branchName, preparation.worktreePath, run.currentBaseHead];
|
|
714
|
+
const created = await platform.exec("git", args, { cwd: run.projectRoot, timeout: BATCH_GIT_TIMEOUT_MS });
|
|
715
|
+
if (created.code !== 0) {
|
|
716
|
+
return {
|
|
717
|
+
kind: "blocked",
|
|
718
|
+
summary: firstNonEmpty(created.stderr, created.stdout) ?? `Unable to create worktree ${preparation.worktreePath}.`,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return { kind: "ready", branchName: preparation.branchName, worktreePath: preparation.worktreePath };
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function gitBranchExists(platform: Platform, cwd: string, branchName: string): Promise<boolean> {
|
|
726
|
+
const result = await platform.exec("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], {
|
|
727
|
+
cwd,
|
|
728
|
+
timeout: BATCH_GIT_TIMEOUT_MS,
|
|
729
|
+
});
|
|
730
|
+
return result.code === 0;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function readGitBranchNameSync(worktreePath: string): string | null {
|
|
734
|
+
const result = runGitSync(worktreePath, ["branch", "--show-current"]);
|
|
735
|
+
return result.code === 0 ? firstNonEmpty(result.stdout) : null;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function inspectSupervisorWorktreeSync(supervisorWorktreePath: string) {
|
|
739
|
+
const branchResult = runGitSync(supervisorWorktreePath, ["branch", "--show-current"]);
|
|
740
|
+
const headResult = runGitSync(supervisorWorktreePath, ["rev-parse", "HEAD"]);
|
|
741
|
+
const dirtyResult = runGitSync(supervisorWorktreePath, ["status", "--porcelain", "--untracked-files=no"]);
|
|
742
|
+
const gitDirResult = runGitSync(supervisorWorktreePath, ["rev-parse", "--git-dir"]);
|
|
743
|
+
const gitDir = gitDirResult.code === 0 && firstNonEmpty(gitDirResult.stdout)
|
|
744
|
+
? path.resolve(supervisorWorktreePath, firstNonEmpty(gitDirResult.stdout)!)
|
|
745
|
+
: null;
|
|
746
|
+
const inProgressOperation = gitDir !== null && [
|
|
747
|
+
"MERGE_HEAD",
|
|
748
|
+
"CHERRY_PICK_HEAD",
|
|
749
|
+
"REVERT_HEAD",
|
|
750
|
+
"REBASE_HEAD",
|
|
751
|
+
"rebase-merge",
|
|
752
|
+
"rebase-apply",
|
|
753
|
+
].some((candidate) => fs.existsSync(path.join(gitDir, candidate)));
|
|
754
|
+
return {
|
|
755
|
+
headAttached: branchResult.code === 0 && firstNonEmpty(branchResult.stdout) !== "HEAD",
|
|
756
|
+
branchName: branchResult.code === 0 ? firstNonEmpty(branchResult.stdout) : null,
|
|
757
|
+
dirtyTracked: dirtyResult.code === 0 && firstNonEmpty(dirtyResult.stdout) !== null,
|
|
758
|
+
inProgressOperation,
|
|
759
|
+
headSha: firstNonEmpty(headResult.stdout) ?? "unknown",
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function mergeBranchSync(
|
|
764
|
+
supervisorWorktreePath: string,
|
|
765
|
+
branchName: string,
|
|
766
|
+
): { ok: true; newBaseHead: string } | { ok: false; summary: string } {
|
|
767
|
+
const merge = runGitSync(supervisorWorktreePath, ["merge", "--no-edit", branchName]);
|
|
768
|
+
if (merge.code !== 0) {
|
|
769
|
+
const abort = runGitSync(supervisorWorktreePath, ["merge", "--abort"]);
|
|
770
|
+
const summary = abort.code === 0
|
|
771
|
+
? firstNonEmpty(merge.stderr, merge.stdout)
|
|
772
|
+
: firstNonEmpty(
|
|
773
|
+
merge.stderr,
|
|
774
|
+
merge.stdout,
|
|
775
|
+
abort.stderr,
|
|
776
|
+
abort.stdout,
|
|
777
|
+
`Merge failed for ${branchName} and merge --abort did not cleanly recover the supervisor worktree.`,
|
|
778
|
+
);
|
|
779
|
+
return { ok: false, summary: summary ?? `Merge failed for ${branchName}.` };
|
|
780
|
+
}
|
|
781
|
+
const head = runGitSync(supervisorWorktreePath, ["rev-parse", "HEAD"]);
|
|
782
|
+
const newBaseHead = firstNonEmpty(head.stdout);
|
|
783
|
+
if (head.code !== 0 || !newBaseHead) {
|
|
784
|
+
return { ok: false, summary: "Merged branch but could not resolve the updated supervisor HEAD." };
|
|
785
|
+
}
|
|
786
|
+
return { ok: true, newBaseHead };
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function cleanupWorktreeSync(
|
|
790
|
+
repoRoot: string,
|
|
791
|
+
worktreePath: string,
|
|
792
|
+
): { ok: true } | { ok: false; summary: string } {
|
|
793
|
+
const cleanup = runGitSync(repoRoot, ["worktree", "remove", "--force", worktreePath]);
|
|
794
|
+
if (cleanup.code !== 0) {
|
|
795
|
+
return { ok: false, summary: firstNonEmpty(cleanup.stderr, cleanup.stdout) ?? `Unable to remove ${worktreePath}.` };
|
|
796
|
+
}
|
|
797
|
+
return { ok: true };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function runGitSync(cwd: string, args: string[]): { stdout: string; stderr: string; code: number } {
|
|
801
|
+
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
802
|
+
return {
|
|
803
|
+
stdout: result.stdout ?? "",
|
|
804
|
+
stderr: result.stderr ?? "",
|
|
805
|
+
code: result.status ?? 1,
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function firstNonEmpty(...values: Array<string | null | undefined>): string | null {
|
|
810
|
+
for (const value of values) {
|
|
811
|
+
if (typeof value === "string") {
|
|
812
|
+
const trimmed = value.trim();
|
|
813
|
+
if (trimmed.length > 0) {
|
|
814
|
+
return trimmed;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function buildLiveBatchSupervisorDeps(platform: Platform): UltraPlanBatchSupervisorDeps {
|
|
822
|
+
return {
|
|
823
|
+
computeFrontier: computeUltraPlanBatchEligibleFrontier,
|
|
824
|
+
async runWorker(node, run) {
|
|
825
|
+
const prepared = await ensureBatchNodeWorktree(platform, run, node);
|
|
826
|
+
node.updatedAt = new Date().toISOString();
|
|
827
|
+
if (prepared.kind === "blocked") {
|
|
828
|
+
return { kind: "blocked", blockerKind: "supervisor", summary: prepared.summary };
|
|
829
|
+
}
|
|
830
|
+
node.branchName = prepared.branchName;
|
|
831
|
+
node.worktreePath = prepared.worktreePath;
|
|
832
|
+
try {
|
|
833
|
+
return await runUltraPlanBatchWorker({
|
|
834
|
+
platform,
|
|
835
|
+
sessionId: node.sessionId,
|
|
836
|
+
worktreeCwd: prepared.worktreePath,
|
|
837
|
+
});
|
|
838
|
+
} catch (error) {
|
|
839
|
+
return {
|
|
840
|
+
kind: "blocked",
|
|
841
|
+
blockerKind: "supervisor",
|
|
842
|
+
summary: error instanceof Error
|
|
843
|
+
? `Worker ${node.sessionId} failed after worktree preparation: ${error.message}`
|
|
844
|
+
: `Worker ${node.sessionId} failed after worktree preparation.`,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
},
|
|
848
|
+
async mergeNode(node, run) {
|
|
849
|
+
if (!node.branchName || !node.worktreePath) {
|
|
850
|
+
return {
|
|
851
|
+
kind: "blocked",
|
|
852
|
+
code: "supervisor-worktree-invalid",
|
|
853
|
+
currentBaseHead: run.currentBaseHead,
|
|
854
|
+
worktreePath: node.worktreePath ?? run.supervisorWorktreePath ?? run.projectRoot,
|
|
855
|
+
summary: `Cannot merge ${node.sessionId} without a prepared branch and worktree.`,
|
|
856
|
+
countsAgainstParallelism: false,
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
try {
|
|
860
|
+
if (!run.supervisorWorktreePath) {
|
|
861
|
+
return {
|
|
862
|
+
kind: "blocked",
|
|
863
|
+
code: "supervisor-worktree-invalid",
|
|
864
|
+
currentBaseHead: run.currentBaseHead,
|
|
865
|
+
worktreePath: node.worktreePath,
|
|
866
|
+
summary: "Supervisor worktree path is missing.",
|
|
867
|
+
countsAgainstParallelism: false,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
if (resolveRepoIdentityRootFromFs(run.supervisorWorktreePath) !== resolveRepoIdentityRootFromFs(run.projectRoot)) {
|
|
871
|
+
return {
|
|
872
|
+
kind: "blocked",
|
|
873
|
+
code: "project-identity-failed",
|
|
874
|
+
currentBaseHead: run.currentBaseHead,
|
|
875
|
+
worktreePath: node.worktreePath,
|
|
876
|
+
summary: "Supervisor worktree no longer resolves to the same repository identity as the batch run.",
|
|
877
|
+
countsAgainstParallelism: false,
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
} catch (error) {
|
|
881
|
+
return {
|
|
882
|
+
kind: "blocked",
|
|
883
|
+
code: "project-identity-failed",
|
|
884
|
+
currentBaseHead: run.currentBaseHead,
|
|
885
|
+
worktreePath: node.worktreePath,
|
|
886
|
+
summary: error instanceof Error ? error.message : "Unable to resolve supervisor worktree identity.",
|
|
887
|
+
countsAgainstParallelism: false,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
return mergeUltraPlanBatchWorktree({
|
|
891
|
+
supervisorBranch: run.baseBranch,
|
|
892
|
+
currentBaseHead: run.currentBaseHead,
|
|
893
|
+
branchName: node.branchName,
|
|
894
|
+
worktreePath: node.worktreePath,
|
|
895
|
+
deps: {
|
|
896
|
+
inspectSupervisorWorktree: () => inspectSupervisorWorktreeSync(run.supervisorWorktreePath ?? run.projectRoot),
|
|
897
|
+
mergeBranch: (branchName) => mergeBranchSync(run.supervisorWorktreePath ?? run.projectRoot, branchName),
|
|
898
|
+
cleanupWorktree: (worktreePath) => cleanupWorktreeSync(run.projectRoot, worktreePath),
|
|
899
|
+
},
|
|
900
|
+
});
|
|
901
|
+
},
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function snapshotBatchRun(run: UltraPlanBatchRun): string {
|
|
906
|
+
return JSON.stringify({
|
|
907
|
+
state: run.state,
|
|
908
|
+
batchBlockerCode: run.batchBlockerCode,
|
|
909
|
+
batchBlockerSummary: run.batchBlockerSummary,
|
|
910
|
+
batchResumeRequestedAt: run.batchResumeRequestedAt,
|
|
911
|
+
currentBaseHead: run.currentBaseHead,
|
|
912
|
+
nodes: run.nodes.map((node) => ({
|
|
913
|
+
sessionId: node.sessionId,
|
|
914
|
+
state: node.state,
|
|
915
|
+
blockerKind: node.blockerKind,
|
|
916
|
+
blockerSummary: node.blockerSummary,
|
|
917
|
+
resumeRequestedAt: node.resumeRequestedAt,
|
|
918
|
+
branchName: node.branchName,
|
|
919
|
+
worktreePath: node.worktreePath,
|
|
920
|
+
})),
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function withBatchUpdatedAt(run: UltraPlanBatchRun, nowIso: string): UltraPlanBatchRun {
|
|
925
|
+
return {
|
|
926
|
+
...run,
|
|
927
|
+
updatedAt: nowIso,
|
|
928
|
+
nodes: run.nodes.map((node) => ({ ...node, updatedAt: nowIso })),
|
|
929
|
+
waves: deriveBatchWaves(run.nodes),
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
async function executeLiveBatchRun(
|
|
934
|
+
input: { platform: Platform; cwd: string; run: UltraPlanBatchRun; mode: "start" | "resume" },
|
|
935
|
+
): Promise<UltraPlanBatchRun> {
|
|
936
|
+
const ownerSessionId = makeBatchOwnerSessionId();
|
|
937
|
+
let current = cloneBatchRun(input.run);
|
|
938
|
+
let leaseHeld = false;
|
|
939
|
+
let leaseRenewal: BatchLeaseRenewalController | null = null;
|
|
940
|
+
try {
|
|
941
|
+
if (input.mode === "start") {
|
|
942
|
+
persistPlannedBatchRun({ paths: input.platform.paths, cwd: input.cwd, run: current });
|
|
943
|
+
appendBatchJournalEventOrThrow(input.platform.paths, input.cwd, current.runId, {
|
|
944
|
+
runId: current.runId,
|
|
945
|
+
sessionId: null,
|
|
946
|
+
type: "run-created",
|
|
947
|
+
recordedAt: current.createdAt,
|
|
948
|
+
summary: `Created batch run ${current.runId}.`,
|
|
949
|
+
details: { sessionIds: current.nodes.map((node) => node.sessionId), maxParallelism: current.maxParallelism },
|
|
950
|
+
});
|
|
951
|
+
} else {
|
|
952
|
+
saveBatchRunOrThrow(input.platform.paths, input.cwd, current);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const acquiredAt = new Date().toISOString();
|
|
956
|
+
acquireBatchLeaseOrThrow({
|
|
957
|
+
paths: input.platform.paths,
|
|
958
|
+
cwd: input.cwd,
|
|
959
|
+
runId: current.runId,
|
|
960
|
+
ownerSessionId,
|
|
961
|
+
nowIso: acquiredAt,
|
|
962
|
+
});
|
|
963
|
+
leaseHeld = true;
|
|
964
|
+
appendBatchJournalEventOrThrow(input.platform.paths, input.cwd, current.runId, {
|
|
965
|
+
runId: current.runId,
|
|
966
|
+
sessionId: null,
|
|
967
|
+
type: "lease-acquired",
|
|
968
|
+
recordedAt: acquiredAt,
|
|
969
|
+
summary: `Supervisor lease acquired by ${ownerSessionId}.`,
|
|
970
|
+
});
|
|
971
|
+
const leaseRenewalController = startBatchLeaseRenewal({
|
|
972
|
+
paths: input.platform.paths,
|
|
973
|
+
cwd: input.cwd,
|
|
974
|
+
runId: current.runId,
|
|
975
|
+
ownerSessionId,
|
|
976
|
+
});
|
|
977
|
+
leaseRenewal = leaseRenewalController;
|
|
978
|
+
|
|
979
|
+
current = withBatchUpdatedAt({ ...current, state: "running" }, acquiredAt);
|
|
980
|
+
saveBatchRunOrThrow(input.platform.paths, input.cwd, current);
|
|
981
|
+
|
|
982
|
+
const deps = buildLiveBatchSupervisorDeps(input.platform);
|
|
983
|
+
let firstPass = true;
|
|
984
|
+
for (let pass = 0; pass < 100; pass++) {
|
|
985
|
+
const previous = cloneBatchRun(current);
|
|
986
|
+
const previousSnapshot = snapshotBatchRun(previous);
|
|
987
|
+
const supervisorPass = firstPass && input.mode === "resume"
|
|
988
|
+
? resumeUltraPlanBatchSupervisor({ run: current, deps })
|
|
989
|
+
: runUltraPlanBatchSupervisor({ run: current, deps });
|
|
990
|
+
const next = await leaseRenewalController.runWithRenewal(supervisorPass);
|
|
991
|
+
current = withBatchUpdatedAt(next, new Date().toISOString());
|
|
992
|
+
saveBatchRunOrThrow(input.platform.paths, input.cwd, current);
|
|
993
|
+
appendBatchNodeTransitionEvents(input.platform.paths, input.cwd, previous, current);
|
|
994
|
+
firstPass = false;
|
|
995
|
+
if (current.state !== "running") {
|
|
996
|
+
break;
|
|
997
|
+
}
|
|
998
|
+
if (snapshotBatchRun(current) === previousSnapshot) {
|
|
999
|
+
current = withBatchUpdatedAt({
|
|
1000
|
+
...current,
|
|
1001
|
+
state: "blocked",
|
|
1002
|
+
batchBlockerCode: "invalid-run",
|
|
1003
|
+
batchBlockerSummary: "Batch supervisor made no observable progress while the run was still marked running.",
|
|
1004
|
+
}, new Date().toISOString());
|
|
1005
|
+
saveBatchRunOrThrow(input.platform.paths, input.cwd, current);
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (leaseHeld) {
|
|
1011
|
+
const releasedAt = new Date().toISOString();
|
|
1012
|
+
const released = releaseUltraPlanBatchActiveRunLease(
|
|
1013
|
+
input.platform.paths,
|
|
1014
|
+
input.cwd,
|
|
1015
|
+
{ runId: current.runId, ownerSessionId },
|
|
1016
|
+
current.state,
|
|
1017
|
+
releasedAt,
|
|
1018
|
+
);
|
|
1019
|
+
if (!released.ok) {
|
|
1020
|
+
throw new Error(released.error.message);
|
|
1021
|
+
}
|
|
1022
|
+
appendBatchJournalEventOrThrow(input.platform.paths, input.cwd, current.runId, {
|
|
1023
|
+
runId: current.runId,
|
|
1024
|
+
sessionId: null,
|
|
1025
|
+
type: "lease-released",
|
|
1026
|
+
recordedAt: releasedAt,
|
|
1027
|
+
summary: `Supervisor lease released after batch entered ${current.state}.`,
|
|
1028
|
+
});
|
|
1029
|
+
leaseHeld = false;
|
|
1030
|
+
}
|
|
1031
|
+
return current;
|
|
1032
|
+
} catch (error) {
|
|
1033
|
+
current = withBatchUpdatedAt({
|
|
1034
|
+
...current,
|
|
1035
|
+
state: "blocked",
|
|
1036
|
+
batchBlockerCode: "invalid-run",
|
|
1037
|
+
batchBlockerSummary: error instanceof Error ? error.message : "UltraPlan batch supervision failed.",
|
|
1038
|
+
}, new Date().toISOString());
|
|
1039
|
+
saveBatchRunOrThrow(input.platform.paths, input.cwd, current);
|
|
1040
|
+
if (leaseHeld) {
|
|
1041
|
+
const releasedAt = new Date().toISOString();
|
|
1042
|
+
const released = releaseUltraPlanBatchActiveRunLease(
|
|
1043
|
+
input.platform.paths,
|
|
1044
|
+
input.cwd,
|
|
1045
|
+
{ runId: current.runId, ownerSessionId },
|
|
1046
|
+
current.state,
|
|
1047
|
+
releasedAt,
|
|
1048
|
+
);
|
|
1049
|
+
if (released.ok) {
|
|
1050
|
+
appendBatchJournalEventOrThrow(input.platform.paths, input.cwd, current.runId, {
|
|
1051
|
+
runId: current.runId,
|
|
1052
|
+
sessionId: null,
|
|
1053
|
+
type: "lease-released",
|
|
1054
|
+
recordedAt: releasedAt,
|
|
1055
|
+
summary: `Supervisor lease released after batch entered ${current.state}.`,
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return current;
|
|
1060
|
+
} finally {
|
|
1061
|
+
leaseRenewal?.stop();
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function renderBatchOutcomeTitle(run: UltraPlanBatchRun): string {
|
|
1066
|
+
if (run.state === "complete") {
|
|
1067
|
+
return "Ultraplan batch complete";
|
|
1068
|
+
}
|
|
1069
|
+
if (run.state === "blocked") {
|
|
1070
|
+
return "Ultraplan batch blocked";
|
|
1071
|
+
}
|
|
1072
|
+
return "Ultraplan batch paused";
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function stampBatchResumeApproval(run: UltraPlanBatchRun, nowIso: string): UltraPlanBatchRun {
|
|
1076
|
+
return {
|
|
1077
|
+
...cloneBatchRun(run),
|
|
1078
|
+
batchResumeRequestedAt: nowIso,
|
|
1079
|
+
updatedAt: nowIso,
|
|
1080
|
+
nodes: run.nodes.map((node) => ({ ...node, updatedAt: nowIso })),
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
async function handleResumeBatch(
|
|
1085
|
+
platform: Platform,
|
|
1086
|
+
ctx: any,
|
|
1087
|
+
batchState: Extract<UltraPlanRunBatchState, { kind: "resume-batch" }>,
|
|
1088
|
+
): Promise<void> {
|
|
1089
|
+
const nowIso = new Date().toISOString();
|
|
1090
|
+
if (isLiveBatchLease(batchState.lease, nowIso)) {
|
|
1091
|
+
notifyError(
|
|
1092
|
+
ctx,
|
|
1093
|
+
"invalid-run",
|
|
1094
|
+
`invalid-run: batch ${batchState.run.runId} is already supervised by ${batchState.lease.ownerSessionId}` ,
|
|
1095
|
+
);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
let run = cloneBatchRun(batchState.run);
|
|
1100
|
+
if (run.state === "blocked" && run.batchBlockerCode) {
|
|
1101
|
+
const action = await ctx.ui.select(renderUltraPlanBatchSummary(run), ["Retry blocked batch", "Inspect batch", "Cancel"]);
|
|
1102
|
+
if (!action || action === "Cancel") {
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (action === "Inspect batch") {
|
|
1106
|
+
notifyInfo(ctx, "Ultraplan batch", renderUltraPlanBatchSummary(run));
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
run = stampBatchResumeApproval(run, nowIso);
|
|
1110
|
+
saveBatchRunOrThrow(platform.paths, ctx.cwd, run);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
const finalRun = await executeLiveBatchRun({ platform, cwd: ctx.cwd, run, mode: "resume" });
|
|
1114
|
+
notifyInfo(ctx, renderBatchOutcomeTitle(finalRun), renderUltraPlanBatchSummary(finalRun));
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
export function resolveUltraPlanRunBatchStateForTesting(
|
|
1118
|
+
input: { paths: Platform["paths"]; cwd: string },
|
|
1119
|
+
): UltraPlanRunBatchState {
|
|
1120
|
+
return resolveUltraPlanRunBatchState(input);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
export function planUltraPlanBatchRunForTesting(
|
|
1124
|
+
input: { paths: Platform["paths"]; cwd: string; sessionIds: string[]; maxParallelism: number },
|
|
1125
|
+
): UltraPlanBatchRun {
|
|
1126
|
+
return persistPlannedBatchRun({
|
|
1127
|
+
paths: input.paths,
|
|
1128
|
+
cwd: input.cwd,
|
|
1129
|
+
run: buildBatchRun(input),
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
export function renderUltraPlanBatchStatusForTesting(
|
|
1134
|
+
input: { paths: Platform["paths"]; cwd: string },
|
|
1135
|
+
): string {
|
|
1136
|
+
const batchState = resolveUltraPlanRunBatchState(input);
|
|
1137
|
+
switch (batchState.kind) {
|
|
1138
|
+
case "invalid-run":
|
|
1139
|
+
return batchState.message;
|
|
1140
|
+
case "resume-batch":
|
|
1141
|
+
return renderUltraPlanBatchSummary(batchState.run);
|
|
1142
|
+
default:
|
|
1143
|
+
return "No active batch";
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
export function resumeUltraPlanBatchRunForTesting(
|
|
1148
|
+
input: {
|
|
1149
|
+
paths: Platform["paths"];
|
|
1150
|
+
cwd: string;
|
|
1151
|
+
batchResumeRequestedAt?: string;
|
|
1152
|
+
retrySessionId?: string;
|
|
1153
|
+
resumeRequestedAt?: string;
|
|
1154
|
+
},
|
|
1155
|
+
): UltraPlanBatchRun {
|
|
1156
|
+
const batchState = resolveUltraPlanRunBatchState({ paths: input.paths, cwd: input.cwd });
|
|
1157
|
+
if (batchState.kind !== "resume-batch") {
|
|
1158
|
+
throw new Error(batchState.kind === "invalid-run" ? batchState.message : "No active batch to resume");
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const updatedAt = input.resumeRequestedAt ?? input.batchResumeRequestedAt ?? batchState.run.updatedAt;
|
|
1162
|
+
const next: UltraPlanBatchRun = {
|
|
1163
|
+
...batchState.run,
|
|
1164
|
+
batchResumeRequestedAt: input.batchResumeRequestedAt ?? batchState.run.batchResumeRequestedAt,
|
|
1165
|
+
updatedAt,
|
|
1166
|
+
nodes: batchState.run.nodes.map((node) =>
|
|
1167
|
+
node.sessionId === input.retrySessionId
|
|
1168
|
+
? { ...node, resumeRequestedAt: input.resumeRequestedAt ?? node.resumeRequestedAt, updatedAt }
|
|
1169
|
+
: { ...node },
|
|
1170
|
+
),
|
|
1171
|
+
waves: batchState.run.waves.map((wave) => ({ ...wave, sessionIds: [...wave.sessionIds] })),
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
saveBatchRunOrThrow(input.paths, input.cwd, next);
|
|
1175
|
+
return next;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
export function abandonUltraPlanBatchForTesting(
|
|
1179
|
+
input: { paths: Platform["paths"]; cwd: string; runId: string },
|
|
1180
|
+
): UltraPlanBatchRun {
|
|
1181
|
+
const run = loadUltraPlanBatchRun(input.paths, input.cwd, input.runId);
|
|
1182
|
+
if (!run.ok) {
|
|
1183
|
+
throw new Error(run.error.message);
|
|
1184
|
+
}
|
|
1185
|
+
const next = abandonUltraPlanBatchRun(run.value);
|
|
1186
|
+
saveBatchRunOrThrow(input.paths, input.cwd, next);
|
|
1187
|
+
return next;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
export function abandonUltraPlanBatchNodeForTesting(
|
|
1191
|
+
input: { paths: Platform["paths"]; cwd: string; runId: string; sessionId: string },
|
|
1192
|
+
): UltraPlanBatchRun {
|
|
1193
|
+
const run = loadUltraPlanBatchRun(input.paths, input.cwd, input.runId);
|
|
1194
|
+
if (!run.ok) {
|
|
1195
|
+
throw new Error(run.error.message);
|
|
1196
|
+
}
|
|
1197
|
+
const next = abandonUltraPlanBatchNode(run.value, input.sessionId);
|
|
1198
|
+
saveBatchRunOrThrow(input.paths, input.cwd, next);
|
|
1199
|
+
return next;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
async function handleBatchPlanning(platform: Platform, ctx: any): Promise<void> {
|
|
1203
|
+
const sessionIdsInput = await ctx.ui.input("Batch session ids");
|
|
1204
|
+
if (!sessionIdsInput) {
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
const sessionIds = sessionIdsInput
|
|
1208
|
+
.split(",")
|
|
1209
|
+
.map((value: string) => value.trim())
|
|
1210
|
+
.filter((value: string) => value.length > 0);
|
|
1211
|
+
if (sessionIds.length === 0) {
|
|
1212
|
+
notifyError(ctx, "invalid-run", "invalid-run: batch planning requires at least one session id");
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const maxParallelismInput = await ctx.ui.input("Max parallelism");
|
|
1217
|
+
const maxParallelism = Number.parseInt(maxParallelismInput ?? "", 10);
|
|
1218
|
+
if (!Number.isFinite(maxParallelism) || maxParallelism <= 0) {
|
|
1219
|
+
notifyError(ctx, "invalid-run", "invalid-run: maxParallelism must be a positive integer");
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const confirmation = await ctx.ui.select("Batch plan", ["Start batch", "Cancel"]);
|
|
1224
|
+
if (confirmation !== "Start batch") {
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
try {
|
|
1229
|
+
const run = await buildLiveBatchRun({
|
|
1230
|
+
platform,
|
|
1231
|
+
cwd: ctx.cwd,
|
|
1232
|
+
sessionIds,
|
|
1233
|
+
maxParallelism,
|
|
1234
|
+
});
|
|
1235
|
+
const finalRun = await executeLiveBatchRun({ platform, cwd: ctx.cwd, run, mode: "start" });
|
|
1236
|
+
notifyInfo(ctx, renderBatchOutcomeTitle(finalRun), renderUltraPlanBatchSummary(finalRun));
|
|
1237
|
+
} catch (error) {
|
|
1238
|
+
notifyError(
|
|
1239
|
+
ctx,
|
|
1240
|
+
"invalid-run",
|
|
1241
|
+
`invalid-run: ${error instanceof Error ? error.message : "Unable to start UltraPlan batch."}` ,
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
async function handleRun(platform: Platform, ctx: any, args?: string): Promise<void> {
|
|
1247
|
+
if (!ctx.hasUI) {
|
|
1248
|
+
notifyWarning(ctx, "Ultraplan run requires interactive mode");
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const batchState = resolveUltraPlanRunBatchState({ paths: platform.paths, cwd: ctx.cwd });
|
|
1253
|
+
if (batchState.kind === "invalid-run") {
|
|
1254
|
+
notifyError(ctx, "invalid-run", batchState.message);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
if (batchState.kind === "resume-batch") {
|
|
1258
|
+
await handleResumeBatch(platform, ctx, batchState);
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const wantsBatchPlanning = args?.trim().split(/\s+/).slice(1).includes("batch") ?? false;
|
|
1263
|
+
if (!wantsBatchPlanning) {
|
|
1264
|
+
const visible = loadVisibleSessions(platform, ctx.cwd);
|
|
1265
|
+
if (visible.kind === "ok" && visible.sessions.length > 1) {
|
|
1266
|
+
const mode = await ctx.ui.select("Ultraplan run mode", ["Single session", "Batch sessions", "Cancel"]);
|
|
1267
|
+
if (!mode || mode === "Cancel") {
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
if (mode === "Batch sessions") {
|
|
1271
|
+
await handleBatchPlanning(platform, ctx);
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
if (wantsBatchPlanning) {
|
|
1277
|
+
await handleBatchPlanning(platform, ctx);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
const selected = await selectSession(platform, ctx);
|
|
1282
|
+
if (!selected) {
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
if (selected.recommendation?.action === "inspect") {
|
|
1286
|
+
await presentSelectedSession(platform, ctx, selected.session, "status");
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
await runSelectedSession(platform, ctx, selected.session);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
async function handleStatus(platform: Platform, ctx: any): Promise<void> {
|
|
1294
|
+
if (!ctx.hasUI) {
|
|
1295
|
+
notifyWarning(ctx, "Ultraplan status requires interactive mode");
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const selected = await selectSession(platform, ctx, { includeDone: true });
|
|
1300
|
+
if (!selected) {
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
await presentSelectedSession(platform, ctx, selected.session, "status");
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
async function handleNextSelection(
|
|
1308
|
+
platform: Platform,
|
|
1309
|
+
ctx: any,
|
|
1310
|
+
selection: SessionPickerSelection,
|
|
1311
|
+
options?: { allowChooseAnother?: boolean },
|
|
1312
|
+
): Promise<void> {
|
|
1313
|
+
if (!selection.recommendation) {
|
|
1314
|
+
await presentSelectedSession(platform, ctx, selection.session, "status");
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
const choices = [
|
|
1319
|
+
...(selection.recommendation.action === "run" ? ["Run this session", "Inspect session"] : ["Inspect session"]),
|
|
1320
|
+
...(options?.allowChooseAnother === false ? [] : ["Choose another session"]),
|
|
1321
|
+
"Cancel",
|
|
1322
|
+
];
|
|
1323
|
+
const choice = await ctx.ui.select(
|
|
1324
|
+
renderUltraPlanRecommendationSummary(selection.recommendation),
|
|
1325
|
+
choices,
|
|
1326
|
+
{ helpText: "Pick an action · Esc to cancel" },
|
|
1327
|
+
);
|
|
1328
|
+
if (!choice || choice === "Cancel") {
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
if (choice === "Choose another session") {
|
|
1332
|
+
if (options?.allowChooseAnother === false) {
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
const alternative = await selectSession(platform, ctx);
|
|
1336
|
+
if (!alternative) {
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
if (alternative.recommendation?.action === "inspect") {
|
|
1340
|
+
await presentSelectedSession(platform, ctx, alternative.session, "status");
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
await handleNextSelection(platform, ctx, alternative, { allowChooseAnother: false });
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
if (choice === "Inspect session") {
|
|
1347
|
+
await presentSelectedSession(platform, ctx, selection.session, "status");
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
await runSelectedSession(platform, ctx, selection.session);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
async function handleNext(platform: Platform, ctx: any): Promise<void> {
|
|
1355
|
+
if (!ctx.hasUI) {
|
|
1356
|
+
notifyWarning(ctx, "Ultraplan next requires interactive mode");
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
const state = loadSessionPickerState(platform, ctx);
|
|
1361
|
+
if (!state?.topRecommendation) {
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
await handleNextSelection(platform, ctx, {
|
|
1366
|
+
session: state.topRecommendation.session,
|
|
1367
|
+
recommendation: state.topRecommendation,
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
export async function handleUltraplan(platform: Platform, ctx: any, args?: string): Promise<void> {
|
|
1372
|
+
const subcommand = parseUltraplanSubcommand(args);
|
|
1373
|
+
|
|
1374
|
+
// Strip the subcommand from args before forwarding so handlers see only their own positional args.
|
|
1375
|
+
const subcommandArgs = args ? args.replace(/^\s*\S+\s*/, "").trim() : "";
|
|
1376
|
+
|
|
1377
|
+
switch (subcommand) {
|
|
1378
|
+
case null:
|
|
1379
|
+
if (args?.trim()) {
|
|
1380
|
+
await handlePlan(platform, ctx, args);
|
|
1381
|
+
} else {
|
|
1382
|
+
await handleAuthoringBareEntry({ platform, ctx });
|
|
1383
|
+
}
|
|
1384
|
+
return;
|
|
1385
|
+
case "plan":
|
|
1386
|
+
await handlePlan(platform, ctx, subcommandArgs);
|
|
1387
|
+
return;
|
|
1388
|
+
case "discover":
|
|
1389
|
+
await handleStageSubcommand("discover", platform, ctx, subcommandArgs);
|
|
1390
|
+
return;
|
|
1391
|
+
case "research":
|
|
1392
|
+
await handleStageSubcommand("research", platform, ctx, subcommandArgs);
|
|
1393
|
+
return;
|
|
1394
|
+
case "synthesize":
|
|
1395
|
+
await handleStageSubcommand("synthesize", platform, ctx, subcommandArgs);
|
|
1396
|
+
return;
|
|
1397
|
+
case "review":
|
|
1398
|
+
await handleStageSubcommand("review", platform, ctx, subcommandArgs);
|
|
1399
|
+
return;
|
|
1400
|
+
case "approve":
|
|
1401
|
+
await handleStageSubcommand("approve", platform, ctx, subcommandArgs);
|
|
1402
|
+
return;
|
|
1403
|
+
case "resume":
|
|
1404
|
+
await handleResume(platform, ctx, subcommandArgs);
|
|
1405
|
+
return;
|
|
1406
|
+
case "quick":
|
|
1407
|
+
notifyWarning(
|
|
1408
|
+
ctx,
|
|
1409
|
+
"/supi:ultraplan quick is deprecated",
|
|
1410
|
+
"This single-shot path will be removed next release. Prefer /supi:ultraplan or /supi:ultraplan plan.",
|
|
1411
|
+
);
|
|
1412
|
+
await handleAuthoring(platform, ctx, subcommandArgs);
|
|
1413
|
+
return;
|
|
1414
|
+
case "run":
|
|
1415
|
+
await handleRun(platform, ctx, args);
|
|
1416
|
+
return;
|
|
1417
|
+
case "status":
|
|
1418
|
+
await handleStatus(platform, ctx);
|
|
1419
|
+
return;
|
|
1420
|
+
case "next":
|
|
1421
|
+
await handleNext(platform, ctx);
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function buildUltraPlanAuthoringPrompt(initialRequest: string): string {
|
|
1427
|
+
const requestSection = initialRequest.trim()
|
|
1428
|
+
? ["Initial user prompt (verbatim):", "```", initialRequest.trim(), "```"]
|
|
1429
|
+
: [
|
|
1430
|
+
"No initial prompt was provided.",
|
|
1431
|
+
"Start by asking the user what they want this UltraPlan to accomplish.",
|
|
1432
|
+
];
|
|
1433
|
+
|
|
1434
|
+
return [
|
|
1435
|
+
"# UltraPlan conversational authoring",
|
|
1436
|
+
"",
|
|
1437
|
+
"You are authoring a new UltraPlan session from natural-language chat, not from command-line form fields.",
|
|
1438
|
+
"",
|
|
1439
|
+
...requestSection,
|
|
1440
|
+
"",
|
|
1441
|
+
"## Interaction contract",
|
|
1442
|
+
"- Infer the title, one-line goal, applicable stacks (frontend/backend/infrastructure), domains, and scenarios from the user's prompt and repository context.",
|
|
1443
|
+
"- Ask clarifying questions only when the missing answer would materially change the plan.",
|
|
1444
|
+
"- When asking, keep it in chat. Prefer a structured question tool when available; give 2-5 suggested answers and mark the recommended one.",
|
|
1445
|
+
"- Always preserve an open-ended path: if the tool provides an automatic Other option, rely on it; otherwise explicitly say the user can type their own answer.",
|
|
1446
|
+
"- Do not ask the user to type JSON, rerun `/supi:ultraplan` with no arguments, or fill title/goal/domain TUI prompts.",
|
|
1447
|
+
"",
|
|
1448
|
+
"## Completion contract",
|
|
1449
|
+
"- Once you understand the request well enough, call `ultraplan_create` with the complete inferred plan.",
|
|
1450
|
+
"- Include only stacks that actually have work; every included stack needs at least one domain, and every domain needs at least one scenario.",
|
|
1451
|
+
"- Use scenario titles that are concrete execution targets. Add steps only when they clarify non-obvious sequencing or verification.",
|
|
1452
|
+
"- After `ultraplan_create` succeeds, summarize what was created and tell the user they can run `/supi:ultraplan run`.",
|
|
1453
|
+
].join("\n");
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
async function handleAuthoring(platform: Platform, ctx: any, args?: string): Promise<void> {
|
|
1457
|
+
const initialRequest = args?.trim() ?? "";
|
|
1458
|
+
platform.sendMessage(
|
|
1459
|
+
{
|
|
1460
|
+
customType: "supi-ultraplan-author",
|
|
1461
|
+
content: [{ type: "text", text: buildUltraPlanAuthoringPrompt(initialRequest) }],
|
|
1462
|
+
display: "none",
|
|
1463
|
+
},
|
|
1464
|
+
{ deliverAs: "steer", triggerTurn: true },
|
|
1465
|
+
);
|
|
1466
|
+
|
|
1467
|
+
notifyInfo(
|
|
1468
|
+
ctx,
|
|
1469
|
+
"UltraPlan authoring started",
|
|
1470
|
+
initialRequest ? "The agent will refine the prompt in chat and save the plan when ready." : "Describe what you want to build; the agent will refine it in chat.",
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
export function registerUltraplanCommand(platform: Platform): void {
|
|
1475
|
+
platform.registerCommand("supi:ultraplan", {
|
|
1476
|
+
description: "Author, run, inspect, or batch ultraplan sessions",
|
|
1477
|
+
getArgumentCompletions(prefix: string) {
|
|
1478
|
+
const lower = prefix.toLowerCase();
|
|
1479
|
+
const nestedRun = /^run\s+(.*)$/.exec(lower);
|
|
1480
|
+
if (nestedRun) {
|
|
1481
|
+
const nestedPrefix = nestedRun[1] ?? "";
|
|
1482
|
+
if ("batch".startsWith(nestedPrefix)) {
|
|
1483
|
+
return [
|
|
1484
|
+
{
|
|
1485
|
+
value: `run batch `,
|
|
1486
|
+
label: "run batch",
|
|
1487
|
+
description: "Start and supervise a batched run across multiple sessions",
|
|
1488
|
+
},
|
|
1489
|
+
];
|
|
1490
|
+
}
|
|
1491
|
+
return null;
|
|
1492
|
+
}
|
|
1493
|
+
const matches = SUBCOMMANDS
|
|
1494
|
+
.filter((subcommand) => subcommand.name.startsWith(lower))
|
|
1495
|
+
.map((subcommand) => ({
|
|
1496
|
+
value: `${subcommand.name} `,
|
|
1497
|
+
label: subcommand.name,
|
|
1498
|
+
description: subcommand.description,
|
|
1499
|
+
}));
|
|
1500
|
+
return matches.length > 0 ? matches : null;
|
|
1501
|
+
},
|
|
1502
|
+
async handler(args: string | undefined, ctx: any) {
|
|
1503
|
+
await handleUltraplan(platform, ctx, args);
|
|
1504
|
+
},
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
|
|
1509
|
+
/**
|
|
1510
|
+
* Test-only entry point exposing the migration-integrated visible-session loader. Production
|
|
1511
|
+
* code uses the internal `loadVisibleSessions` helper; tests import this wrapper to avoid
|
|
1512
|
+
* reaching through the module boundary.
|
|
1513
|
+
*/
|
|
1514
|
+
export function loadVisibleSessionsForTesting(
|
|
1515
|
+
input: { platform: Platform; cwd: string; options?: { includeDone?: boolean } },
|
|
1516
|
+
): VisibleSessionsLoadResult {
|
|
1517
|
+
return loadVisibleSessions(input.platform, input.cwd, input.options);
|
|
1518
|
+
}
|