opencode-magi 0.7.0 → 0.9.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/README.md +26 -9
- package/dist/config/resolve.js +36 -1
- package/dist/config/validate.js +137 -15
- package/dist/github/commands.js +48 -6
- package/dist/orchestrator/merge.js +367 -23
- package/dist/orchestrator/report.js +1 -1
- package/dist/orchestrator/review.js +485 -46
- package/dist/orchestrator/triage.js +249 -64
- package/dist/prompts/compose.js +59 -2
- package/dist/prompts/contracts.js +20 -1
- package/dist/prompts/output.js +19 -1
- package/dist/prompts/templates/merge/conflict.md +10 -0
- package/dist/prompts/templates/review/rereview.md +2 -0
- package/dist/prompts/templates/review/review.md +2 -0
- package/dist/prompts/templates/triage/acceptance.md +1 -1
- package/dist/prompts/templates/triage/signal.md +10 -0
- package/package.json +8 -8
- package/schema.json +60 -5
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { prRunOutputDir } from "../config/output";
|
|
4
|
-
import { closePullRequest, configureGitIdentity, fetchMergeQueueRequirement, fetchPullRequest, fetchUnresolvedThreads, mergePullRequest, postApproval, postChangesRequested, postCloseComment, postReply, pushHead, removeWorktree, resolveThread, waitForAutoMerge, waitForMergeQueue, } from "../github/commands";
|
|
5
|
-
import { composeEditPrompt, composeRereviewCloseReconsiderationPrompt, composeRereviewPrompt, } from "../prompts/compose";
|
|
4
|
+
import { abortMerge, closePullRequest, configureGitIdentity, currentHeadSha, fetchBaseBranch, fetchMergeQueueRequirement, fetchPullRequest, fetchUnresolvedThreads, listUnmergedFiles, mergePullRequest, mergeBaseNoCommit, postApproval, postChangesRequested, postCloseComment, postReply, pushHead, removeWorktree, resolveThread, waitForAutoMerge, waitForMergeQueue, } from "../github/commands";
|
|
5
|
+
import { composeEditPrompt, composeMergeConflictPrompt, composeRereviewCloseReconsiderationPrompt, composeRereviewPrompt, } from "../prompts/compose";
|
|
6
6
|
import { parseEditOutput, parseRereviewCloseReconsiderationOutput, parseRereviewOutput, } from "../prompts/output";
|
|
7
7
|
import { throwIfAborted, withAbortSignal } from "./abort";
|
|
8
8
|
import { waitForChecksWithClassification } from "./ci";
|
|
@@ -11,7 +11,7 @@ import { closeMinorityReviewers, mergeVerdictForPolicy } from "./majority";
|
|
|
11
11
|
import { runModelWithRepair } from "./model";
|
|
12
12
|
import { mapPool } from "./pool";
|
|
13
13
|
import { formatMergeReport } from "./report";
|
|
14
|
-
import { inlineCommentTargetsForDiff, runReview, } from "./review";
|
|
14
|
+
import { inlineCommentTargetsForDiff, assignThreadsByReviewFindingMarker, formatReviewMarker, postSingleConsensusReview, runReview, reviewPostingAccount, } from "./review";
|
|
15
15
|
import { checkSafetyGate, hasSafetyGate } from "./safety";
|
|
16
16
|
function outputDir(input) {
|
|
17
17
|
return prRunOutputDir({
|
|
@@ -131,6 +131,7 @@ async function postRereviewOutput(input, reviewerKey, output) {
|
|
|
131
131
|
const reviewer = input.repository.agents.reviewers.find((item) => item.key === reviewerKey);
|
|
132
132
|
if (!reviewer)
|
|
133
133
|
throw new Error(`Unknown reviewer: ${reviewerKey}`);
|
|
134
|
+
const account = reviewPostingAccount(input.repository, reviewer);
|
|
134
135
|
if (input.dryRun) {
|
|
135
136
|
if (output.verdict === "MERGE")
|
|
136
137
|
return `dry-run:would-approve:${reviewerKey}`;
|
|
@@ -139,16 +140,16 @@ async function postRereviewOutput(input, reviewerKey, output) {
|
|
|
139
140
|
}
|
|
140
141
|
return `dry-run:would-request-changes:${reviewerKey}`;
|
|
141
142
|
}
|
|
142
|
-
await Promise.all(output.resolve.map((item) => resolveThread(input.exec, input.repository,
|
|
143
|
-
const replies = await Promise.all(output.followUps.map((item) => postReply(input.exec, input.repository, input.pr,
|
|
143
|
+
await Promise.all(output.resolve.map((item) => resolveThread(input.exec, input.repository, account, item.threadId)));
|
|
144
|
+
const replies = await Promise.all(output.followUps.map((item) => postReply(input.exec, input.repository, input.pr, account, item.commentId, item.body)));
|
|
144
145
|
if (output.verdict === "MERGE") {
|
|
145
|
-
return postApproval(input.exec, input.repository, input.pr,
|
|
146
|
+
return postApproval(input.exec, input.repository, input.pr, account);
|
|
146
147
|
}
|
|
147
148
|
if (output.verdict === "CLOSE") {
|
|
148
|
-
return postCloseComment(input.exec, input.repository, input.pr,
|
|
149
|
+
return postCloseComment(input.exec, input.repository, input.pr, account, output.reason ?? "Close requested.");
|
|
149
150
|
}
|
|
150
151
|
if (output.newFindings.length) {
|
|
151
|
-
return postChangesRequested(input.exec, input.repository, input.pr,
|
|
152
|
+
return postChangesRequested(input.exec, input.repository, input.pr, account, output.newFindings.map((finding) => ({
|
|
152
153
|
fix: "Please address this before merging.",
|
|
153
154
|
issue: finding.body,
|
|
154
155
|
path: finding.path,
|
|
@@ -212,10 +213,23 @@ async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionI
|
|
|
212
213
|
worktreePath,
|
|
213
214
|
});
|
|
214
215
|
const artifactDir = outputDir(input);
|
|
216
|
+
const singleReviewMode = input.repository.review?.mode !== "multi";
|
|
217
|
+
const reviewerKeys = input.repository.agents.reviewers.map((reviewer) => reviewer.key);
|
|
218
|
+
const singleModeThreads = singleReviewMode
|
|
219
|
+
? assignThreadsByReviewFindingMarker({
|
|
220
|
+
fallbackReviewerKeys: reviewerKeys,
|
|
221
|
+
pr: input.pr,
|
|
222
|
+
reviewerKeys,
|
|
223
|
+
threads: options.dryRunThreads == null
|
|
224
|
+
? await fetchUnresolvedThreads(input.exec, input.repository, input.pr, input.repository.review?.account ?? "")
|
|
225
|
+
: Object.values(options.dryRunThreads).flat(),
|
|
226
|
+
})
|
|
227
|
+
: undefined;
|
|
215
228
|
let entries = await mapPool(input.repository.agents.reviewers, input.repository.concurrency.reviewers, async (reviewer) => {
|
|
216
229
|
throwIfAborted(input.signal);
|
|
217
|
-
const unresolved =
|
|
218
|
-
|
|
230
|
+
const unresolved = singleModeThreads?.[reviewer.key] ??
|
|
231
|
+
options.dryRunThreads?.[reviewer.key] ??
|
|
232
|
+
(await fetchUnresolvedThreads(input.exec, input.repository, input.pr, reviewPostingAccount(input.repository, reviewer)));
|
|
219
233
|
const hasReviewerSession = Boolean(sessionIds[reviewer.key]);
|
|
220
234
|
const prompt = await composeRereviewPrompt({
|
|
221
235
|
baseSha: meta.baseRefOid,
|
|
@@ -388,14 +402,44 @@ async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionI
|
|
|
388
402
|
};
|
|
389
403
|
}));
|
|
390
404
|
}
|
|
391
|
-
const posted = Object.fromEntries(await Promise.all(entries.map(async (entry) => [
|
|
392
|
-
entry.reviewer,
|
|
393
|
-
await postRereviewOutput(input, entry.reviewer, entry.output),
|
|
394
|
-
])));
|
|
395
405
|
const verdict = mergeVerdictForPolicy(entries.map((entry) => ({
|
|
396
406
|
reviewer: entry.reviewer,
|
|
397
407
|
verdict: entry.verdict,
|
|
398
408
|
})), input.repository.merge.approvalPolicy);
|
|
409
|
+
const outputs = Object.fromEntries(entries.map((entry) => [entry.reviewer, entry.output]));
|
|
410
|
+
const posted = singleReviewMode
|
|
411
|
+
? input.dryRun
|
|
412
|
+
? { consensus: `dry-run:would-post-single-review:${verdict}` }
|
|
413
|
+
: {
|
|
414
|
+
consensus: await (async () => {
|
|
415
|
+
const account = input.repository.review?.account ?? "";
|
|
416
|
+
await Promise.all(entries.flatMap((entry) => entry.output.resolve.map((item) => resolveThread(input.exec, input.repository, account, item.threadId))));
|
|
417
|
+
await Promise.all(entries.flatMap((entry) => entry.output.followUps.map((item) => postReply(input.exec, input.repository, input.pr, account, item.commentId, [
|
|
418
|
+
`**Reviewer:** ${entry.reviewer}`,
|
|
419
|
+
"",
|
|
420
|
+
item.body,
|
|
421
|
+
"",
|
|
422
|
+
formatReviewMarker({
|
|
423
|
+
head: headSha,
|
|
424
|
+
pr: input.pr,
|
|
425
|
+
reviewer: entry.reviewer,
|
|
426
|
+
verdict: entry.output.verdict,
|
|
427
|
+
}),
|
|
428
|
+
].join("\n")))));
|
|
429
|
+
return postSingleConsensusReview({
|
|
430
|
+
exec: input.exec,
|
|
431
|
+
headSha,
|
|
432
|
+
outputs,
|
|
433
|
+
pr: input.pr,
|
|
434
|
+
repository: input.repository,
|
|
435
|
+
verdict,
|
|
436
|
+
});
|
|
437
|
+
})(),
|
|
438
|
+
}
|
|
439
|
+
: Object.fromEntries(await Promise.all(entries.map(async (entry) => [
|
|
440
|
+
entry.reviewer,
|
|
441
|
+
await postRereviewOutput(input, entry.reviewer, entry.output),
|
|
442
|
+
])));
|
|
399
443
|
await writeFile(join(artifactDir, `rereview-majority.cycle-${cycle}.json`), JSON.stringify({
|
|
400
444
|
approvalPolicy: input.repository.merge.approvalPolicy,
|
|
401
445
|
verdict,
|
|
@@ -405,7 +449,7 @@ async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionI
|
|
|
405
449
|
})),
|
|
406
450
|
}, null, 2));
|
|
407
451
|
return {
|
|
408
|
-
outputs
|
|
452
|
+
outputs,
|
|
409
453
|
posted,
|
|
410
454
|
verdict,
|
|
411
455
|
};
|
|
@@ -433,6 +477,224 @@ async function mergeWithQueue(input, exec, editorAccount) {
|
|
|
433
477
|
}
|
|
434
478
|
return waitForMergeQueue(exec, input.repository, input.pr);
|
|
435
479
|
}
|
|
480
|
+
async function runConflictEditor(input) {
|
|
481
|
+
const editor = input.run.repository.agents.editor;
|
|
482
|
+
if (!editor)
|
|
483
|
+
throw new Error("merge.editor is required for magi_merge");
|
|
484
|
+
await configureGitIdentity(input.run.exec, input.worktreePath, {
|
|
485
|
+
email: editor.author?.email,
|
|
486
|
+
name: editor.author?.name,
|
|
487
|
+
});
|
|
488
|
+
const artifactDir = outputDir(input.run);
|
|
489
|
+
const prompt = await composeMergeConflictPrompt({
|
|
490
|
+
baseBranch: input.baseBranch,
|
|
491
|
+
baseSha: input.baseSha,
|
|
492
|
+
conflictedFiles: JSON.stringify(input.conflictedFiles, null, 2),
|
|
493
|
+
directory: input.run.directory,
|
|
494
|
+
headSha: input.headSha,
|
|
495
|
+
pr: input.run.pr,
|
|
496
|
+
repository: input.run.repository,
|
|
497
|
+
worktreePath: input.worktreePath,
|
|
498
|
+
});
|
|
499
|
+
await input.run.onProgress?.({ cycle: input.cycle, type: "editor_started" });
|
|
500
|
+
const result = await withEditorFailureProgress({
|
|
501
|
+
cycle: input.cycle,
|
|
502
|
+
onProgress: input.run.onProgress,
|
|
503
|
+
run: () => runModelWithRepair({
|
|
504
|
+
client: input.run.client,
|
|
505
|
+
model: editor.model,
|
|
506
|
+
onProgress: async (progress) => {
|
|
507
|
+
if (progress.type === "session_created") {
|
|
508
|
+
await input.run.onProgress?.({
|
|
509
|
+
cycle: input.cycle,
|
|
510
|
+
options: progress.options,
|
|
511
|
+
sessionId: progress.sessionId,
|
|
512
|
+
type: "editor_session",
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
if (progress.type === "repair") {
|
|
516
|
+
await input.run.onProgress?.({
|
|
517
|
+
cycle: input.cycle,
|
|
518
|
+
type: "editor_repair",
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
if (progress.type === "response") {
|
|
522
|
+
await input.run.onProgress?.({
|
|
523
|
+
cycle: input.cycle,
|
|
524
|
+
sessionId: progress.sessionId,
|
|
525
|
+
type: "editor_response",
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
options: editor.options,
|
|
530
|
+
parentSessionId: input.run.parentSessionId,
|
|
531
|
+
parse: parseEditOutput,
|
|
532
|
+
permission: editor.permission,
|
|
533
|
+
prompt,
|
|
534
|
+
repairAttempts: input.run.config.output?.repairAttempts ?? 3,
|
|
535
|
+
schemaName: "edit",
|
|
536
|
+
signal: input.run.signal,
|
|
537
|
+
title: `magi resolve conflict ${input.run.repository.alias}#${input.run.pr}`,
|
|
538
|
+
}),
|
|
539
|
+
});
|
|
540
|
+
await writeFile(join(artifactDir, "editor.conflict.prompt.txt"), prompt);
|
|
541
|
+
await writeFile(join(artifactDir, "editor.conflict.raw.txt"), result.raw);
|
|
542
|
+
await writeFile(join(artifactDir, "editor.conflict.json"), JSON.stringify(result.value, null, 2));
|
|
543
|
+
await input.run.onProgress?.({ cycle: input.cycle, type: "editor_completed" });
|
|
544
|
+
return result.value;
|
|
545
|
+
}
|
|
546
|
+
async function recoverMergeQueueConflict(input) {
|
|
547
|
+
await input.run.onProgress?.({
|
|
548
|
+
phase: "checking merge queue conflict",
|
|
549
|
+
type: "phase",
|
|
550
|
+
});
|
|
551
|
+
const meta = await fetchPullRequest(input.exec, input.run.repository, input.run.pr);
|
|
552
|
+
if (meta.state && meta.state.toUpperCase() !== "OPEN")
|
|
553
|
+
return undefined;
|
|
554
|
+
await fetchBaseBranch(input.exec, input.run.repository, meta, input.worktreePath);
|
|
555
|
+
await mergeBaseNoCommit(input.exec, meta.baseRefOid, input.worktreePath);
|
|
556
|
+
const conflictedFiles = await listUnmergedFiles(input.exec, input.worktreePath);
|
|
557
|
+
if (!conflictedFiles.length) {
|
|
558
|
+
await abortMerge(input.exec, input.worktreePath);
|
|
559
|
+
return undefined;
|
|
560
|
+
}
|
|
561
|
+
await input.run.onProgress?.({
|
|
562
|
+
phase: "resolving merge conflict",
|
|
563
|
+
type: "phase",
|
|
564
|
+
});
|
|
565
|
+
const editorOutput = await runConflictEditor({
|
|
566
|
+
baseBranch: meta.baseRefName,
|
|
567
|
+
baseSha: meta.baseRefOid,
|
|
568
|
+
conflictedFiles,
|
|
569
|
+
cycle: input.cycle,
|
|
570
|
+
headSha: input.previousHeadSha,
|
|
571
|
+
run: input.run,
|
|
572
|
+
worktreePath: input.worktreePath,
|
|
573
|
+
});
|
|
574
|
+
if (editorOutput.mode !== "EDITED") {
|
|
575
|
+
return {
|
|
576
|
+
ciReports: input.ciReports,
|
|
577
|
+
editorOutput,
|
|
578
|
+
headSha: input.previousHeadSha,
|
|
579
|
+
outputs: {},
|
|
580
|
+
posted: {},
|
|
581
|
+
status: "changes_unresolved",
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
const remainingConflicts = await listUnmergedFiles(input.exec, input.worktreePath);
|
|
585
|
+
const editedHeadSha = await currentHeadSha(input.exec, input.worktreePath);
|
|
586
|
+
if (remainingConflicts.length || editedHeadSha === input.previousHeadSha) {
|
|
587
|
+
return {
|
|
588
|
+
ciReports: input.ciReports,
|
|
589
|
+
editorOutput,
|
|
590
|
+
headSha: input.previousHeadSha,
|
|
591
|
+
outputs: {},
|
|
592
|
+
posted: {},
|
|
593
|
+
status: "changes_unresolved",
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
const editor = input.run.repository.agents.editor;
|
|
597
|
+
if (!editor)
|
|
598
|
+
throw new Error("merge.editor is required for magi_merge");
|
|
599
|
+
const headOwner = meta.headRepositoryOwner?.login;
|
|
600
|
+
const headRepo = meta.headRepository?.name;
|
|
601
|
+
if (!headOwner || !headRepo) {
|
|
602
|
+
throw new Error("Pull request head repository is missing");
|
|
603
|
+
}
|
|
604
|
+
await pushHead(input.exec, input.run.repository, input.worktreePath, editor.account, { owner: headOwner, ref: meta.headRefName, repo: headRepo });
|
|
605
|
+
const ciReports = [...input.ciReports];
|
|
606
|
+
let ciFailureContext = "";
|
|
607
|
+
await input.run.onProgress?.({
|
|
608
|
+
phase: "waiting for checks after conflict resolution",
|
|
609
|
+
type: "phase",
|
|
610
|
+
});
|
|
611
|
+
const checkResult = await waitForChecksWithClassification({
|
|
612
|
+
afterEdit: {
|
|
613
|
+
cycle: input.cycle,
|
|
614
|
+
headSha: editedHeadSha,
|
|
615
|
+
previousHeadSha: input.previousHeadSha,
|
|
616
|
+
worktreePath: input.worktreePath,
|
|
617
|
+
},
|
|
618
|
+
client: input.run.client,
|
|
619
|
+
directory: input.run.directory,
|
|
620
|
+
exec: input.exec,
|
|
621
|
+
headSha: editedHeadSha,
|
|
622
|
+
onProgress: (phase) => input.run.onProgress?.({ phase, type: "phase" }),
|
|
623
|
+
parentSessionId: input.run.parentSessionId,
|
|
624
|
+
pr: input.run.pr,
|
|
625
|
+
repairAttempts: input.run.config.output?.repairAttempts ?? 3,
|
|
626
|
+
repository: input.run.repository,
|
|
627
|
+
signal: input.run.signal,
|
|
628
|
+
wait: input.run.repository.checks.waitAfterEdit,
|
|
629
|
+
});
|
|
630
|
+
ciFailureContext = checkResult?.ciFailureContext ?? "";
|
|
631
|
+
if (checkResult &&
|
|
632
|
+
(checkResult.report.scopeOutsideRecovered.length ||
|
|
633
|
+
checkResult.report.scopeOutsideUnresolved.length ||
|
|
634
|
+
checkResult.report.scopeInside.length)) {
|
|
635
|
+
ciReports.push(checkResult.report);
|
|
636
|
+
await input.run.onProgress?.({
|
|
637
|
+
report: checkResult.report,
|
|
638
|
+
type: "ci_report",
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
await input.run.onProgress?.({
|
|
642
|
+
phase: "rereview after conflict resolution",
|
|
643
|
+
type: "phase",
|
|
644
|
+
});
|
|
645
|
+
const rereview = await runRereview(input.run, input.worktreePath, input.previousHeadSha, input.cycle, input.sessionIds, ciFailureContext);
|
|
646
|
+
if (rereview.verdict === "CLOSE") {
|
|
647
|
+
if (!input.run.repository.automation.close) {
|
|
648
|
+
return {
|
|
649
|
+
ciReports,
|
|
650
|
+
editorOutput,
|
|
651
|
+
headSha: editedHeadSha,
|
|
652
|
+
outputs: rereview.outputs,
|
|
653
|
+
posted: rereview.posted,
|
|
654
|
+
status: "close_requested",
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
await input.run.onProgress?.({ phase: "closing PR", type: "phase" });
|
|
658
|
+
await closePullRequest(input.exec, input.run.repository, input.run.pr, editor.account);
|
|
659
|
+
return {
|
|
660
|
+
ciReports,
|
|
661
|
+
editorOutput,
|
|
662
|
+
headSha: editedHeadSha,
|
|
663
|
+
outputs: rereview.outputs,
|
|
664
|
+
posted: rereview.posted,
|
|
665
|
+
status: "closed",
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
if (rereview.verdict === "MERGE") {
|
|
669
|
+
if (hasBlockingCiReports(ciReports)) {
|
|
670
|
+
return {
|
|
671
|
+
ciReports,
|
|
672
|
+
editorOutput,
|
|
673
|
+
headSha: editedHeadSha,
|
|
674
|
+
outputs: rereview.outputs,
|
|
675
|
+
posted: rereview.posted,
|
|
676
|
+
status: "ci_unresolved",
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
await input.run.onProgress?.({ phase: "re-enqueueing PR", type: "phase" });
|
|
680
|
+
const status = await mergeWithQueue(input.run, input.exec, editor.account);
|
|
681
|
+
return {
|
|
682
|
+
ciReports,
|
|
683
|
+
editorOutput,
|
|
684
|
+
headSha: editedHeadSha,
|
|
685
|
+
outputs: rereview.outputs,
|
|
686
|
+
posted: rereview.posted,
|
|
687
|
+
status,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
ciReports,
|
|
692
|
+
editorOutput,
|
|
693
|
+
headSha: editedHeadSha,
|
|
694
|
+
outputs: rereview.outputs,
|
|
695
|
+
posted: rereview.posted,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
436
698
|
export function hasBlockingCiReports(reports) {
|
|
437
699
|
return reports.some((report) => report.scopeInside.length || report.scopeOutsideUnresolved.length);
|
|
438
700
|
}
|
|
@@ -656,6 +918,28 @@ export async function runMerge(input) {
|
|
|
656
918
|
outputs: reportOutputs,
|
|
657
919
|
posted: reportPosted,
|
|
658
920
|
});
|
|
921
|
+
let previousHeadSha = review.headSha;
|
|
922
|
+
const ciReports = [...review.ciReports];
|
|
923
|
+
const threadAttempts = {};
|
|
924
|
+
let dryRunThreads = input.dryRun
|
|
925
|
+
? syntheticReviewThreads(reportOutputs)
|
|
926
|
+
: undefined;
|
|
927
|
+
let conflictRecoveryAttempted = false;
|
|
928
|
+
const applyConflictRecovery = (recovery) => {
|
|
929
|
+
editorOutputs.push({
|
|
930
|
+
...recovery.editorOutput,
|
|
931
|
+
label: "Conflict recovery",
|
|
932
|
+
});
|
|
933
|
+
ciReports.length = 0;
|
|
934
|
+
ciReports.push(...recovery.ciReports);
|
|
935
|
+
reportCiReports = [...recovery.ciReports];
|
|
936
|
+
if (Object.keys(recovery.outputs).length)
|
|
937
|
+
reportOutputs = recovery.outputs;
|
|
938
|
+
if (Object.keys(recovery.posted).length)
|
|
939
|
+
reportPosted = recovery.posted;
|
|
940
|
+
previousHeadSha = recovery.headSha;
|
|
941
|
+
dryRunThreads = undefined;
|
|
942
|
+
};
|
|
659
943
|
if (review.verdict === "SAFETY_BLOCKED") {
|
|
660
944
|
await input.onProgress?.({
|
|
661
945
|
status: "safety_blocked",
|
|
@@ -693,15 +977,45 @@ export async function runMerge(input) {
|
|
|
693
977
|
}
|
|
694
978
|
await input.onProgress?.({ phase: "merging PR", type: "phase" });
|
|
695
979
|
const status = await mergeWithQueue(input, exec, editor.account);
|
|
696
|
-
|
|
697
|
-
|
|
980
|
+
if (status === "dequeued" &&
|
|
981
|
+
input.repository.automation.conflict &&
|
|
982
|
+
input.repository.merge.mergeQueue) {
|
|
983
|
+
if (!review.worktreePath)
|
|
984
|
+
throw new Error("Review worktree is missing");
|
|
985
|
+
conflictRecoveryAttempted = true;
|
|
986
|
+
const recovery = await recoverMergeQueueConflict({
|
|
987
|
+
ciReports,
|
|
988
|
+
cycle: 1,
|
|
989
|
+
exec,
|
|
990
|
+
previousHeadSha,
|
|
991
|
+
run: abortableInput,
|
|
992
|
+
sessionIds: review.sessionIds,
|
|
993
|
+
worktreePath: review.worktreePath,
|
|
994
|
+
});
|
|
995
|
+
if (recovery) {
|
|
996
|
+
applyConflictRecovery(recovery);
|
|
997
|
+
if (recovery.status) {
|
|
998
|
+
await input.onProgress?.({
|
|
999
|
+
status: recovery.status,
|
|
1000
|
+
type: "merge_completed",
|
|
1001
|
+
});
|
|
1002
|
+
return complete({
|
|
1003
|
+
cycles: 1,
|
|
1004
|
+
pr: input.pr,
|
|
1005
|
+
status: recovery.status,
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
else {
|
|
1010
|
+
await input.onProgress?.({ status, type: "merge_completed" });
|
|
1011
|
+
return complete({ cycles: 0, pr: input.pr, status });
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
else {
|
|
1015
|
+
await input.onProgress?.({ status, type: "merge_completed" });
|
|
1016
|
+
return complete({ cycles: 0, pr: input.pr, status });
|
|
1017
|
+
}
|
|
698
1018
|
}
|
|
699
|
-
let previousHeadSha = review.headSha;
|
|
700
|
-
const ciReports = [...review.ciReports];
|
|
701
|
-
const threadAttempts = {};
|
|
702
|
-
let dryRunThreads = input.dryRun
|
|
703
|
-
? syntheticReviewThreads(reportOutputs)
|
|
704
|
-
: undefined;
|
|
705
1019
|
for (let cycle = 1;; cycle += 1) {
|
|
706
1020
|
const unresolvedThreads = input.dryRun
|
|
707
1021
|
? flattenSyntheticThreads(dryRunThreads ?? {})
|
|
@@ -886,6 +1200,36 @@ export async function runMerge(input) {
|
|
|
886
1200
|
}
|
|
887
1201
|
await input.onProgress?.({ phase: "merging PR", type: "phase" });
|
|
888
1202
|
const status = await mergeWithQueue(input, exec, editor.account);
|
|
1203
|
+
if (status === "dequeued" &&
|
|
1204
|
+
input.repository.automation.conflict &&
|
|
1205
|
+
input.repository.merge.mergeQueue &&
|
|
1206
|
+
!conflictRecoveryAttempted) {
|
|
1207
|
+
conflictRecoveryAttempted = true;
|
|
1208
|
+
const recovery = await recoverMergeQueueConflict({
|
|
1209
|
+
ciReports,
|
|
1210
|
+
cycle: cycle + 1,
|
|
1211
|
+
exec,
|
|
1212
|
+
previousHeadSha,
|
|
1213
|
+
run: abortableInput,
|
|
1214
|
+
sessionIds: review.sessionIds,
|
|
1215
|
+
worktreePath: review.worktreePath,
|
|
1216
|
+
});
|
|
1217
|
+
if (recovery) {
|
|
1218
|
+
applyConflictRecovery(recovery);
|
|
1219
|
+
if (recovery.status) {
|
|
1220
|
+
await input.onProgress?.({
|
|
1221
|
+
status: recovery.status,
|
|
1222
|
+
type: "merge_completed",
|
|
1223
|
+
});
|
|
1224
|
+
return complete({
|
|
1225
|
+
cycles: cycle + 1,
|
|
1226
|
+
pr: input.pr,
|
|
1227
|
+
status: recovery.status,
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
889
1233
|
await input.onProgress?.({ status, type: "merge_completed" });
|
|
890
1234
|
return complete({ cycles: cycle, pr: input.pr, status });
|
|
891
1235
|
}
|
|
@@ -138,7 +138,7 @@ function editorLines(outputs) {
|
|
|
138
138
|
return [
|
|
139
139
|
"- **Editor**:",
|
|
140
140
|
...outputs.flatMap((output, index) => {
|
|
141
|
-
const label = ` - Cycle ${index + 1}`;
|
|
141
|
+
const label = ` - ${output.label ?? `Cycle ${index + 1}`}`;
|
|
142
142
|
if (output.mode === "REPLIED") {
|
|
143
143
|
return [
|
|
144
144
|
`${label}: replied without code changes`,
|