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