@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.
- package/LICENSE +21 -0
- package/README.md +329 -0
- package/dist/src/agents/antigravity-launch.js +162 -0
- package/dist/src/agents/antigravity.js +251 -0
- package/dist/src/agents/claude-launch.js +117 -0
- package/dist/src/agents/claude.js +271 -0
- package/dist/src/agents/codex-launch.js +82 -0
- package/dist/src/agents/codex.js +261 -0
- package/dist/src/agents/composite-runner.js +13 -0
- package/dist/src/agents/extraction.js +442 -0
- package/dist/src/agents/failure-feedback.js +132 -0
- package/dist/src/agents/gemini-launch.js +96 -0
- package/dist/src/agents/gemini.js +392 -0
- package/dist/src/agents/job-agent-assignments.js +180 -0
- package/dist/src/agents/lifecycle-feedback.js +147 -0
- package/dist/src/agents/limit-detector.js +83 -0
- package/dist/src/agents/opencode-launch.js +138 -0
- package/dist/src/agents/opencode.js +156 -0
- package/dist/src/agents/plan-breakdown-feedback.js +97 -0
- package/dist/src/agents/pool.js +168 -0
- package/dist/src/agents/prompt.js +462 -0
- package/dist/src/agents/provider-sessions.js +105 -0
- package/dist/src/agents/review-feedback.js +255 -0
- package/dist/src/agents/selection.js +113 -0
- package/dist/src/agents/service.js +1087 -0
- package/dist/src/agents/types.js +1 -0
- package/dist/src/agents/wave-assessment-feedback.js +69 -0
- package/dist/src/attention/derive.js +337 -0
- package/dist/src/capabilities/index.js +2 -0
- package/dist/src/capabilities/projection.js +183 -0
- package/dist/src/capabilities/types.js +1 -0
- package/dist/src/capabilities/validator.js +87 -0
- package/dist/src/cli/commands/broker-smoke.js +96 -0
- package/dist/src/cli/commands/broker.js +56 -0
- package/dist/src/cli/commands/init.js +4 -0
- package/dist/src/cli/commands/journal.js +43 -0
- package/dist/src/cli/commands/run-loop.js +56 -0
- package/dist/src/cli/commands/run.js +1241 -0
- package/dist/src/cli/commands/tui.js +187 -0
- package/dist/src/cli/dispatcher.js +51 -0
- package/dist/src/cli/errors.js +2 -0
- package/dist/src/cli/init.js +92 -0
- package/dist/src/cli/options.js +86 -0
- package/dist/src/cli/output.js +231 -0
- package/dist/src/cli/run-journal.js +342 -0
- package/dist/src/cli/run-types.js +1 -0
- package/dist/src/cli/run.js +32 -0
- package/dist/src/cli/runtime/dispatch-lifecycle.js +772 -0
- package/dist/src/cli/runtime/provider-runtime-types.js +1 -0
- package/dist/src/cli/runtime/provider-runtime.js +152 -0
- package/dist/src/cli/runtime/provider-tools.js +73 -0
- package/dist/src/cli/runtime/providers/antigravity.js +26 -0
- package/dist/src/cli/runtime/providers/claude.js +20 -0
- package/dist/src/cli/runtime/providers/codex.js +20 -0
- package/dist/src/cli/runtime/providers/gemini.js +20 -0
- package/dist/src/cli/runtime/providers/opencode.js +20 -0
- package/dist/src/cli/runtime/run-context.js +131 -0
- package/dist/src/cli/tui/index.js +3 -0
- package/dist/src/cli/tui/screen.js +388 -0
- package/dist/src/cli/tui/tracker.js +558 -0
- package/dist/src/cli/tui/types.js +1 -0
- package/dist/src/cli/tui/views.js +1006 -0
- package/dist/src/config/loader.js +136 -0
- package/dist/src/config/schemas.js +403 -0
- package/dist/src/config/types.js +1 -0
- package/dist/src/context/types.js +331 -0
- package/dist/src/domain/entities.js +1 -0
- package/dist/src/git/backburner-git-tool-service.js +680 -0
- package/dist/src/git/gitops-lifecycle.js +56 -0
- package/dist/src/git/merge-prep-workspace.js +357 -0
- package/dist/src/git/types.js +484 -0
- package/dist/src/github/broker-model-content.js +215 -0
- package/dist/src/github/broker-server.js +237 -0
- package/dist/src/github/broker.js +638 -0
- package/dist/src/github/composed.js +53 -0
- package/dist/src/github/gateway.js +478 -0
- package/dist/src/github/normalize.js +222 -0
- package/dist/src/github/pr-classification.js +73 -0
- package/dist/src/github/scoped-broker.js +35 -0
- package/dist/src/github/security.js +126 -0
- package/dist/src/github/service.js +332 -0
- package/dist/src/github/types.js +1 -0
- package/dist/src/github/utils.js +16 -0
- package/dist/src/journal/index.js +4 -0
- package/dist/src/journal/reader.js +62 -0
- package/dist/src/journal/renderer.js +50 -0
- package/dist/src/journal/service.js +72 -0
- package/dist/src/journal/types.js +1 -0
- package/dist/src/journal/writer.js +19 -0
- package/dist/src/mcps/bridge.js +166 -0
- package/dist/src/mcps/index.js +5 -0
- package/dist/src/mcps/reconcile.js +113 -0
- package/dist/src/mcps/smithery-client.js +186 -0
- package/dist/src/mcps/smithery.js +164 -0
- package/dist/src/mcps/types.js +1 -0
- package/dist/src/memory/json-store.js +71 -0
- package/dist/src/memory/prompt.js +16 -0
- package/dist/src/memory/scope.js +31 -0
- package/dist/src/memory/store.js +1 -0
- package/dist/src/memory/tool-service.js +142 -0
- package/dist/src/memory/types.js +12 -0
- package/dist/src/onboarding/config-writer.js +96 -0
- package/dist/src/onboarding/engine.js +86 -0
- package/dist/src/onboarding/prompt.js +102 -0
- package/dist/src/onboarding/services/provider-discovery.js +58 -0
- package/dist/src/onboarding/services/repo-discovery.js +246 -0
- package/dist/src/onboarding/steps/check-github-auth.js +32 -0
- package/dist/src/onboarding/steps/check-requirements.js +34 -0
- package/dist/src/onboarding/steps/configure-models.js +105 -0
- package/dist/src/onboarding/steps/configure-paths.js +33 -0
- package/dist/src/onboarding/steps/discover-providers.js +38 -0
- package/dist/src/onboarding/steps/discover-repos.js +39 -0
- package/dist/src/onboarding/steps/generate-config.js +106 -0
- package/dist/src/onboarding/steps/readiness-validation.js +158 -0
- package/dist/src/onboarding/steps/select-repos.js +76 -0
- package/dist/src/onboarding/steps/show-summary.js +97 -0
- package/dist/src/onboarding/steps/welcome.js +27 -0
- package/dist/src/onboarding/types.js +1 -0
- package/dist/src/onboarding/validators/git-access.js +63 -0
- package/dist/src/onboarding/validators/github-auth.js +85 -0
- package/dist/src/onboarding/validators/tool-checker.js +153 -0
- package/dist/src/onboarding/validators/workspace.js +83 -0
- package/dist/src/prompts/resolver.js +106 -0
- package/dist/src/retrospectives/candidates.js +224 -0
- package/dist/src/retrospectives/derive.js +169 -0
- package/dist/src/retrospectives/ingest.js +321 -0
- package/dist/src/retrospectives/management-prs.js +380 -0
- package/dist/src/retrospectives/proposals.js +199 -0
- package/dist/src/retrospectives/scopes.js +272 -0
- package/dist/src/retrospectives/scoring.js +171 -0
- package/dist/src/retrospectives/types.js +1 -0
- package/dist/src/state/loader.js +473 -0
- package/dist/src/state/types.js +15 -0
- package/dist/src/tasks/derivation/builders.js +70 -0
- package/dist/src/tasks/derivation/handlers/comment-response.js +169 -0
- package/dist/src/tasks/derivation/handlers/implement-plan.js +103 -0
- package/dist/src/tasks/derivation/handlers/index.js +54 -0
- package/dist/src/tasks/derivation/handlers/packet-pr-control.js +92 -0
- package/dist/src/tasks/derivation/handlers/packet-worktree.js +29 -0
- package/dist/src/tasks/derivation/handlers/plan-breakdown.js +45 -0
- package/dist/src/tasks/derivation/handlers/prepare-for-merge.js +91 -0
- package/dist/src/tasks/derivation/handlers/product-discovery.js +36 -0
- package/dist/src/tasks/derivation/handlers/retry-failed-task.js +125 -0
- package/dist/src/tasks/derivation/handlers/review-pr.js +28 -0
- package/dist/src/tasks/derivation/handlers/shared.js +24 -0
- package/dist/src/tasks/derivation/handlers/sync-parent-branch.js +100 -0
- package/dist/src/tasks/derivation/handlers/wave-assessment.js +223 -0
- package/dist/src/tasks/derivation/handlers/write-plan.js +101 -0
- package/dist/src/tasks/derivation/handlers/write-product-spec.js +49 -0
- package/dist/src/tasks/derivation/runner.js +18 -0
- package/dist/src/tasks/derivation/types.js +1 -0
- package/dist/src/tasks/derive.js +810 -0
- package/dist/src/tasks/mcp-skip.js +10 -0
- package/dist/src/tasks/retry.js +10 -0
- package/dist/src/tasks/task.js +184 -0
- package/dist/src/tasks/types.js +1 -0
- package/dist/src/utils/command-runner.js +192 -0
- package/dist/src/utils/json.js +38 -0
- package/dist/src/utils/logger.js +43 -0
- package/dist/src/utils/paths.js +38 -0
- package/dist/src/utils/slug.js +10 -0
- package/dist/src/workflows/registry.js +174 -0
- package/dist/src/workflows/triage.js +90 -0
- package/dist/src/workflows/workstream-machine/derive-events.js +121 -0
- package/dist/src/workflows/workstream-machine/evaluate.js +24 -0
- package/dist/src/workflows/workstream-machine/events.js +1 -0
- package/dist/src/workflows/workstream-machine/machine.js +355 -0
- package/dist/src/workflows/workstream-machine/selectors.js +19 -0
- package/dist/src/workflows/workstream-machine/types.js +1 -0
- package/dist/src/workstreams/branch-sync.js +87 -0
- package/dist/src/workstreams/derive.js +629 -0
- package/dist/src/workstreams/mcp-blockers.js +420 -0
- package/dist/src/workstreams/packet-candidates.js +42 -0
- package/dist/src/workstreams/packet-lifecycle-feedback.js +60 -0
- package/dist/src/workstreams/packet-lifecycle-report.js +199 -0
- package/dist/src/workstreams/packet-plan.js +597 -0
- package/dist/src/workstreams/packet-projections.js +190 -0
- package/dist/src/workstreams/parent-branch-sync-action.js +312 -0
- package/dist/src/workstreams/parent-branch-sync.js +148 -0
- package/dist/src/workstreams/phases.js +4 -0
- package/dist/src/workstreams/plan-breakdown-generator/export.js +19 -0
- package/dist/src/workstreams/plan-breakdown-generator/index.js +6 -0
- package/dist/src/workstreams/plan-breakdown-generator/inspect.js +50 -0
- package/dist/src/workstreams/plan-breakdown-generator/patch.js +139 -0
- package/dist/src/workstreams/plan-breakdown-generator/session.js +19 -0
- package/dist/src/workstreams/plan-breakdown-generator/store.js +27 -0
- package/dist/src/workstreams/plan-breakdown-generator/tools.js +407 -0
- package/dist/src/workstreams/plan-breakdown-generator/types.js +6 -0
- package/dist/src/workstreams/plan-breakdown-generator/validate.js +212 -0
- package/dist/src/workstreams/plan-breakdown-schema.js +175 -0
- package/dist/src/workstreams/utils.js +15 -0
- package/dist/src/workstreams/wave-assessment/checkpoint.js +172 -0
- package/dist/src/workstreams/wave-assessment/index.js +3 -0
- package/dist/src/workstreams/wave-assessment/session.js +84 -0
- package/dist/src/workstreams/wave-assessment/store.js +51 -0
- package/dist/src/workstreams/wave-assessment/tools.js +419 -0
- package/dist/src/workstreams/wave-assessment/types.js +1 -0
- package/dist/src/workstreams/wave-assessment/validate.js +78 -0
- package/dist/src/worktrees/service.js +839 -0
- package/dist/src/worktrees/workspace-ownership.js +34 -0
- package/dist/src/worktrees/workspace-recovery.js +177 -0
- 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
|
+
}
|