supipowers 1.5.3 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +135 -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 +268 -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,150 @@
|
|
|
1
|
+
import type { UltraPlanBatchNode, UltraPlanBatchRun } from "../../types.js";
|
|
2
|
+
|
|
3
|
+
function buildSessionNodeMap(run: UltraPlanBatchRun): Map<string, UltraPlanBatchNode> {
|
|
4
|
+
return new Map(run.nodes.map((node) => [node.sessionId, node]));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getUltraPlanBatchGraphErrors(run: UltraPlanBatchRun): string[] {
|
|
8
|
+
const errors: string[] = [];
|
|
9
|
+
const seenNodeIds = new Set<string>();
|
|
10
|
+
const seenSessionIds = new Set<string>();
|
|
11
|
+
const nodesBySessionId = new Map<string, UltraPlanBatchNode>();
|
|
12
|
+
|
|
13
|
+
for (const node of run.nodes) {
|
|
14
|
+
if (seenNodeIds.has(node.nodeId)) {
|
|
15
|
+
errors.push(`duplicate batch nodeId ${node.nodeId}`);
|
|
16
|
+
}
|
|
17
|
+
seenNodeIds.add(node.nodeId);
|
|
18
|
+
|
|
19
|
+
if (seenSessionIds.has(node.sessionId)) {
|
|
20
|
+
errors.push(`duplicate batch sessionId ${node.sessionId}`);
|
|
21
|
+
}
|
|
22
|
+
seenSessionIds.add(node.sessionId);
|
|
23
|
+
nodesBySessionId.set(node.sessionId, node);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const wavesByIndex = new Map<number, Set<string>>();
|
|
27
|
+
const sessionsInWaves = new Set<string>();
|
|
28
|
+
for (const wave of run.waves) {
|
|
29
|
+
if (wavesByIndex.has(wave.waveIndex)) {
|
|
30
|
+
errors.push(`duplicate batch waveIndex ${wave.waveIndex}`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const sessions = new Set<string>();
|
|
34
|
+
for (const sessionId of wave.sessionIds) {
|
|
35
|
+
if (sessions.has(sessionId)) {
|
|
36
|
+
errors.push(`batch wave ${wave.waveIndex} lists ${sessionId} more than once`);
|
|
37
|
+
}
|
|
38
|
+
if (sessionsInWaves.has(sessionId)) {
|
|
39
|
+
errors.push(`batch session ${sessionId} appears in more than one wave`);
|
|
40
|
+
}
|
|
41
|
+
sessions.add(sessionId);
|
|
42
|
+
sessionsInWaves.add(sessionId);
|
|
43
|
+
}
|
|
44
|
+
wavesByIndex.set(wave.waveIndex, sessions);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const node of run.nodes) {
|
|
48
|
+
const waveMembers = wavesByIndex.get(node.waveIndex);
|
|
49
|
+
if (!waveMembers) {
|
|
50
|
+
errors.push(`batch node ${node.sessionId} references missing wave ${node.waveIndex}`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (!waveMembers.has(node.sessionId)) {
|
|
54
|
+
errors.push(`batch node ${node.sessionId} is missing from wave ${node.waveIndex}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const wave of run.waves) {
|
|
59
|
+
for (const sessionId of wave.sessionIds) {
|
|
60
|
+
const node = nodesBySessionId.get(sessionId);
|
|
61
|
+
if (!node) {
|
|
62
|
+
errors.push(`batch wave ${wave.waveIndex} references unknown session ${sessionId}`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (node.waveIndex !== wave.waveIndex) {
|
|
66
|
+
errors.push(`batch wave ${wave.waveIndex} includes ${sessionId}, but the node is assigned to wave ${node.waveIndex}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const node of run.nodes) {
|
|
72
|
+
for (const dependencySessionId of node.dependencies) {
|
|
73
|
+
const dependencyNode = nodesBySessionId.get(dependencySessionId);
|
|
74
|
+
if (!dependencyNode) {
|
|
75
|
+
errors.push(`batch node ${node.sessionId} depends on unknown session ${dependencySessionId}`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (dependencyNode.waveIndex > node.waveIndex) {
|
|
79
|
+
errors.push(
|
|
80
|
+
`batch node ${node.sessionId} depends on ${dependencySessionId} from a later wave (${dependencyNode.waveIndex} > ${node.waveIndex})`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const visiting = new Set<string>();
|
|
87
|
+
const visited = new Set<string>();
|
|
88
|
+
|
|
89
|
+
function visit(sessionId: string): void {
|
|
90
|
+
if (visiting.has(sessionId)) {
|
|
91
|
+
errors.push(`dependency cycle detected at session ${sessionId}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (visited.has(sessionId)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
visiting.add(sessionId);
|
|
99
|
+
const node = nodesBySessionId.get(sessionId);
|
|
100
|
+
if (node) {
|
|
101
|
+
for (const dependencySessionId of node.dependencies) {
|
|
102
|
+
if (nodesBySessionId.has(dependencySessionId)) {
|
|
103
|
+
visit(dependencySessionId);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
visiting.delete(sessionId);
|
|
108
|
+
visited.add(sessionId);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const node of run.nodes) {
|
|
112
|
+
visit(node.sessionId);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return [...new Set(errors)];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function buildUltraPlanBatchRunGraph(run: UltraPlanBatchRun): UltraPlanBatchRun {
|
|
119
|
+
const errors = getUltraPlanBatchGraphErrors(run);
|
|
120
|
+
if (errors.length > 0) {
|
|
121
|
+
throw new Error(errors.join("; "));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return run;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function dependenciesAreMerged(node: UltraPlanBatchNode, nodesBySessionId: Map<string, UltraPlanBatchNode>): boolean {
|
|
128
|
+
return node.dependencies.every((dependencySessionId) => nodesBySessionId.get(dependencySessionId)?.state === "merged");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isDependencyBlocked(node: UltraPlanBatchNode): boolean {
|
|
132
|
+
return node.state === "blocked" && node.blockerKind === "dependency";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function computeUltraPlanBatchEligibleFrontier(run: UltraPlanBatchRun): UltraPlanBatchNode[] {
|
|
136
|
+
const validatedRun = buildUltraPlanBatchRunGraph(run);
|
|
137
|
+
const nodesBySessionId = buildSessionNodeMap(validatedRun);
|
|
138
|
+
|
|
139
|
+
return validatedRun.nodes.filter((node) => {
|
|
140
|
+
if (node.state === "pending") {
|
|
141
|
+
return dependenciesAreMerged(node, nodesBySessionId);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (isDependencyBlocked(node)) {
|
|
145
|
+
return dependenciesAreMerged(node, nodesBySessionId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return false;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
UltraPlanBatchJournalEvent,
|
|
3
|
+
UltraPlanBatchNode,
|
|
4
|
+
UltraPlanBatchRun,
|
|
5
|
+
} from "../../types.js";
|
|
6
|
+
import { computeUltraPlanBatchEligibleFrontier } from "./planner.js";
|
|
7
|
+
|
|
8
|
+
function findDependencyNames(node: UltraPlanBatchNode): string {
|
|
9
|
+
return node.dependencies.join(", ");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function findCleanupWarning(
|
|
13
|
+
node: UltraPlanBatchNode,
|
|
14
|
+
journal: UltraPlanBatchJournalEvent[],
|
|
15
|
+
): string | null {
|
|
16
|
+
const match = journal.find((event) => event.sessionId === node.sessionId && event.type === "cleanup-warning");
|
|
17
|
+
return match?.summary ?? null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function renderUltraPlanBatchNodeSummary(
|
|
21
|
+
node: UltraPlanBatchNode,
|
|
22
|
+
_run: UltraPlanBatchRun,
|
|
23
|
+
journal: UltraPlanBatchJournalEvent[] = [],
|
|
24
|
+
): string {
|
|
25
|
+
if (node.state === "merged") {
|
|
26
|
+
const cleanupWarning = findCleanupWarning(node, journal);
|
|
27
|
+
if (cleanupWarning) {
|
|
28
|
+
return `${node.sessionId} merged with cleanup warning: ${cleanupWarning}`;
|
|
29
|
+
}
|
|
30
|
+
return `${node.sessionId} merged`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (node.state === "blocked" && node.blockerKind === "dependency") {
|
|
34
|
+
return `${node.sessionId} is waiting for dependencies: ${findDependencyNames(node)}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (node.state === "running") {
|
|
38
|
+
return `${node.sessionId} is running`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return `${node.sessionId} is ${node.state}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function renderUltraPlanBatchSummary(
|
|
45
|
+
run: UltraPlanBatchRun,
|
|
46
|
+
journal: UltraPlanBatchJournalEvent[] = [],
|
|
47
|
+
): string {
|
|
48
|
+
const lines: string[] = [];
|
|
49
|
+
if (run.state === "blocked" && run.batchBlockerCode) {
|
|
50
|
+
lines.push(`Batch blocked: ${run.batchBlockerCode}`);
|
|
51
|
+
if (run.batchBlockerSummary) {
|
|
52
|
+
lines.push(run.batchBlockerSummary);
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
lines.push(`Batch state: ${run.state}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const activeWave = run.nodes
|
|
59
|
+
.filter((node) => node.state !== "merged" && node.state !== "abandoned")
|
|
60
|
+
.map((node) => node.waveIndex)
|
|
61
|
+
.sort((left, right) => left - right)[0];
|
|
62
|
+
if (activeWave !== undefined) {
|
|
63
|
+
lines.push(`Active wave: ${activeWave}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const frontier = computeUltraPlanBatchEligibleFrontier(run).map((node) => node.sessionId);
|
|
67
|
+
if (frontier.length > 0) {
|
|
68
|
+
lines.push(`Frontier: ${frontier.join(", ")}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const runningWorkers = run.nodes.filter((node) => node.state === "running").map((node) => node.sessionId);
|
|
72
|
+
if (runningWorkers.length > 0) {
|
|
73
|
+
lines.push(`Running workers: ${runningWorkers.join(", ")}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const keptWorktrees = run.nodes
|
|
77
|
+
.filter((node) => node.worktreePath !== null)
|
|
78
|
+
.map((node) => node.worktreePath as string);
|
|
79
|
+
if (keptWorktrees.length > 0) {
|
|
80
|
+
lines.push(`Kept worktrees: ${keptWorktrees.join(", ")}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const node of run.nodes) {
|
|
84
|
+
if (node.state === "blocked" && node.blockerKind === "dependency") {
|
|
85
|
+
lines.push(renderUltraPlanBatchNodeSummary(node, run, journal));
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (node.waveIndex > activeWave!
|
|
90
|
+
&& node.state === "pending"
|
|
91
|
+
&& node.dependencies.length > 0) {
|
|
92
|
+
lines.push(`Later wave queued: ${node.sessionId} becomes eligible after ${findDependencyNames(node)} merges.`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return lines.join("\n");
|
|
97
|
+
}
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { PlatformPaths } from "../../platform/types.js";
|
|
4
|
+
import type {
|
|
5
|
+
UltraPlanBatchActiveRunLease,
|
|
6
|
+
UltraPlanBatchJournalEvent,
|
|
7
|
+
UltraPlanBatchRun,
|
|
8
|
+
UltraPlanStorageError,
|
|
9
|
+
UltraPlanStorageResult,
|
|
10
|
+
} from "../../types.js";
|
|
11
|
+
import {
|
|
12
|
+
getUltraPlanSchemaErrors,
|
|
13
|
+
UltraPlanBatchJournalEventSchema,
|
|
14
|
+
validateUltraPlanBatchActiveRunLease,
|
|
15
|
+
validateUltraPlanBatchRun,
|
|
16
|
+
} from "../contracts.js";
|
|
17
|
+
import {
|
|
18
|
+
getUltraplanActiveBatchRunPath,
|
|
19
|
+
getUltraplanBatchJournalPath,
|
|
20
|
+
getUltraplanBatchRunPath,
|
|
21
|
+
} from "../project-paths.js";
|
|
22
|
+
|
|
23
|
+
function success<T>(value: T): UltraPlanStorageResult<T> {
|
|
24
|
+
return { ok: true, value };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function failure(
|
|
28
|
+
pathname: string,
|
|
29
|
+
kind: UltraPlanStorageError["kind"],
|
|
30
|
+
message: string,
|
|
31
|
+
details?: string[],
|
|
32
|
+
): UltraPlanStorageResult<never> {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
error: {
|
|
36
|
+
kind,
|
|
37
|
+
path: pathname,
|
|
38
|
+
message,
|
|
39
|
+
...(details ? { details } : {}),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function ensureDir(filePath: string): void {
|
|
45
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readJsonFile(filePath: string): UltraPlanStorageResult<unknown> {
|
|
49
|
+
if (!fs.existsSync(filePath)) {
|
|
50
|
+
return failure(filePath, "missing", `Artifact not found: ${filePath}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
return success(JSON.parse(fs.readFileSync(filePath, "utf8")));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return failure(
|
|
57
|
+
filePath,
|
|
58
|
+
"invalid-json",
|
|
59
|
+
error instanceof Error ? error.message : `Invalid JSON in ${filePath}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeJsonFile(filePath: string, payload: unknown): UltraPlanStorageResult<string> {
|
|
65
|
+
try {
|
|
66
|
+
ensureDir(filePath);
|
|
67
|
+
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
68
|
+
return success(filePath);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return failure(
|
|
71
|
+
filePath,
|
|
72
|
+
"io",
|
|
73
|
+
error instanceof Error ? error.message : `Unable to write ${filePath}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
function validateBatchRunForStorage(
|
|
80
|
+
filePath: string,
|
|
81
|
+
value: unknown,
|
|
82
|
+
): UltraPlanStorageResult<UltraPlanBatchRun> {
|
|
83
|
+
const validation = validateUltraPlanBatchRun(value);
|
|
84
|
+
if (!validation.ok) {
|
|
85
|
+
return failure(filePath, "validation-error", `Artifact failed schema validation: ${filePath}`, validation.errors);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
return success(validation.value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function validateBatchLeaseForStorage(
|
|
93
|
+
filePath: string,
|
|
94
|
+
value: unknown,
|
|
95
|
+
): UltraPlanStorageResult<UltraPlanBatchActiveRunLease> {
|
|
96
|
+
const validation = validateUltraPlanBatchActiveRunLease(value);
|
|
97
|
+
if (!validation.ok) {
|
|
98
|
+
return failure(filePath, "validation-error", `Artifact failed schema validation: ${filePath}`, validation.errors);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
return success(validation.value);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseLeaseTimestamp(
|
|
106
|
+
filePath: string,
|
|
107
|
+
fieldName: string,
|
|
108
|
+
value: string | null,
|
|
109
|
+
): UltraPlanStorageResult<number | null> {
|
|
110
|
+
if (value === null) {
|
|
111
|
+
return success(null);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const millis = Date.parse(value);
|
|
115
|
+
if (Number.isNaN(millis)) {
|
|
116
|
+
return failure(filePath, "validation-error", `Artifact failed schema validation: ${filePath}`, [
|
|
117
|
+
`${fieldName} must be a valid ISO timestamp`,
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return success(millis);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
export function loadUltraPlanBatchRun(
|
|
126
|
+
paths: PlatformPaths,
|
|
127
|
+
cwd: string,
|
|
128
|
+
runId: string,
|
|
129
|
+
): UltraPlanStorageResult<UltraPlanBatchRun> {
|
|
130
|
+
const filePath = getUltraplanBatchRunPath(paths, cwd, runId);
|
|
131
|
+
const parsed = readJsonFile(filePath);
|
|
132
|
+
if (!parsed.ok) {
|
|
133
|
+
return parsed;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return validateBatchRunForStorage(filePath, parsed.value);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function saveUltraPlanBatchRun(
|
|
140
|
+
paths: PlatformPaths,
|
|
141
|
+
cwd: string,
|
|
142
|
+
run: UltraPlanBatchRun,
|
|
143
|
+
): UltraPlanStorageResult<string> {
|
|
144
|
+
const filePath = getUltraplanBatchRunPath(paths, cwd, run.runId);
|
|
145
|
+
const validation = validateBatchRunForStorage(filePath, run);
|
|
146
|
+
if (!validation.ok) {
|
|
147
|
+
return validation;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return writeJsonFile(filePath, validation.value);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function loadUltraPlanBatchJournal(
|
|
154
|
+
paths: PlatformPaths,
|
|
155
|
+
cwd: string,
|
|
156
|
+
runId: string,
|
|
157
|
+
): UltraPlanStorageResult<UltraPlanBatchJournalEvent[]> {
|
|
158
|
+
const filePath = getUltraplanBatchJournalPath(paths, cwd, runId);
|
|
159
|
+
if (!fs.existsSync(filePath)) {
|
|
160
|
+
return success([]);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const entries: UltraPlanBatchJournalEvent[] = [];
|
|
165
|
+
const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
166
|
+
for (let index = 0; index < lines.length; index++) {
|
|
167
|
+
const line = lines[index]!;
|
|
168
|
+
let parsed: unknown;
|
|
169
|
+
try {
|
|
170
|
+
parsed = JSON.parse(line);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return failure(
|
|
173
|
+
filePath,
|
|
174
|
+
"invalid-json",
|
|
175
|
+
error instanceof Error ? error.message : `Invalid JSON in ${filePath}`,
|
|
176
|
+
[`journal line ${index + 1} is not valid JSON`],
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const errors = getUltraPlanSchemaErrors(UltraPlanBatchJournalEventSchema, parsed);
|
|
181
|
+
if (errors.length > 0) {
|
|
182
|
+
return failure(filePath, "validation-error", `Artifact failed schema validation: ${filePath}`, errors);
|
|
183
|
+
}
|
|
184
|
+
entries.push(parsed as UltraPlanBatchJournalEvent);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return success(entries);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
return failure(
|
|
190
|
+
filePath,
|
|
191
|
+
"io",
|
|
192
|
+
error instanceof Error ? error.message : `Unable to read ${filePath}`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function appendUltraPlanBatchJournalEvent(
|
|
198
|
+
paths: PlatformPaths,
|
|
199
|
+
cwd: string,
|
|
200
|
+
runId: string,
|
|
201
|
+
event: UltraPlanBatchJournalEvent,
|
|
202
|
+
): UltraPlanStorageResult<string> {
|
|
203
|
+
const filePath = getUltraplanBatchJournalPath(paths, cwd, runId);
|
|
204
|
+
const errors = getUltraPlanSchemaErrors(UltraPlanBatchJournalEventSchema, event);
|
|
205
|
+
if (errors.length > 0) {
|
|
206
|
+
return failure(filePath, "validation-error", `Artifact failed schema validation: ${filePath}`, errors);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
ensureDir(filePath);
|
|
211
|
+
fs.appendFileSync(filePath, `${JSON.stringify(event)}\n`, "utf8");
|
|
212
|
+
return success(filePath);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
return failure(
|
|
215
|
+
filePath,
|
|
216
|
+
"io",
|
|
217
|
+
error instanceof Error ? error.message : `Unable to append ${filePath}`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function loadUltraPlanBatchActiveRunLease(
|
|
223
|
+
paths: PlatformPaths,
|
|
224
|
+
cwd: string,
|
|
225
|
+
): UltraPlanStorageResult<UltraPlanBatchActiveRunLease | null> {
|
|
226
|
+
const filePath = getUltraplanActiveBatchRunPath(paths, cwd);
|
|
227
|
+
if (!fs.existsSync(filePath)) {
|
|
228
|
+
return success(null);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const parsed = readJsonFile(filePath);
|
|
232
|
+
if (!parsed.ok) {
|
|
233
|
+
return parsed;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return validateBatchLeaseForStorage(filePath, parsed.value);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function saveUltraPlanBatchActiveRunLease(
|
|
240
|
+
paths: PlatformPaths,
|
|
241
|
+
cwd: string,
|
|
242
|
+
lease: UltraPlanBatchActiveRunLease,
|
|
243
|
+
): UltraPlanStorageResult<string> {
|
|
244
|
+
const filePath = getUltraplanActiveBatchRunPath(paths, cwd);
|
|
245
|
+
const validation = validateBatchLeaseForStorage(filePath, lease);
|
|
246
|
+
if (!validation.ok) {
|
|
247
|
+
return validation;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return writeJsonFile(filePath, validation.value);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function clearUltraPlanBatchActiveRunLease(
|
|
254
|
+
paths: PlatformPaths,
|
|
255
|
+
cwd: string,
|
|
256
|
+
): UltraPlanStorageResult<string> {
|
|
257
|
+
const filePath = getUltraplanActiveBatchRunPath(paths, cwd);
|
|
258
|
+
try {
|
|
259
|
+
if (fs.existsSync(filePath)) {
|
|
260
|
+
fs.rmSync(filePath);
|
|
261
|
+
}
|
|
262
|
+
return success(filePath);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
return failure(
|
|
265
|
+
filePath,
|
|
266
|
+
"io",
|
|
267
|
+
error instanceof Error ? error.message : `Unable to remove ${filePath}`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function loadUltraPlanActiveBatchRun(
|
|
273
|
+
paths: PlatformPaths,
|
|
274
|
+
cwd: string,
|
|
275
|
+
): UltraPlanStorageResult<UltraPlanBatchRun | null> {
|
|
276
|
+
const lease = loadUltraPlanBatchActiveRunLease(paths, cwd);
|
|
277
|
+
if (!lease.ok) {
|
|
278
|
+
return lease;
|
|
279
|
+
}
|
|
280
|
+
if (lease.value === null) {
|
|
281
|
+
return success(null);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return loadUltraPlanBatchRun(paths, cwd, lease.value.runId);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function acquireUltraPlanBatchActiveRunLease(
|
|
288
|
+
paths: PlatformPaths,
|
|
289
|
+
cwd: string,
|
|
290
|
+
lease: UltraPlanBatchActiveRunLease,
|
|
291
|
+
options?: { nowIso?: string },
|
|
292
|
+
): UltraPlanStorageResult<UltraPlanBatchActiveRunLease> {
|
|
293
|
+
const filePath = getUltraplanActiveBatchRunPath(paths, cwd);
|
|
294
|
+
const validation = validateBatchLeaseForStorage(filePath, lease);
|
|
295
|
+
if (!validation.ok) {
|
|
296
|
+
return validation;
|
|
297
|
+
}
|
|
298
|
+
if (
|
|
299
|
+
validation.value.ownerSessionId === null
|
|
300
|
+
|| validation.value.leaseAcquiredAt === null
|
|
301
|
+
|| validation.value.leaseExpiresAt === null
|
|
302
|
+
) {
|
|
303
|
+
return failure(filePath, "validation-error", `Artifact failed schema validation: ${filePath}`, [
|
|
304
|
+
"acquired lease must include ownerSessionId, leaseAcquiredAt, and leaseExpiresAt",
|
|
305
|
+
]);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const trustedNow = parseLeaseTimestamp(filePath, "nowIso", options?.nowIso ?? new Date().toISOString());
|
|
309
|
+
if (!trustedNow.ok) {
|
|
310
|
+
return trustedNow;
|
|
311
|
+
}
|
|
312
|
+
const requestedAcquiredAt = parseLeaseTimestamp(filePath, "leaseAcquiredAt", validation.value.leaseAcquiredAt);
|
|
313
|
+
if (!requestedAcquiredAt.ok) {
|
|
314
|
+
return requestedAcquiredAt;
|
|
315
|
+
}
|
|
316
|
+
const requestedExpiresAt = parseLeaseTimestamp(filePath, "leaseExpiresAt", validation.value.leaseExpiresAt);
|
|
317
|
+
if (!requestedExpiresAt.ok) {
|
|
318
|
+
return requestedExpiresAt;
|
|
319
|
+
}
|
|
320
|
+
if ((requestedAcquiredAt.value ?? 0) > (trustedNow.value ?? 0)) {
|
|
321
|
+
return failure(filePath, "validation-error", `Artifact failed schema validation: ${filePath}`, [
|
|
322
|
+
"leaseAcquiredAt cannot be in the future relative to nowIso",
|
|
323
|
+
]);
|
|
324
|
+
}
|
|
325
|
+
if ((requestedExpiresAt.value ?? 0) <= (trustedNow.value ?? 0)) {
|
|
326
|
+
return failure(filePath, "validation-error", `Artifact failed schema validation: ${filePath}`, [
|
|
327
|
+
"leaseExpiresAt must be in the future relative to nowIso",
|
|
328
|
+
]);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const current = loadUltraPlanBatchActiveRunLease(paths, cwd);
|
|
332
|
+
if (!current.ok) {
|
|
333
|
+
return current;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const currentLease = current.value;
|
|
337
|
+
if (currentLease !== null) {
|
|
338
|
+
const currentIsHeld = currentLease.ownerSessionId !== null
|
|
339
|
+
&& currentLease.leaseAcquiredAt !== null
|
|
340
|
+
&& currentLease.leaseExpiresAt !== null;
|
|
341
|
+
const sameOwner = currentLease.runId === validation.value.runId
|
|
342
|
+
&& currentLease.ownerSessionId === validation.value.ownerSessionId;
|
|
343
|
+
if (currentIsHeld && !sameOwner) {
|
|
344
|
+
const existingExpiry = parseLeaseTimestamp(filePath, "leaseExpiresAt", currentLease.leaseExpiresAt);
|
|
345
|
+
if (!existingExpiry.ok) {
|
|
346
|
+
return existingExpiry;
|
|
347
|
+
}
|
|
348
|
+
if ((existingExpiry.value ?? 0) > (trustedNow.value ?? 0)) {
|
|
349
|
+
return failure(filePath, "validation-error", `Artifact failed schema validation: ${filePath}`, [
|
|
350
|
+
`active-run lease already held by ${currentLease.ownerSessionId}`,
|
|
351
|
+
]);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const saved = saveUltraPlanBatchActiveRunLease(paths, cwd, validation.value);
|
|
357
|
+
if (!saved.ok) {
|
|
358
|
+
return saved;
|
|
359
|
+
}
|
|
360
|
+
return success(validation.value);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function releaseUltraPlanBatchActiveRunLease(
|
|
364
|
+
paths: PlatformPaths,
|
|
365
|
+
cwd: string,
|
|
366
|
+
leaseOwner: Pick<UltraPlanBatchActiveRunLease, "runId" | "ownerSessionId">,
|
|
367
|
+
nextState: UltraPlanBatchRun["state"],
|
|
368
|
+
releasedAt: string,
|
|
369
|
+
): UltraPlanStorageResult<string> {
|
|
370
|
+
const filePath = getUltraplanActiveBatchRunPath(paths, cwd);
|
|
371
|
+
if (!leaseOwner.ownerSessionId) {
|
|
372
|
+
return failure(filePath, "validation-error", `Artifact failed schema validation: ${filePath}`, [
|
|
373
|
+
"release requires the persisted ownerSessionId",
|
|
374
|
+
]);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const current = loadUltraPlanBatchActiveRunLease(paths, cwd);
|
|
378
|
+
if (!current.ok) {
|
|
379
|
+
return current;
|
|
380
|
+
}
|
|
381
|
+
if (current.value === null) {
|
|
382
|
+
return failure(filePath, "validation-error", `Artifact failed schema validation: ${filePath}`, [
|
|
383
|
+
"active-run lease is missing",
|
|
384
|
+
]);
|
|
385
|
+
}
|
|
386
|
+
if (current.value.runId !== leaseOwner.runId) {
|
|
387
|
+
return failure(filePath, "validation-error", `Artifact failed schema validation: ${filePath}`, [
|
|
388
|
+
`active-run lease points at ${current.value.runId}, not ${leaseOwner.runId}`,
|
|
389
|
+
]);
|
|
390
|
+
}
|
|
391
|
+
if (current.value.ownerSessionId !== leaseOwner.ownerSessionId) {
|
|
392
|
+
return failure(filePath, "validation-error", `Artifact failed schema validation: ${filePath}`, [
|
|
393
|
+
`active-run lease is owned by ${current.value.ownerSessionId}, not ${leaseOwner.ownerSessionId}`,
|
|
394
|
+
]);
|
|
395
|
+
}
|
|
396
|
+
if (current.value.leaseAcquiredAt === null || current.value.leaseExpiresAt === null) {
|
|
397
|
+
return failure(filePath, "validation-error", `Artifact failed schema validation: ${filePath}`, [
|
|
398
|
+
"active-run lease is not currently held",
|
|
399
|
+
]);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (nextState === "complete" || nextState === "abandoned") {
|
|
403
|
+
return clearUltraPlanBatchActiveRunLease(paths, cwd);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (nextState !== "paused" && nextState !== "blocked") {
|
|
407
|
+
return failure(filePath, "validation-error", `Artifact failed schema validation: ${filePath}`, [
|
|
408
|
+
`cannot release active-run lease for state ${nextState}`,
|
|
409
|
+
]);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const releasedLease: UltraPlanBatchActiveRunLease = {
|
|
413
|
+
runId: leaseOwner.runId,
|
|
414
|
+
ownerSessionId: null,
|
|
415
|
+
leaseAcquiredAt: null,
|
|
416
|
+
leaseExpiresAt: null,
|
|
417
|
+
updatedAt: releasedAt,
|
|
418
|
+
};
|
|
419
|
+
return saveUltraPlanBatchActiveRunLease(paths, cwd, releasedLease);
|
|
420
|
+
}
|