@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,629 @@
1
+ import { reduceWorkstreamPhase } from "./phases.js";
2
+ import { markMergedPacketPullRequests } from "./packet-plan.js";
3
+ function deriveProductState(input) {
4
+ const approvalLabel = input.repo?.labels.agentProductApproved;
5
+ const issueApproved = approvalLabel !== undefined && input.issue?.labels.includes(approvalLabel);
6
+ const previous = input.previous?.product;
7
+ if (input.activeWorktree?.productSpecWrittenAt || previous?.status === "spec_written") {
8
+ return {
9
+ status: "spec_written",
10
+ ...(approvalLabel ? { approvalLabel } : {}),
11
+ ...(previous?.approvedAt ? { approvedAt: previous.approvedAt } : issueApproved && input.now ? { approvedAt: input.now } : {})
12
+ };
13
+ }
14
+ if (input.nextPhase === "product_specification" || issueApproved) {
15
+ return {
16
+ status: "approved_for_spec",
17
+ ...(approvalLabel ? { approvalLabel } : {}),
18
+ ...(previous?.approvedAt ? { approvedAt: previous.approvedAt } : input.now ? { approvedAt: input.now } : {})
19
+ };
20
+ }
21
+ if (input.nextPhase === "product_discovery" || previous?.status === "in_discovery") {
22
+ return {
23
+ status: "in_discovery",
24
+ ...(approvalLabel ? { approvalLabel } : {})
25
+ };
26
+ }
27
+ return previous;
28
+ }
29
+ export function deriveWorkstreams(input) {
30
+ const reposById = new Map(input.repos.map((repo) => [repo.id, repo]));
31
+ const reposBySlug = new Map(input.repos.map((repo) => [`${repo.owner}/${repo.name}`, repo]));
32
+ const previousPullRequestIds = new Set((input.previousPullRequests ?? []).map((pr) => pr.id));
33
+ const currentIssues = [...input.currentIssues].sort(compareIssues);
34
+ const currentPullRequests = [...input.currentPullRequests].sort(comparePullRequests);
35
+ const previousWorkstreams = [...input.previousWorkstreams].sort(compareWorkstreams);
36
+ const worktrees = [...input.worktrees].sort(compareWorktrees);
37
+ const currentPrsByRepoAndNumber = new Map(currentPullRequests.map((pr) => [`${pr.repoId}:${pr.number}`, pr]));
38
+ const currentPrsByRepoAndBranch = new Map(currentPullRequests.map((pr) => [`${pr.repoId}:${pr.headBranch}`, pr]));
39
+ const attentionByWorkstreamId = new Map((input.attention ?? []).map((record) => [record.workstreamId, record]));
40
+ const worktreesByParentId = new Map();
41
+ for (const worktree of worktrees) {
42
+ let parentId = getIssueWorkstreamIdForWorktree(worktree);
43
+ if (!parentId && worktree.packet?.parentWorkstreamId) {
44
+ parentId = worktree.packet.parentWorkstreamId;
45
+ }
46
+ if (parentId) {
47
+ if (!worktreesByParentId.has(parentId)) {
48
+ worktreesByParentId.set(parentId, []);
49
+ }
50
+ worktreesByParentId.get(parentId).push(worktree);
51
+ }
52
+ }
53
+ const issueBuilders = new Map();
54
+ for (const previous of previousWorkstreams) {
55
+ if (previous.origin.type !== "issue") {
56
+ continue;
57
+ }
58
+ const packetPrNumbers = new Set(Object.values(previous.packetPlan?.packets ?? {})
59
+ .map((p) => p.prNumber)
60
+ .filter(isDefined));
61
+ issueBuilders.set(previous.id, {
62
+ id: previous.id,
63
+ repoId: undefined,
64
+ repoKey: previous.repoKey,
65
+ originIssueNumber: previous.origin.number,
66
+ previous,
67
+ currentIssue: undefined,
68
+ worktrees: [],
69
+ linkedPrNumbers: new Set(previous.linkedPrs
70
+ .map((pr) => pr.number)
71
+ .filter((num) => !packetPrNumbers.has(num)))
72
+ });
73
+ }
74
+ for (const issue of currentIssues) {
75
+ const id = buildIssueWorkstreamId(issue.repoOwner, issue.repoName, issue.number);
76
+ const existing = issueBuilders.get(id);
77
+ issueBuilders.set(id, {
78
+ id,
79
+ repoId: issue.repoId,
80
+ repoKey: issue.repoKey,
81
+ originIssueNumber: issue.number,
82
+ previous: existing?.previous ?? undefined,
83
+ currentIssue: issue,
84
+ worktrees: existing?.worktrees ?? [],
85
+ linkedPrNumbers: existing?.linkedPrNumbers ?? new Set()
86
+ });
87
+ }
88
+ for (const [parentId, relatedWorktrees] of worktreesByParentId) {
89
+ const issueNumber = extractIssueNumberFromWorkstreamId(parentId);
90
+ if (issueNumber === undefined) {
91
+ continue;
92
+ }
93
+ const existing = issueBuilders.get(parentId);
94
+ const builder = existing ?? {
95
+ id: parentId,
96
+ repoId: relatedWorktrees[0]?.repoId,
97
+ repoKey: relatedWorktrees[0].repoSlug,
98
+ originIssueNumber: issueNumber,
99
+ previous: undefined,
100
+ currentIssue: undefined,
101
+ worktrees: [],
102
+ linkedPrNumbers: new Set()
103
+ };
104
+ for (const worktree of relatedWorktrees) {
105
+ builder.worktrees.push(worktree);
106
+ if (worktree.packet) {
107
+ if (worktree.prNumber !== undefined) {
108
+ builder.linkedPrNumbers.delete(worktree.prNumber);
109
+ }
110
+ const pr = currentPrsByRepoAndBranch.get(`${worktree.repoId}:${worktree.branchName}`);
111
+ if (pr) {
112
+ builder.linkedPrNumbers.delete(pr.number);
113
+ }
114
+ continue;
115
+ }
116
+ if (worktree.prNumber !== undefined) {
117
+ builder.linkedPrNumbers.add(worktree.prNumber);
118
+ }
119
+ const pr = currentPrsByRepoAndBranch.get(`${worktree.repoId}:${worktree.branchName}`);
120
+ if (pr) {
121
+ builder.linkedPrNumbers.add(pr.number);
122
+ }
123
+ }
124
+ issueBuilders.set(parentId, builder);
125
+ }
126
+ const issueWorkstreams = [...issueBuilders.values()]
127
+ .map((builder) => {
128
+ const sortedWorktrees = [...builder.worktrees]
129
+ .filter((wt) => !wt.packet)
130
+ .sort(compareWorktrees);
131
+ const linkedPrs = [...builder.linkedPrNumbers]
132
+ .sort((left, right) => left - right)
133
+ .map((prNumber) => buildLinkedPullRequest(builder.previous, currentPrsByRepoAndNumber.get(`${builder.repoId ?? builder.currentIssue?.repoId}:${prNumber}`), prNumber, "implementation"));
134
+ const activeWorktree = pickActiveWorktree(sortedWorktrees);
135
+ const repoId = builder.repoId ?? builder.currentIssue?.repoId;
136
+ const activePrNumber = activeWorktree?.prNumber ??
137
+ (activeWorktree
138
+ ? currentPrsByRepoAndBranch.get(`${activeWorktree.repoId}:${activeWorktree.branchName}`)?.number
139
+ : undefined) ??
140
+ pickActivePrNumber(linkedPrs, currentPrsByRepoAndNumber, repoId);
141
+ const activePullRequest = activePrNumber !== undefined && repoId !== undefined
142
+ ? currentPrsByRepoAndNumber.get(`${repoId}:${activePrNumber}`)
143
+ : undefined;
144
+ const repo = reposById.get(repoId ?? "");
145
+ const status = deriveIssueStatus(builder.currentIssue, linkedPrs, currentPrsByRepoAndNumber, repoId, builder.previous?.status);
146
+ const resolution = deriveResolution(linkedPrs, currentPrsByRepoAndNumber, repoId, builder.previous?.resolution);
147
+ const integration = deriveIntegrationState(activePullRequest, activePrNumber, activeWorktree, builder.previous, repo?.defaultBranch);
148
+ const previousPacketPlan = builder.previous?.packetPlan;
149
+ let packetPlan = previousPacketPlan && repoId
150
+ ? markMergedPacketPullRequests({
151
+ packetPlan: previousPacketPlan,
152
+ parentWorkstreamId: builder.id,
153
+ parentIntegrationBranch: integration?.branchName ?? builder.previous?.integration?.branchName,
154
+ repoId,
155
+ packetWorktrees: builder.worktrees,
156
+ pullRequests: currentPullRequests
157
+ })
158
+ : previousPacketPlan;
159
+ const nextPhase = reduceWorkstreamPhase({
160
+ previousPhase: builder.previous?.phase,
161
+ originType: "issue",
162
+ issuePresent: builder.currentIssue !== undefined,
163
+ activePullRequestState: activePullRequest?.state,
164
+ activePullRequestLabels: activePullRequest?.labels ?? [],
165
+ attentionReasons: attentionByWorkstreamId.get(builder.id)?.reasons ?? [],
166
+ inProgressLabel: repo?.labels.agentInProgress,
167
+ reviewLabel: repo?.labels.agentReview,
168
+ approvalLabel: repo?.labels.agentPlanApproved,
169
+ activeWorktreeStatus: activeWorktree?.status,
170
+ status,
171
+ resolution
172
+ });
173
+ const product = deriveProductState({
174
+ repo,
175
+ issue: builder.currentIssue,
176
+ previous: builder.previous,
177
+ activeWorktree,
178
+ nextPhase,
179
+ now: input.attention?.find((record) => record.workstreamId === builder.id)?.lastObservedAt
180
+ });
181
+ if (packetPlan) {
182
+ const packets = Object.values(packetPlan.packets);
183
+ const allMerged = packets.length > 0 && packets.every((p) => p.status === "merged_to_parent");
184
+ const anyStarted = packets.some((p) => p.status !== "planned");
185
+ const reasons = attentionByWorkstreamId.get(builder.id)?.reasons ?? [];
186
+ // Treat nextPhase === "implementation_ready", "implementing", or "implementation_done" as a plan approval signal.
187
+ // This is safe because a workstream cannot transition into or remain in these implementation phases
188
+ // without the plan first being approved. Checking this early nextPhase value helps eliminate
189
+ // the one-cycle lag that would occur if we only relied on the previous cycle's phase.
190
+ const hasApproval = reasons.includes("plan_approved") ||
191
+ nextPhase === "implementation_ready" ||
192
+ nextPhase === "implementing" ||
193
+ nextPhase === "implementation_done";
194
+ let newStatus = packetPlan.status;
195
+ if (allMerged) {
196
+ newStatus = "complete";
197
+ }
198
+ else if (anyStarted || nextPhase === "implementing") {
199
+ newStatus = "in_progress";
200
+ }
201
+ else if (hasApproval) {
202
+ newStatus = "approved";
203
+ }
204
+ else {
205
+ const wasApproved = packetPlan.status === "approved";
206
+ const prevPhase = builder.previous?.phase;
207
+ // If the plan was previously approved and we were already in an implementation phase,
208
+ // but we no longer have approval (e.g. the plan approval label was explicitly removed),
209
+ // we transition back to "proposed".
210
+ // If the previous phase was something else (e.g. "planning" or "plan_feedback") and we
211
+ // don't have approval now, we preserve the "approved" status. This protects the approved status
212
+ // from being lost during transient cycle run failures or when the plan breakdown is re-evaluating.
213
+ const lostApproval = wasApproved &&
214
+ (prevPhase === "implementation_ready" ||
215
+ prevPhase === "implementing" ||
216
+ prevPhase === "implementation_done") &&
217
+ !hasApproval;
218
+ if (lostApproval) {
219
+ newStatus = "proposed";
220
+ }
221
+ else if (!wasApproved) {
222
+ newStatus = "proposed";
223
+ }
224
+ }
225
+ packetPlan = {
226
+ ...packetPlan,
227
+ status: newStatus
228
+ };
229
+ }
230
+ const workstream = {
231
+ id: builder.id,
232
+ repoKey: builder.repoKey,
233
+ origin: {
234
+ type: "issue",
235
+ number: builder.originIssueNumber
236
+ },
237
+ linkedIssueNumber: builder.originIssueNumber,
238
+ linkedPrs,
239
+ ...(activePrNumber !== undefined ? { activePrNumber } : {}),
240
+ ...(activeWorktree ? { activeWorktreeId: activeWorktree.id } : {}),
241
+ phase: nextPhase,
242
+ attention: deriveAttention(linkedPrs, currentPrsByRepoAndNumber, repoId, repo, builder.previous?.attention),
243
+ status,
244
+ resolution,
245
+ ...(product ? { product } : {}),
246
+ ...(integration ? { integration } : {}),
247
+ ...(packetPlan ? { packetPlan } : {}),
248
+ ...(builder.previous?.mcpBlockers ? { mcpBlockers: builder.previous.mcpBlockers } : {}),
249
+ ...(builder.previous?.mcpSkipExclusions ? { mcpSkipExclusions: builder.previous.mcpSkipExclusions } : {})
250
+ };
251
+ if (activePrNumber !== undefined) {
252
+ workstream.activePrNumber = activePrNumber;
253
+ }
254
+ if (activeWorktree) {
255
+ workstream.activeWorktreeId = activeWorktree.id;
256
+ }
257
+ else if (builder.previous?.activeWorktreeId) {
258
+ workstream.activeWorktreeId = builder.previous.activeWorktreeId;
259
+ }
260
+ return workstream;
261
+ })
262
+ .sort(compareWorkstreams);
263
+ const matchedPacketPrKeys = collectMatchedPacketPrKeys({
264
+ pullRequests: currentPullRequests,
265
+ issueWorkstreams,
266
+ reposById,
267
+ worktrees,
268
+ previousPullRequestIds
269
+ });
270
+ // Filter out matched packet PRs from issue workstream linkedPrs.
271
+ // These PRs are now managed as part of the packet plan and should not be treated
272
+ // as primary implementation PRs of the issue workstream itself.
273
+ for (const workstream of issueWorkstreams) {
274
+ workstream.linkedPrs = workstream.linkedPrs.filter((pr) => !matchedPacketPrKeys.has(buildRepoPrKey(workstream.repoKey, pr.number)));
275
+ }
276
+ const linkedIssuePrKeys = new Set(issueWorkstreams.flatMap((workstream) => workstream.linkedPrs.map((pr) => `${workstream.repoKey}:${pr.number}`)));
277
+ const previousPrWorkstreams = previousWorkstreams.filter((workstream) => workstream.origin.type === "pr");
278
+ const currentPrsByWorkstreamId = new Map(currentPullRequests.map((pr) => [buildPrWorkstreamId(pr.repoOwner, pr.repoName, pr.number), pr]));
279
+ const prWorkstreamIds = new Set([
280
+ ...previousPrWorkstreams
281
+ .filter((workstream) => {
282
+ const currentPr = currentPrsByWorkstreamId.get(workstream.id);
283
+ if (!currentPr) {
284
+ return true;
285
+ }
286
+ return !matchedPacketPrKeys.has(buildRepoPrKey(currentPr.repoKey, currentPr.number));
287
+ })
288
+ .map((workstream) => workstream.id),
289
+ ...currentPullRequests
290
+ .filter((pr) => !matchedPacketPrKeys.has(buildRepoPrKey(pr.repoKey, pr.number)))
291
+ .filter((pr) => isRelevantStandalonePr(pr, reposById.get(pr.repoId), worktrees, previousPullRequestIds))
292
+ .map((pr) => buildPrWorkstreamId(pr.repoOwner, pr.repoName, pr.number))
293
+ ]);
294
+ const prWorkstreams = [...prWorkstreamIds]
295
+ .sort((left, right) => left.localeCompare(right))
296
+ .map((id) => {
297
+ const previous = previousPrWorkstreams.find((workstream) => workstream.id === id);
298
+ const currentPr = currentPullRequests.find((pr) => buildPrWorkstreamId(pr.repoOwner, pr.repoName, pr.number) === id);
299
+ const repoKey = currentPr?.repoKey ?? previous?.repoKey;
300
+ const prNumber = currentPr?.number ?? previous?.origin.number;
301
+ if (!repoKey || prNumber === undefined) {
302
+ return undefined;
303
+ }
304
+ if (linkedIssuePrKeys.has(`${repoKey}:${prNumber}`)) {
305
+ return undefined;
306
+ }
307
+ const repo = reposBySlug.get(repoKey);
308
+ const repoId = repo?.id;
309
+ const relatedWorktrees = worktreesByParentId.get(id) ?? [];
310
+ const primaryWorktree = currentPr && repoId
311
+ ? worktrees.find((candidate) => candidate.repoId === repoId &&
312
+ candidate.branchName === currentPr.headBranch &&
313
+ !candidate.issueId &&
314
+ !candidate.packet)
315
+ : undefined;
316
+ const allWorktrees = primaryWorktree ? [primaryWorktree, ...relatedWorktrees] : relatedWorktrees;
317
+ const activeWorktree = pickActiveWorktree(allWorktrees.filter((wt) => !wt.packet));
318
+ const linkedPrNumbers = new Set();
319
+ linkedPrNumbers.add(prNumber);
320
+ for (const wt of allWorktrees) {
321
+ if (wt.packet) {
322
+ continue;
323
+ }
324
+ if (wt.prNumber !== undefined) {
325
+ linkedPrNumbers.add(wt.prNumber);
326
+ }
327
+ const packetPr = currentPrsByRepoAndBranch.get(`${wt.repoId}:${wt.branchName}`);
328
+ if (packetPr) {
329
+ linkedPrNumbers.add(packetPr.number);
330
+ }
331
+ }
332
+ const linkedPrs = [...linkedPrNumbers]
333
+ .sort((left, right) => left - right)
334
+ .map((num) => buildLinkedPullRequest(previous, currentPrsByRepoAndNumber.get(`${repoId}:${num}`), num, num === prNumber ? undefined : "implementation"));
335
+ const activePrNumber = activeWorktree?.prNumber ??
336
+ (activeWorktree
337
+ ? currentPrsByRepoAndBranch.get(`${activeWorktree.repoId}:${activeWorktree.branchName}`)?.number
338
+ : undefined) ??
339
+ pickActivePrNumber(linkedPrs, currentPrsByRepoAndNumber, repoId);
340
+ const activePullRequest = activePrNumber !== undefined && repoId !== undefined
341
+ ? currentPrsByRepoAndNumber.get(`${repoId}:${activePrNumber}`)
342
+ : undefined;
343
+ const integration = deriveIntegrationState(activePullRequest, activePrNumber, activeWorktree, previous, repo?.defaultBranch);
344
+ const workstream = {
345
+ id,
346
+ repoKey,
347
+ origin: {
348
+ type: "pr",
349
+ number: prNumber
350
+ },
351
+ linkedPrs,
352
+ ...(activePrNumber !== undefined ? { activePrNumber } : {}),
353
+ phase: reduceWorkstreamPhase({
354
+ previousPhase: previous?.phase,
355
+ originType: "pr",
356
+ issuePresent: false,
357
+ activePullRequestState: activePullRequest?.state,
358
+ activePullRequestLabels: activePullRequest?.labels ?? [],
359
+ attentionReasons: attentionByWorkstreamId.get(id)?.reasons ?? [],
360
+ inProgressLabel: repo?.labels.agentInProgress,
361
+ reviewLabel: repo?.labels.agentReview,
362
+ approvalLabel: repo?.labels.agentPlanApproved,
363
+ activeWorktreeStatus: activeWorktree?.status,
364
+ status: activePullRequest ? derivePrStatus(activePullRequest) : previous?.status ?? "open",
365
+ resolution: activePullRequest ? derivePrResolution(activePullRequest) : previous?.resolution ?? "unresolved"
366
+ }),
367
+ attention: activePullRequest
368
+ ? deriveAttentionForPr(activePullRequest, repo, previous?.attention)
369
+ : previous?.attention ?? "idle",
370
+ status: activePullRequest ? derivePrStatus(activePullRequest) : previous?.status ?? "open",
371
+ resolution: activePullRequest ? derivePrResolution(activePullRequest) : previous?.resolution ?? "unresolved",
372
+ ...(integration ? { integration } : {}),
373
+ ...(previous?.packetPlan ? { packetPlan: previous.packetPlan } : {}),
374
+ ...(previous?.mcpBlockers ? { mcpBlockers: previous.mcpBlockers } : {}),
375
+ ...(previous?.mcpSkipExclusions ? { mcpSkipExclusions: previous.mcpSkipExclusions } : {})
376
+ };
377
+ if (activeWorktree) {
378
+ workstream.activeWorktreeId = activeWorktree.id;
379
+ }
380
+ else if (previous?.activeWorktreeId) {
381
+ workstream.activeWorktreeId = previous.activeWorktreeId;
382
+ }
383
+ return workstream;
384
+ })
385
+ .filter(isDefined);
386
+ return [...issueWorkstreams, ...prWorkstreams].sort(compareWorkstreams);
387
+ }
388
+ function buildLinkedPullRequest(previous, currentPr, prNumber, defaultRole) {
389
+ const previousPr = previous?.linkedPrs.find((pr) => pr.number === prNumber);
390
+ const linkedPr = {
391
+ number: prNumber
392
+ };
393
+ if (currentPr?.state) {
394
+ linkedPr.status = currentPr.state;
395
+ }
396
+ else if (previousPr?.status) {
397
+ linkedPr.status = previousPr.status;
398
+ }
399
+ const role = defaultRole ?? previousPr?.role;
400
+ if (role !== undefined) {
401
+ linkedPr.role = role;
402
+ }
403
+ return linkedPr;
404
+ }
405
+ function deriveAttention(linkedPrs, currentPrsByRepoAndNumber, repoId, repo, previousAttention) {
406
+ if (repoId !== undefined && repo) {
407
+ for (const linkedPr of linkedPrs) {
408
+ const pr = currentPrsByRepoAndNumber.get(`${repoId}:${linkedPr.number}`);
409
+ if (pr?.labels.includes(repo.labels.agentReview)) {
410
+ return "review_requested";
411
+ }
412
+ if (pr?.labels.includes(repo.labels.prepareForMerge)) {
413
+ return "prepare_for_merge_requested";
414
+ }
415
+ }
416
+ }
417
+ return previousAttention ?? "idle";
418
+ }
419
+ function deriveAttentionForPr(pr, repo, previousAttention) {
420
+ if (repo) {
421
+ if (pr.labels.includes(repo.labels.agentReview)) {
422
+ return "review_requested";
423
+ }
424
+ if (pr.labels.includes(repo.labels.prepareForMerge)) {
425
+ return "prepare_for_merge_requested";
426
+ }
427
+ }
428
+ return previousAttention ?? "idle";
429
+ }
430
+ function deriveIssueStatus(issue, linkedPrs, currentPrsByRepoAndNumber, repoId, previousStatus) {
431
+ if (repoId !== undefined) {
432
+ for (const linkedPr of linkedPrs) {
433
+ const pr = currentPrsByRepoAndNumber.get(`${repoId}:${linkedPr.number}`);
434
+ if (pr?.state === "merged") {
435
+ return "merged";
436
+ }
437
+ }
438
+ }
439
+ if (issue?.state === "closed") {
440
+ return "closed";
441
+ }
442
+ // listOpenIssues only returns open issues. If an issue was previously tracked
443
+ // as open but is absent from the current scan, it has been closed on GitHub.
444
+ if (issue === undefined && previousStatus === "open") {
445
+ return "closed";
446
+ }
447
+ return previousStatus ?? "open";
448
+ }
449
+ function derivePrStatus(pr) {
450
+ if (pr.state === "merged") {
451
+ return "merged";
452
+ }
453
+ if (pr.state === "closed") {
454
+ return "closed";
455
+ }
456
+ return "open";
457
+ }
458
+ function deriveResolution(linkedPrs, currentPrsByRepoAndNumber, repoId, previousResolution) {
459
+ if (repoId !== undefined) {
460
+ for (const linkedPr of linkedPrs) {
461
+ const pr = currentPrsByRepoAndNumber.get(`${repoId}:${linkedPr.number}`);
462
+ if (pr?.state === "merged") {
463
+ return "merged";
464
+ }
465
+ }
466
+ }
467
+ return previousResolution ?? "unresolved";
468
+ }
469
+ function derivePrResolution(pr) {
470
+ if (pr.state === "merged") {
471
+ return "merged";
472
+ }
473
+ if (pr.state === "closed") {
474
+ return "closed";
475
+ }
476
+ return "unresolved";
477
+ }
478
+ function pickActiveWorktree(worktrees) {
479
+ return [...worktrees].sort((left, right) => {
480
+ const updatedComparison = right.updatedAt.localeCompare(left.updatedAt);
481
+ if (updatedComparison !== 0) {
482
+ return updatedComparison;
483
+ }
484
+ return left.id.localeCompare(right.id);
485
+ })[0];
486
+ }
487
+ function pickActivePrNumber(linkedPrs, currentPrsByRepoAndNumber, repoId) {
488
+ if (repoId !== undefined) {
489
+ const currentOpenPr = linkedPrs
490
+ .map((pr) => currentPrsByRepoAndNumber.get(`${repoId}:${pr.number}`))
491
+ .filter((pr) => pr !== undefined)
492
+ .sort(comparePullRequests)[0];
493
+ if (currentOpenPr) {
494
+ return currentOpenPr.number;
495
+ }
496
+ }
497
+ return linkedPrs[0]?.number;
498
+ }
499
+ function isRelevantStandalonePr(pr, repo, worktrees, previousPullRequestIds) {
500
+ if (!repo) {
501
+ return false;
502
+ }
503
+ if (pr.labels.includes(repo.labels.agentReview) ||
504
+ pr.labels.includes(repo.labels.agentInProgress) ||
505
+ pr.labels.includes(repo.labels.agentPlanApproved) ||
506
+ pr.labels.includes(repo.labels.planBreakdownNeeded) ||
507
+ pr.labels.includes(repo.labels.prepareForMerge)) {
508
+ return true;
509
+ }
510
+ if (previousPullRequestIds.has(pr.id)) {
511
+ return true;
512
+ }
513
+ return worktrees.some((worktree) => worktree.repoId === pr.repoId && worktree.branchName === pr.headBranch);
514
+ }
515
+ function getIssueWorkstreamIdForWorktree(worktree) {
516
+ if (worktree.issueNumber === undefined) {
517
+ return undefined;
518
+ }
519
+ const [owner, name] = worktree.repoSlug.split("/");
520
+ if (!owner || !name) {
521
+ return undefined;
522
+ }
523
+ return buildIssueWorkstreamId(owner, name, worktree.issueNumber);
524
+ }
525
+ function extractIssueNumberFromWorkstreamId(workstreamId) {
526
+ const match = workstreamId.match(/:issue:(\d+)$/);
527
+ return match ? Number(match[1]) : undefined;
528
+ }
529
+ function buildIssueWorkstreamId(owner, name, issueNumber) {
530
+ return `${owner}/${name}:issue:${issueNumber}`;
531
+ }
532
+ function buildPrWorkstreamId(owner, name, prNumber) {
533
+ return `${owner}/${name}:pr:${prNumber}`;
534
+ }
535
+ function compareIssues(left, right) {
536
+ const repoComparison = left.repoSlug.localeCompare(right.repoSlug);
537
+ if (repoComparison !== 0) {
538
+ return repoComparison;
539
+ }
540
+ return left.number - right.number;
541
+ }
542
+ function comparePullRequests(left, right) {
543
+ const repoComparison = left.repoSlug.localeCompare(right.repoSlug);
544
+ if (repoComparison !== 0) {
545
+ return repoComparison;
546
+ }
547
+ return left.number - right.number;
548
+ }
549
+ function compareWorktrees(left, right) {
550
+ const repoComparison = left.repoSlug.localeCompare(right.repoSlug);
551
+ if (repoComparison !== 0) {
552
+ return repoComparison;
553
+ }
554
+ return left.id.localeCompare(right.id);
555
+ }
556
+ function compareWorkstreams(left, right) {
557
+ const repoComparison = left.repoKey.localeCompare(right.repoKey);
558
+ if (repoComparison !== 0) {
559
+ return repoComparison;
560
+ }
561
+ return left.id.localeCompare(right.id);
562
+ }
563
+ function isDefined(value) {
564
+ return value !== undefined;
565
+ }
566
+ function deriveIntegrationState(activePullRequest, activePrNumber, activeWorktree, previous, defaultBranch) {
567
+ if (activePullRequest) {
568
+ if (activePullRequest.baseBranch !== activePullRequest.defaultBranch) {
569
+ return undefined;
570
+ }
571
+ return {
572
+ branchName: activePullRequest.headBranch,
573
+ prNumber: activePrNumber
574
+ };
575
+ }
576
+ if (activeWorktree) {
577
+ if (defaultBranch !== undefined &&
578
+ activeWorktree.baseBranch !== undefined &&
579
+ activeWorktree.baseBranch !== defaultBranch) {
580
+ return undefined;
581
+ }
582
+ return {
583
+ branchName: activeWorktree.branchName,
584
+ prNumber: activePrNumber
585
+ };
586
+ }
587
+ if (previous?.integration) {
588
+ return previous.integration;
589
+ }
590
+ return undefined;
591
+ }
592
+ function buildRepoPrKey(repoKey, prNumber) {
593
+ return `${repoKey}#${prNumber}`;
594
+ }
595
+ function collectMatchedPacketPrKeys(input) {
596
+ const activeParentIntegrationBranchesByRepo = new Map();
597
+ for (const workstream of input.issueWorkstreams) {
598
+ if (workstream.status === "open" &&
599
+ workstream.resolution === "unresolved" &&
600
+ workstream.integration?.branchName !== undefined) {
601
+ if (!activeParentIntegrationBranchesByRepo.has(workstream.repoKey)) {
602
+ activeParentIntegrationBranchesByRepo.set(workstream.repoKey, new Set());
603
+ }
604
+ activeParentIntegrationBranchesByRepo.get(workstream.repoKey).add(workstream.integration.branchName);
605
+ }
606
+ }
607
+ for (const pr of input.pullRequests) {
608
+ if (pr.baseClassification === "top_level" && pr.state === "open") {
609
+ const repo = input.reposById.get(pr.repoId);
610
+ if (isRelevantStandalonePr(pr, repo, input.worktrees, input.previousPullRequestIds)) {
611
+ if (!activeParentIntegrationBranchesByRepo.has(pr.repoKey)) {
612
+ activeParentIntegrationBranchesByRepo.set(pr.repoKey, new Set());
613
+ }
614
+ activeParentIntegrationBranchesByRepo.get(pr.repoKey).add(pr.headBranch);
615
+ }
616
+ }
617
+ }
618
+ const matched = new Set();
619
+ for (const pr of input.pullRequests) {
620
+ if (pr.baseClassification !== "non_default_parent") {
621
+ continue;
622
+ }
623
+ const branches = activeParentIntegrationBranchesByRepo.get(pr.repoKey);
624
+ if (branches?.has(pr.baseBranch)) {
625
+ matched.add(buildRepoPrKey(pr.repoKey, pr.number));
626
+ }
627
+ }
628
+ return matched;
629
+ }