@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,190 @@
1
+ /**
2
+ * Collects packet-scoped PR projections from a set of scanned entities.
3
+ * This is a pure and deterministic function.
4
+ */
5
+ export function collectPacketPullRequestProjections(input) {
6
+ const { pullRequests, workstreams, worktrees } = input;
7
+ // 1. Build lookups
8
+ const worktreesByParentAndPacket = new Map();
9
+ for (const wt of worktrees) {
10
+ if (wt.packet) {
11
+ const key = `${wt.packet.parentWorkstreamId}\0${wt.packet.packetId}`;
12
+ const list = worktreesByParentAndPacket.get(key) || [];
13
+ list.push(wt);
14
+ worktreesByParentAndPacket.set(key, list);
15
+ }
16
+ }
17
+ const prsByRepoIdAndHeadBranch = new Map();
18
+ for (const pr of pullRequests) {
19
+ const key = `${pr.repoId}\0${pr.headBranch}`;
20
+ const list = prsByRepoIdAndHeadBranch.get(key) || [];
21
+ list.push(pr);
22
+ prsByRepoIdAndHeadBranch.set(key, list);
23
+ }
24
+ const prsByRepoIdAndNumber = new Map();
25
+ for (const pr of pullRequests) {
26
+ const key = `${pr.repoId}\0${pr.number}`;
27
+ prsByRepoIdAndNumber.set(key, pr);
28
+ }
29
+ const projections = [];
30
+ const seenProjections = new Set(); // repoSlug#prNumber#parentWorkstream.id#packet.id
31
+ // 2. Iterate parent workstreams deterministically
32
+ const sortedWorkstreams = [...workstreams].sort((a, b) => {
33
+ return a.repoKey.localeCompare(b.repoKey) || a.id.localeCompare(b.id);
34
+ });
35
+ for (const parentWorkstream of sortedWorkstreams) {
36
+ if (!parentWorkstream.packetPlan) {
37
+ continue;
38
+ }
39
+ const packetEntries = getPacketEntries(parentWorkstream);
40
+ for (const [packetId, packet] of packetEntries) {
41
+ let matchedInThisPacket = false;
42
+ // A. Worktree-backed matching (primary)
43
+ const key = `${parentWorkstream.id}\0${packetId}`;
44
+ const candidateWorktrees = worktreesByParentAndPacket.get(key) || [];
45
+ // Sort worktrees for determinism
46
+ candidateWorktrees.sort((a, b) => a.repoSlug.localeCompare(b.repoSlug) ||
47
+ a.branchName.localeCompare(b.branchName) ||
48
+ a.id.localeCompare(b.id));
49
+ for (const packetWorktree of candidateWorktrees) {
50
+ const parentIntegrationBranch = getParentIntegrationBranch(parentWorkstream, packetWorktree);
51
+ if (!parentIntegrationBranch) {
52
+ continue;
53
+ }
54
+ const prs = prsByRepoIdAndHeadBranch.get(`${packetWorktree.repoId}\0${packetWorktree.branchName}`) || [];
55
+ // Sort PRs for determinism if multiple match the same branch (unlikely)
56
+ const sortedPrs = [...prs].sort((a, b) => a.number - b.number);
57
+ for (const pr of sortedPrs) {
58
+ if (isEligiblePacketPr(pr, parentIntegrationBranch)) {
59
+ const projectionKey = `${pr.repoSlug}#${pr.number}#${parentWorkstream.id}#${packetId}`;
60
+ if (!seenProjections.has(projectionKey)) {
61
+ projections.push({
62
+ pullRequest: pr,
63
+ parentWorkstream,
64
+ packet,
65
+ packetWorktree,
66
+ parentIntegrationBranch,
67
+ parentPullRequestNumber: parentWorkstream.integration?.prNumber
68
+ });
69
+ seenProjections.add(projectionKey);
70
+ matchedInThisPacket = true;
71
+ }
72
+ }
73
+ }
74
+ }
75
+ // B. Fallback matching by packet.prNumber (only if no worktree match)
76
+ if (!matchedInThisPacket && packet.prNumber) {
77
+ const parentIntegrationBranch = getParentIntegrationBranch(parentWorkstream);
78
+ if (parentIntegrationBranch) {
79
+ // Find repoId for fallback.
80
+ // Try to find it from packet worktrees for this parent.
81
+ let repoId = "";
82
+ for (const wt of worktrees) {
83
+ if (wt.packet?.parentWorkstreamId === parentWorkstream.id) {
84
+ repoId = wt.repoId;
85
+ break;
86
+ }
87
+ }
88
+ let pr;
89
+ if (repoId) {
90
+ pr = prsByRepoIdAndNumber.get(`${repoId}\0${packet.prNumber}`);
91
+ }
92
+ else {
93
+ // Fallback to repoSlug comparison since WorkstreamRecord doesn't have repoId
94
+ for (const p of pullRequests) {
95
+ if (p.repoSlug === parentWorkstream.repoKey &&
96
+ p.number === packet.prNumber) {
97
+ pr = p;
98
+ break;
99
+ }
100
+ }
101
+ }
102
+ if (pr && isEligiblePacketPr(pr, parentIntegrationBranch)) {
103
+ const projectionKey = `${pr.repoSlug}#${pr.number}#${parentWorkstream.id}#${packetId}`;
104
+ if (!seenProjections.has(projectionKey)) {
105
+ projections.push({
106
+ pullRequest: pr,
107
+ parentWorkstream,
108
+ packet,
109
+ parentIntegrationBranch,
110
+ parentPullRequestNumber: parentWorkstream.integration?.prNumber
111
+ });
112
+ seenProjections.add(projectionKey);
113
+ }
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ return projections;
120
+ }
121
+ /**
122
+ * Collects packet-scoped PR comment projections.
123
+ */
124
+ export function collectPacketPullRequestCommentProjections(input) {
125
+ const projections = [];
126
+ for (const comment of input.comments) {
127
+ if (comment.parentType !== "pull_request") {
128
+ continue;
129
+ }
130
+ for (const packetProjection of input.packetPullRequestProjections) {
131
+ if (comment.repoId === packetProjection.pullRequest.repoId &&
132
+ comment.parentNumber === packetProjection.pullRequest.number) {
133
+ projections.push({
134
+ comment,
135
+ packetProjection
136
+ });
137
+ }
138
+ }
139
+ }
140
+ // Sort deterministically
141
+ return projections.sort((a, b) => {
142
+ return (a.comment.repoSlug.localeCompare(b.comment.repoSlug) ||
143
+ a.comment.parentNumber - b.comment.parentNumber ||
144
+ a.comment.id.localeCompare(b.comment.id) ||
145
+ a.packetProjection.parentWorkstream.id.localeCompare(b.packetProjection.parentWorkstream.id) ||
146
+ a.packetProjection.packet.id.localeCompare(b.packetProjection.packet.id));
147
+ });
148
+ }
149
+ /**
150
+ * Resolves the parent integration branch for a packet.
151
+ */
152
+ function getParentIntegrationBranch(parentWorkstream, packetWorktree) {
153
+ return (packetWorktree?.packet?.parentIntegrationBranch ||
154
+ parentWorkstream.integration?.branchName);
155
+ }
156
+ /**
157
+ * Returns packet entries sorted by wave order and then packet ID.
158
+ */
159
+ function getPacketEntries(workstream) {
160
+ if (!workstream.packetPlan) {
161
+ return [];
162
+ }
163
+ const packets = workstream.packetPlan.packets;
164
+ const waves = workstream.packetPlan.waves || [];
165
+ const entries = [];
166
+ const wavedIds = new Set();
167
+ for (const wave of waves) {
168
+ const wavePackets = [];
169
+ for (const id of wave) {
170
+ if (packets[id]) {
171
+ wavePackets.push([id, packets[id]]);
172
+ wavedIds.add(id);
173
+ }
174
+ }
175
+ wavePackets.sort((a, b) => a[0].localeCompare(b[0]));
176
+ entries.push(...wavePackets);
177
+ }
178
+ const remainingEntries = Object.entries(packets)
179
+ .filter(([id]) => !wavedIds.has(id))
180
+ .sort((a, b) => a[0].localeCompare(b[0]));
181
+ entries.push(...remainingEntries);
182
+ return entries;
183
+ }
184
+ /**
185
+ * Checks if a PR is eligible to be a packet PR for a given parent integration branch.
186
+ */
187
+ function isEligiblePacketPr(pr, parentIntegrationBranch) {
188
+ return (pr.baseBranch === parentIntegrationBranch &&
189
+ pr.baseClassification === "non_default_parent");
190
+ }
@@ -0,0 +1,312 @@
1
+ import * as path from "node:path";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ export async function executeParentBranchSyncActions(options) {
4
+ const now = options.now ?? (() => new Date().toISOString());
5
+ const tasks = [...options.tasks];
6
+ const executions = [...options.executions];
7
+ const attempts = [];
8
+ for (let i = 0; i < tasks.length; i++) {
9
+ const task = tasks[i];
10
+ if (!task ||
11
+ task.kind !== "sync_parent_branch" ||
12
+ task.status !== "pending" ||
13
+ task.relevant === false) {
14
+ continue;
15
+ }
16
+ const preflight = task.context.preflight;
17
+ if (!preflight) {
18
+ const failedAt = now();
19
+ const execution = await buildActionFailureExecution(task, failedAt, options.logDir, "Missing preflight data in task context.");
20
+ tasks[i] = {
21
+ ...task,
22
+ status: "failed",
23
+ lastExecutionId: execution.id,
24
+ updatedAt: failedAt
25
+ };
26
+ executions.push(execution);
27
+ await options.stateStore.writeExecutionState(executions);
28
+ await options.stateStore.writeTaskState(tasks);
29
+ continue;
30
+ }
31
+ const localPath = task.context.localRepoPath;
32
+ if (!localPath) {
33
+ const failedAt = now();
34
+ const execution = await buildActionFailureExecution(task, failedAt, options.logDir, "Missing local repository path in task context.");
35
+ tasks[i] = {
36
+ ...task,
37
+ status: "failed",
38
+ lastExecutionId: execution.id,
39
+ updatedAt: failedAt
40
+ };
41
+ executions.push(execution);
42
+ await options.stateStore.writeExecutionState(executions);
43
+ await options.stateStore.writeTaskState(tasks);
44
+ const { parentPrNumber } = preflight;
45
+ if (parentPrNumber) {
46
+ try {
47
+ const attempt = {
48
+ taskId: task.id,
49
+ status: "failed",
50
+ attempted: false,
51
+ parentBranch: preflight.parentIntegrationBranch,
52
+ defaultBranch: preflight.defaultBranch,
53
+ parentPrNumber,
54
+ message: "Missing local repository path in task context."
55
+ };
56
+ const comment = formatParentBranchSyncComment(attempt);
57
+ await options.gitHubGateway.postIssueComment({
58
+ owner: task.repoSlug.split("/")[0] ?? "",
59
+ name: task.repoSlug.split("/")[1] ?? "",
60
+ issueNumber: parentPrNumber,
61
+ body: comment
62
+ });
63
+ }
64
+ catch (err) {
65
+ options.logger?.warn(`Failed to post sync feedback to ${task.repoSlug}#${parentPrNumber}: ${err}`);
66
+ }
67
+ }
68
+ continue;
69
+ }
70
+ const { parentIntegrationBranch, defaultBranch, parentPrNumber } = preflight;
71
+ if (parentPrNumber === undefined) {
72
+ const failedAt = now();
73
+ const execution = await buildActionFailureExecution(task, failedAt, options.logDir, "Missing parent PR number in preflight data.");
74
+ tasks[i] = {
75
+ ...task,
76
+ status: "failed",
77
+ lastExecutionId: execution.id,
78
+ updatedAt: failedAt
79
+ };
80
+ executions.push(execution);
81
+ await options.stateStore.writeExecutionState(executions);
82
+ await options.stateStore.writeTaskState(tasks);
83
+ continue;
84
+ }
85
+ options.logger?.info(`Executing orchestrator sync for ${task.repoSlug} branch ${parentIntegrationBranch} (PR #${parentPrNumber})`);
86
+ let beforeComparison;
87
+ try {
88
+ beforeComparison = await options.gitGateway.compareBranchSyncStatus(localPath, parentIntegrationBranch, defaultBranch);
89
+ }
90
+ catch (err) {
91
+ options.logger?.warn(`Could not obtain before-comparison for sync action: ${err}`);
92
+ }
93
+ const startedAt = now();
94
+ let syncResult;
95
+ try {
96
+ syncResult = await options.parentBranchSyncService.syncParentBranch(localPath, parentIntegrationBranch, defaultBranch);
97
+ }
98
+ catch (err) {
99
+ syncResult = {
100
+ status: "failed",
101
+ message: "Parent branch sync service threw an unexpected error.",
102
+ error: err instanceof Error ? err.message : String(err)
103
+ };
104
+ }
105
+ let afterComparison;
106
+ try {
107
+ afterComparison = await options.gitGateway.compareBranchSyncStatus(localPath, parentIntegrationBranch, defaultBranch);
108
+ }
109
+ catch (err) {
110
+ options.logger?.warn(`Could not obtain after-comparison for sync action: ${err}`);
111
+ }
112
+ const endedAt = now();
113
+ const taskStatus = syncResult.status === "synced" || syncResult.status === "already_up_to_date"
114
+ ? "completed"
115
+ : "failed";
116
+ const attempt = {
117
+ taskId: task.id,
118
+ status: syncResult.status,
119
+ attempted: true,
120
+ parentBranch: parentIntegrationBranch,
121
+ defaultBranch,
122
+ message: syncResult.message,
123
+ ...(parentPrNumber !== undefined ? { parentPrNumber } : {}),
124
+ ...(beforeComparison !== undefined ? { beforeComparison } : {}),
125
+ ...(afterComparison !== undefined ? { afterComparison } : {}),
126
+ ...(syncResult.error !== undefined ? { error: syncResult.error } : {})
127
+ };
128
+ attempts.push(attempt);
129
+ const execution = await buildSyncExecutionRecord({
130
+ task,
131
+ attempt,
132
+ logDir: options.logDir,
133
+ startedAt,
134
+ endedAt
135
+ });
136
+ tasks[i] = {
137
+ ...task,
138
+ status: taskStatus,
139
+ lastExecutionId: execution.id,
140
+ updatedAt: endedAt
141
+ };
142
+ executions.push(execution);
143
+ // Persist incrementally
144
+ await options.stateStore.writeExecutionState(executions);
145
+ await options.stateStore.writeTaskState(tasks);
146
+ // Feedback
147
+ if (parentPrNumber) {
148
+ try {
149
+ const comment = formatParentBranchSyncComment(attempt);
150
+ await options.gitHubGateway.postIssueComment({
151
+ owner: task.repoSlug.split("/")[0] ?? "",
152
+ name: task.repoSlug.split("/")[1] ?? "",
153
+ issueNumber: parentPrNumber,
154
+ body: comment
155
+ });
156
+ }
157
+ catch (err) {
158
+ options.logger?.warn(`Failed to post sync feedback to ${task.repoSlug}#${parentPrNumber}: ${err}`);
159
+ }
160
+ }
161
+ }
162
+ return { tasks, executions, attempts };
163
+ }
164
+ async function buildActionFailureExecution(task, timestamp, logDir, message) {
165
+ const executionId = `exec-${task.id.replace(/[^a-zA-Z0-9_-]+/g, "_")}-action-failure-${timestamp.replace(/[:.]/g, "-")}`;
166
+ const artifactRoot = path.join(logDir, "artifacts", executionId);
167
+ await mkdir(artifactRoot, { recursive: true });
168
+ const promptPacketPath = path.join(artifactRoot, "prompt-packet.json");
169
+ const promptArtifactPath = path.join(artifactRoot, "prompt.md");
170
+ const stdoutArtifactPath = path.join(artifactRoot, "stdout.txt");
171
+ const stderrArtifactPath = path.join(artifactRoot, "stderr.txt");
172
+ await Promise.all([
173
+ writeFile(promptArtifactPath, `Task: ${task.id}\nPhase: parent branch sync action\n\n${message}\n`, "utf8"),
174
+ writeFile(stdoutArtifactPath, "", "utf8"),
175
+ writeFile(stderrArtifactPath, `${message}\n`, "utf8")
176
+ ]);
177
+ await writeFile(promptPacketPath, JSON.stringify({
178
+ taskId: task.id,
179
+ taskKind: task.kind,
180
+ status: "failed",
181
+ error: message
182
+ }, null, 2), "utf8");
183
+ return {
184
+ id: executionId,
185
+ taskId: task.id,
186
+ taskKind: task.kind,
187
+ taskStatus: "failed",
188
+ repoId: task.repoId,
189
+ repoSlug: task.repoSlug,
190
+ agentId: "orchestrator",
191
+ agentProvider: "unassigned",
192
+ command: {
193
+ executable: "orchestrator-action",
194
+ args: [],
195
+ cwd: task.context.localRepoPath ?? process.cwd(),
196
+ timeoutMs: 0
197
+ },
198
+ promptArtifactPath,
199
+ stdoutArtifactPath,
200
+ stderrArtifactPath,
201
+ promptPacketPath,
202
+ startedAt: timestamp,
203
+ endedAt: timestamp,
204
+ exitCode: 1,
205
+ errorMessage: message
206
+ };
207
+ }
208
+ async function buildSyncExecutionRecord(input) {
209
+ const { task, attempt, logDir, startedAt, endedAt } = input;
210
+ const executionId = `exec-${task.id.replace(/[^a-zA-Z0-9_-]+/g, "_")}-sync-action-${startedAt.replace(/[:.]/g, "-")}`;
211
+ const artifactRoot = path.join(logDir, "artifacts", executionId);
212
+ await mkdir(artifactRoot, { recursive: true });
213
+ const promptPacketPath = path.join(artifactRoot, "prompt-packet.json");
214
+ const promptArtifactPath = path.join(artifactRoot, "prompt.md");
215
+ const stdoutArtifactPath = path.join(artifactRoot, "stdout.txt");
216
+ const stderrArtifactPath = path.join(artifactRoot, "stderr.txt");
217
+ const resultPacket = {
218
+ taskId: task.id,
219
+ taskKind: task.kind,
220
+ parentBranchSync: {
221
+ status: attempt.status,
222
+ message: attempt.message,
223
+ parentBranch: attempt.parentBranch,
224
+ defaultBranch: attempt.defaultBranch,
225
+ parentPrNumber: attempt.parentPrNumber,
226
+ beforeComparison: attempt.beforeComparison,
227
+ afterComparison: attempt.afterComparison,
228
+ error: attempt.error
229
+ }
230
+ };
231
+ await Promise.all([
232
+ writeFile(promptPacketPath, JSON.stringify(resultPacket, null, 2), "utf8"),
233
+ writeFile(promptArtifactPath, `Task: ${task.id}\nPhase: parent branch sync action\n\n${attempt.message}\n`, "utf8"),
234
+ writeFile(stdoutArtifactPath, `${attempt.message}\n`, "utf8"),
235
+ writeFile(stderrArtifactPath, attempt.error ? `${attempt.error}\n` : "", "utf8")
236
+ ]);
237
+ return {
238
+ id: executionId,
239
+ taskId: task.id,
240
+ taskKind: task.kind,
241
+ taskStatus: attempt.status === "synced" || attempt.status === "already_up_to_date"
242
+ ? "completed"
243
+ : "failed",
244
+ repoId: task.repoId,
245
+ repoSlug: task.repoSlug,
246
+ agentId: "orchestrator",
247
+ agentProvider: "unassigned",
248
+ command: {
249
+ executable: "git-sync-service",
250
+ args: ["merge", attempt.defaultBranch, "into", attempt.parentBranch],
251
+ cwd: task.context.localRepoPath ?? process.cwd(),
252
+ timeoutMs: 0
253
+ },
254
+ promptArtifactPath,
255
+ stdoutArtifactPath,
256
+ stderrArtifactPath,
257
+ promptPacketPath,
258
+ startedAt,
259
+ endedAt,
260
+ exitCode: attempt.status === "synced" || attempt.status === "already_up_to_date" ? 0 : 1,
261
+ ...(attempt.error !== undefined ? { errorMessage: attempt.error } : {}),
262
+ result: {
263
+ parentBranchSync: {
264
+ status: attempt.status,
265
+ message: attempt.message,
266
+ parentBranch: attempt.parentBranch,
267
+ defaultBranch: attempt.defaultBranch,
268
+ ...(attempt.parentPrNumber !== undefined ? { parentPrNumber: attempt.parentPrNumber } : {}),
269
+ ...(attempt.beforeComparison !== undefined ? { beforeComparison: attempt.beforeComparison } : {}),
270
+ ...(attempt.afterComparison !== undefined ? { afterComparison: attempt.afterComparison } : {}),
271
+ ...(attempt.error !== undefined ? { error: attempt.error } : {})
272
+ }
273
+ }
274
+ };
275
+ }
276
+ export function formatParentBranchSyncComment(attempt) {
277
+ const lines = ["🤖 **Parent Branch Sync Attempt**", ""];
278
+ const actionText = attempt.attempted
279
+ ? `Attempted to merge the default branch (\`${attempt.defaultBranch}\`) into the parent branch (\`${attempt.parentBranch}\`) through the git sync service, with no rebase or force push.`
280
+ : "Could not attempt merge (precondition failed)";
281
+ lines.push(`- **Action**: ${actionText}`);
282
+ lines.push(`- **Parent Branch**: \`${attempt.parentBranch}\``);
283
+ lines.push(`- **Default Branch**: \`${attempt.defaultBranch}\``);
284
+ if (attempt.beforeComparison) {
285
+ const comp = attempt.beforeComparison;
286
+ lines.push(`- **Pre-attempt status**: \`${comp.status}\` (ahead: ${comp.parentAheadCount ?? "?"}, behind: ${comp.parentBehindCount ?? "?"})`);
287
+ }
288
+ lines.push(`- **Result**: \`${attempt.status}\``);
289
+ if (attempt.message) {
290
+ lines.push(` > ${attempt.message}`);
291
+ }
292
+ if (attempt.error) {
293
+ lines.push(` > **Error**: ${attempt.error}`);
294
+ }
295
+ if (attempt.afterComparison) {
296
+ const comp = attempt.afterComparison;
297
+ lines.push(`- **Post-attempt status**: \`${comp.status}\` (ahead: ${comp.parentAheadCount ?? "?"}, behind: ${comp.parentBehindCount ?? "?"})`);
298
+ lines.push("");
299
+ if (comp.status === "up_to_date") {
300
+ lines.push("Wave advancement is now eligible. It will be evaluated in the normal orchestration flow.");
301
+ }
302
+ else {
303
+ lines.push("Wave advancement remains blocked because the branch is not yet `up_to_date`.");
304
+ }
305
+ }
306
+ else {
307
+ lines.push("- **Post-attempt status**: `unknown` (comparison failed or unavailable)");
308
+ lines.push("");
309
+ lines.push("Wave advancement remains blocked because the branch status could not be verified after the attempt.");
310
+ }
311
+ return lines.join("\n");
312
+ }
@@ -0,0 +1,148 @@
1
+ export class LocalParentBranchSyncService {
2
+ gitGateway;
3
+ constructor(gitGateway) {
4
+ this.gitGateway = gitGateway;
5
+ }
6
+ async syncParentBranch(localPath, parentBranch, defaultBranch) {
7
+ let initialComparison;
8
+ try {
9
+ await this.gitGateway.fetchRepository(localPath);
10
+ if (!(await this.gitGateway.isWorkingTreeClean(localPath))) {
11
+ return {
12
+ status: "blocked",
13
+ message: "Working tree is dirty; refusing to sync parent branch."
14
+ };
15
+ }
16
+ initialComparison = await this.gitGateway.compareBranchSyncStatus(localPath, parentBranch, defaultBranch);
17
+ if (initialComparison.status === "up_to_date") {
18
+ return {
19
+ status: "already_up_to_date",
20
+ message: "Parent branch is already up to date with the default branch.",
21
+ comparison: initialComparison
22
+ };
23
+ }
24
+ if (initialComparison.status === "missing_parent_branch" ||
25
+ initialComparison.status === "missing_default_branch" ||
26
+ initialComparison.status === "unknown") {
27
+ return {
28
+ status: "blocked",
29
+ message: initialComparison.message,
30
+ comparison: initialComparison
31
+ };
32
+ }
33
+ // If we are here, status is "behind_default" or "diverged"
34
+ const originalBranch = await this.gitGateway.getCurrentBranch(localPath);
35
+ // Check if parentBranch is already checked out in another worktree
36
+ const parentBranchWorktreePath = await this.gitGateway.getWorktreeForBranch(localPath, parentBranch);
37
+ const syncPath = parentBranchWorktreePath || localPath;
38
+ if (syncPath !== localPath && !(await this.gitGateway.isWorkingTreeClean(syncPath))) {
39
+ return {
40
+ status: "blocked",
41
+ message: `Parent branch is checked out in a dirty worktree at "${syncPath}"; refusing to sync parent branch.`
42
+ };
43
+ }
44
+ let syncResult;
45
+ try {
46
+ if (!parentBranchWorktreePath && originalBranch !== parentBranch) {
47
+ await this.gitGateway.checkoutBranch(localPath, parentBranch);
48
+ }
49
+ // Ensure local parentBranch is up to date with its remote counterpart before merging
50
+ syncResult = await this.performMerge(syncPath, parentBranch, "pre-merging parent branch from remote", initialComparison);
51
+ if (!syncResult) {
52
+ syncResult = await this.performMerge(syncPath, defaultBranch, "syncing parent branch with default branch", initialComparison);
53
+ }
54
+ if (!syncResult) {
55
+ await this.gitGateway.pushBranch(syncPath, parentBranch);
56
+ // Fetch again to ensure refreshed comparison uses the latest remote state
57
+ await this.gitGateway.fetchRepository(localPath);
58
+ const refreshedComparison = await this.gitGateway.compareBranchSyncStatus(localPath, parentBranch, defaultBranch);
59
+ if (refreshedComparison.status === "up_to_date") {
60
+ syncResult = {
61
+ status: "synced",
62
+ message: "Parent branch successfully synced with the default branch.",
63
+ comparison: refreshedComparison
64
+ };
65
+ }
66
+ else {
67
+ syncResult = {
68
+ status: "failed",
69
+ message: `Refreshed comparison shows status "${refreshedComparison.status}" instead of "up_to_date": ${refreshedComparison.message}`,
70
+ comparison: refreshedComparison
71
+ };
72
+ }
73
+ }
74
+ }
75
+ catch (error) {
76
+ syncResult = {
77
+ status: "failed",
78
+ message: "An unexpected error occurred while syncing parent branch.",
79
+ comparison: initialComparison,
80
+ error: error instanceof Error ? error.message : String(error)
81
+ };
82
+ }
83
+ // Cleanup phase: restore original branch and verify cleanliness
84
+ // Only if we were operating in the original localPath and we actually switched branches
85
+ if (!parentBranchWorktreePath && originalBranch && originalBranch !== parentBranch) {
86
+ try {
87
+ await this.gitGateway.checkoutBranch(localPath, originalBranch);
88
+ }
89
+ catch (checkoutError) {
90
+ return {
91
+ status: "failed",
92
+ message: `Sync status was ${syncResult?.status || "unknown"}; however, failed to return to original branch "${originalBranch}".`,
93
+ comparison: syncResult?.comparison || initialComparison,
94
+ error: checkoutError instanceof Error ? checkoutError.message : String(checkoutError)
95
+ };
96
+ }
97
+ }
98
+ if (!(await this.gitGateway.isWorkingTreeClean(syncPath))) {
99
+ return {
100
+ status: "failed",
101
+ message: `Sync status was ${syncResult?.status || "unknown"}; however, the working tree at "${syncPath}" is dirty after cleanup.`,
102
+ comparison: syncResult?.comparison || initialComparison
103
+ };
104
+ }
105
+ return syncResult || { status: "failed", message: "Sync process failed to produce a result." };
106
+ }
107
+ catch (outerError) {
108
+ return {
109
+ status: "failed",
110
+ message: "An unexpected error occurred in the sync service.",
111
+ ...(initialComparison ? { comparison: initialComparison } : {}),
112
+ error: outerError instanceof Error ? outerError.message : String(outerError)
113
+ };
114
+ }
115
+ }
116
+ async performMerge(localPath, branch, phaseName, initialComparison) {
117
+ try {
118
+ await this.gitGateway.mergeRemoteBranch(localPath, "origin", branch);
119
+ return undefined;
120
+ }
121
+ catch (mergeError) {
122
+ try {
123
+ await this.gitGateway.abortMerge(localPath);
124
+ }
125
+ catch (abortError) {
126
+ return {
127
+ status: "failed",
128
+ message: `An error occurred during ${phaseName} and aborting the merge failed.`,
129
+ ...(initialComparison ? { comparison: initialComparison } : {}),
130
+ error: `Merge error: ${mergeError instanceof Error ? mergeError.message : String(mergeError)}. Abort error: ${abortError instanceof Error ? abortError.message : String(abortError)}`
131
+ };
132
+ }
133
+ // If abort succeeded, it was likely a merge conflict, but we check if the error message suggests something else
134
+ const errorMessage = mergeError instanceof Error ? mergeError.message : String(mergeError);
135
+ const lowerMessage = errorMessage.toLowerCase();
136
+ const isConflict = lowerMessage.includes("conflict") ||
137
+ lowerMessage.includes("automatic merge failed");
138
+ return {
139
+ status: isConflict ? "conflict" : "failed",
140
+ message: isConflict
141
+ ? `Merge conflict occurred while ${phaseName}; merge aborted.`
142
+ : `Merge failed while ${phaseName}; merge aborted.`,
143
+ ...(initialComparison ? { comparison: initialComparison } : {}),
144
+ error: errorMessage
145
+ };
146
+ }
147
+ }
148
+ }
@@ -0,0 +1,4 @@
1
+ import { evaluateWorkstreamMachine } from "../workflows/workstream-machine/evaluate.js";
2
+ export function reduceWorkstreamPhase(input) {
3
+ return evaluateWorkstreamMachine(input);
4
+ }
@@ -0,0 +1,19 @@
1
+ import { validateSession } from "./validate.js";
2
+ export function exportSession(session) {
3
+ const issues = [...session.validationIssues, ...validateSession(session)];
4
+ const errors = issues.filter((i) => i.severity === "error");
5
+ if (errors.length > 0) {
6
+ throw new Error(`Cannot export invalid session: ${errors.map((e) => e.message).join("; ")}`);
7
+ }
8
+ return {
9
+ title: session.title,
10
+ summary: session.summary,
11
+ tasks: Object.values(session.tasks),
12
+ waves: session.waveOrder.map((id) => session.waves[id]),
13
+ recommendedFirstWaveId: session.recommendedFirstWaveId,
14
+ graphConcerns: session.graphConcerns,
15
+ humanDecisionsNeeded: session.humanDecisionsNeeded,
16
+ suggestedNextAction: session.suggestedNextAction,
17
+ validationIssues: issues.length > 0 ? issues : undefined
18
+ };
19
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./types.js";
2
+ export * from "./session.js";
3
+ export * from "./patch.js";
4
+ export * from "./inspect.js";
5
+ export * from "./validate.js";
6
+ export * from "./export.js";