@backburner/cli 0.1.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/LICENSE +21 -0
- package/README.md +329 -0
- package/dist/src/agents/antigravity-launch.js +162 -0
- package/dist/src/agents/antigravity.js +251 -0
- package/dist/src/agents/claude-launch.js +117 -0
- package/dist/src/agents/claude.js +271 -0
- package/dist/src/agents/codex-launch.js +82 -0
- package/dist/src/agents/codex.js +261 -0
- package/dist/src/agents/composite-runner.js +13 -0
- package/dist/src/agents/extraction.js +442 -0
- package/dist/src/agents/failure-feedback.js +132 -0
- package/dist/src/agents/gemini-launch.js +96 -0
- package/dist/src/agents/gemini.js +392 -0
- package/dist/src/agents/job-agent-assignments.js +180 -0
- package/dist/src/agents/lifecycle-feedback.js +147 -0
- package/dist/src/agents/limit-detector.js +83 -0
- package/dist/src/agents/opencode-launch.js +138 -0
- package/dist/src/agents/opencode.js +156 -0
- package/dist/src/agents/plan-breakdown-feedback.js +97 -0
- package/dist/src/agents/pool.js +168 -0
- package/dist/src/agents/prompt.js +462 -0
- package/dist/src/agents/provider-sessions.js +105 -0
- package/dist/src/agents/review-feedback.js +255 -0
- package/dist/src/agents/selection.js +113 -0
- package/dist/src/agents/service.js +1087 -0
- package/dist/src/agents/types.js +1 -0
- package/dist/src/agents/wave-assessment-feedback.js +69 -0
- package/dist/src/attention/derive.js +337 -0
- package/dist/src/capabilities/index.js +2 -0
- package/dist/src/capabilities/projection.js +183 -0
- package/dist/src/capabilities/types.js +1 -0
- package/dist/src/capabilities/validator.js +87 -0
- package/dist/src/cli/commands/broker-smoke.js +96 -0
- package/dist/src/cli/commands/broker.js +56 -0
- package/dist/src/cli/commands/init.js +4 -0
- package/dist/src/cli/commands/journal.js +43 -0
- package/dist/src/cli/commands/run-loop.js +56 -0
- package/dist/src/cli/commands/run.js +1241 -0
- package/dist/src/cli/commands/tui.js +187 -0
- package/dist/src/cli/dispatcher.js +51 -0
- package/dist/src/cli/errors.js +2 -0
- package/dist/src/cli/init.js +92 -0
- package/dist/src/cli/options.js +86 -0
- package/dist/src/cli/output.js +231 -0
- package/dist/src/cli/run-journal.js +342 -0
- package/dist/src/cli/run-types.js +1 -0
- package/dist/src/cli/run.js +32 -0
- package/dist/src/cli/runtime/dispatch-lifecycle.js +772 -0
- package/dist/src/cli/runtime/provider-runtime-types.js +1 -0
- package/dist/src/cli/runtime/provider-runtime.js +152 -0
- package/dist/src/cli/runtime/provider-tools.js +73 -0
- package/dist/src/cli/runtime/providers/antigravity.js +26 -0
- package/dist/src/cli/runtime/providers/claude.js +20 -0
- package/dist/src/cli/runtime/providers/codex.js +20 -0
- package/dist/src/cli/runtime/providers/gemini.js +20 -0
- package/dist/src/cli/runtime/providers/opencode.js +20 -0
- package/dist/src/cli/runtime/run-context.js +131 -0
- package/dist/src/cli/tui/index.js +3 -0
- package/dist/src/cli/tui/screen.js +388 -0
- package/dist/src/cli/tui/tracker.js +558 -0
- package/dist/src/cli/tui/types.js +1 -0
- package/dist/src/cli/tui/views.js +1006 -0
- package/dist/src/config/loader.js +136 -0
- package/dist/src/config/schemas.js +403 -0
- package/dist/src/config/types.js +1 -0
- package/dist/src/context/types.js +331 -0
- package/dist/src/domain/entities.js +1 -0
- package/dist/src/git/backburner-git-tool-service.js +680 -0
- package/dist/src/git/gitops-lifecycle.js +56 -0
- package/dist/src/git/merge-prep-workspace.js +357 -0
- package/dist/src/git/types.js +484 -0
- package/dist/src/github/broker-model-content.js +215 -0
- package/dist/src/github/broker-server.js +237 -0
- package/dist/src/github/broker.js +638 -0
- package/dist/src/github/composed.js +53 -0
- package/dist/src/github/gateway.js +478 -0
- package/dist/src/github/normalize.js +222 -0
- package/dist/src/github/pr-classification.js +73 -0
- package/dist/src/github/scoped-broker.js +35 -0
- package/dist/src/github/security.js +126 -0
- package/dist/src/github/service.js +332 -0
- package/dist/src/github/types.js +1 -0
- package/dist/src/github/utils.js +16 -0
- package/dist/src/journal/index.js +4 -0
- package/dist/src/journal/reader.js +62 -0
- package/dist/src/journal/renderer.js +50 -0
- package/dist/src/journal/service.js +72 -0
- package/dist/src/journal/types.js +1 -0
- package/dist/src/journal/writer.js +19 -0
- package/dist/src/mcps/bridge.js +166 -0
- package/dist/src/mcps/index.js +5 -0
- package/dist/src/mcps/reconcile.js +113 -0
- package/dist/src/mcps/smithery-client.js +186 -0
- package/dist/src/mcps/smithery.js +164 -0
- package/dist/src/mcps/types.js +1 -0
- package/dist/src/memory/json-store.js +71 -0
- package/dist/src/memory/prompt.js +16 -0
- package/dist/src/memory/scope.js +31 -0
- package/dist/src/memory/store.js +1 -0
- package/dist/src/memory/tool-service.js +142 -0
- package/dist/src/memory/types.js +12 -0
- package/dist/src/onboarding/config-writer.js +96 -0
- package/dist/src/onboarding/engine.js +86 -0
- package/dist/src/onboarding/prompt.js +102 -0
- package/dist/src/onboarding/services/provider-discovery.js +58 -0
- package/dist/src/onboarding/services/repo-discovery.js +246 -0
- package/dist/src/onboarding/steps/check-github-auth.js +32 -0
- package/dist/src/onboarding/steps/check-requirements.js +34 -0
- package/dist/src/onboarding/steps/configure-models.js +105 -0
- package/dist/src/onboarding/steps/configure-paths.js +33 -0
- package/dist/src/onboarding/steps/discover-providers.js +38 -0
- package/dist/src/onboarding/steps/discover-repos.js +39 -0
- package/dist/src/onboarding/steps/generate-config.js +106 -0
- package/dist/src/onboarding/steps/readiness-validation.js +158 -0
- package/dist/src/onboarding/steps/select-repos.js +76 -0
- package/dist/src/onboarding/steps/show-summary.js +97 -0
- package/dist/src/onboarding/steps/welcome.js +27 -0
- package/dist/src/onboarding/types.js +1 -0
- package/dist/src/onboarding/validators/git-access.js +63 -0
- package/dist/src/onboarding/validators/github-auth.js +85 -0
- package/dist/src/onboarding/validators/tool-checker.js +153 -0
- package/dist/src/onboarding/validators/workspace.js +83 -0
- package/dist/src/prompts/resolver.js +106 -0
- package/dist/src/retrospectives/candidates.js +224 -0
- package/dist/src/retrospectives/derive.js +169 -0
- package/dist/src/retrospectives/ingest.js +321 -0
- package/dist/src/retrospectives/management-prs.js +380 -0
- package/dist/src/retrospectives/proposals.js +199 -0
- package/dist/src/retrospectives/scopes.js +272 -0
- package/dist/src/retrospectives/scoring.js +171 -0
- package/dist/src/retrospectives/types.js +1 -0
- package/dist/src/state/loader.js +473 -0
- package/dist/src/state/types.js +15 -0
- package/dist/src/tasks/derivation/builders.js +70 -0
- package/dist/src/tasks/derivation/handlers/comment-response.js +169 -0
- package/dist/src/tasks/derivation/handlers/implement-plan.js +103 -0
- package/dist/src/tasks/derivation/handlers/index.js +54 -0
- package/dist/src/tasks/derivation/handlers/packet-pr-control.js +92 -0
- package/dist/src/tasks/derivation/handlers/packet-worktree.js +29 -0
- package/dist/src/tasks/derivation/handlers/plan-breakdown.js +45 -0
- package/dist/src/tasks/derivation/handlers/prepare-for-merge.js +91 -0
- package/dist/src/tasks/derivation/handlers/product-discovery.js +36 -0
- package/dist/src/tasks/derivation/handlers/retry-failed-task.js +125 -0
- package/dist/src/tasks/derivation/handlers/review-pr.js +28 -0
- package/dist/src/tasks/derivation/handlers/shared.js +24 -0
- package/dist/src/tasks/derivation/handlers/sync-parent-branch.js +100 -0
- package/dist/src/tasks/derivation/handlers/wave-assessment.js +223 -0
- package/dist/src/tasks/derivation/handlers/write-plan.js +101 -0
- package/dist/src/tasks/derivation/handlers/write-product-spec.js +49 -0
- package/dist/src/tasks/derivation/runner.js +18 -0
- package/dist/src/tasks/derivation/types.js +1 -0
- package/dist/src/tasks/derive.js +810 -0
- package/dist/src/tasks/mcp-skip.js +10 -0
- package/dist/src/tasks/retry.js +10 -0
- package/dist/src/tasks/task.js +184 -0
- package/dist/src/tasks/types.js +1 -0
- package/dist/src/utils/command-runner.js +192 -0
- package/dist/src/utils/json.js +38 -0
- package/dist/src/utils/logger.js +43 -0
- package/dist/src/utils/paths.js +38 -0
- package/dist/src/utils/slug.js +10 -0
- package/dist/src/workflows/registry.js +174 -0
- package/dist/src/workflows/triage.js +90 -0
- package/dist/src/workflows/workstream-machine/derive-events.js +121 -0
- package/dist/src/workflows/workstream-machine/evaluate.js +24 -0
- package/dist/src/workflows/workstream-machine/events.js +1 -0
- package/dist/src/workflows/workstream-machine/machine.js +355 -0
- package/dist/src/workflows/workstream-machine/selectors.js +19 -0
- package/dist/src/workflows/workstream-machine/types.js +1 -0
- package/dist/src/workstreams/branch-sync.js +87 -0
- package/dist/src/workstreams/derive.js +629 -0
- package/dist/src/workstreams/mcp-blockers.js +420 -0
- package/dist/src/workstreams/packet-candidates.js +42 -0
- package/dist/src/workstreams/packet-lifecycle-feedback.js +60 -0
- package/dist/src/workstreams/packet-lifecycle-report.js +199 -0
- package/dist/src/workstreams/packet-plan.js +597 -0
- package/dist/src/workstreams/packet-projections.js +190 -0
- package/dist/src/workstreams/parent-branch-sync-action.js +312 -0
- package/dist/src/workstreams/parent-branch-sync.js +148 -0
- package/dist/src/workstreams/phases.js +4 -0
- package/dist/src/workstreams/plan-breakdown-generator/export.js +19 -0
- package/dist/src/workstreams/plan-breakdown-generator/index.js +6 -0
- package/dist/src/workstreams/plan-breakdown-generator/inspect.js +50 -0
- package/dist/src/workstreams/plan-breakdown-generator/patch.js +139 -0
- package/dist/src/workstreams/plan-breakdown-generator/session.js +19 -0
- package/dist/src/workstreams/plan-breakdown-generator/store.js +27 -0
- package/dist/src/workstreams/plan-breakdown-generator/tools.js +407 -0
- package/dist/src/workstreams/plan-breakdown-generator/types.js +6 -0
- package/dist/src/workstreams/plan-breakdown-generator/validate.js +212 -0
- package/dist/src/workstreams/plan-breakdown-schema.js +175 -0
- package/dist/src/workstreams/utils.js +15 -0
- package/dist/src/workstreams/wave-assessment/checkpoint.js +172 -0
- package/dist/src/workstreams/wave-assessment/index.js +3 -0
- package/dist/src/workstreams/wave-assessment/session.js +84 -0
- package/dist/src/workstreams/wave-assessment/store.js +51 -0
- package/dist/src/workstreams/wave-assessment/tools.js +419 -0
- package/dist/src/workstreams/wave-assessment/types.js +1 -0
- package/dist/src/workstreams/wave-assessment/validate.js +78 -0
- package/dist/src/worktrees/service.js +839 -0
- package/dist/src/worktrees/workspace-ownership.js +34 -0
- package/dist/src/worktrees/workspace-recovery.js +177 -0
- package/package.json +61 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a PlanBreakdownResult into a WorkstreamPacketPlan.
|
|
3
|
+
*/
|
|
4
|
+
export function buildPacketPlanFromPlanBreakdown(input) {
|
|
5
|
+
const { workstreamId, planBreakdown, generatedAt } = input;
|
|
6
|
+
const packets = {};
|
|
7
|
+
const seenTaskIds = new Set();
|
|
8
|
+
for (const task of planBreakdown.tasks) {
|
|
9
|
+
if (!task.id) {
|
|
10
|
+
throw new Error("Task ID must not be empty in plan breakdown");
|
|
11
|
+
}
|
|
12
|
+
if (seenTaskIds.has(task.id)) {
|
|
13
|
+
throw new Error(`Duplicate task ID found in plan breakdown: ${task.id}`);
|
|
14
|
+
}
|
|
15
|
+
seenTaskIds.add(task.id);
|
|
16
|
+
packets[task.id] = {
|
|
17
|
+
id: task.id,
|
|
18
|
+
title: task.title,
|
|
19
|
+
summary: task.description,
|
|
20
|
+
acceptanceCriteria: task.acceptanceCriteria,
|
|
21
|
+
filesLikelyTouched: task.filesLikelyInvolved,
|
|
22
|
+
dependsOn: task.dependsOn,
|
|
23
|
+
canRunInParallelWith: task.canRunInParallelWith,
|
|
24
|
+
blocks: task.blocks,
|
|
25
|
+
status: "planned"
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const waves = planBreakdown.waves.map((wave) => wave.taskIds);
|
|
29
|
+
const packetPlan = {
|
|
30
|
+
status: "proposed",
|
|
31
|
+
packets,
|
|
32
|
+
waves,
|
|
33
|
+
currentWaveIndex: 0,
|
|
34
|
+
source: {
|
|
35
|
+
kind: "plan_breakdown_result",
|
|
36
|
+
workstreamId,
|
|
37
|
+
generatedAt
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const errors = validatePacketPlan(packetPlan);
|
|
41
|
+
if (errors.length > 0) {
|
|
42
|
+
throw new Error(`Invalid packet plan: ${errors.join("; ")}`);
|
|
43
|
+
}
|
|
44
|
+
return packetPlan;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Validates a WorkstreamPacketPlan for consistency.
|
|
48
|
+
*/
|
|
49
|
+
export function validatePacketPlan(packetPlan) {
|
|
50
|
+
const errors = [];
|
|
51
|
+
const packetIds = new Set(Object.keys(packetPlan.packets));
|
|
52
|
+
// 1. Every packet ID must be non-empty and unique.
|
|
53
|
+
// (Unique is guaranteed by Record keys, but we check if each packet.id matches the key)
|
|
54
|
+
for (const [id, packet] of Object.entries(packetPlan.packets)) {
|
|
55
|
+
if (!id) {
|
|
56
|
+
errors.push("Packet ID must not be empty");
|
|
57
|
+
}
|
|
58
|
+
if (packet.id !== id) {
|
|
59
|
+
errors.push(`Packet ID mismatch: dictionary key "${id}" does not match packet.id "${packet.id}"`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// 2. waves must be a non-empty array.
|
|
63
|
+
if (!Array.isArray(packetPlan.waves) || packetPlan.waves.length === 0) {
|
|
64
|
+
errors.push("Waves must be a non-empty array");
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const seenInWaves = new Set();
|
|
68
|
+
packetPlan.waves.forEach((wave, waveIndex) => {
|
|
69
|
+
// 3. Each wave must be a non-empty array.
|
|
70
|
+
if (!Array.isArray(wave) || wave.length === 0) {
|
|
71
|
+
errors.push(`Wave ${waveIndex} must be a non-empty array`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
wave.forEach((packetId) => {
|
|
75
|
+
// 4. Every packet ID referenced by a wave must exist in packets.
|
|
76
|
+
if (!packetIds.has(packetId)) {
|
|
77
|
+
errors.push(`Wave ${waveIndex} references unknown packet ID: "${packetId}"`);
|
|
78
|
+
}
|
|
79
|
+
// 5. A packet ID must appear in exactly one wave.
|
|
80
|
+
if (seenInWaves.has(packetId)) {
|
|
81
|
+
errors.push(`Packet ID "${packetId}" appears in more than one wave`);
|
|
82
|
+
}
|
|
83
|
+
seenInWaves.add(packetId);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
// 6. Every packet in packets must appear in exactly one wave.
|
|
87
|
+
for (const id of packetIds) {
|
|
88
|
+
if (!seenInWaves.has(id)) {
|
|
89
|
+
errors.push(`Packet ID "${id}" is missing from all waves`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Validation for dependencies and other references
|
|
94
|
+
for (const packet of Object.values(packetPlan.packets)) {
|
|
95
|
+
const checkRefs = (refs, type) => {
|
|
96
|
+
const seenRefs = new Set();
|
|
97
|
+
for (const ref of refs) {
|
|
98
|
+
// 7. A packet must not depend on itself.
|
|
99
|
+
if (ref === packet.id) {
|
|
100
|
+
errors.push(`Packet "${packet.id}" has a self-reference in ${type}`);
|
|
101
|
+
}
|
|
102
|
+
// 8. dependsOn, blocks, and canRunInParallelWith references must point to existing packet IDs.
|
|
103
|
+
if (!packetIds.has(ref)) {
|
|
104
|
+
errors.push(`Packet "${packet.id}" references unknown packet ID "${ref}" in ${type}`);
|
|
105
|
+
}
|
|
106
|
+
// 10. Do not allow duplicate IDs inside dependsOn, blocks, or canRunInParallelWith.
|
|
107
|
+
if (seenRefs.has(ref)) {
|
|
108
|
+
errors.push(`Packet "${packet.id}" has duplicate reference "${ref}" in ${type}`);
|
|
109
|
+
}
|
|
110
|
+
seenRefs.add(ref);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
checkRefs(packet.dependsOn, "dependsOn");
|
|
114
|
+
checkRefs(packet.blocks, "blocks");
|
|
115
|
+
checkRefs(packet.canRunInParallelWith, "canRunInParallelWith");
|
|
116
|
+
// 9. If packet A depends on packet B, B must appear in an earlier wave than A.
|
|
117
|
+
// A wave is a dispatch batch; same-wave dependencies would let dependent
|
|
118
|
+
// packets dispatch together.
|
|
119
|
+
const waveOfA = packetPlan.waves.findIndex((wave) => wave.includes(packet.id));
|
|
120
|
+
for (const depId of packet.dependsOn) {
|
|
121
|
+
const waveOfB = packetPlan.waves.findIndex((wave) => wave.includes(depId));
|
|
122
|
+
if (waveOfB !== -1 && waveOfA !== -1 && waveOfB === waveOfA) {
|
|
123
|
+
errors.push(`Packet "${packet.id}" (wave ${waveOfA}) depends on "${depId}" in the same wave; dependent packets must be split into later waves`);
|
|
124
|
+
}
|
|
125
|
+
else if (waveOfB !== -1 && waveOfA !== -1 && waveOfB > waveOfA) {
|
|
126
|
+
errors.push(`Packet "${packet.id}" (wave ${waveOfA}) depends on "${depId}" which is in a later wave (${waveOfB})`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return errors;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Updates a workstream list from completed plan_breakdown and wave_plan_replanning executions.
|
|
134
|
+
*/
|
|
135
|
+
export function applyPlanBreakdownResultsToWorkstreams(input) {
|
|
136
|
+
const { workstreams, tasks, executions } = input;
|
|
137
|
+
// 1. Identify relevant executions
|
|
138
|
+
const relevantExecutions = executions.filter((e) => isPacketPlanResultExecution(e) &&
|
|
139
|
+
e.taskStatus === "completed" &&
|
|
140
|
+
e.result?.planBreakdown);
|
|
141
|
+
if (relevantExecutions.length === 0) {
|
|
142
|
+
return workstreams;
|
|
143
|
+
}
|
|
144
|
+
// 2. Map executions to workstreams
|
|
145
|
+
const latestExecutionPerWorkstream = new Map();
|
|
146
|
+
for (const execution of relevantExecutions) {
|
|
147
|
+
const task = tasks.find((t) => t.id === execution.taskId);
|
|
148
|
+
if (!task)
|
|
149
|
+
continue;
|
|
150
|
+
if (execution.taskKind === "wave_plan_replanning" && task.status !== "completed") {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const workstreamId = task.workstreamId;
|
|
154
|
+
if (!workstreamId || !workstreams.some((ws) => ws.id === workstreamId)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const existing = latestExecutionPerWorkstream.get(workstreamId);
|
|
158
|
+
if (!existing || isExecutionNewer(execution, existing)) {
|
|
159
|
+
latestExecutionPerWorkstream.set(workstreamId, execution);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// 3. Apply to workstreams
|
|
163
|
+
return workstreams.map((ws) => {
|
|
164
|
+
const execution = latestExecutionPerWorkstream.get(ws.id);
|
|
165
|
+
if (!execution || !execution.result?.planBreakdown) {
|
|
166
|
+
return ws;
|
|
167
|
+
}
|
|
168
|
+
// Choose the latest valid plan-breakdown execution
|
|
169
|
+
try {
|
|
170
|
+
const newPacketPlan = buildPacketPlanFromPlanBreakdown({
|
|
171
|
+
workstreamId: ws.id,
|
|
172
|
+
planBreakdown: execution.result.planBreakdown,
|
|
173
|
+
generatedAt: execution.endedAt
|
|
174
|
+
});
|
|
175
|
+
// Replace existing packetPlan only if the relevant execution is newer or there is no existing plan.
|
|
176
|
+
// Replanning executions may replace older plans that lack generatedAt because they are explicit
|
|
177
|
+
// orchestrator-owned plan revisions.
|
|
178
|
+
if (shouldReplacePacketPlanFromExecution(ws, execution)) {
|
|
179
|
+
const packetPlan = execution.taskKind === "wave_plan_replanning"
|
|
180
|
+
? {
|
|
181
|
+
...newPacketPlan,
|
|
182
|
+
status: shouldPreserveReplannedPacketPlanStatus(ws.packetPlan?.status)
|
|
183
|
+
? ws.packetPlan.status
|
|
184
|
+
: newPacketPlan.status,
|
|
185
|
+
waveAssessments: ws.packetPlan?.waveAssessments
|
|
186
|
+
}
|
|
187
|
+
: newPacketPlan;
|
|
188
|
+
return {
|
|
189
|
+
...ws,
|
|
190
|
+
packetPlan
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
// Preserve existing if conversion fails
|
|
196
|
+
console.error(`Failed to apply plan breakdown result to workstream ${ws.id}:`, err);
|
|
197
|
+
}
|
|
198
|
+
return ws;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function isPacketPlanResultExecution(execution) {
|
|
202
|
+
return execution.taskKind === "plan_breakdown" || execution.taskKind === "wave_plan_replanning";
|
|
203
|
+
}
|
|
204
|
+
function shouldReplacePacketPlanFromExecution(workstream, execution) {
|
|
205
|
+
if (!workstream.packetPlan) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
const existingGeneratedAt = workstream.packetPlan.source?.generatedAt;
|
|
209
|
+
if (execution.taskKind === "wave_plan_replanning") {
|
|
210
|
+
return !existingGeneratedAt || isExecutionNewerThanGeneratedAt(execution.endedAt, existingGeneratedAt);
|
|
211
|
+
}
|
|
212
|
+
return Boolean(existingGeneratedAt && execution.endedAt > existingGeneratedAt);
|
|
213
|
+
}
|
|
214
|
+
function shouldPreserveReplannedPacketPlanStatus(status) {
|
|
215
|
+
return status === "approved" || status === "in_progress";
|
|
216
|
+
}
|
|
217
|
+
function isExecutionNewerThanGeneratedAt(executionEndedAt, generatedAt) {
|
|
218
|
+
const executionTime = Date.parse(executionEndedAt);
|
|
219
|
+
const generatedTime = Date.parse(generatedAt);
|
|
220
|
+
if (Number.isFinite(executionTime) && Number.isFinite(generatedTime)) {
|
|
221
|
+
return executionTime > generatedTime;
|
|
222
|
+
}
|
|
223
|
+
if (!Number.isFinite(generatedTime)) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
return executionEndedAt > generatedAt;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Returns the current wave index for a packet plan, defaulting to 0.
|
|
230
|
+
*/
|
|
231
|
+
export function getCurrentWaveIndex(packetPlan) {
|
|
232
|
+
return packetPlan.currentWaveIndex ?? 0;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Returns the packet IDs for the current wave.
|
|
236
|
+
*/
|
|
237
|
+
export function getCurrentWavePacketIds(packetPlan) {
|
|
238
|
+
const waveIndex = getCurrentWaveIndex(packetPlan);
|
|
239
|
+
if (waveIndex < 0 || waveIndex >= packetPlan.waves.length) {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
return packetPlan.waves[waveIndex] ?? [];
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Returns true when the current wave exists, is non-empty, and every packet
|
|
246
|
+
* in that wave has been merged to the parent integration branch.
|
|
247
|
+
*/
|
|
248
|
+
export function isCurrentWaveComplete(packetPlan) {
|
|
249
|
+
const waveIndex = getCurrentWaveIndex(packetPlan);
|
|
250
|
+
if (waveIndex < 0 || waveIndex >= packetPlan.waves.length) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
const currentWave = packetPlan.waves[waveIndex];
|
|
254
|
+
if (!currentWave || currentWave.length === 0) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
return currentWave.every((packetId) => {
|
|
258
|
+
const packet = packetPlan.packets[packetId];
|
|
259
|
+
return packet?.status === "merged_to_parent";
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Returns true if the packet plan has at least one wave after the current wave.
|
|
264
|
+
*/
|
|
265
|
+
export function hasFutureWave(packetPlan) {
|
|
266
|
+
const waveIndex = getCurrentWaveIndex(packetPlan);
|
|
267
|
+
return waveIndex >= 0 && waveIndex < packetPlan.waves.length - 1;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Previews what would happen if a packet-plan workstream advanced from its current wave
|
|
271
|
+
* to the next wave. Returns a preview object detailing the state and blockages.
|
|
272
|
+
*/
|
|
273
|
+
export function buildPacketWaveAdvancePreview(input) {
|
|
274
|
+
const { packetPlan } = input;
|
|
275
|
+
if (!packetPlan) {
|
|
276
|
+
return {
|
|
277
|
+
canAdvance: false,
|
|
278
|
+
blockedReason: "no_packet_plan",
|
|
279
|
+
currentWavePacketIds: [],
|
|
280
|
+
completedCurrentWavePacketIds: [],
|
|
281
|
+
incompleteCurrentWavePacketIds: [],
|
|
282
|
+
nextWavePacketIds: [],
|
|
283
|
+
remainingFuturePacketIds: []
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const currentWaveIndex = getCurrentWaveIndex(packetPlan);
|
|
287
|
+
const nextWaveIndex = currentWaveIndex + 1;
|
|
288
|
+
const currentWave = packetPlan.waves[currentWaveIndex];
|
|
289
|
+
if (currentWaveIndex < 0 ||
|
|
290
|
+
currentWaveIndex >= packetPlan.waves.length ||
|
|
291
|
+
!currentWave ||
|
|
292
|
+
currentWave.length === 0) {
|
|
293
|
+
return {
|
|
294
|
+
canAdvance: false,
|
|
295
|
+
blockedReason: "invalid_current_wave_index",
|
|
296
|
+
currentWaveIndex,
|
|
297
|
+
nextWaveIndex,
|
|
298
|
+
currentWavePacketIds: [],
|
|
299
|
+
completedCurrentWavePacketIds: [],
|
|
300
|
+
incompleteCurrentWavePacketIds: [],
|
|
301
|
+
nextWavePacketIds: [],
|
|
302
|
+
remainingFuturePacketIds: []
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const currentWavePacketIds = [...currentWave];
|
|
306
|
+
const completedCurrentWavePacketIds = currentWavePacketIds.filter((packetId) => packetPlan.packets[packetId]?.status === "merged_to_parent");
|
|
307
|
+
const incompleteCurrentWavePacketIds = currentWavePacketIds.filter((packetId) => packetPlan.packets[packetId]?.status !== "merged_to_parent");
|
|
308
|
+
const nextWavePacketIds = [...(packetPlan.waves[nextWaveIndex] ?? [])];
|
|
309
|
+
const remainingFuturePacketIds = packetPlan.waves.slice(nextWaveIndex + 1).flat();
|
|
310
|
+
if (!isCurrentWaveComplete(packetPlan)) {
|
|
311
|
+
return {
|
|
312
|
+
canAdvance: false,
|
|
313
|
+
blockedReason: "current_wave_incomplete",
|
|
314
|
+
currentWaveIndex,
|
|
315
|
+
nextWaveIndex,
|
|
316
|
+
currentWavePacketIds,
|
|
317
|
+
completedCurrentWavePacketIds,
|
|
318
|
+
incompleteCurrentWavePacketIds,
|
|
319
|
+
nextWavePacketIds,
|
|
320
|
+
remainingFuturePacketIds
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (!hasFutureWave(packetPlan)) {
|
|
324
|
+
return {
|
|
325
|
+
canAdvance: false,
|
|
326
|
+
blockedReason: "no_future_wave",
|
|
327
|
+
currentWaveIndex,
|
|
328
|
+
nextWaveIndex,
|
|
329
|
+
currentWavePacketIds,
|
|
330
|
+
completedCurrentWavePacketIds,
|
|
331
|
+
incompleteCurrentWavePacketIds,
|
|
332
|
+
nextWavePacketIds,
|
|
333
|
+
remainingFuturePacketIds
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
canAdvance: true,
|
|
338
|
+
currentWaveIndex,
|
|
339
|
+
nextWaveIndex,
|
|
340
|
+
currentWavePacketIds,
|
|
341
|
+
completedCurrentWavePacketIds,
|
|
342
|
+
incompleteCurrentWavePacketIds,
|
|
343
|
+
nextWavePacketIds,
|
|
344
|
+
remainingFuturePacketIds
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Advances a packet plan to the next wave if the current wave is complete
|
|
349
|
+
* and a future wave exists. Returns a result object indicating success or failure.
|
|
350
|
+
*/
|
|
351
|
+
export function advancePacketPlanToNextWave(input) {
|
|
352
|
+
const preview = buildPacketWaveAdvancePreview(input);
|
|
353
|
+
if (!preview.canAdvance || preview.nextWaveIndex === undefined || !input.packetPlan) {
|
|
354
|
+
return {
|
|
355
|
+
advanced: false,
|
|
356
|
+
preview,
|
|
357
|
+
packetPlan: input.packetPlan
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
advanced: true,
|
|
362
|
+
preview,
|
|
363
|
+
packetPlan: {
|
|
364
|
+
...input.packetPlan,
|
|
365
|
+
currentWaveIndex: preview.nextWaveIndex
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Advances a workstream's packet plan to the next wave if the current wave is complete
|
|
371
|
+
* and a future wave exists. Returns a result object indicating success or failure,
|
|
372
|
+
* along with the (potentially updated) workstream.
|
|
373
|
+
*/
|
|
374
|
+
export function advanceWorkstreamPacketPlanToNextWave(workstream) {
|
|
375
|
+
const packetPlanResult = advancePacketPlanToNextWave({
|
|
376
|
+
packetPlan: workstream.packetPlan
|
|
377
|
+
});
|
|
378
|
+
if (!packetPlanResult.advanced) {
|
|
379
|
+
return {
|
|
380
|
+
advanced: false,
|
|
381
|
+
packetPlanResult,
|
|
382
|
+
workstream
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
advanced: true,
|
|
387
|
+
packetPlanResult,
|
|
388
|
+
workstream: {
|
|
389
|
+
...workstream,
|
|
390
|
+
packetPlan: packetPlanResult.packetPlan
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Advances packet waves for workstreams that are explicitly marked as ready.
|
|
396
|
+
* Returns a new collection of workstreams and lists of advanced, blocked, and skipped items.
|
|
397
|
+
*/
|
|
398
|
+
export function advanceReadyWorkstreamPacketWaves(input) {
|
|
399
|
+
const readyIds = new Set(input.readyWorkstreamIds);
|
|
400
|
+
const workstreams = {};
|
|
401
|
+
const advanced = [];
|
|
402
|
+
const blocked = [];
|
|
403
|
+
const skipped = [];
|
|
404
|
+
for (const [workstreamId, workstream] of Object.entries(input.workstreams)) {
|
|
405
|
+
if (!readyIds.has(workstreamId)) {
|
|
406
|
+
workstreams[workstreamId] = workstream;
|
|
407
|
+
skipped.push({ workstreamId, reason: "not_ready" });
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const result = advanceWorkstreamPacketPlanToNextWave(workstream);
|
|
411
|
+
if (!result.advanced) {
|
|
412
|
+
workstreams[workstreamId] = workstream;
|
|
413
|
+
blocked.push({
|
|
414
|
+
workstreamId,
|
|
415
|
+
reason: result.packetPlanResult.preview.blockedReason ?? "current_wave_incomplete",
|
|
416
|
+
result
|
|
417
|
+
});
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
workstreams[workstreamId] = result.workstream;
|
|
421
|
+
advanced.push({
|
|
422
|
+
workstreamId,
|
|
423
|
+
fromWaveIndex: result.packetPlanResult.preview.currentWaveIndex ?? 0,
|
|
424
|
+
toWaveIndex: result.packetPlanResult.preview.nextWaveIndex ?? 0,
|
|
425
|
+
nextWavePacketIds: result.packetPlanResult.preview.nextWavePacketIds,
|
|
426
|
+
result
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
return { workstreams, advanced, blocked, skipped };
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Returns true when the current wave is complete and at least one future wave exists.
|
|
433
|
+
*/
|
|
434
|
+
export function isParentSyncNeededBeforeNextWave(packetPlan) {
|
|
435
|
+
return isCurrentWaveComplete(packetPlan) && hasFutureWave(packetPlan);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Builds a preflight object for parent branch synchronization when a wave is complete
|
|
439
|
+
* and future waves remain.
|
|
440
|
+
*/
|
|
441
|
+
export function buildParentBranchSyncPreflight(input) {
|
|
442
|
+
const { workstream, defaultBranch } = input;
|
|
443
|
+
const { packetPlan } = workstream;
|
|
444
|
+
if (!packetPlan) {
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
if (!isParentSyncNeededBeforeNextWave(packetPlan)) {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
const parentIntegrationBranch = workstream.integration?.branchName;
|
|
451
|
+
if (!parentIntegrationBranch) {
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
if (!defaultBranch) {
|
|
455
|
+
return undefined;
|
|
456
|
+
}
|
|
457
|
+
const currentWaveIndex = getCurrentWaveIndex(packetPlan);
|
|
458
|
+
const currentWave = packetPlan.waves[currentWaveIndex];
|
|
459
|
+
if (!currentWave || currentWave.length === 0) {
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
const nextWaveIndex = currentWaveIndex + 1;
|
|
463
|
+
const completedPacketIds = [...currentWave];
|
|
464
|
+
const pendingFuturePacketIds = packetPlan.waves.slice(nextWaveIndex).flat();
|
|
465
|
+
const preflight = {
|
|
466
|
+
workstreamId: workstream.id,
|
|
467
|
+
repoKey: workstream.repoKey,
|
|
468
|
+
parentIntegrationBranch,
|
|
469
|
+
defaultBranch,
|
|
470
|
+
planRevision: resolvePacketPlanRevision(packetPlan),
|
|
471
|
+
currentWaveIndex,
|
|
472
|
+
nextWaveIndex,
|
|
473
|
+
completedPacketIds,
|
|
474
|
+
pendingFuturePacketIds,
|
|
475
|
+
planNodeIds: Object.keys(packetPlan.packets).sort()
|
|
476
|
+
};
|
|
477
|
+
if (workstream.integration?.prNumber !== undefined) {
|
|
478
|
+
preflight.parentPrNumber = workstream.integration.prNumber;
|
|
479
|
+
}
|
|
480
|
+
return preflight;
|
|
481
|
+
}
|
|
482
|
+
function resolvePacketPlanRevision(packetPlan) {
|
|
483
|
+
return (packetPlan.source?.generatedAt ??
|
|
484
|
+
packetPlan.updatedAt ??
|
|
485
|
+
`${getCurrentWaveIndex(packetPlan)}:${Object.keys(packetPlan.packets).sort().join(",")}`);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Returns the packets in the current wave that are still in "planned" status
|
|
489
|
+
* and do not have a branch name or PR number.
|
|
490
|
+
*/
|
|
491
|
+
export function getUndispatchedCurrentWavePackets(packetPlan) {
|
|
492
|
+
const packetIds = getCurrentWavePacketIds(packetPlan);
|
|
493
|
+
const undispatched = [];
|
|
494
|
+
for (const id of packetIds) {
|
|
495
|
+
const packet = packetPlan.packets[id];
|
|
496
|
+
if (packet &&
|
|
497
|
+
packet.status === "planned" &&
|
|
498
|
+
!packet.branchName &&
|
|
499
|
+
!packet.prNumber) {
|
|
500
|
+
undispatched.push(packet);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return undispatched;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Marks a packet as having an open PR, returning a new packet plan.
|
|
507
|
+
*/
|
|
508
|
+
export function markPacketPlanningPrOpen(input) {
|
|
509
|
+
const { packetPlan, packetId, branchName, prNumber, updatedAt } = input;
|
|
510
|
+
const packet = packetPlan.packets[packetId];
|
|
511
|
+
if (!packet) {
|
|
512
|
+
return packetPlan;
|
|
513
|
+
}
|
|
514
|
+
const updatedPacket = {
|
|
515
|
+
...packet,
|
|
516
|
+
status: "pr_open",
|
|
517
|
+
branchName,
|
|
518
|
+
prNumber
|
|
519
|
+
};
|
|
520
|
+
const finalUpdatedAt = updatedAt ?? packet.updatedAt;
|
|
521
|
+
if (finalUpdatedAt) {
|
|
522
|
+
updatedPacket.updatedAt = finalUpdatedAt;
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
...packetPlan,
|
|
526
|
+
packets: {
|
|
527
|
+
...packetPlan.packets,
|
|
528
|
+
[packetId]: updatedPacket
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Marks packet plan entries as merged when their corresponding packet PR has merged into the parent integration branch.
|
|
534
|
+
*/
|
|
535
|
+
export function markMergedPacketPullRequests(input) {
|
|
536
|
+
const { packetPlan, parentWorkstreamId, parentIntegrationBranch, repoId, packetWorktrees, pullRequests, updatedAt } = input;
|
|
537
|
+
const relevantWorktrees = packetWorktrees.filter((wt) => wt.packet &&
|
|
538
|
+
wt.repoId === repoId &&
|
|
539
|
+
wt.packet.parentWorkstreamId === parentWorkstreamId &&
|
|
540
|
+
packetPlan.packets[wt.packet.packetId]);
|
|
541
|
+
if (relevantWorktrees.length === 0) {
|
|
542
|
+
return packetPlan;
|
|
543
|
+
}
|
|
544
|
+
const updatedPackets = { ...packetPlan.packets };
|
|
545
|
+
let changed = false;
|
|
546
|
+
for (const worktree of relevantWorktrees) {
|
|
547
|
+
const packetId = worktree.packet.packetId;
|
|
548
|
+
const packet = packetPlan.packets[packetId];
|
|
549
|
+
if (packet.status === "merged_to_parent") {
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
const expectedIntegrationBranch = worktree.packet.parentIntegrationBranch || parentIntegrationBranch;
|
|
553
|
+
if (!expectedIntegrationBranch) {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
// Match a PR in the same repo by prNumber first, then branchName
|
|
557
|
+
let matchedPr = worktree.prNumber !== undefined
|
|
558
|
+
? pullRequests.find((pr) => pr.repoId === repoId && pr.number === worktree.prNumber)
|
|
559
|
+
: undefined;
|
|
560
|
+
if (!matchedPr) {
|
|
561
|
+
matchedPr = pullRequests.find((pr) => pr.repoId === repoId && pr.headBranch === worktree.branchName);
|
|
562
|
+
}
|
|
563
|
+
if (matchedPr &&
|
|
564
|
+
matchedPr.state === "merged" &&
|
|
565
|
+
matchedPr.baseBranch === expectedIntegrationBranch) {
|
|
566
|
+
const updatedPacket = {
|
|
567
|
+
...packet,
|
|
568
|
+
status: "merged_to_parent"
|
|
569
|
+
};
|
|
570
|
+
if (updatedAt) {
|
|
571
|
+
updatedPacket.updatedAt = updatedAt;
|
|
572
|
+
}
|
|
573
|
+
// Sync metadata with the matched PR
|
|
574
|
+
updatedPacket.prNumber = matchedPr.number;
|
|
575
|
+
updatedPacket.branchName = matchedPr.headBranch;
|
|
576
|
+
updatedPackets[packetId] = updatedPacket;
|
|
577
|
+
changed = true;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (!changed) {
|
|
581
|
+
return packetPlan;
|
|
582
|
+
}
|
|
583
|
+
return {
|
|
584
|
+
...packetPlan,
|
|
585
|
+
packets: updatedPackets
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
export function hasGeneratedPlanBreakdown(workstream) {
|
|
589
|
+
return workstream?.packetPlan?.source?.kind === "plan_breakdown_result";
|
|
590
|
+
}
|
|
591
|
+
function isExecutionNewer(a, b) {
|
|
592
|
+
if (a.endedAt > b.endedAt)
|
|
593
|
+
return true;
|
|
594
|
+
if (a.endedAt < b.endedAt)
|
|
595
|
+
return false;
|
|
596
|
+
return a.id > b.id;
|
|
597
|
+
}
|