@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,1241 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import * as path from "node:path";
3
+ import { readFile } from "node:fs/promises";
4
+ import { GitHubTaskFailureNotifier } from "../../agents/failure-feedback.js";
5
+ import { GitHubPullRequestReviewPublisher } from "../../agents/review-feedback.js";
6
+ import { GitHubPlanBreakdownPublisher } from "../../agents/plan-breakdown-feedback.js";
7
+ import { GitHubWaveAssessmentPublisher } from "../../agents/wave-assessment-feedback.js";
8
+ import { GitHubTaskLifecycleNotifier } from "../../agents/lifecycle-feedback.js";
9
+ import { buildSinglePreDispatchFailureExecution, materializePreDispatchFailure, safelyNotifyFailure } from "../../agents/service.js";
10
+ import { validateCapabilities } from "../../capabilities/validator.js";
11
+ import { DefaultContextBuilder } from "../../context/types.js";
12
+ import { runDispatchWithMergePrepLifecycle } from "../runtime/dispatch-lifecycle.js";
13
+ import { createRunContext } from "../runtime/run-context.js";
14
+ import { setupProviderRuntime } from "../runtime/provider-runtime.js";
15
+ import { classifyPullRequestBase } from "../../github/pr-classification.js";
16
+ import { startGitHubBrokerServer } from "../../github/broker-server.js";
17
+ import { deriveTasks } from "../../tasks/derive.js";
18
+ import { buildEntityMaps, resolveTaskJournalIds, resolveRepoActionState, journalWorkstreamTransitions, journalTaskTransitions, journalWorktreeTransitions, journalNewExecutions, journalPacketWaveEvents } from "../run-journal.js";
19
+ import { deriveWorkstreams } from "../../workstreams/derive.js";
20
+ import { collectPacketPullRequestProjections } from "../../workstreams/packet-projections.js";
21
+ import { GitHubPacketLifecyclePublisher } from "../../workstreams/packet-lifecycle-feedback.js";
22
+ import { evaluatePacketWaveAdvancement } from "../../workstreams/branch-sync.js";
23
+ import { applyPlanBreakdownResultsToWorkstreams, buildParentBranchSyncPreflight, markPacketPlanningPrOpen } from "../../workstreams/packet-plan.js";
24
+ import { applyWaveAssessmentResultsToWorkstreams } from "../../workstreams/wave-assessment/checkpoint.js";
25
+ import { deriveAttentionRecords } from "../../attention/derive.js";
26
+ import { collectPacketPullRequestCandidates } from "../../workstreams/packet-candidates.js";
27
+ import { LocalParentBranchSyncService, } from "../../workstreams/parent-branch-sync.js";
28
+ import { executeParentBranchSyncActions } from "../../workstreams/parent-branch-sync-action.js";
29
+ import { registerRetrospectiveCandidates } from "../../retrospectives/candidates.js";
30
+ import { deriveRetrospectiveTasks } from "../../retrospectives/derive.js";
31
+ import { ingestRetrospectiveExecutions } from "../../retrospectives/ingest.js";
32
+ import { scoreRetrospectiveProblems, buildScopeValidationConfigFromRuntime } from "../../retrospectives/scoring.js";
33
+ import { validateLearningProposals } from "../../retrospectives/proposals.js";
34
+ import { materializeManagementLearningProposals } from "../../retrospectives/management-prs.js";
35
+ import { applyMcpSkipExclusion, rehydrateTaskMcpSkipContexts } from "../../workstreams/mcp-blockers.js";
36
+ import { CliUsageError } from "../errors.js";
37
+ /**
38
+ * Builds a packet-wave advancement evaluation for the current run based on summary workstreams
39
+ * and collected parent branch-sync preflights.
40
+ *
41
+ * This helper is pure and synchronous.
42
+ */
43
+ export function buildPacketWaveAdvancementEvaluationForRun(input) {
44
+ return evaluatePacketWaveAdvancement({
45
+ workstreams: input.workstreams,
46
+ parentBranchSyncPreflights: input.parentBranchSyncPreflights,
47
+ waveAssessments: input.waveAssessments
48
+ });
49
+ }
50
+ export async function processProviderLimitResumption(input) {
51
+ const providerLimits = [...(input.previousState.providerLimits ?? [])];
52
+ const currentTime = Date.parse(input.now);
53
+ const expiredProviders = new Set();
54
+ let providerLimitsChanged = false;
55
+ for (let index = 0; index < providerLimits.length; index += 1) {
56
+ const record = providerLimits[index];
57
+ if (record.status !== "limited" || !record.resetTime) {
58
+ continue;
59
+ }
60
+ const resetTime = Date.parse(record.resetTime);
61
+ if (!Number.isFinite(resetTime) || resetTime > currentTime) {
62
+ continue;
63
+ }
64
+ expiredProviders.add(record.provider);
65
+ providerLimits[index] = {
66
+ ...record,
67
+ status: "available",
68
+ updatedAt: input.now
69
+ };
70
+ providerLimitsChanged = true;
71
+ }
72
+ if (expiredProviders.size === 0) {
73
+ return input.previousState;
74
+ }
75
+ const currentIssuesById = new Map(input.currentIssues.map((issue) => [issue.id, issue]));
76
+ const currentPullRequestsById = new Map(input.currentPullRequests.map((pr) => [pr.id, pr]));
77
+ const tasks = [...input.previousState.tasks];
78
+ const executions = [...input.previousState.executions];
79
+ let tasksChanged = false;
80
+ let executionsChanged = false;
81
+ const queuedIndexes = tasks
82
+ .map((task, index) => ({ task, index }))
83
+ .filter(({ task }) => task.status === "queued" && task.waitingOnProvider !== undefined && expiredProviders.has(task.waitingOnProvider))
84
+ .sort((left, right) => {
85
+ const providerComparison = left.task.waitingOnProvider.localeCompare(right.task.waitingOnProvider);
86
+ if (providerComparison !== 0)
87
+ return providerComparison;
88
+ const leftQueuedAt = left.task.context.providerLimitQueuedAt ?? left.task.updatedAt;
89
+ const rightQueuedAt = right.task.context.providerLimitQueuedAt ?? right.task.updatedAt;
90
+ const queueComparison = leftQueuedAt.localeCompare(rightQueuedAt);
91
+ if (queueComparison !== 0)
92
+ return queueComparison;
93
+ return left.task.id.localeCompare(right.task.id);
94
+ });
95
+ for (const { task, index } of queuedIndexes) {
96
+ if (isQueuedTaskStillValid(task, currentIssuesById, currentPullRequestsById)) {
97
+ tasks[index] = {
98
+ ...task,
99
+ status: "pending",
100
+ waitingOnProvider: undefined,
101
+ updatedAt: input.now,
102
+ context: {
103
+ ...task.context,
104
+ notifiedQueued: false,
105
+ providerLimitQueuedAt: task.context.providerLimitQueuedAt ?? task.updatedAt,
106
+ providerLimitResumedProvider: task.waitingOnProvider
107
+ }
108
+ };
109
+ tasksChanged = true;
110
+ await input.lifecycleNotifier.notifyResumed(tasks[index]);
111
+ input.logger.info?.(`[provider-limits] Resumed queued task ${task.id} after ${task.waitingOnProvider} became available.`);
112
+ continue;
113
+ }
114
+ const failure = await buildSinglePreDispatchFailureExecution({
115
+ task,
116
+ reason: "Task was canceled or the owning PR/issue was closed while queued.",
117
+ logDir: input.logDir,
118
+ now: () => input.now
119
+ });
120
+ tasks[index] = {
121
+ ...failure.updatedTask,
122
+ waitingOnProvider: undefined,
123
+ context: {
124
+ ...failure.updatedTask.context,
125
+ notifiedQueued: false,
126
+ providerLimitQueuedAt: undefined,
127
+ providerLimitResumedProvider: undefined
128
+ }
129
+ };
130
+ executions.push(failure.execution);
131
+ tasksChanged = true;
132
+ executionsChanged = true;
133
+ await input.lifecycleNotifier.notifyCanceled(task);
134
+ input.logger.info?.(`[provider-limits] Canceled queued task ${task.id}; owning PR/issue is no longer open.`);
135
+ }
136
+ if (providerLimitsChanged) {
137
+ await input.stateStore.writeProviderLimitState(providerLimits);
138
+ }
139
+ if (executionsChanged) {
140
+ await input.stateStore.writeExecutionState(executions);
141
+ }
142
+ if (tasksChanged) {
143
+ await input.stateStore.writeTaskState(tasks);
144
+ }
145
+ return {
146
+ ...input.previousState,
147
+ providerLimits,
148
+ tasks,
149
+ executions
150
+ };
151
+ }
152
+ function isQueuedTaskStillValid(task, currentIssuesById, currentPullRequestsById) {
153
+ const sourceId = task.parentSourceId ?? task.sourceId;
154
+ const sourceKind = task.parentSourceKind ?? task.sourceKind;
155
+ if (sourceKind === "issue") {
156
+ const issue = currentIssuesById.get(sourceId);
157
+ return issue !== undefined && issue.state === "open" && !hasCanceledLabel(issue.labels);
158
+ }
159
+ if (sourceKind === "pull_request") {
160
+ const pullRequest = currentPullRequestsById.get(sourceId);
161
+ return pullRequest !== undefined && pullRequest.state === "open" && !hasCanceledLabel(pullRequest.labels);
162
+ }
163
+ return true;
164
+ }
165
+ function hasCanceledLabel(labels) {
166
+ return labels.some((label) => {
167
+ const normalized = label.toLowerCase();
168
+ return normalized === "canceled" || normalized === "cancelled" || normalized === "agent-canceled" || normalized === "agent-cancelled";
169
+ });
170
+ }
171
+ function applyMcpSkipRequests(input) {
172
+ let tasks = input.tasks;
173
+ let workstreams = input.workstreams;
174
+ let changed = false;
175
+ const appliedExclusions = [];
176
+ for (const attention of input.attention) {
177
+ if (!attention.reasons.includes("mcp_skip_requested") ||
178
+ attention.mcpSkipTargetTaskId === undefined ||
179
+ attention.mcpSkipMcpHandle === undefined) {
180
+ continue;
181
+ }
182
+ const workstreamIndex = workstreams.findIndex((workstream) => workstream.id === attention.workstreamId);
183
+ if (workstreamIndex === -1) {
184
+ continue;
185
+ }
186
+ const workstream = workstreams[workstreamIndex];
187
+ const blocker = (workstream.mcpBlockers ?? []).find((candidate) => candidate.blockedTaskId === attention.mcpSkipTargetTaskId &&
188
+ candidate.mcpHandle === attention.mcpSkipMcpHandle &&
189
+ candidate.resolutionState === "waiting_for_user");
190
+ if (!blocker) {
191
+ continue;
192
+ }
193
+ const updatedWorkstream = applyMcpSkipExclusion(workstream, blocker.blockedTaskId, blocker.blockedTaskKind, blocker.packetId, blocker.mcpHandle, blocker.affectedCapabilityNames, input.now);
194
+ if (updatedWorkstream !== workstream) {
195
+ workstreams = [
196
+ ...workstreams.slice(0, workstreamIndex),
197
+ updatedWorkstream,
198
+ ...workstreams.slice(workstreamIndex + 1)
199
+ ];
200
+ const exclusion = updatedWorkstream.mcpSkipExclusions?.find((candidate) => candidate.taskId === blocker.blockedTaskId &&
201
+ candidate.mcpHandle === blocker.mcpHandle);
202
+ if (exclusion) {
203
+ appliedExclusions.push(exclusion);
204
+ }
205
+ changed = true;
206
+ }
207
+ const taskIndex = tasks.findIndex((task) => task.id === blocker.blockedTaskId);
208
+ if (taskIndex === -1) {
209
+ continue;
210
+ }
211
+ const task = tasks[taskIndex];
212
+ const mcpSkipContexts = [...(task.context.mcpSkipContexts ?? [])];
213
+ const alreadyRecorded = mcpSkipContexts.some((context) => context.mcpHandle === blocker.mcpHandle);
214
+ if (!alreadyRecorded) {
215
+ mcpSkipContexts.push({
216
+ mcpHandle: blocker.mcpHandle,
217
+ affectedCapabilityNames: blocker.affectedCapabilityNames,
218
+ skipReason: "User explicitly skipped MCP authorization for this task."
219
+ });
220
+ }
221
+ tasks = [
222
+ ...tasks.slice(0, taskIndex),
223
+ {
224
+ ...task,
225
+ status: task.status === "failed" ? "pending" : task.status,
226
+ context: {
227
+ ...task.context,
228
+ recoveryNeeded: false,
229
+ mcpSkipContexts
230
+ },
231
+ updatedAt: input.now
232
+ },
233
+ ...tasks.slice(taskIndex + 1)
234
+ ];
235
+ changed = true;
236
+ }
237
+ return { tasks, workstreams, changed, appliedExclusions };
238
+ }
239
+ export async function runCli(argv, options = {}) {
240
+ const [command, ...rest] = argv;
241
+ if (command !== "run") {
242
+ throw new CliUsageError('Expected command: backburner run [--code-root <path>] [--management-dir <path>] [--outputs-dir <path>] [--log-dir <path>] [--broker-port <port>] [--headless] [--once]');
243
+ }
244
+ const startedAt = new Date().toISOString();
245
+ const cycleId = randomUUID();
246
+ options.statusTracker?.onCycleStart(options.cycleNo ?? 1);
247
+ const context = await createRunContext({ rest, options });
248
+ const { managementDir, outputsDir, stateDir, logDir, logger, codeRoot, brokerPort, commandRunner, gitGateway, gitOpsLifecycle, workspaceRecoveryService, managementGitOps, config, agentPool, outputsGitOpsPrepared, managementRepo, stateStore, journal, memoryStore, memoryToolService, backburnerGitToolService, previousState: loadedPreviousState, currentMcpStatuses, gitHubGateway, syncService, scanService, implementationWorkflowService, triageFollowUpService, mergePrepService, enabledRepos, results } = context;
249
+ let previousState = loadedPreviousState;
250
+ await journal.write({
251
+ cycleId,
252
+ scope: "system",
253
+ scopeId: cycleId,
254
+ category: "observation",
255
+ severity: "info",
256
+ summary: `Orchestration cycle started with ${enabledRepos.length} enabled repo(s)`,
257
+ observedState: { codeRoot, managementDir, outputsDir, enabledRepoCount: enabledRepos.length }
258
+ });
259
+ if (enabledRepos.length === 0) {
260
+ await journal.recordNoAction({
261
+ cycleId,
262
+ scope: "system",
263
+ scopeId: cycleId,
264
+ reason: "No enabled repositories configured",
265
+ derivedState: { totalRepos: config.repos.length, enabledRepos: 0 }
266
+ });
267
+ }
268
+ for (const repo of enabledRepos) {
269
+ options.statusTracker?.onRepoSyncStart(`${repo.owner}/${repo.name}`);
270
+ logger.info(`Syncing ${repo.owner}/${repo.name} into ${path.join(codeRoot, repo.localPath)}`);
271
+ const result = await syncService.syncRepository(repo, codeRoot);
272
+ if (result.status === "failed") {
273
+ logger.warn(`Failed to sync ${result.repoSlug}: ${result.error?.message ?? result.message}`);
274
+ }
275
+ results.push(result);
276
+ await journal.write({
277
+ cycleId,
278
+ scope: "repo",
279
+ scopeId: result.repoId,
280
+ repoId: result.repoId,
281
+ category: "sync",
282
+ severity: result.status === "synced" ? "info" : "error",
283
+ summary: result.status === "synced"
284
+ ? `Synced ${result.repoSlug}`
285
+ : `Failed to sync ${result.repoSlug}: ${result.error?.message ?? result.message}`,
286
+ observedState: {
287
+ repoSlug: result.repoSlug,
288
+ status: result.status,
289
+ cloned: result.cloned,
290
+ fetched: result.fetched,
291
+ defaultBranch: result.defaultBranch,
292
+ syncedAt: result.syncedAt
293
+ }
294
+ });
295
+ }
296
+ options.statusTracker?.onGitHubScanStart();
297
+ logger.info(`Scanning ${enabledRepos.length} configured repos via GitHub`);
298
+ const scanResult = await scanService.scanRepositories(enabledRepos, results, previousState);
299
+ const generatedAt = new Date().toISOString();
300
+ const lifecycleNotifier = new GitHubTaskLifecycleNotifier(gitHubGateway, enabledRepos, logger);
301
+ previousState = await processProviderLimitResumption({
302
+ previousState,
303
+ stateStore,
304
+ currentIssues: scanResult.issues,
305
+ currentPullRequests: scanResult.prs,
306
+ lifecycleNotifier,
307
+ logDir,
308
+ now: generatedAt,
309
+ logger
310
+ });
311
+ for (const repoResult of scanResult.repoResults) {
312
+ const scan = repoResult.githubScan;
313
+ if (scan) {
314
+ await journal.write({
315
+ cycleId,
316
+ scope: "repo",
317
+ scopeId: repoResult.repoId,
318
+ repoId: repoResult.repoId,
319
+ category: "observation",
320
+ severity: scan.status === "failed" ? "error" : scan.status === "partial" ? "warning" : "info",
321
+ summary: `GitHub scan ${scan.status} for ${repoResult.repoSlug}: ${scan.issueCount} issues, ${scan.pullRequestCount} PRs`,
322
+ observedState: {
323
+ repoSlug: repoResult.repoSlug,
324
+ scanStatus: scan.status,
325
+ issueCount: scan.issueCount,
326
+ pullRequestCount: scan.pullRequestCount,
327
+ commentCount: scan.commentCount,
328
+ failureCount: scan.failures.length
329
+ }
330
+ });
331
+ }
332
+ }
333
+ const baseLinkedWorktrees = implementationWorkflowService.linkPullRequests(previousState.worktrees, scanResult.prs, enabledRepos);
334
+ const initialWorkstreams = deriveWorkstreams({
335
+ repos: enabledRepos,
336
+ currentIssues: scanResult.issues,
337
+ currentPullRequests: scanResult.prs,
338
+ previousPullRequests: previousState.prs,
339
+ worktrees: baseLinkedWorktrees,
340
+ previousWorkstreams: previousState.workstreams
341
+ });
342
+ const packetPullRequestProjections = collectPacketPullRequestProjections({
343
+ pullRequests: scanResult.prs,
344
+ workstreams: initialWorkstreams,
345
+ worktrees: baseLinkedWorktrees
346
+ });
347
+ const linkedWorktrees = implementationWorkflowService.reconcilePacketWorktreeReadinessFromProjections(baseLinkedWorktrees, packetPullRequestProjections, enabledRepos, previousState.tasks, initialWorkstreams, scanResult.prs);
348
+ const initialAttention = deriveAttentionRecords({
349
+ repos: enabledRepos,
350
+ workstreams: initialWorkstreams,
351
+ previousState,
352
+ currentIssues: scanResult.issues,
353
+ currentPullRequests: scanResult.prs,
354
+ currentComments: scanResult.comments,
355
+ tasks: previousState.tasks,
356
+ now: generatedAt
357
+ });
358
+ let phaseAwareWorkstreams = deriveWorkstreams({
359
+ repos: enabledRepos,
360
+ currentIssues: scanResult.issues,
361
+ currentPullRequests: scanResult.prs,
362
+ previousPullRequests: previousState.prs,
363
+ worktrees: linkedWorktrees,
364
+ previousWorkstreams: previousState.workstreams,
365
+ attention: initialAttention
366
+ });
367
+ let summaryWorkstreams = phaseAwareWorkstreams;
368
+ const candidateResult = registerRetrospectiveCandidates({
369
+ previousCandidates: previousState.retrospectiveCandidates,
370
+ workstreams: phaseAwareWorkstreams,
371
+ issues: scanResult.issues,
372
+ pullRequests: scanResult.prs,
373
+ tasks: previousState.tasks,
374
+ executions: previousState.executions,
375
+ worktrees: linkedWorktrees,
376
+ managementPrs: previousState.managementPrs,
377
+ ...(managementRepo ? { managementRepo } : {}),
378
+ now: generatedAt
379
+ });
380
+ let tasks = deriveTasks({
381
+ repos: enabledRepos,
382
+ repoSyncRecords: scanResult.repoResults,
383
+ previousState,
384
+ currentIssues: scanResult.issues,
385
+ currentPullRequests: scanResult.prs,
386
+ currentComments: scanResult.comments,
387
+ worktrees: linkedWorktrees,
388
+ workstreams: phaseAwareWorkstreams,
389
+ attention: initialAttention,
390
+ contextBuilder: new DefaultContextBuilder(),
391
+ now: generatedAt,
392
+ ...(managementRepo ? { managementRepo } : {})
393
+ });
394
+ const mcpSkipApplication = applyMcpSkipRequests({
395
+ tasks,
396
+ workstreams: phaseAwareWorkstreams,
397
+ attention: initialAttention,
398
+ now: generatedAt
399
+ });
400
+ if (mcpSkipApplication.changed) {
401
+ tasks = mcpSkipApplication.tasks;
402
+ phaseAwareWorkstreams = mcpSkipApplication.workstreams;
403
+ summaryWorkstreams = phaseAwareWorkstreams;
404
+ }
405
+ const skipContextHydration = rehydrateTaskMcpSkipContexts({
406
+ tasks,
407
+ workstreams: phaseAwareWorkstreams
408
+ });
409
+ if (skipContextHydration.changed) {
410
+ tasks = skipContextHydration.tasks;
411
+ }
412
+ // Build entity maps for issue/PR number resolution in journal entries
413
+ const entityMaps = buildEntityMaps(scanResult.issues, scanResult.prs, enabledRepos);
414
+ const previousTaskIds = new Set(previousState.tasks.map((t) => t.id));
415
+ const newTasks = tasks.filter((t) => !previousTaskIds.has(t.id));
416
+ for (const task of newTasks) {
417
+ const taskIds = resolveTaskJournalIds(task, entityMaps, phaseAwareWorkstreams);
418
+ if (task.relevant) {
419
+ await journal.write({
420
+ cycleId,
421
+ scope: task.workstreamId ? "workstream" : "repo",
422
+ scopeId: task.workstreamId ?? task.repoId,
423
+ repoId: task.repoId,
424
+ ...(task.workstreamId !== undefined ? { workstreamId: task.workstreamId } : {}),
425
+ ...(taskIds.issueNumber !== undefined ? { issueNumber: taskIds.issueNumber } : {}),
426
+ ...(taskIds.prNumber !== undefined ? { prNumber: taskIds.prNumber } : {}),
427
+ category: "task_created",
428
+ severity: "info",
429
+ summary: `Task created: ${task.kind} for ${task.repoSlug}`,
430
+ observedState: { sourceKind: task.sourceKind, sourceId: task.sourceId },
431
+ derivedState: { taskId: task.id, kind: task.kind, relevant: true, reason: task.reason },
432
+ expectedNextAction: `Dispatch ${task.kind} to agent`
433
+ });
434
+ }
435
+ else {
436
+ await journal.write({
437
+ cycleId,
438
+ scope: task.workstreamId ? "workstream" : "repo",
439
+ scopeId: task.workstreamId ?? task.repoId,
440
+ repoId: task.repoId,
441
+ ...(task.workstreamId !== undefined ? { workstreamId: task.workstreamId } : {}),
442
+ ...(taskIds.issueNumber !== undefined ? { issueNumber: taskIds.issueNumber } : {}),
443
+ ...(taskIds.prNumber !== undefined ? { prNumber: taskIds.prNumber } : {}),
444
+ category: "task_skipped",
445
+ severity: "info",
446
+ summary: `Task skipped: ${task.kind} for ${task.repoSlug} — ${task.reason}`,
447
+ noActionReason: task.reason,
448
+ observedState: { sourceKind: task.sourceKind, sourceId: task.sourceId },
449
+ derivedState: { taskId: task.id, kind: task.kind, relevant: false, reason: task.reason }
450
+ });
451
+ }
452
+ }
453
+ // Comprehensive per-repo no-action journaling (checks sync failure, scan failure, workstream phases, etc.)
454
+ const syncRecordById = new Map(scanResult.repoResults.map((r) => [r.repoId, r]));
455
+ for (const repo of enabledRepos) {
456
+ const actionState = resolveRepoActionState({
457
+ repoId: repo.id,
458
+ repoSlug: `${repo.owner}/${repo.name}`,
459
+ syncRecord: syncRecordById.get(repo.id),
460
+ tasks,
461
+ workstreams: phaseAwareWorkstreams,
462
+ issues: scanResult.issues,
463
+ prs: scanResult.prs
464
+ });
465
+ if (!actionState.hasAction && actionState.noActionReason !== undefined) {
466
+ await journal.recordNoAction({
467
+ cycleId,
468
+ scope: "repo",
469
+ scopeId: repo.id,
470
+ repoId: repo.id,
471
+ reason: actionState.noActionReason,
472
+ observedState: {
473
+ repoSlug: `${repo.owner}/${repo.name}`,
474
+ taskCount: tasks.filter((t) => t.repoId === repo.id).length
475
+ }
476
+ });
477
+ }
478
+ }
479
+ // Journal workstream transitions (phase/attention/status changes)
480
+ await journalWorkstreamTransitions({
481
+ cycleId,
482
+ previousWorkstreams: previousState.workstreams,
483
+ currentWorkstreams: phaseAwareWorkstreams,
484
+ journal,
485
+ entityMaps
486
+ });
487
+ // Track execution IDs before dispatch for dedup
488
+ const executionIdsBeforeCycle = new Set(previousState.executions.map((e) => e.id));
489
+ await stateStore.writeRepoSyncState(scanResult.repoResults);
490
+ await stateStore.writeEntityState({
491
+ issueMetadata: buildMetadata(generatedAt, scanResult.issues.length, enabledRepos.length, scanResult.failures),
492
+ prMetadata: buildMetadata(generatedAt, scanResult.prs.length, enabledRepos.length, scanResult.failures),
493
+ commentMetadata: buildMetadata(generatedAt, scanResult.comments.length, enabledRepos.length, scanResult.failures),
494
+ issues: scanResult.issues,
495
+ prs: scanResult.prs,
496
+ comments: scanResult.comments
497
+ });
498
+ await stateStore.writeWorktreeState(linkedWorktrees);
499
+ await stateStore.writeWorkstreamState(phaseAwareWorkstreams);
500
+ await stateStore.writeAttentionState(initialAttention);
501
+ await stateStore.writeTaskState(tasks);
502
+ await stateStore.writeRetrospectiveCandidateState(candidateResult.candidates);
503
+ const parentBranchSyncService = options.parentBranchSyncService ?? new LocalParentBranchSyncService(gitGateway);
504
+ const syncActionResult = await executeParentBranchSyncActions({
505
+ tasks,
506
+ executions: previousState.executions,
507
+ parentBranchSyncService,
508
+ gitGateway,
509
+ gitHubGateway,
510
+ stateStore,
511
+ logDir,
512
+ logger
513
+ });
514
+ let tasksAfterSync = syncActionResult.tasks;
515
+ const executionsAfterSync = syncActionResult.executions;
516
+ const postSyncSkipContextHydration = rehydrateTaskMcpSkipContexts({
517
+ tasks: tasksAfterSync,
518
+ workstreams: phaseAwareWorkstreams
519
+ });
520
+ if (postSyncSkipContextHydration.changed) {
521
+ tasksAfterSync = postSyncSkipContextHydration.tasks;
522
+ await stateStore.writeTaskState(tasksAfterSync);
523
+ }
524
+ let brokerServer;
525
+ let brokerLogger;
526
+ let agentRunner = options.agentRunner;
527
+ let capabilitiesByProvider;
528
+ let providerBrokerToolAllowLists;
529
+ let brokerToolScope;
530
+ let packetPrsCreatedThisRun = [];
531
+ let packetWaveAdvancementsThisRun = [];
532
+ let packetWaveBlockedThisRun = [];
533
+ const failureNotifier = new GitHubTaskFailureNotifier(gitHubGateway, enabledRepos, logger);
534
+ const reviewPublisher = new GitHubPullRequestReviewPublisher(gitHubGateway, enabledRepos, logger);
535
+ const planBreakdownPublisher = new GitHubPlanBreakdownPublisher(gitHubGateway, enabledRepos, logger);
536
+ const waveAssessmentPublisher = new GitHubWaveAssessmentPublisher(gitHubGateway, enabledRepos, logger);
537
+ const capabilityValidation = validateCapabilities(config.capabilities, config.mcps, currentMcpStatuses);
538
+ let dispatchResult = {
539
+ tasks: tasksAfterSync,
540
+ executions: executionsAfterSync,
541
+ workstreams: phaseAwareWorkstreams
542
+ };
543
+ let dispatchSetupFailed = false;
544
+ try {
545
+ const providerRuntime = await setupProviderRuntime({
546
+ agentRunner,
547
+ commandRunner,
548
+ config,
549
+ currentMcpStatuses,
550
+ gitHubGateway,
551
+ logger,
552
+ logDir,
553
+ brokerPort,
554
+ startBrokerServer: options.startBrokerServer ?? startGitHubBrokerServer,
555
+ memoryToolService,
556
+ backburnerGitToolService
557
+ });
558
+ agentRunner = providerRuntime.agentRunner;
559
+ capabilitiesByProvider = providerRuntime.capabilitiesByProvider;
560
+ providerBrokerToolAllowLists = providerRuntime.providerBrokerToolAllowLists;
561
+ brokerToolScope = providerRuntime.brokerToolScope;
562
+ brokerServer = providerRuntime.brokerServer;
563
+ brokerLogger = providerRuntime.brokerLogger;
564
+ }
565
+ catch (error) {
566
+ dispatchSetupFailed = true;
567
+ const failedAt = new Date().toISOString();
568
+ const message = error instanceof Error ? error.message : String(error);
569
+ logger.warn(`Dispatch setup failed before agent execution began: ${message}`);
570
+ const failureResult = await materializePreDispatchFailure({
571
+ tasks: tasksAfterSync,
572
+ existingExecutions: executionsAfterSync,
573
+ logDir,
574
+ error,
575
+ now: () => failedAt
576
+ });
577
+ await stateStore.writeExecutionState(failureResult.executions);
578
+ await stateStore.writeTaskState(failureResult.tasks);
579
+ let preDispatchTasks = [...failureResult.tasks];
580
+ let preDispatchTasksDirty = false;
581
+ for (let pi = 0; pi < preDispatchTasks.length; pi++) {
582
+ const task = preDispatchTasks[pi];
583
+ if (task.status !== "failed" || !task.lastExecutionId) {
584
+ continue;
585
+ }
586
+ const execution = failureResult.executions.find((entry) => entry.id === task.lastExecutionId);
587
+ if (!execution) {
588
+ continue;
589
+ }
590
+ const failureCommentId = await safelyNotifyFailure(failureNotifier, task, execution);
591
+ if (failureCommentId !== undefined) {
592
+ preDispatchTasks[pi] = { ...task, lastFailureCommentId: failureCommentId };
593
+ preDispatchTasksDirty = true;
594
+ }
595
+ }
596
+ if (preDispatchTasksDirty) {
597
+ await stateStore.writeTaskState(preDispatchTasks);
598
+ }
599
+ dispatchResult = { ...failureResult, tasks: preDispatchTasks, workstreams: phaseAwareWorkstreams };
600
+ // Journal failed executions from pre-dispatch setup failure
601
+ await journalNewExecutions({
602
+ cycleId,
603
+ executionIdsBeforeCycle,
604
+ dispatchResult: failureResult,
605
+ journal,
606
+ entityMaps,
607
+ workstreams: phaseAwareWorkstreams
608
+ });
609
+ await journalTaskTransitions({
610
+ cycleId,
611
+ tasksBefore: tasks,
612
+ tasksAfter: failureResult.tasks,
613
+ journal,
614
+ entityMaps,
615
+ workstreams: phaseAwareWorkstreams
616
+ });
617
+ if (brokerServer) {
618
+ await brokerServer.close();
619
+ brokerServer = undefined;
620
+ }
621
+ if (brokerLogger) {
622
+ await brokerLogger.close();
623
+ brokerLogger = undefined;
624
+ }
625
+ }
626
+ if (!dispatchSetupFailed) {
627
+ try {
628
+ let dispatchAgentRunner = agentRunner;
629
+ if (!dispatchAgentRunner) {
630
+ throw new Error("Agent runner was not initialized.");
631
+ }
632
+ if (options.statusTracker) {
633
+ const tracker = options.statusTracker;
634
+ const realRunner = dispatchAgentRunner;
635
+ dispatchAgentRunner = {
636
+ dispatch: async (task, context, agentConfig, sessionDecision, promptContent) => {
637
+ tracker.onTaskStart(task, agentConfig);
638
+ try {
639
+ const res = await realRunner.dispatch(task, context, agentConfig, sessionDecision, promptContent);
640
+ tracker.onTaskEnd(task.id);
641
+ return res;
642
+ }
643
+ catch (err) {
644
+ tracker.onTaskEnd(task.id);
645
+ throw err;
646
+ }
647
+ }
648
+ };
649
+ }
650
+ logger.info(`Dispatching pending tasks (${tasksAfterSync.filter((task) => task.status === "pending").length})`);
651
+ const initialDispatchResult = await runDispatchWithMergePrepLifecycle({
652
+ tasks: tasksAfterSync,
653
+ worktrees: linkedWorktrees,
654
+ workstreams: phaseAwareWorkstreams,
655
+ capabilityValidation,
656
+ mcpStatuses: currentMcpStatuses,
657
+ declaredMcps: config.mcps,
658
+ capabilitiesConfig: config.capabilities,
659
+ agents: config.agents,
660
+ enabledRepos,
661
+ codeRoot,
662
+ executions: executionsAfterSync,
663
+ stateStore,
664
+ logDir,
665
+ agentRunner: dispatchAgentRunner,
666
+ failureNotifier,
667
+ reviewPublisher,
668
+ planBreakdownPublisher,
669
+ waveAssessmentPublisher,
670
+ lifecycleNotifier,
671
+ jobMemoryRuntime: { store: memoryStore, toolService: memoryToolService },
672
+ logger,
673
+ promptsConfig: config.prompts,
674
+ managementRoot: managementDir,
675
+ ...(capabilitiesByProvider ? { capabilitiesByProvider } : {}),
676
+ ...(providerBrokerToolAllowLists ? { providerBrokerToolAllowLists } : {}),
677
+ ...(brokerToolScope ? { brokerToolScope } : {}),
678
+ mergePrepService,
679
+ githubGateway: gitHubGateway,
680
+ backburnerGitToolService,
681
+ commandRunner,
682
+ gitGateway,
683
+ workspaceRecoveryService,
684
+ agentPool
685
+ });
686
+ const triageFollowUp = await triageFollowUpService.materializeStartImplementation({
687
+ repos: enabledRepos,
688
+ issues: scanResult.issues,
689
+ repoPaths: new Map(scanResult.repoResults
690
+ .filter((result) => result.status === "synced")
691
+ .map((result) => [result.repoId, result.localPath])),
692
+ codeRoot,
693
+ state: {
694
+ ...previousState,
695
+ prs: scanResult.prs,
696
+ executions: initialDispatchResult.executions,
697
+ worktrees: linkedWorktrees
698
+ },
699
+ tasks: initialDispatchResult.tasks
700
+ });
701
+ const triageAwareWorktrees = implementationWorkflowService.reconcileTaskState(triageFollowUp.worktrees, triageFollowUp.tasks);
702
+ const projectionAwareTriageWorktrees = implementationWorkflowService.reconcilePacketWorktreeReadinessFromProjections(triageAwareWorktrees, packetPullRequestProjections, enabledRepos, triageFollowUp.tasks, initialDispatchResult.workstreams, scanResult.prs);
703
+ await stateStore.writeWorktreeState(projectionAwareTriageWorktrees);
704
+ await stateStore.writeTaskState(triageFollowUp.tasks);
705
+ const retrospectiveDerivation = deriveRetrospectiveTasks({
706
+ existingTasks: triageFollowUp.tasks,
707
+ candidates: candidateResult.candidates,
708
+ problems: previousState.retrospectiveProblems,
709
+ opportunities: previousState.retrospectiveOpportunities,
710
+ proposals: previousState.learningProposals,
711
+ agents: config.agents,
712
+ contextBuilder: new DefaultContextBuilder(),
713
+ now: generatedAt
714
+ });
715
+ let finalTasksToDispatch = triageFollowUp.tasks;
716
+ if (retrospectiveDerivation.tasks.length > 0) {
717
+ finalTasksToDispatch = [...triageFollowUp.tasks, ...retrospectiveDerivation.tasks];
718
+ }
719
+ const finalSkipContextHydration = rehydrateTaskMcpSkipContexts({
720
+ tasks: finalTasksToDispatch,
721
+ workstreams: initialDispatchResult.workstreams
722
+ });
723
+ if (finalSkipContextHydration.changed) {
724
+ finalTasksToDispatch = finalSkipContextHydration.tasks;
725
+ }
726
+ if (retrospectiveDerivation.tasks.length > 0 || finalSkipContextHydration.changed) {
727
+ await stateStore.writeTaskState(finalTasksToDispatch);
728
+ }
729
+ dispatchResult = await runDispatchWithMergePrepLifecycle({
730
+ tasks: finalTasksToDispatch,
731
+ worktrees: projectionAwareTriageWorktrees,
732
+ workstreams: initialDispatchResult.workstreams,
733
+ capabilityValidation,
734
+ mcpStatuses: currentMcpStatuses,
735
+ declaredMcps: config.mcps,
736
+ capabilitiesConfig: config.capabilities,
737
+ agents: config.agents,
738
+ enabledRepos,
739
+ codeRoot,
740
+ executions: initialDispatchResult.executions,
741
+ stateStore,
742
+ logDir,
743
+ agentRunner: dispatchAgentRunner,
744
+ failureNotifier,
745
+ reviewPublisher,
746
+ planBreakdownPublisher,
747
+ waveAssessmentPublisher,
748
+ lifecycleNotifier,
749
+ jobMemoryRuntime: { store: memoryStore, toolService: memoryToolService },
750
+ logger,
751
+ promptsConfig: config.prompts,
752
+ managementRoot: managementDir,
753
+ ...(capabilitiesByProvider ? { capabilitiesByProvider } : {}),
754
+ ...(providerBrokerToolAllowLists ? { providerBrokerToolAllowLists } : {}),
755
+ ...(brokerToolScope ? { brokerToolScope } : {}),
756
+ mergePrepService,
757
+ githubGateway: gitHubGateway,
758
+ backburnerGitToolService,
759
+ commandRunner,
760
+ gitGateway,
761
+ workspaceRecoveryService,
762
+ agentPool
763
+ });
764
+ // Journal new executions (dedup via executionIdsBeforeCycle)
765
+ await journalNewExecutions({
766
+ cycleId,
767
+ executionIdsBeforeCycle,
768
+ dispatchResult,
769
+ journal,
770
+ entityMaps,
771
+ workstreams: dispatchResult.workstreams
772
+ });
773
+ // Journal task status transitions after dispatch
774
+ await journalTaskTransitions({
775
+ cycleId,
776
+ tasksBefore: tasks,
777
+ tasksAfter: dispatchResult.tasks,
778
+ journal,
779
+ entityMaps,
780
+ workstreams: dispatchResult.workstreams
781
+ });
782
+ const ingestionResult = await ingestRetrospectiveExecutions({
783
+ tasks: [...dispatchResult.tasks, ...previousState.tasks],
784
+ executions: dispatchResult.executions,
785
+ candidates: retrospectiveDerivation.candidates,
786
+ problems: retrospectiveDerivation.problems,
787
+ opportunities: retrospectiveDerivation.opportunities,
788
+ proposals: previousState.learningProposals,
789
+ readArtifact: (p) => readFile(p, "utf8"),
790
+ now: generatedAt
791
+ });
792
+ const scopeValidationConfig = buildScopeValidationConfigFromRuntime({
793
+ repos: config.repos,
794
+ agents: config.agents
795
+ });
796
+ const scoredProblems = scoreRetrospectiveProblems(ingestionResult.problems, scopeValidationConfig, generatedAt);
797
+ const validationResult = await validateLearningProposals({
798
+ proposals: ingestionResult.proposals,
799
+ opportunities: ingestionResult.opportunities,
800
+ problems: scoredProblems,
801
+ scopeValidationConfig,
802
+ managementRoot: managementDir,
803
+ readArtifact: async (p) => {
804
+ try {
805
+ return await readFile(p, "utf8");
806
+ }
807
+ catch {
808
+ return undefined;
809
+ }
810
+ },
811
+ now: generatedAt
812
+ });
813
+ const materializationResult = await materializeManagementLearningProposals({
814
+ proposals: validationResult.proposals,
815
+ managementPrs: previousState.managementPrs,
816
+ candidates: ingestionResult.candidates,
817
+ problems: scoredProblems,
818
+ opportunities: ingestionResult.opportunities,
819
+ managementRoot: managementDir,
820
+ gitGateway,
821
+ githubGateway: gitHubGateway,
822
+ commandRunner,
823
+ now: generatedAt
824
+ });
825
+ await stateStore.writeRetrospectiveCandidateState(ingestionResult.candidates);
826
+ await stateStore.writeRetrospectiveProblemState(scoredProblems);
827
+ await stateStore.writeRetrospectiveOpportunityState(ingestionResult.opportunities);
828
+ await stateStore.writeLearningProposalState(materializationResult.proposals);
829
+ await stateStore.writeManagementPrState(materializationResult.managementPrs);
830
+ const reconciledWorktrees = implementationWorkflowService.reconcileTaskState(projectionAwareTriageWorktrees, dispatchResult.tasks);
831
+ const projectionAwareReconciledWorktrees = implementationWorkflowService.reconcilePacketWorktreeReadinessFromProjections(reconciledWorktrees, packetPullRequestProjections, enabledRepos, dispatchResult.tasks, dispatchResult.workstreams, scanResult.prs);
832
+ const finalWorktrees = await implementationWorkflowService.finalizePushableHandoffs(projectionAwareReconciledWorktrees, dispatchResult.tasks, logger);
833
+ await stateStore.writeWorktreeState(finalWorktrees);
834
+ const finalWorkstreams = deriveWorkstreams({
835
+ repos: enabledRepos,
836
+ currentIssues: scanResult.issues,
837
+ currentPullRequests: scanResult.prs,
838
+ previousPullRequests: previousState.prs,
839
+ worktrees: finalWorktrees,
840
+ previousWorkstreams: dispatchResult.workstreams
841
+ });
842
+ const finalAttentionObservedAt = new Date().toISOString();
843
+ const finalAttention = deriveAttentionRecords({
844
+ repos: enabledRepos,
845
+ workstreams: finalWorkstreams,
846
+ previousState,
847
+ currentIssues: scanResult.issues,
848
+ currentPullRequests: scanResult.prs,
849
+ currentComments: scanResult.comments,
850
+ tasks: dispatchResult.tasks,
851
+ now: finalAttentionObservedAt
852
+ });
853
+ const finalPhaseAwareWorkstreams = deriveWorkstreams({
854
+ repos: enabledRepos,
855
+ currentIssues: scanResult.issues,
856
+ currentPullRequests: scanResult.prs,
857
+ previousPullRequests: previousState.prs,
858
+ worktrees: finalWorktrees,
859
+ previousWorkstreams: finalWorkstreams,
860
+ attention: finalAttention
861
+ });
862
+ const breakdownEnrichedWorkstreams = applyPlanBreakdownResultsToWorkstreams({
863
+ workstreams: finalPhaseAwareWorkstreams,
864
+ tasks: dispatchResult.tasks,
865
+ executions: dispatchResult.executions
866
+ });
867
+ const enrichedWorkstreams = applyWaveAssessmentResultsToWorkstreams({
868
+ workstreams: breakdownEnrichedWorkstreams,
869
+ assessments: dispatchResult.executions
870
+ .map((execution) => execution.result?.waveAssessment)
871
+ .filter((assessment) => assessment !== undefined)
872
+ });
873
+ // Packet planning artifact creation
874
+ const repoPathMap = new Map(scanResult.repoResults
875
+ .filter((result) => result.status === "synced")
876
+ .map((result) => [result.repoId, result.localPath]));
877
+ let currentWorktrees = finalWorktrees;
878
+ let currentWorkstreams = enrichedWorkstreams;
879
+ // Preserve new packet plans before provisioning external packet artifacts.
880
+ await stateStore.writeWorkstreamState(currentWorkstreams);
881
+ for (const workstream of currentWorkstreams) {
882
+ if (!workstream.packetPlan) {
883
+ continue;
884
+ }
885
+ const repo = enabledRepos.find((r) => r.id === workstream.repoKey || `${r.owner}/${r.name}` === workstream.repoKey);
886
+ const repoPath = repo ? repoPathMap.get(repo.id) : undefined;
887
+ if (!repo || !repoPath) {
888
+ continue;
889
+ }
890
+ const packetWorktreeResults = await implementationWorkflowService.ensurePacketPlanningWorktreesForCurrentWave({
891
+ repo,
892
+ repoPath,
893
+ codeRoot,
894
+ parentWorkstream: workstream,
895
+ existingWorktrees: currentWorktrees
896
+ });
897
+ if (packetWorktreeResults.length > 0) {
898
+ for (const result of packetWorktreeResults) {
899
+ const existingIndex = currentWorktrees.findIndex((wt) => wt.id === result.worktree.id);
900
+ if (existingIndex !== -1) {
901
+ currentWorktrees = [
902
+ ...currentWorktrees.slice(0, existingIndex),
903
+ result.worktree,
904
+ ...currentWorktrees.slice(existingIndex + 1)
905
+ ];
906
+ }
907
+ else {
908
+ currentWorktrees = [...currentWorktrees, result.worktree];
909
+ }
910
+ }
911
+ // A PR create failure must leave a persisted worktree that can be retried.
912
+ await stateStore.writeWorktreeState(currentWorktrees);
913
+ let runningWorkstream = workstream;
914
+ for (const result of packetWorktreeResults) {
915
+ const { worktree: updatedWorktree, pullRequestCreated } = await implementationWorkflowService.ensurePullRequestForPacketWorktree(repo, runningWorkstream, result.worktree);
916
+ if (pullRequestCreated && updatedWorktree.packet && updatedWorktree.prNumber) {
917
+ packetPrsCreatedThisRun.push({
918
+ workstreamId: runningWorkstream.id,
919
+ repoKey: runningWorkstream.repoKey,
920
+ parentPrNumber: runningWorkstream.integration?.prNumber,
921
+ packetId: updatedWorktree.packet.packetId,
922
+ waveIndex: updatedWorktree.packet.waveIndex,
923
+ branchName: updatedWorktree.branchName,
924
+ prNumber: updatedWorktree.prNumber
925
+ });
926
+ }
927
+ // Update WorktreeRecord list
928
+ const existingIndex = currentWorktrees.findIndex((wt) => wt.id === updatedWorktree.id);
929
+ if (existingIndex !== -1) {
930
+ currentWorktrees = [
931
+ ...currentWorktrees.slice(0, existingIndex),
932
+ updatedWorktree,
933
+ ...currentWorktrees.slice(existingIndex + 1)
934
+ ];
935
+ }
936
+ else {
937
+ currentWorktrees = [...currentWorktrees, updatedWorktree];
938
+ }
939
+ // Update WorkstreamRecord state
940
+ const packetId = updatedWorktree.packet?.packetId;
941
+ if (packetId && updatedWorktree.prNumber) {
942
+ const updatedPacketPlan = markPacketPlanningPrOpen({
943
+ packetPlan: runningWorkstream.packetPlan,
944
+ packetId,
945
+ branchName: updatedWorktree.branchName,
946
+ prNumber: updatedWorktree.prNumber,
947
+ updatedAt: new Date().toISOString()
948
+ });
949
+ runningWorkstream = { ...runningWorkstream, packetPlan: updatedPacketPlan };
950
+ currentWorkstreams = currentWorkstreams.map((ws) => ws.id === runningWorkstream.id ? runningWorkstream : ws);
951
+ }
952
+ }
953
+ }
954
+ }
955
+ await stateStore.writeWorktreeState(currentWorktrees);
956
+ const parentBranchSyncPreflights = await collectParentBranchSyncPreflights({
957
+ workstreams: currentWorkstreams,
958
+ repos: enabledRepos,
959
+ pullRequests: scanResult.prs,
960
+ repoResults: scanResult.repoResults,
961
+ tasks: [...dispatchResult.tasks, ...previousState.tasks],
962
+ executions: dispatchResult.executions,
963
+ gitGateway
964
+ });
965
+ const parentBranchSyncedThisRun = syncActionResult.attempts.some((attempt) => attempt.attempted && attempt.status === "synced");
966
+ const packetWaveAdvancementEvaluation = buildPacketWaveAdvancementEvaluationForRun({
967
+ workstreams: currentWorkstreams,
968
+ parentBranchSyncPreflights: parentBranchSyncedThisRun ? [] : parentBranchSyncPreflights,
969
+ waveAssessments: currentWorkstreams.flatMap((workstream) => workstream.packetPlan?.waveAssessments ?? [])
970
+ });
971
+ packetWaveAdvancementsThisRun = packetWaveAdvancementEvaluation.result.advanced.map((adv) => {
972
+ const workstream = currentWorkstreams.find((ws) => ws.id === adv.workstreamId);
973
+ return {
974
+ workstreamId: adv.workstreamId,
975
+ repoKey: workstream?.repoKey ?? "unknown",
976
+ parentPrNumber: workstream?.integration?.prNumber,
977
+ fromWaveIndex: adv.fromWaveIndex,
978
+ toWaveIndex: adv.toWaveIndex,
979
+ completedPacketIds: adv.result.packetPlanResult.preview.completedCurrentWavePacketIds,
980
+ newlyCurrentPacketIds: adv.nextWavePacketIds
981
+ };
982
+ });
983
+ packetWaveBlockedThisRun = packetWaveAdvancementEvaluation.result.blocked.map((blocked) => {
984
+ const workstream = currentWorkstreams.find((ws) => ws.id === blocked.workstreamId);
985
+ return {
986
+ workstreamId: blocked.workstreamId,
987
+ repoKey: workstream?.repoKey ?? "unknown",
988
+ parentPrNumber: workstream?.integration?.prNumber,
989
+ reason: blocked.reason,
990
+ currentWaveIndex: blocked.result.packetPlanResult.preview.currentWaveIndex,
991
+ incompletePacketIds: blocked.result.packetPlanResult.preview.incompleteCurrentWavePacketIds
992
+ };
993
+ });
994
+ if (packetWaveAdvancementEvaluation.result.advanced.length > 0) {
995
+ currentWorkstreams = currentWorkstreams.map((workstream) => packetWaveAdvancementEvaluation.result.workstreams[workstream.id] ?? workstream);
996
+ }
997
+ const persistedAttentionObservedAt = new Date().toISOString();
998
+ const persistedAttention = deriveAttentionRecords({
999
+ repos: enabledRepos,
1000
+ workstreams: currentWorkstreams,
1001
+ previousState,
1002
+ currentIssues: scanResult.issues,
1003
+ currentPullRequests: scanResult.prs,
1004
+ currentComments: scanResult.comments,
1005
+ tasks: dispatchResult.tasks,
1006
+ now: persistedAttentionObservedAt
1007
+ });
1008
+ await stateStore.writeWorkstreamState(currentWorkstreams);
1009
+ await stateStore.writeAttentionState(persistedAttention);
1010
+ const lifecyclePublisher = new GitHubPacketLifecyclePublisher(gitHubGateway, logger);
1011
+ await lifecyclePublisher.publishLifecycleFeedback({
1012
+ workstreams: currentWorkstreams,
1013
+ repos: enabledRepos,
1014
+ packetPrsCreatedThisRun,
1015
+ waveAdvancementsThisRun: packetWaveAdvancementsThisRun,
1016
+ blockedThisRun: packetWaveBlockedThisRun,
1017
+ parentBranchSyncPreflights
1018
+ });
1019
+ summaryWorkstreams = currentWorkstreams;
1020
+ // Journal worktree and packet wave transitions
1021
+ await journalWorktreeTransitions({
1022
+ cycleId,
1023
+ previousWorktrees: previousState.worktrees,
1024
+ currentWorktrees: currentWorktrees,
1025
+ journal
1026
+ });
1027
+ await journalPacketWaveEvents({
1028
+ cycleId,
1029
+ packetWaveAdvancementsThisRun,
1030
+ packetWaveBlockedThisRun,
1031
+ journal
1032
+ });
1033
+ }
1034
+ finally {
1035
+ if (brokerServer) {
1036
+ await brokerServer.close();
1037
+ }
1038
+ if (brokerLogger) {
1039
+ await brokerLogger.close();
1040
+ }
1041
+ }
1042
+ }
1043
+ const outputsGitOps = await gitOpsLifecycle.finalizeOutputsRoot(outputsDir, startedAt, outputsGitOpsPrepared);
1044
+ const completedAt = new Date().toISOString();
1045
+ logger.info(`Run complete: ${dispatchResult.tasks.length} tasks tracked, ${dispatchResult.tasks.filter((task) => task.status === "failed").length} failed`);
1046
+ const summary = {
1047
+ command: "run",
1048
+ codeRoot,
1049
+ managementDir,
1050
+ outputsDir,
1051
+ stateDir,
1052
+ logDir,
1053
+ startedAt,
1054
+ completedAt,
1055
+ managementGitOps,
1056
+ outputsGitOps,
1057
+ repoCount: enabledRepos.length,
1058
+ syncedRepoCount: scanResult.repoResults.filter((result) => result.status === "synced").length,
1059
+ failedRepoCount: scanResult.repoResults.filter((result) => result.status === "failed").length,
1060
+ scannedRepoCount: scanResult.repoResults.filter((result) => result.githubScan?.status === "scanned").length,
1061
+ partiallyScannedRepoCount: scanResult.repoResults.filter((result) => result.githubScan?.status === "partial").length,
1062
+ failedScanRepoCount: scanResult.repoResults.filter((result) => result.githubScan?.status === "failed").length,
1063
+ issueCount: scanResult.issues.length,
1064
+ pullRequestCount: scanResult.prs.length,
1065
+ commentCount: scanResult.comments.length,
1066
+ taskCount: dispatchResult.tasks.length,
1067
+ pendingTaskCount: dispatchResult.tasks.filter((task) => task.status === "pending").length,
1068
+ dispatchedTaskCount: dispatchResult.tasks.filter((task) => task.status === "dispatched").length,
1069
+ completedTaskCount: dispatchResult.tasks.filter((task) => task.status === "completed").length,
1070
+ failedTaskCount: dispatchResult.tasks.filter((task) => task.status === "failed").length,
1071
+ executionCount: dispatchResult.executions.length,
1072
+ productDiscoveryCount: summaryWorkstreams.filter((workstream) => workstream.phase === "product_discovery").length,
1073
+ productSpecificationCount: summaryWorkstreams.filter((workstream) => workstream.phase === "product_specification").length,
1074
+ waitingForProductApprovalCount: summaryWorkstreams.filter((workstream) => workstream.product?.status === "in_discovery").length,
1075
+ pullRequestBaseClassifications: scanResult.prs
1076
+ .map((pr) => classifyPullRequestBase({
1077
+ pullRequestNumber: pr.number,
1078
+ repoSlug: pr.repoSlug,
1079
+ baseBranch: pr.baseBranch,
1080
+ defaultBranch: pr.defaultBranch
1081
+ }))
1082
+ .sort((left, right) => {
1083
+ const slugCompare = left.repoSlug.localeCompare(right.repoSlug);
1084
+ if (slugCompare !== 0) {
1085
+ return slugCompare;
1086
+ }
1087
+ return left.pullRequestNumber - right.pullRequestNumber;
1088
+ }),
1089
+ packetPullRequestCandidates: collectPacketPullRequestCandidates({
1090
+ pullRequests: scanResult.prs,
1091
+ workstreams: summaryWorkstreams
1092
+ }),
1093
+ parentBranchSyncPreflights: await collectParentBranchSyncPreflights({
1094
+ workstreams: summaryWorkstreams,
1095
+ repos: enabledRepos,
1096
+ pullRequests: scanResult.prs,
1097
+ repoResults: scanResult.repoResults,
1098
+ tasks: [...dispatchResult.tasks, ...previousState.tasks],
1099
+ executions: dispatchResult.executions,
1100
+ gitGateway
1101
+ }),
1102
+ packetPrsCreatedThisRun,
1103
+ packetWaveAdvancementsThisRun,
1104
+ packetWaveBlockedThisRun,
1105
+ packetPlans: summaryWorkstreams
1106
+ .filter((ws) => ws.packetPlan)
1107
+ .map((ws) => ({
1108
+ workstreamId: ws.id,
1109
+ repoKey: ws.repoKey,
1110
+ parentPrNumber: ws.integration?.prNumber,
1111
+ status: ws.packetPlan.status,
1112
+ packetCount: Object.keys(ws.packetPlan.packets).length,
1113
+ waveCount: ws.packetPlan.waves.length,
1114
+ currentWaveIndex: ws.packetPlan.currentWaveIndex,
1115
+ waves: ws.packetPlan.waves.map((packetIds, index) => ({
1116
+ index,
1117
+ packetIds,
1118
+ packets: packetIds.map((id) => {
1119
+ const p = ws.packetPlan.packets[id];
1120
+ return {
1121
+ id,
1122
+ status: p?.status ?? "planned",
1123
+ branchName: p?.branchName,
1124
+ prNumber: p?.prNumber
1125
+ };
1126
+ })
1127
+ }))
1128
+ }))
1129
+ .sort((a, b) => a.workstreamId.localeCompare(b.workstreamId)),
1130
+ results: scanResult.repoResults
1131
+ .slice()
1132
+ .sort((left, right) => left.repoSlug.localeCompare(right.repoSlug)),
1133
+ tasks: dispatchResult.tasks
1134
+ };
1135
+ return summary;
1136
+ }
1137
+ async function collectParentBranchSyncPreflights(input) {
1138
+ const repoByKey = new Map();
1139
+ for (const repo of input.repos) {
1140
+ repoByKey.set(repo.id, repo);
1141
+ repoByKey.set(`${repo.owner}/${repo.name}`, repo);
1142
+ }
1143
+ const repoResultById = new Map(input.repoResults.map((result) => [result.repoId, result]));
1144
+ const repoResultBySlug = new Map(input.repoResults.map((result) => [result.repoSlug, result]));
1145
+ const pullRequestByRepoAndNumber = new Map();
1146
+ for (const pullRequest of input.pullRequests) {
1147
+ pullRequestByRepoAndNumber.set(`${pullRequest.repoId}:${pullRequest.number}`, pullRequest);
1148
+ pullRequestByRepoAndNumber.set(`${pullRequest.repoKey}:${pullRequest.number}`, pullRequest);
1149
+ pullRequestByRepoAndNumber.set(`${pullRequest.repoSlug}:${pullRequest.number}`, pullRequest);
1150
+ }
1151
+ const basePreflights = input.workstreams
1152
+ .map((workstream) => {
1153
+ const repo = repoByKey.get(workstream.repoKey);
1154
+ const repoResult = repo
1155
+ ? repoResultById.get(repo.id) ?? repoResultBySlug.get(`${repo.owner}/${repo.name}`)
1156
+ : repoResultBySlug.get(workstream.repoKey);
1157
+ const defaultBranch = repo?.defaultBranch ?? repoResult?.defaultBranch ?? undefined;
1158
+ const preflight = buildParentBranchSyncPreflight({
1159
+ workstream,
1160
+ defaultBranch: defaultBranch ?? undefined
1161
+ });
1162
+ if (preflight?.parentPrNumber !== undefined) {
1163
+ const parentPr = pullRequestByRepoAndNumber.get(`${workstream.repoKey}:${preflight.parentPrNumber}`) ??
1164
+ (repo
1165
+ ? pullRequestByRepoAndNumber.get(`${repo.id}:${preflight.parentPrNumber}`) ??
1166
+ pullRequestByRepoAndNumber.get(`${repo.owner}/${repo.name}:${preflight.parentPrNumber}`)
1167
+ : undefined);
1168
+ if (parentPr?.headSha) {
1169
+ preflight.parentHeadSha = parentPr.headSha;
1170
+ }
1171
+ }
1172
+ if (preflight) {
1173
+ const originalPlanningSessionId = resolveWaveAssessmentOriginalPlanningSessionId(workstream, preflight, input.tasks, input.executions);
1174
+ if (originalPlanningSessionId) {
1175
+ preflight.originalPlanningSessionId = originalPlanningSessionId;
1176
+ }
1177
+ }
1178
+ return { preflight, repoResult };
1179
+ })
1180
+ .filter((item) => item.preflight !== undefined);
1181
+ const enriched = [];
1182
+ for (const { preflight, repoResult } of basePreflights) {
1183
+ if (!repoResult?.localPath) {
1184
+ enriched.push({ preflight, branchSyncComparisonSkippedReason: "missing_local_path" });
1185
+ continue;
1186
+ }
1187
+ try {
1188
+ const branchSyncComparison = await input.gitGateway.compareBranchSyncStatus(repoResult.localPath, preflight.parentIntegrationBranch, preflight.defaultBranch);
1189
+ enriched.push({ preflight, branchSyncComparison });
1190
+ }
1191
+ catch {
1192
+ enriched.push({
1193
+ preflight,
1194
+ branchSyncComparison: {
1195
+ parentBranch: preflight.parentIntegrationBranch,
1196
+ defaultBranch: preflight.defaultBranch,
1197
+ parentRef: `refs/remotes/origin/${preflight.parentIntegrationBranch}`,
1198
+ defaultRef: `refs/remotes/origin/${preflight.defaultBranch}`,
1199
+ status: "unknown",
1200
+ message: "Could not compare branch sync status."
1201
+ }
1202
+ });
1203
+ }
1204
+ }
1205
+ return enriched.sort((left, right) => {
1206
+ const repoCompare = left.preflight.repoKey.localeCompare(right.preflight.repoKey);
1207
+ if (repoCompare !== 0)
1208
+ return repoCompare;
1209
+ return left.preflight.workstreamId.localeCompare(right.preflight.workstreamId);
1210
+ });
1211
+ }
1212
+ function resolveWaveAssessmentOriginalPlanningSessionId(workstream, preflight, tasks, executions) {
1213
+ if (!preflight) {
1214
+ return undefined;
1215
+ }
1216
+ const taskById = new Map(tasks.map((task) => [task.id, task]));
1217
+ const latestPlanBreakdownExecution = executions
1218
+ .filter((execution) => execution.taskKind === "plan_breakdown" && execution.taskStatus === "completed")
1219
+ .filter((execution) => taskById.get(execution.taskId)?.workstreamId === workstream.id)
1220
+ .sort((left, right) => right.endedAt.localeCompare(left.endedAt))[0];
1221
+ if (latestPlanBreakdownExecution?.result?.planBreakdownSessionId) {
1222
+ return latestPlanBreakdownExecution.result.planBreakdownSessionId;
1223
+ }
1224
+ const matchingAssessment = (workstream.packetPlan?.waveAssessments ?? []).find((assessment) => assessment.checkpoint.identity.workstreamId === preflight.workstreamId &&
1225
+ assessment.checkpoint.identity.currentWaveIndex === preflight.currentWaveIndex &&
1226
+ assessment.checkpoint.identity.nextWaveIndex === preflight.nextWaveIndex &&
1227
+ assessment.checkpoint.identity.parentPrNumber === (preflight.parentPrNumber ?? 0) &&
1228
+ assessment.checkpoint.identity.parentIntegrationBranch === preflight.parentIntegrationBranch &&
1229
+ assessment.checkpoint.identity.defaultBranch === preflight.defaultBranch &&
1230
+ assessment.checkpoint.identity.parentHeadSha === (preflight.parentHeadSha ?? "unknown") &&
1231
+ assessment.checkpoint.identity.planRevision === (preflight.planRevision ?? "unknown"));
1232
+ return matchingAssessment?.checkpoint.identity.originalPlanningSessionId;
1233
+ }
1234
+ function buildMetadata(generatedAt, entityCount, repoCount, failures) {
1235
+ return {
1236
+ generatedAt,
1237
+ repoCount,
1238
+ entityCount,
1239
+ partialFailures: failures
1240
+ };
1241
+ }