@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.
Files changed (202) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +329 -0
  3. package/dist/src/agents/antigravity-launch.js +162 -0
  4. package/dist/src/agents/antigravity.js +251 -0
  5. package/dist/src/agents/claude-launch.js +117 -0
  6. package/dist/src/agents/claude.js +271 -0
  7. package/dist/src/agents/codex-launch.js +82 -0
  8. package/dist/src/agents/codex.js +261 -0
  9. package/dist/src/agents/composite-runner.js +13 -0
  10. package/dist/src/agents/extraction.js +442 -0
  11. package/dist/src/agents/failure-feedback.js +132 -0
  12. package/dist/src/agents/gemini-launch.js +96 -0
  13. package/dist/src/agents/gemini.js +392 -0
  14. package/dist/src/agents/job-agent-assignments.js +180 -0
  15. package/dist/src/agents/lifecycle-feedback.js +147 -0
  16. package/dist/src/agents/limit-detector.js +83 -0
  17. package/dist/src/agents/opencode-launch.js +138 -0
  18. package/dist/src/agents/opencode.js +156 -0
  19. package/dist/src/agents/plan-breakdown-feedback.js +97 -0
  20. package/dist/src/agents/pool.js +168 -0
  21. package/dist/src/agents/prompt.js +462 -0
  22. package/dist/src/agents/provider-sessions.js +105 -0
  23. package/dist/src/agents/review-feedback.js +255 -0
  24. package/dist/src/agents/selection.js +113 -0
  25. package/dist/src/agents/service.js +1087 -0
  26. package/dist/src/agents/types.js +1 -0
  27. package/dist/src/agents/wave-assessment-feedback.js +69 -0
  28. package/dist/src/attention/derive.js +337 -0
  29. package/dist/src/capabilities/index.js +2 -0
  30. package/dist/src/capabilities/projection.js +183 -0
  31. package/dist/src/capabilities/types.js +1 -0
  32. package/dist/src/capabilities/validator.js +87 -0
  33. package/dist/src/cli/commands/broker-smoke.js +96 -0
  34. package/dist/src/cli/commands/broker.js +56 -0
  35. package/dist/src/cli/commands/init.js +4 -0
  36. package/dist/src/cli/commands/journal.js +43 -0
  37. package/dist/src/cli/commands/run-loop.js +56 -0
  38. package/dist/src/cli/commands/run.js +1241 -0
  39. package/dist/src/cli/commands/tui.js +187 -0
  40. package/dist/src/cli/dispatcher.js +51 -0
  41. package/dist/src/cli/errors.js +2 -0
  42. package/dist/src/cli/init.js +92 -0
  43. package/dist/src/cli/options.js +86 -0
  44. package/dist/src/cli/output.js +231 -0
  45. package/dist/src/cli/run-journal.js +342 -0
  46. package/dist/src/cli/run-types.js +1 -0
  47. package/dist/src/cli/run.js +32 -0
  48. package/dist/src/cli/runtime/dispatch-lifecycle.js +772 -0
  49. package/dist/src/cli/runtime/provider-runtime-types.js +1 -0
  50. package/dist/src/cli/runtime/provider-runtime.js +152 -0
  51. package/dist/src/cli/runtime/provider-tools.js +73 -0
  52. package/dist/src/cli/runtime/providers/antigravity.js +26 -0
  53. package/dist/src/cli/runtime/providers/claude.js +20 -0
  54. package/dist/src/cli/runtime/providers/codex.js +20 -0
  55. package/dist/src/cli/runtime/providers/gemini.js +20 -0
  56. package/dist/src/cli/runtime/providers/opencode.js +20 -0
  57. package/dist/src/cli/runtime/run-context.js +131 -0
  58. package/dist/src/cli/tui/index.js +3 -0
  59. package/dist/src/cli/tui/screen.js +388 -0
  60. package/dist/src/cli/tui/tracker.js +558 -0
  61. package/dist/src/cli/tui/types.js +1 -0
  62. package/dist/src/cli/tui/views.js +1006 -0
  63. package/dist/src/config/loader.js +136 -0
  64. package/dist/src/config/schemas.js +403 -0
  65. package/dist/src/config/types.js +1 -0
  66. package/dist/src/context/types.js +331 -0
  67. package/dist/src/domain/entities.js +1 -0
  68. package/dist/src/git/backburner-git-tool-service.js +680 -0
  69. package/dist/src/git/gitops-lifecycle.js +56 -0
  70. package/dist/src/git/merge-prep-workspace.js +357 -0
  71. package/dist/src/git/types.js +484 -0
  72. package/dist/src/github/broker-model-content.js +215 -0
  73. package/dist/src/github/broker-server.js +237 -0
  74. package/dist/src/github/broker.js +638 -0
  75. package/dist/src/github/composed.js +53 -0
  76. package/dist/src/github/gateway.js +478 -0
  77. package/dist/src/github/normalize.js +222 -0
  78. package/dist/src/github/pr-classification.js +73 -0
  79. package/dist/src/github/scoped-broker.js +35 -0
  80. package/dist/src/github/security.js +126 -0
  81. package/dist/src/github/service.js +332 -0
  82. package/dist/src/github/types.js +1 -0
  83. package/dist/src/github/utils.js +16 -0
  84. package/dist/src/journal/index.js +4 -0
  85. package/dist/src/journal/reader.js +62 -0
  86. package/dist/src/journal/renderer.js +50 -0
  87. package/dist/src/journal/service.js +72 -0
  88. package/dist/src/journal/types.js +1 -0
  89. package/dist/src/journal/writer.js +19 -0
  90. package/dist/src/mcps/bridge.js +166 -0
  91. package/dist/src/mcps/index.js +5 -0
  92. package/dist/src/mcps/reconcile.js +113 -0
  93. package/dist/src/mcps/smithery-client.js +186 -0
  94. package/dist/src/mcps/smithery.js +164 -0
  95. package/dist/src/mcps/types.js +1 -0
  96. package/dist/src/memory/json-store.js +71 -0
  97. package/dist/src/memory/prompt.js +16 -0
  98. package/dist/src/memory/scope.js +31 -0
  99. package/dist/src/memory/store.js +1 -0
  100. package/dist/src/memory/tool-service.js +142 -0
  101. package/dist/src/memory/types.js +12 -0
  102. package/dist/src/onboarding/config-writer.js +96 -0
  103. package/dist/src/onboarding/engine.js +86 -0
  104. package/dist/src/onboarding/prompt.js +102 -0
  105. package/dist/src/onboarding/services/provider-discovery.js +58 -0
  106. package/dist/src/onboarding/services/repo-discovery.js +246 -0
  107. package/dist/src/onboarding/steps/check-github-auth.js +32 -0
  108. package/dist/src/onboarding/steps/check-requirements.js +34 -0
  109. package/dist/src/onboarding/steps/configure-models.js +105 -0
  110. package/dist/src/onboarding/steps/configure-paths.js +33 -0
  111. package/dist/src/onboarding/steps/discover-providers.js +38 -0
  112. package/dist/src/onboarding/steps/discover-repos.js +39 -0
  113. package/dist/src/onboarding/steps/generate-config.js +106 -0
  114. package/dist/src/onboarding/steps/readiness-validation.js +158 -0
  115. package/dist/src/onboarding/steps/select-repos.js +76 -0
  116. package/dist/src/onboarding/steps/show-summary.js +97 -0
  117. package/dist/src/onboarding/steps/welcome.js +27 -0
  118. package/dist/src/onboarding/types.js +1 -0
  119. package/dist/src/onboarding/validators/git-access.js +63 -0
  120. package/dist/src/onboarding/validators/github-auth.js +85 -0
  121. package/dist/src/onboarding/validators/tool-checker.js +153 -0
  122. package/dist/src/onboarding/validators/workspace.js +83 -0
  123. package/dist/src/prompts/resolver.js +106 -0
  124. package/dist/src/retrospectives/candidates.js +224 -0
  125. package/dist/src/retrospectives/derive.js +169 -0
  126. package/dist/src/retrospectives/ingest.js +321 -0
  127. package/dist/src/retrospectives/management-prs.js +380 -0
  128. package/dist/src/retrospectives/proposals.js +199 -0
  129. package/dist/src/retrospectives/scopes.js +272 -0
  130. package/dist/src/retrospectives/scoring.js +171 -0
  131. package/dist/src/retrospectives/types.js +1 -0
  132. package/dist/src/state/loader.js +473 -0
  133. package/dist/src/state/types.js +15 -0
  134. package/dist/src/tasks/derivation/builders.js +70 -0
  135. package/dist/src/tasks/derivation/handlers/comment-response.js +169 -0
  136. package/dist/src/tasks/derivation/handlers/implement-plan.js +103 -0
  137. package/dist/src/tasks/derivation/handlers/index.js +54 -0
  138. package/dist/src/tasks/derivation/handlers/packet-pr-control.js +92 -0
  139. package/dist/src/tasks/derivation/handlers/packet-worktree.js +29 -0
  140. package/dist/src/tasks/derivation/handlers/plan-breakdown.js +45 -0
  141. package/dist/src/tasks/derivation/handlers/prepare-for-merge.js +91 -0
  142. package/dist/src/tasks/derivation/handlers/product-discovery.js +36 -0
  143. package/dist/src/tasks/derivation/handlers/retry-failed-task.js +125 -0
  144. package/dist/src/tasks/derivation/handlers/review-pr.js +28 -0
  145. package/dist/src/tasks/derivation/handlers/shared.js +24 -0
  146. package/dist/src/tasks/derivation/handlers/sync-parent-branch.js +100 -0
  147. package/dist/src/tasks/derivation/handlers/wave-assessment.js +223 -0
  148. package/dist/src/tasks/derivation/handlers/write-plan.js +101 -0
  149. package/dist/src/tasks/derivation/handlers/write-product-spec.js +49 -0
  150. package/dist/src/tasks/derivation/runner.js +18 -0
  151. package/dist/src/tasks/derivation/types.js +1 -0
  152. package/dist/src/tasks/derive.js +810 -0
  153. package/dist/src/tasks/mcp-skip.js +10 -0
  154. package/dist/src/tasks/retry.js +10 -0
  155. package/dist/src/tasks/task.js +184 -0
  156. package/dist/src/tasks/types.js +1 -0
  157. package/dist/src/utils/command-runner.js +192 -0
  158. package/dist/src/utils/json.js +38 -0
  159. package/dist/src/utils/logger.js +43 -0
  160. package/dist/src/utils/paths.js +38 -0
  161. package/dist/src/utils/slug.js +10 -0
  162. package/dist/src/workflows/registry.js +174 -0
  163. package/dist/src/workflows/triage.js +90 -0
  164. package/dist/src/workflows/workstream-machine/derive-events.js +121 -0
  165. package/dist/src/workflows/workstream-machine/evaluate.js +24 -0
  166. package/dist/src/workflows/workstream-machine/events.js +1 -0
  167. package/dist/src/workflows/workstream-machine/machine.js +355 -0
  168. package/dist/src/workflows/workstream-machine/selectors.js +19 -0
  169. package/dist/src/workflows/workstream-machine/types.js +1 -0
  170. package/dist/src/workstreams/branch-sync.js +87 -0
  171. package/dist/src/workstreams/derive.js +629 -0
  172. package/dist/src/workstreams/mcp-blockers.js +420 -0
  173. package/dist/src/workstreams/packet-candidates.js +42 -0
  174. package/dist/src/workstreams/packet-lifecycle-feedback.js +60 -0
  175. package/dist/src/workstreams/packet-lifecycle-report.js +199 -0
  176. package/dist/src/workstreams/packet-plan.js +597 -0
  177. package/dist/src/workstreams/packet-projections.js +190 -0
  178. package/dist/src/workstreams/parent-branch-sync-action.js +312 -0
  179. package/dist/src/workstreams/parent-branch-sync.js +148 -0
  180. package/dist/src/workstreams/phases.js +4 -0
  181. package/dist/src/workstreams/plan-breakdown-generator/export.js +19 -0
  182. package/dist/src/workstreams/plan-breakdown-generator/index.js +6 -0
  183. package/dist/src/workstreams/plan-breakdown-generator/inspect.js +50 -0
  184. package/dist/src/workstreams/plan-breakdown-generator/patch.js +139 -0
  185. package/dist/src/workstreams/plan-breakdown-generator/session.js +19 -0
  186. package/dist/src/workstreams/plan-breakdown-generator/store.js +27 -0
  187. package/dist/src/workstreams/plan-breakdown-generator/tools.js +407 -0
  188. package/dist/src/workstreams/plan-breakdown-generator/types.js +6 -0
  189. package/dist/src/workstreams/plan-breakdown-generator/validate.js +212 -0
  190. package/dist/src/workstreams/plan-breakdown-schema.js +175 -0
  191. package/dist/src/workstreams/utils.js +15 -0
  192. package/dist/src/workstreams/wave-assessment/checkpoint.js +172 -0
  193. package/dist/src/workstreams/wave-assessment/index.js +3 -0
  194. package/dist/src/workstreams/wave-assessment/session.js +84 -0
  195. package/dist/src/workstreams/wave-assessment/store.js +51 -0
  196. package/dist/src/workstreams/wave-assessment/tools.js +419 -0
  197. package/dist/src/workstreams/wave-assessment/types.js +1 -0
  198. package/dist/src/workstreams/wave-assessment/validate.js +78 -0
  199. package/dist/src/worktrees/service.js +839 -0
  200. package/dist/src/worktrees/workspace-ownership.js +34 -0
  201. package/dist/src/worktrees/workspace-recovery.js +177 -0
  202. 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
+ }