@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,420 @@
1
+ import { getRequiredCapabilityNames } from "../workflows/registry.js";
2
+ import { validateCapabilities } from "../capabilities/validator.js";
3
+ /**
4
+ * Upserts an MCP blocker on a workstream for a specific task and capability.
5
+ */
6
+ export function upsertMcpBlocker(workstream, task, blockedCapability, mcpStatus, packetId, now) {
7
+ const blockerId = `mcp-blocker:${workstream.id}:${blockedCapability.uses}:${task.id}`;
8
+ const existingBlockers = workstream.mcpBlockers ?? [];
9
+ const index = existingBlockers.findIndex((b) => b.id === blockerId);
10
+ let updatedBlockers;
11
+ if (index !== -1) {
12
+ const existing = existingBlockers[index];
13
+ const unionNames = Array.from(new Set([...existing.affectedCapabilityNames, blockedCapability.name]));
14
+ const updatedBlocker = {
15
+ ...existing,
16
+ affectedCapabilityNames: unionNames,
17
+ safeReason: sanitizeMcpBlockerReason(existing.safeReason),
18
+ updatedAt: now,
19
+ };
20
+ updatedBlockers = [
21
+ ...existingBlockers.slice(0, index),
22
+ updatedBlocker,
23
+ ...existingBlockers.slice(index + 1)
24
+ ];
25
+ }
26
+ else {
27
+ const safeReason = sanitizeMcpBlockerReason(mcpStatus.actionRequired?.reason ??
28
+ blockedCapability.message ??
29
+ "MCP requires user authorization or configuration.");
30
+ const newBlocker = {
31
+ id: blockerId,
32
+ kind: "mcp_action_required",
33
+ mcpHandle: blockedCapability.uses,
34
+ affectedCapabilityNames: [blockedCapability.name],
35
+ safeReason,
36
+ blockedTaskKind: task.kind,
37
+ blockedTaskId: task.id,
38
+ packetId,
39
+ createdAt: now,
40
+ updatedAt: now,
41
+ surfacedAt: undefined,
42
+ resolutionState: "waiting_for_user"
43
+ };
44
+ updatedBlockers = [...existingBlockers, newBlocker];
45
+ }
46
+ return {
47
+ ...workstream,
48
+ mcpBlockers: updatedBlockers
49
+ };
50
+ }
51
+ function sanitizeMcpBlockerReason(value) {
52
+ const sanitized = value
53
+ .replace(/https?:\/\/\S+/gi, "[redacted-url]")
54
+ .replace(/\b(token|secret|api[_-]?key)\s*[:=]\s*\S+/gi, "$1=[redacted]")
55
+ .replace(/\bBearer\s+\S+/gi, "Bearer [redacted]")
56
+ .slice(0, 300)
57
+ .trim();
58
+ return sanitized || "MCP requires user authorization or configuration.";
59
+ }
60
+ /**
61
+ * Removes the blocker with the given ID from the workstream's blockers entirely.
62
+ */
63
+ export function clearMcpBlocker(workstream, blockerId, _now) {
64
+ const existingBlockers = workstream.mcpBlockers ?? [];
65
+ const index = existingBlockers.findIndex((b) => b.id === blockerId);
66
+ if (index === -1) {
67
+ return workstream;
68
+ }
69
+ const updatedBlockers = [
70
+ ...existingBlockers.slice(0, index),
71
+ ...existingBlockers.slice(index + 1)
72
+ ];
73
+ return {
74
+ ...workstream,
75
+ mcpBlockers: updatedBlockers
76
+ };
77
+ }
78
+ /**
79
+ * Creates/updates a skip exclusion on the workstream and clears the matching blocker.
80
+ */
81
+ export function applyMcpSkipExclusion(workstream, taskId, taskKind, packetId, mcpHandle, affectedCapabilityNames, now) {
82
+ const exclusionId = `mcp-exclusion:${workstream.id}:${mcpHandle}:${taskId}`;
83
+ const existingExclusions = workstream.mcpSkipExclusions ?? [];
84
+ const index = existingExclusions.findIndex((e) => e.id === exclusionId);
85
+ let updatedExclusions;
86
+ if (index !== -1) {
87
+ const existing = existingExclusions[index];
88
+ const unionNames = Array.from(new Set([...existing.affectedCapabilityNames, ...affectedCapabilityNames]));
89
+ const updatedExclusion = {
90
+ ...existing,
91
+ affectedCapabilityNames: unionNames
92
+ };
93
+ updatedExclusions = [
94
+ ...existingExclusions.slice(0, index),
95
+ updatedExclusion,
96
+ ...existingExclusions.slice(index + 1)
97
+ ];
98
+ }
99
+ else {
100
+ const newExclusion = {
101
+ id: exclusionId,
102
+ taskId,
103
+ taskKind,
104
+ packetId,
105
+ mcpHandle,
106
+ affectedCapabilityNames,
107
+ createdAt: now
108
+ };
109
+ updatedExclusions = [...existingExclusions, newExclusion];
110
+ }
111
+ const resultWorkstream = {
112
+ ...workstream,
113
+ mcpSkipExclusions: updatedExclusions
114
+ };
115
+ const blockerId = `mcp-blocker:${workstream.id}:${mcpHandle}:${taskId}`;
116
+ return clearMcpBlocker(resultWorkstream, blockerId, now);
117
+ }
118
+ /**
119
+ * Returns true if the task is actively blocked by any blocker on the workstream.
120
+ */
121
+ export function isTaskActivelyBlocked(workstream, taskId) {
122
+ const blockers = workstream.mcpBlockers ?? [];
123
+ return blockers.some((b) => b.blockedTaskId === taskId && b.resolutionState === "waiting_for_user");
124
+ }
125
+ /**
126
+ * Reconciles workstream blockers with current MCP statuses, clearing blockers
127
+ * for MCPs that are now "ready".
128
+ */
129
+ export function reconcileBlockersWithMcpStatuses(workstream, mcpStatuses, now) {
130
+ const blockers = workstream.mcpBlockers ?? [];
131
+ const cleared = [];
132
+ let currentWorkstream = workstream;
133
+ for (const blocker of blockers) {
134
+ if (blocker.resolutionState === "waiting_for_user") {
135
+ const statusRecord = mcpStatuses.find((s) => s.handle === blocker.mcpHandle);
136
+ if (statusRecord && statusRecord.status === "ready") {
137
+ cleared.push(blocker);
138
+ currentWorkstream = clearMcpBlocker(currentWorkstream, blocker.id, now);
139
+ }
140
+ }
141
+ }
142
+ return {
143
+ workstream: currentWorkstream,
144
+ cleared
145
+ };
146
+ }
147
+ /**
148
+ * Filters the blockers on a workstream that have not yet been surfaced to the user.
149
+ */
150
+ export function getUnsurfacedBlockers(workstream) {
151
+ return (workstream.mcpBlockers ?? []).filter((b) => b.surfacedAt === undefined);
152
+ }
153
+ export function markMcpBlockersSurfaced(workstream, blockerIds, surfacedAt) {
154
+ if (blockerIds.size === 0) {
155
+ return workstream;
156
+ }
157
+ const blockers = workstream.mcpBlockers ?? [];
158
+ let changed = false;
159
+ const updatedBlockers = blockers.map((blocker) => {
160
+ if (!blockerIds.has(blocker.id) || blocker.surfacedAt !== undefined) {
161
+ return blocker;
162
+ }
163
+ changed = true;
164
+ return {
165
+ ...blocker,
166
+ surfacedAt,
167
+ updatedAt: surfacedAt
168
+ };
169
+ });
170
+ return changed
171
+ ? {
172
+ ...workstream,
173
+ mcpBlockers: updatedBlockers
174
+ }
175
+ : workstream;
176
+ }
177
+ /**
178
+ * Filters skip exclusions on a workstream applicable to a specific task.
179
+ */
180
+ export function getApplicableSkipExclusions(workstream, taskId) {
181
+ return (workstream.mcpSkipExclusions ?? []).filter((e) => e.taskId === taskId);
182
+ }
183
+ export function rehydrateTaskMcpSkipContexts(input) {
184
+ const workstreamsById = new Map(input.workstreams.map((workstream) => [workstream.id, workstream]));
185
+ let changed = false;
186
+ const tasks = input.tasks.map((task) => {
187
+ if (!task.workstreamId) {
188
+ return task;
189
+ }
190
+ const workstream = workstreamsById.get(task.workstreamId);
191
+ if (!workstream) {
192
+ return task;
193
+ }
194
+ const applicableExclusions = (workstream.mcpSkipExclusions ?? []).filter((exclusion) => exclusionMatchesTaskScope(exclusion, task));
195
+ if (applicableExclusions.length === 0) {
196
+ return task;
197
+ }
198
+ const existingContexts = task.context.mcpSkipContexts ?? [];
199
+ const contextsByHandle = new Map(existingContexts.map((context) => [context.mcpHandle, context]));
200
+ let taskChanged = false;
201
+ for (const exclusion of applicableExclusions) {
202
+ const existing = contextsByHandle.get(exclusion.mcpHandle);
203
+ if (!existing) {
204
+ contextsByHandle.set(exclusion.mcpHandle, {
205
+ mcpHandle: exclusion.mcpHandle,
206
+ affectedCapabilityNames: [...exclusion.affectedCapabilityNames],
207
+ skipReason: "User explicitly skipped MCP authorization for this task."
208
+ });
209
+ taskChanged = true;
210
+ continue;
211
+ }
212
+ const mergedCapabilityNames = Array.from(new Set([...existing.affectedCapabilityNames, ...exclusion.affectedCapabilityNames]));
213
+ if (mergedCapabilityNames.length !== existing.affectedCapabilityNames.length) {
214
+ contextsByHandle.set(exclusion.mcpHandle, {
215
+ ...existing,
216
+ affectedCapabilityNames: mergedCapabilityNames
217
+ });
218
+ taskChanged = true;
219
+ }
220
+ }
221
+ if (!taskChanged) {
222
+ return task;
223
+ }
224
+ changed = true;
225
+ return {
226
+ ...task,
227
+ context: {
228
+ ...task.context,
229
+ mcpSkipContexts: [...contextsByHandle.values()]
230
+ }
231
+ };
232
+ });
233
+ return { tasks, changed };
234
+ }
235
+ function exclusionMatchesTaskScope(exclusion, task) {
236
+ if (exclusion.taskId !== task.id || exclusion.taskKind !== task.kind) {
237
+ return false;
238
+ }
239
+ const taskPacketId = task.context.packetId;
240
+ if (exclusion.packetId !== undefined || taskPacketId !== undefined) {
241
+ return exclusion.packetId === taskPacketId;
242
+ }
243
+ return true;
244
+ }
245
+ /**
246
+ * Evaluates pending tasks against blocked required capabilities, recording blockers
247
+ * and preventing blocked tasks from dispatching.
248
+ */
249
+ export function evaluateMcpDispatchGate(input) {
250
+ const updatedWorkstreams = [];
251
+ let workstreamsChanged = false;
252
+ const clearedBlockers = [];
253
+ // 1. First, reconcile blockers with the current MCP statuses
254
+ for (const ws of input.workstreams) {
255
+ const recon = reconcileBlockersWithMcpStatuses(ws, input.mcpStatuses, input.now);
256
+ if (recon.workstream !== ws) {
257
+ workstreamsChanged = true;
258
+ }
259
+ updatedWorkstreams.push(recon.workstream);
260
+ clearedBlockers.push(...recon.cleared);
261
+ }
262
+ const blockedTaskIds = new Set();
263
+ const evaluations = [];
264
+ const workstreamMap = new Map();
265
+ for (const ws of updatedWorkstreams) {
266
+ workstreamMap.set(ws.id, ws);
267
+ }
268
+ // 2. Check each task and block it if it requires a blocked capability
269
+ for (const task of input.tasks) {
270
+ if (task.status !== "pending") {
271
+ continue;
272
+ }
273
+ const requiredNames = getRequiredCapabilityNames(task.kind);
274
+ if (requiredNames.length === 0) {
275
+ continue;
276
+ }
277
+ const candidateBlockedRequired = input.capabilityValidation.blocked.filter((b) => requiredNames.includes(b.name) && b.reason === "mcp_action_required");
278
+ if (candidateBlockedRequired.length > 0) {
279
+ const wsId = task.workstreamId;
280
+ if (!wsId) {
281
+ console.warn(`[evaluateMcpDispatchGate] Task ${task.id} has no workstreamId.`);
282
+ continue;
283
+ }
284
+ let ws = workstreamMap.get(wsId);
285
+ if (!ws) {
286
+ console.warn(`[evaluateMcpDispatchGate] Workstream ${wsId} not found for task ${task.id}.`);
287
+ continue;
288
+ }
289
+ const skippedCapabilityNames = new Set([
290
+ ...(task.context.mcpSkipContexts ?? []).flatMap((context) => context.affectedCapabilityNames),
291
+ ...getApplicableSkipExclusions(ws, task.id).flatMap((exclusion) => exclusion.affectedCapabilityNames)
292
+ ]);
293
+ const blockedRequired = candidateBlockedRequired.filter((blockedCapability) => !skippedCapabilityNames.has(blockedCapability.name));
294
+ if (blockedRequired.length === 0) {
295
+ continue;
296
+ }
297
+ // Resolve packetId from worktrees using path and branchName
298
+ let localPath = undefined;
299
+ let branchName = undefined;
300
+ if (task.kind !== "prepare_pr_for_merge") {
301
+ localPath = task.context.localRepoPath;
302
+ branchName = task.context.branchName;
303
+ }
304
+ else {
305
+ const preflight = task.context.prepareForMerge;
306
+ if (preflight) {
307
+ localPath = preflight.persistentWorktreePath;
308
+ branchName = preflight.headBranch;
309
+ }
310
+ }
311
+ const matchingWorktree = input.worktrees.find((wt) => wt.path === localPath && wt.branchName === branchName);
312
+ const packetId = task.context.packetId ?? matchingWorktree?.packet?.packetId;
313
+ for (const entry of blockedRequired) {
314
+ const mcpStatusRecord = input.mcpStatuses.find((s) => s.handle === entry.uses);
315
+ if (!mcpStatusRecord) {
316
+ const fallbackStatus = {
317
+ handle: entry.uses,
318
+ providerId: "smithery",
319
+ providerMcpId: entry.uses,
320
+ status: "error",
321
+ lastCheckedAt: input.now
322
+ };
323
+ const nextWs = upsertMcpBlocker(ws, task, entry, fallbackStatus, packetId, input.now);
324
+ if (nextWs !== ws) {
325
+ workstreamsChanged = true;
326
+ ws = nextWs;
327
+ workstreamMap.set(wsId, ws);
328
+ }
329
+ }
330
+ else {
331
+ const nextWs = upsertMcpBlocker(ws, task, entry, mcpStatusRecord, packetId, input.now);
332
+ if (nextWs !== ws) {
333
+ workstreamsChanged = true;
334
+ ws = nextWs;
335
+ workstreamMap.set(wsId, ws);
336
+ }
337
+ }
338
+ }
339
+ blockedTaskIds.add(task.id);
340
+ evaluations.push({
341
+ taskId: task.id,
342
+ taskKind: task.kind,
343
+ blockedRequiredCapabilities: blockedRequired
344
+ });
345
+ }
346
+ }
347
+ const finalWorkstreams = input.workstreams.map((ws) => workstreamMap.get(ws.id) ?? ws);
348
+ const tasksToDispatch = input.tasks.filter((t) => !blockedTaskIds.has(t.id));
349
+ return {
350
+ tasksToDispatch,
351
+ blockedTaskIds,
352
+ workstreams: finalWorkstreams,
353
+ workstreamsChanged,
354
+ evaluations,
355
+ clearedBlockers
356
+ };
357
+ }
358
+ /**
359
+ * Classifies whether a task's failure is due to a required capability's MCP status transitioning to action_required.
360
+ * Returns the list of blocked required capabilities.
361
+ */
362
+ export function classifyMidTaskMcpFailure(input) {
363
+ const requiredNames = getRequiredCapabilityNames(input.task.kind);
364
+ if (requiredNames.length === 0) {
365
+ return [];
366
+ }
367
+ const validation = validateCapabilities(input.capabilities, input.declaredMcps, input.refreshedMcpStatuses);
368
+ const skippedCapabilityNames = new Set((input.task.context.mcpSkipContexts ?? []).flatMap((context) => context.affectedCapabilityNames));
369
+ return validation.blocked.filter((b) => requiredNames.includes(b.name) &&
370
+ b.reason === "mcp_action_required" &&
371
+ !skippedCapabilityNames.has(b.name));
372
+ }
373
+ /**
374
+ * Resumes task execution from a mid-task MCP block when the blocker is cleared or skipped.
375
+ * For each task in `tasks` with status === "failed":
376
+ * - If there is a cleared blocker matching the task's ID, or
377
+ * - If there is an applied skip exclusion matching the task's ID,
378
+ * Transition task status to "pending", set context.recoveryNeeded = false,
379
+ * and if skipped, append an entry to context.mcpSkipContexts.
380
+ */
381
+ export function resumeMidTaskBlockedTasks(input) {
382
+ const resumedTaskIds = new Set();
383
+ const updatedTasks = input.tasks.map((task) => {
384
+ if (task.status !== "failed") {
385
+ return task;
386
+ }
387
+ const matchedClearedBlockers = input.clearedBlockers.filter((b) => b.blockedTaskId === task.id);
388
+ const matchedSkipExclusions = input.skipExclusionsAppliedThisCycle.filter((e) => e.taskId === task.id);
389
+ if (matchedClearedBlockers.length === 0 && matchedSkipExclusions.length === 0) {
390
+ return task;
391
+ }
392
+ resumedTaskIds.add(task.id);
393
+ // If it's a skip case, we construct the skip contexts
394
+ const mcpSkipContexts = [...(task.context.mcpSkipContexts ?? [])];
395
+ for (const exclusion of matchedSkipExclusions) {
396
+ const alreadyExists = mcpSkipContexts.some((ctx) => ctx.mcpHandle === exclusion.mcpHandle);
397
+ if (!alreadyExists) {
398
+ mcpSkipContexts.push({
399
+ mcpHandle: exclusion.mcpHandle,
400
+ affectedCapabilityNames: exclusion.affectedCapabilityNames,
401
+ skipReason: "User explicitly chose to skip this blocked MCP capability."
402
+ });
403
+ }
404
+ }
405
+ return {
406
+ ...task,
407
+ status: "pending",
408
+ context: {
409
+ ...task.context,
410
+ recoveryNeeded: false,
411
+ ...(mcpSkipContexts.length > 0 ? { mcpSkipContexts } : {})
412
+ },
413
+ updatedAt: input.now
414
+ };
415
+ });
416
+ return {
417
+ tasks: updatedTasks,
418
+ resumedTaskIds
419
+ };
420
+ }
@@ -0,0 +1,42 @@
1
+ export function collectPacketPullRequestCandidates(input) {
2
+ const candidates = [];
3
+ const nonDefaultPrs = input.pullRequests.filter((pr) => pr.baseClassification === "non_default_parent");
4
+ for (const pr of nonDefaultPrs) {
5
+ const matchingWorkstreams = input.workstreams
6
+ .filter((ws) => ws.repoKey === pr.repoSlug)
7
+ .filter((ws) => ws.status === "open" && ws.resolution === "unresolved")
8
+ .filter((ws) => ws.integration?.branchName === pr.baseBranch)
9
+ .sort((a, b) => a.id.localeCompare(b.id));
10
+ const parentWorkstream = matchingWorkstreams[0];
11
+ if (parentWorkstream) {
12
+ candidates.push({
13
+ repoSlug: pr.repoSlug,
14
+ pullRequestNumber: pr.number,
15
+ title: pr.title,
16
+ baseBranch: pr.baseBranch,
17
+ headBranch: pr.headBranch,
18
+ parentWorkstreamId: parentWorkstream.id,
19
+ parentIntegrationBranch: parentWorkstream.integration?.branchName,
20
+ parentPullRequestNumber: parentWorkstream.integration?.prNumber,
21
+ status: "matched_parent_workstream"
22
+ });
23
+ }
24
+ else {
25
+ candidates.push({
26
+ repoSlug: pr.repoSlug,
27
+ pullRequestNumber: pr.number,
28
+ title: pr.title,
29
+ baseBranch: pr.baseBranch,
30
+ headBranch: pr.headBranch,
31
+ status: "orphan_non_default_parent"
32
+ });
33
+ }
34
+ }
35
+ return candidates.sort((a, b) => {
36
+ const repoComparison = a.repoSlug.localeCompare(b.repoSlug);
37
+ if (repoComparison !== 0) {
38
+ return repoComparison;
39
+ }
40
+ return a.pullRequestNumber - b.pullRequestNumber;
41
+ });
42
+ }
@@ -0,0 +1,60 @@
1
+ import { buildPacketLifecycleWorkstreamReport, formatPacketLifecycleReport } from "./packet-lifecycle-report.js";
2
+ const STATUS_COMMENT_MARKER = "<!-- backburner:packet-wave-status -->";
3
+ export class GitHubPacketLifecyclePublisher {
4
+ gitHubGateway;
5
+ logger;
6
+ constructor(gitHubGateway, logger) {
7
+ this.gitHubGateway = gitHubGateway;
8
+ this.logger = logger;
9
+ }
10
+ async publishLifecycleFeedback(input) {
11
+ const { workstreams, repos, packetPrsCreatedThisRun, waveAdvancementsThisRun, blockedThisRun, parentBranchSyncPreflights } = input;
12
+ for (const workstream of workstreams) {
13
+ if (!workstream.packetPlan || !workstream.integration?.prNumber) {
14
+ continue;
15
+ }
16
+ const repo = repos.find((r) => r.id === workstream.repoKey || `${r.owner}/${r.name}` === workstream.repoKey);
17
+ if (!repo) {
18
+ continue;
19
+ }
20
+ const report = buildPacketLifecycleWorkstreamReport({
21
+ workstream,
22
+ packetPrsCreatedThisRun,
23
+ waveAdvancementsThisRun,
24
+ blockedThisRun,
25
+ parentBranchSyncPreflights,
26
+ agentSyncParentBranchLabel: repo.labels.agentSyncParentBranch
27
+ });
28
+ if (!report) {
29
+ continue;
30
+ }
31
+ const body = `🤖 ${STATUS_COMMENT_MARKER}\n${formatPacketLifecycleReport(report)}`;
32
+ try {
33
+ const comments = await this.gitHubGateway.listPullRequestComments(repo.owner, repo.name, workstream.integration.prNumber);
34
+ const existingComment = comments.find((c) => c.body.includes(STATUS_COMMENT_MARKER) && c.body.startsWith("🤖"));
35
+ if (existingComment) {
36
+ this.logger?.info(`Updating lifecycle status comment on ${repo.owner}/${repo.name} PR #${workstream.integration.prNumber}`);
37
+ await this.gitHubGateway.updateIssueComment({
38
+ owner: repo.owner,
39
+ name: repo.name,
40
+ commentId: existingComment.id,
41
+ body
42
+ });
43
+ }
44
+ else {
45
+ this.logger?.info(`Posting new lifecycle status comment on ${repo.owner}/${repo.name} PR #${workstream.integration.prNumber}`);
46
+ await this.gitHubGateway.postIssueComment({
47
+ owner: repo.owner,
48
+ name: repo.name,
49
+ issueNumber: workstream.integration.prNumber,
50
+ body
51
+ });
52
+ }
53
+ }
54
+ catch (error) {
55
+ const message = error instanceof Error ? error.message : String(error);
56
+ this.logger?.warn(`Failed to publish lifecycle feedback for ${workstream.id}: ${message}`);
57
+ }
58
+ }
59
+ }
60
+ }