opencode-magi 0.7.0 → 0.8.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/dist/config/resolve.js +28 -1
- package/dist/config/validate.js +97 -3
- package/dist/github/commands.js +32 -0
- package/dist/orchestrator/merge.js +310 -10
- package/dist/orchestrator/report.js +1 -1
- package/dist/orchestrator/review.js +97 -1
- 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 +1 -1
- package/schema.json +57 -4
package/dist/config/resolve.js
CHANGED
|
@@ -24,6 +24,31 @@ const DEFAULT_TRIAGE_CATEGORIES = [
|
|
|
24
24
|
types: ["Feature"],
|
|
25
25
|
},
|
|
26
26
|
];
|
|
27
|
+
export const DEFAULT_TRIAGE_LABEL_RULES = [
|
|
28
|
+
{ remove: ["triage"], when: { disposition: "accepted" } },
|
|
29
|
+
{
|
|
30
|
+
add: ["duplicate"],
|
|
31
|
+
remove: ["triage"],
|
|
32
|
+
when: { disposition: "duplicate" },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
add: ["duplicate"],
|
|
36
|
+
remove: ["triage"],
|
|
37
|
+
when: { disposition: "already_handled" },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
add: ["wontfix"],
|
|
41
|
+
remove: ["triage"],
|
|
42
|
+
when: { disposition: "rejected" },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
add: ["invalid"],
|
|
46
|
+
remove: ["triage"],
|
|
47
|
+
when: { disposition: "invalid" },
|
|
48
|
+
},
|
|
49
|
+
{ add: ["question"], when: { disposition: "needs_category" } },
|
|
50
|
+
{ add: ["question"], when: { disposition: "needs_acceptance" } },
|
|
51
|
+
];
|
|
27
52
|
export function reviewerKey(reviewer, index) {
|
|
28
53
|
return reviewer.id ?? `reviewer-${index + 1}`;
|
|
29
54
|
}
|
|
@@ -138,6 +163,7 @@ export function resolveRepository(config) {
|
|
|
138
163
|
agents: resolveAgents(config),
|
|
139
164
|
automation: {
|
|
140
165
|
close: config.merge?.automation?.close ?? false,
|
|
166
|
+
conflict: config.merge?.automation?.conflict ?? false,
|
|
141
167
|
merge: config.merge?.automation?.merge ?? true,
|
|
142
168
|
},
|
|
143
169
|
checks: {
|
|
@@ -190,9 +216,9 @@ export function resolveRepository(config) {
|
|
|
190
216
|
},
|
|
191
217
|
triage: {
|
|
192
218
|
automation: {
|
|
193
|
-
clear: config.triage?.automation?.clear ?? ["triage"],
|
|
194
219
|
close: config.triage?.automation?.close ?? false,
|
|
195
220
|
create: config.triage?.automation?.create ?? false,
|
|
221
|
+
label: config.triage?.automation?.label ?? DEFAULT_TRIAGE_LABEL_RULES,
|
|
196
222
|
merge: config.triage?.automation?.merge ?? false,
|
|
197
223
|
review: config.triage?.automation?.review ?? false,
|
|
198
224
|
},
|
|
@@ -215,6 +241,7 @@ export function resolveRepository(config) {
|
|
|
215
241
|
blockedLabels: config.triage?.safety?.blockedLabels ?? [],
|
|
216
242
|
requiredLabels: config.triage?.safety?.requiredLabels ?? ["triage"],
|
|
217
243
|
},
|
|
244
|
+
signals: config.triage?.signals ?? [],
|
|
218
245
|
worktree: config.triage?.worktree,
|
|
219
246
|
},
|
|
220
247
|
};
|
package/dist/config/validate.js
CHANGED
|
@@ -80,6 +80,7 @@ const TRIAGE_KEYS = new Set([
|
|
|
80
80
|
"prompts",
|
|
81
81
|
"reporter",
|
|
82
82
|
"safety",
|
|
83
|
+
"signals",
|
|
83
84
|
"voters",
|
|
84
85
|
"worktree",
|
|
85
86
|
]);
|
|
@@ -93,17 +94,24 @@ const REVIEW_MERGE_KEYS = new Set([
|
|
|
93
94
|
const REVIEW_CHECKS_KEYS = new Set(["exclude", "retryFailedJobs", "wait"]);
|
|
94
95
|
const MERGE_CHECKS_KEYS = new Set(["wait"]);
|
|
95
96
|
const AUTOMATION_KEYS = new Set(["close", "merge"]);
|
|
97
|
+
const MERGE_AUTOMATION_KEYS = new Set(["close", "conflict", "merge"]);
|
|
96
98
|
const CLEAR_KEYS = new Set(["branch", "output", "session", "worktree"]);
|
|
97
99
|
const CONCURRENCY_KEYS = new Set(["reviewers", "runs"]);
|
|
98
100
|
const OUTPUT_KEYS = new Set(["repairAttempts"]);
|
|
99
101
|
const TRIAGE_AUTOMATION_KEYS = new Set([
|
|
100
|
-
"clear",
|
|
101
102
|
"close",
|
|
102
103
|
"create",
|
|
104
|
+
"label",
|
|
103
105
|
"merge",
|
|
104
106
|
"review",
|
|
105
107
|
]);
|
|
106
108
|
const TRIAGE_CATEGORY_KEYS = new Set(["description", "id", "labels", "types"]);
|
|
109
|
+
const TRIAGE_LABEL_RULE_KEYS = new Set(["add", "remove", "when"]);
|
|
110
|
+
const TRIAGE_LABEL_RULE_WHEN_KEYS = new Set([
|
|
111
|
+
"category",
|
|
112
|
+
"disposition",
|
|
113
|
+
"signals",
|
|
114
|
+
]);
|
|
107
115
|
const TRIAGE_CONCURRENCY_KEYS = new Set(["runs"]);
|
|
108
116
|
const TRIAGE_SAFETY_KEYS = new Set([
|
|
109
117
|
"allowAuthors",
|
|
@@ -112,6 +120,18 @@ const TRIAGE_SAFETY_KEYS = new Set([
|
|
|
112
120
|
"blockedLabels",
|
|
113
121
|
"requiredLabels",
|
|
114
122
|
]);
|
|
123
|
+
const TRIAGE_SIGNAL_KEYS = new Set(["description", "id"]);
|
|
124
|
+
const TRIAGE_DISPOSITIONS = new Set([
|
|
125
|
+
"accepted",
|
|
126
|
+
"rejected",
|
|
127
|
+
"invalid",
|
|
128
|
+
"duplicate",
|
|
129
|
+
"already_handled",
|
|
130
|
+
"needs_category",
|
|
131
|
+
"needs_acceptance",
|
|
132
|
+
"blocked",
|
|
133
|
+
"failed",
|
|
134
|
+
]);
|
|
115
135
|
const SAFETY_KEYS = new Set([
|
|
116
136
|
"allowAuthors",
|
|
117
137
|
"blockedPaths",
|
|
@@ -552,7 +572,7 @@ function validateMerge(config, errors, options) {
|
|
|
552
572
|
errors.push("merge must be an object");
|
|
553
573
|
}
|
|
554
574
|
validateKnownKeys(merge, "merge", MERGE_KEYS, errors);
|
|
555
|
-
validateBooleanObject(merge?.automation, "merge.automation",
|
|
575
|
+
validateBooleanObject(merge?.automation, "merge.automation", MERGE_AUTOMATION_KEYS, errors);
|
|
556
576
|
const checks = merge?.checks;
|
|
557
577
|
validateKnownKeys(checks, "merge.checks", MERGE_CHECKS_KEYS, errors);
|
|
558
578
|
validateBoolean(checks?.wait, "merge.checks.wait", errors);
|
|
@@ -691,6 +711,79 @@ function validateTriageCategories(categories, path, errors) {
|
|
|
691
711
|
validateString(category.description, `${itemPath}.description`, errors);
|
|
692
712
|
});
|
|
693
713
|
}
|
|
714
|
+
function validateTriageSignals(signals, path, errors) {
|
|
715
|
+
if (signals == null)
|
|
716
|
+
return;
|
|
717
|
+
if (!Array.isArray(signals)) {
|
|
718
|
+
errors.push(`${path} must be an array`);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const ids = new Set();
|
|
722
|
+
signals.forEach((item, index) => {
|
|
723
|
+
const itemPath = `${path}[${index}]`;
|
|
724
|
+
if (!isPlainObject(item)) {
|
|
725
|
+
errors.push(`${itemPath} must be an object`);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const signal = item;
|
|
729
|
+
validateKnownKeys(signal, itemPath, TRIAGE_SIGNAL_KEYS, errors);
|
|
730
|
+
if (!signal.id) {
|
|
731
|
+
errors.push(`${itemPath}.id is required`);
|
|
732
|
+
}
|
|
733
|
+
else if (typeof signal.id !== "string") {
|
|
734
|
+
errors.push(`${itemPath}.id must be a string`);
|
|
735
|
+
}
|
|
736
|
+
else if (!TRIAGE_CATEGORY_ID_PATTERN.test(signal.id)) {
|
|
737
|
+
errors.push(`${itemPath}.id must match /^[A-Za-z0-9_-]+$/`);
|
|
738
|
+
}
|
|
739
|
+
else if (ids.has(signal.id)) {
|
|
740
|
+
errors.push(`${itemPath}.id must be unique`);
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
ids.add(signal.id);
|
|
744
|
+
}
|
|
745
|
+
if (!signal.description) {
|
|
746
|
+
errors.push(`${itemPath}.description is required`);
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
validateString(signal.description, `${itemPath}.description`, errors);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
function validateTriageLabelRules(rules, path, errors) {
|
|
754
|
+
if (rules == null)
|
|
755
|
+
return;
|
|
756
|
+
if (!Array.isArray(rules)) {
|
|
757
|
+
errors.push(`${path} must be an array`);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
rules.forEach((item, index) => {
|
|
761
|
+
const itemPath = `${path}[${index}]`;
|
|
762
|
+
if (!isPlainObject(item)) {
|
|
763
|
+
errors.push(`${itemPath} must be an object`);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
const rule = item;
|
|
767
|
+
validateKnownKeys(rule, itemPath, TRIAGE_LABEL_RULE_KEYS, errors);
|
|
768
|
+
validateStringArray(rule.add, `${itemPath}.add`, errors);
|
|
769
|
+
validateStringArray(rule.remove, `${itemPath}.remove`, errors);
|
|
770
|
+
if (!isPlainObject(rule.when)) {
|
|
771
|
+
errors.push(`${itemPath}.when must be an object`);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
validateKnownKeys(rule.when, `${itemPath}.when`, TRIAGE_LABEL_RULE_WHEN_KEYS, errors);
|
|
775
|
+
if (!Object.keys(rule.when).length) {
|
|
776
|
+
errors.push(`${itemPath}.when must not be empty`);
|
|
777
|
+
}
|
|
778
|
+
if (rule.when.disposition != null &&
|
|
779
|
+
(typeof rule.when.disposition !== "string" ||
|
|
780
|
+
!TRIAGE_DISPOSITIONS.has(rule.when.disposition))) {
|
|
781
|
+
errors.push(`${itemPath}.when.disposition must be a triage disposition`);
|
|
782
|
+
}
|
|
783
|
+
validateString(rule.when.category, `${itemPath}.when.category`, errors);
|
|
784
|
+
validateStringArray(rule.when.signals, `${itemPath}.when.signals`, errors);
|
|
785
|
+
});
|
|
786
|
+
}
|
|
694
787
|
function validateSafety(config, errors) {
|
|
695
788
|
const safety = config.review?.safety;
|
|
696
789
|
if (safety != null && !isPlainObject(safety)) {
|
|
@@ -764,7 +857,7 @@ function validateTriage(config, errors, options) {
|
|
|
764
857
|
validateBoolean(automation?.create, "triage.automation.create", errors);
|
|
765
858
|
validateBoolean(automation?.merge, "triage.automation.merge", errors);
|
|
766
859
|
validateBoolean(automation?.review, "triage.automation.review", errors);
|
|
767
|
-
|
|
860
|
+
validateTriageLabelRules(automation?.label, "triage.automation.label", errors);
|
|
768
861
|
if (automation?.review && !automation.create) {
|
|
769
862
|
errors.push("triage.automation.review requires triage.automation.create to be true");
|
|
770
863
|
}
|
|
@@ -779,6 +872,7 @@ function validateTriage(config, errors, options) {
|
|
|
779
872
|
errors.push("triage.concurrency.runs must be a positive integer");
|
|
780
873
|
}
|
|
781
874
|
validateTriageCategories(triage.categories, "triage.categories", errors);
|
|
875
|
+
validateTriageSignals(triage.signals, "triage.signals", errors);
|
|
782
876
|
validateKnownKeys(safety, "triage.safety", TRIAGE_SAFETY_KEYS, errors);
|
|
783
877
|
validateStringArray(safety?.allowAuthors, "triage.safety.allowAuthors", errors);
|
|
784
878
|
validateStringArray(safety?.allowMentionActors, "triage.safety.allowMentionActors", errors);
|
package/dist/github/commands.js
CHANGED
|
@@ -404,6 +404,15 @@ export async function assignIssue(exec, repository, issue, account) {
|
|
|
404
404
|
const token = await ghToken(exec, repository, account);
|
|
405
405
|
return exec(`gh issue edit ${issue} --repo ${shellQuote(repoSpecifier(repository))} --add-assignee ${shellQuote(account)}`, ghTokenEnv(token));
|
|
406
406
|
}
|
|
407
|
+
export async function addIssueLabels(exec, repository, issue, labels, account) {
|
|
408
|
+
const token = await ghToken(exec, repository, account);
|
|
409
|
+
const added = [];
|
|
410
|
+
for (const label of labels) {
|
|
411
|
+
await exec(`gh issue edit ${issue} --repo ${shellQuote(repoSpecifier(repository))} --add-label ${shellQuote(label)}`, ghTokenEnv(token));
|
|
412
|
+
added.push(label);
|
|
413
|
+
}
|
|
414
|
+
return added;
|
|
415
|
+
}
|
|
407
416
|
export async function removeIssueLabels(exec, repository, issue, labels, account) {
|
|
408
417
|
const token = await ghToken(exec, repository, account);
|
|
409
418
|
const removed = [];
|
|
@@ -685,6 +694,29 @@ export async function waitForAutoMerge(exec, repository, pr, intervalMs = 30_000
|
|
|
685
694
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
686
695
|
}
|
|
687
696
|
}
|
|
697
|
+
export async function fetchBaseBranch(exec, repository, meta, worktreePath) {
|
|
698
|
+
await exec(`git fetch --no-tags ${shellQuote(repositoryGitUrl(repository, repository.github.owner, repository.github.repo))} ${shellQuote(`refs/heads/${meta.baseRefName}`)}`, { cwd: worktreePath });
|
|
699
|
+
}
|
|
700
|
+
export async function mergeBaseNoCommit(exec, baseSha, worktreePath) {
|
|
701
|
+
await exec(`git merge --no-commit --no-ff ${shellQuote(baseSha)}`, {
|
|
702
|
+
cwd: worktreePath,
|
|
703
|
+
}).catch(() => undefined);
|
|
704
|
+
}
|
|
705
|
+
export async function listUnmergedFiles(exec, worktreePath) {
|
|
706
|
+
const output = await exec("git diff --name-only --diff-filter=U", {
|
|
707
|
+
cwd: worktreePath,
|
|
708
|
+
});
|
|
709
|
+
return output
|
|
710
|
+
.split("\n")
|
|
711
|
+
.map((line) => line.trim())
|
|
712
|
+
.filter(Boolean);
|
|
713
|
+
}
|
|
714
|
+
export async function abortMerge(exec, worktreePath) {
|
|
715
|
+
await exec("git merge --abort", { cwd: worktreePath }).catch(() => undefined);
|
|
716
|
+
}
|
|
717
|
+
export async function currentHeadSha(exec, worktreePath) {
|
|
718
|
+
return (await exec("git rev-parse HEAD", { cwd: worktreePath })).trim();
|
|
719
|
+
}
|
|
688
720
|
export async function closePullRequest(exec, repository, pr, account) {
|
|
689
721
|
const token = await ghToken(exec, repository, account);
|
|
690
722
|
return exec(`gh pr close ${pr} --repo ${shellQuote(repoSpecifier(repository))}`, ghTokenEnv(token));
|
|
@@ -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";
|
|
@@ -433,6 +433,224 @@ async function mergeWithQueue(input, exec, editorAccount) {
|
|
|
433
433
|
}
|
|
434
434
|
return waitForMergeQueue(exec, input.repository, input.pr);
|
|
435
435
|
}
|
|
436
|
+
async function runConflictEditor(input) {
|
|
437
|
+
const editor = input.run.repository.agents.editor;
|
|
438
|
+
if (!editor)
|
|
439
|
+
throw new Error("merge.editor is required for magi_merge");
|
|
440
|
+
await configureGitIdentity(input.run.exec, input.worktreePath, {
|
|
441
|
+
email: editor.author?.email,
|
|
442
|
+
name: editor.author?.name,
|
|
443
|
+
});
|
|
444
|
+
const artifactDir = outputDir(input.run);
|
|
445
|
+
const prompt = await composeMergeConflictPrompt({
|
|
446
|
+
baseBranch: input.baseBranch,
|
|
447
|
+
baseSha: input.baseSha,
|
|
448
|
+
conflictedFiles: JSON.stringify(input.conflictedFiles, null, 2),
|
|
449
|
+
directory: input.run.directory,
|
|
450
|
+
headSha: input.headSha,
|
|
451
|
+
pr: input.run.pr,
|
|
452
|
+
repository: input.run.repository,
|
|
453
|
+
worktreePath: input.worktreePath,
|
|
454
|
+
});
|
|
455
|
+
await input.run.onProgress?.({ cycle: input.cycle, type: "editor_started" });
|
|
456
|
+
const result = await withEditorFailureProgress({
|
|
457
|
+
cycle: input.cycle,
|
|
458
|
+
onProgress: input.run.onProgress,
|
|
459
|
+
run: () => runModelWithRepair({
|
|
460
|
+
client: input.run.client,
|
|
461
|
+
model: editor.model,
|
|
462
|
+
onProgress: async (progress) => {
|
|
463
|
+
if (progress.type === "session_created") {
|
|
464
|
+
await input.run.onProgress?.({
|
|
465
|
+
cycle: input.cycle,
|
|
466
|
+
options: progress.options,
|
|
467
|
+
sessionId: progress.sessionId,
|
|
468
|
+
type: "editor_session",
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
if (progress.type === "repair") {
|
|
472
|
+
await input.run.onProgress?.({
|
|
473
|
+
cycle: input.cycle,
|
|
474
|
+
type: "editor_repair",
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
if (progress.type === "response") {
|
|
478
|
+
await input.run.onProgress?.({
|
|
479
|
+
cycle: input.cycle,
|
|
480
|
+
sessionId: progress.sessionId,
|
|
481
|
+
type: "editor_response",
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
options: editor.options,
|
|
486
|
+
parentSessionId: input.run.parentSessionId,
|
|
487
|
+
parse: parseEditOutput,
|
|
488
|
+
permission: editor.permission,
|
|
489
|
+
prompt,
|
|
490
|
+
repairAttempts: input.run.config.output?.repairAttempts ?? 3,
|
|
491
|
+
schemaName: "edit",
|
|
492
|
+
signal: input.run.signal,
|
|
493
|
+
title: `magi resolve conflict ${input.run.repository.alias}#${input.run.pr}`,
|
|
494
|
+
}),
|
|
495
|
+
});
|
|
496
|
+
await writeFile(join(artifactDir, "editor.conflict.prompt.txt"), prompt);
|
|
497
|
+
await writeFile(join(artifactDir, "editor.conflict.raw.txt"), result.raw);
|
|
498
|
+
await writeFile(join(artifactDir, "editor.conflict.json"), JSON.stringify(result.value, null, 2));
|
|
499
|
+
await input.run.onProgress?.({ cycle: input.cycle, type: "editor_completed" });
|
|
500
|
+
return result.value;
|
|
501
|
+
}
|
|
502
|
+
async function recoverMergeQueueConflict(input) {
|
|
503
|
+
await input.run.onProgress?.({
|
|
504
|
+
phase: "checking merge queue conflict",
|
|
505
|
+
type: "phase",
|
|
506
|
+
});
|
|
507
|
+
const meta = await fetchPullRequest(input.exec, input.run.repository, input.run.pr);
|
|
508
|
+
if (meta.state && meta.state.toUpperCase() !== "OPEN")
|
|
509
|
+
return undefined;
|
|
510
|
+
await fetchBaseBranch(input.exec, input.run.repository, meta, input.worktreePath);
|
|
511
|
+
await mergeBaseNoCommit(input.exec, meta.baseRefOid, input.worktreePath);
|
|
512
|
+
const conflictedFiles = await listUnmergedFiles(input.exec, input.worktreePath);
|
|
513
|
+
if (!conflictedFiles.length) {
|
|
514
|
+
await abortMerge(input.exec, input.worktreePath);
|
|
515
|
+
return undefined;
|
|
516
|
+
}
|
|
517
|
+
await input.run.onProgress?.({
|
|
518
|
+
phase: "resolving merge conflict",
|
|
519
|
+
type: "phase",
|
|
520
|
+
});
|
|
521
|
+
const editorOutput = await runConflictEditor({
|
|
522
|
+
baseBranch: meta.baseRefName,
|
|
523
|
+
baseSha: meta.baseRefOid,
|
|
524
|
+
conflictedFiles,
|
|
525
|
+
cycle: input.cycle,
|
|
526
|
+
headSha: input.previousHeadSha,
|
|
527
|
+
run: input.run,
|
|
528
|
+
worktreePath: input.worktreePath,
|
|
529
|
+
});
|
|
530
|
+
if (editorOutput.mode !== "EDITED") {
|
|
531
|
+
return {
|
|
532
|
+
ciReports: input.ciReports,
|
|
533
|
+
editorOutput,
|
|
534
|
+
headSha: input.previousHeadSha,
|
|
535
|
+
outputs: {},
|
|
536
|
+
posted: {},
|
|
537
|
+
status: "changes_unresolved",
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
const remainingConflicts = await listUnmergedFiles(input.exec, input.worktreePath);
|
|
541
|
+
const editedHeadSha = await currentHeadSha(input.exec, input.worktreePath);
|
|
542
|
+
if (remainingConflicts.length || editedHeadSha === input.previousHeadSha) {
|
|
543
|
+
return {
|
|
544
|
+
ciReports: input.ciReports,
|
|
545
|
+
editorOutput,
|
|
546
|
+
headSha: input.previousHeadSha,
|
|
547
|
+
outputs: {},
|
|
548
|
+
posted: {},
|
|
549
|
+
status: "changes_unresolved",
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
const editor = input.run.repository.agents.editor;
|
|
553
|
+
if (!editor)
|
|
554
|
+
throw new Error("merge.editor is required for magi_merge");
|
|
555
|
+
const headOwner = meta.headRepositoryOwner?.login;
|
|
556
|
+
const headRepo = meta.headRepository?.name;
|
|
557
|
+
if (!headOwner || !headRepo) {
|
|
558
|
+
throw new Error("Pull request head repository is missing");
|
|
559
|
+
}
|
|
560
|
+
await pushHead(input.exec, input.run.repository, input.worktreePath, editor.account, { owner: headOwner, ref: meta.headRefName, repo: headRepo });
|
|
561
|
+
const ciReports = [...input.ciReports];
|
|
562
|
+
let ciFailureContext = "";
|
|
563
|
+
await input.run.onProgress?.({
|
|
564
|
+
phase: "waiting for checks after conflict resolution",
|
|
565
|
+
type: "phase",
|
|
566
|
+
});
|
|
567
|
+
const checkResult = await waitForChecksWithClassification({
|
|
568
|
+
afterEdit: {
|
|
569
|
+
cycle: input.cycle,
|
|
570
|
+
headSha: editedHeadSha,
|
|
571
|
+
previousHeadSha: input.previousHeadSha,
|
|
572
|
+
worktreePath: input.worktreePath,
|
|
573
|
+
},
|
|
574
|
+
client: input.run.client,
|
|
575
|
+
directory: input.run.directory,
|
|
576
|
+
exec: input.exec,
|
|
577
|
+
headSha: editedHeadSha,
|
|
578
|
+
onProgress: (phase) => input.run.onProgress?.({ phase, type: "phase" }),
|
|
579
|
+
parentSessionId: input.run.parentSessionId,
|
|
580
|
+
pr: input.run.pr,
|
|
581
|
+
repairAttempts: input.run.config.output?.repairAttempts ?? 3,
|
|
582
|
+
repository: input.run.repository,
|
|
583
|
+
signal: input.run.signal,
|
|
584
|
+
wait: input.run.repository.checks.waitAfterEdit,
|
|
585
|
+
});
|
|
586
|
+
ciFailureContext = checkResult?.ciFailureContext ?? "";
|
|
587
|
+
if (checkResult &&
|
|
588
|
+
(checkResult.report.scopeOutsideRecovered.length ||
|
|
589
|
+
checkResult.report.scopeOutsideUnresolved.length ||
|
|
590
|
+
checkResult.report.scopeInside.length)) {
|
|
591
|
+
ciReports.push(checkResult.report);
|
|
592
|
+
await input.run.onProgress?.({
|
|
593
|
+
report: checkResult.report,
|
|
594
|
+
type: "ci_report",
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
await input.run.onProgress?.({
|
|
598
|
+
phase: "rereview after conflict resolution",
|
|
599
|
+
type: "phase",
|
|
600
|
+
});
|
|
601
|
+
const rereview = await runRereview(input.run, input.worktreePath, input.previousHeadSha, input.cycle, input.sessionIds, ciFailureContext);
|
|
602
|
+
if (rereview.verdict === "CLOSE") {
|
|
603
|
+
if (!input.run.repository.automation.close) {
|
|
604
|
+
return {
|
|
605
|
+
ciReports,
|
|
606
|
+
editorOutput,
|
|
607
|
+
headSha: editedHeadSha,
|
|
608
|
+
outputs: rereview.outputs,
|
|
609
|
+
posted: rereview.posted,
|
|
610
|
+
status: "close_requested",
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
await input.run.onProgress?.({ phase: "closing PR", type: "phase" });
|
|
614
|
+
await closePullRequest(input.exec, input.run.repository, input.run.pr, editor.account);
|
|
615
|
+
return {
|
|
616
|
+
ciReports,
|
|
617
|
+
editorOutput,
|
|
618
|
+
headSha: editedHeadSha,
|
|
619
|
+
outputs: rereview.outputs,
|
|
620
|
+
posted: rereview.posted,
|
|
621
|
+
status: "closed",
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
if (rereview.verdict === "MERGE") {
|
|
625
|
+
if (hasBlockingCiReports(ciReports)) {
|
|
626
|
+
return {
|
|
627
|
+
ciReports,
|
|
628
|
+
editorOutput,
|
|
629
|
+
headSha: editedHeadSha,
|
|
630
|
+
outputs: rereview.outputs,
|
|
631
|
+
posted: rereview.posted,
|
|
632
|
+
status: "ci_unresolved",
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
await input.run.onProgress?.({ phase: "re-enqueueing PR", type: "phase" });
|
|
636
|
+
const status = await mergeWithQueue(input.run, input.exec, editor.account);
|
|
637
|
+
return {
|
|
638
|
+
ciReports,
|
|
639
|
+
editorOutput,
|
|
640
|
+
headSha: editedHeadSha,
|
|
641
|
+
outputs: rereview.outputs,
|
|
642
|
+
posted: rereview.posted,
|
|
643
|
+
status,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
ciReports,
|
|
648
|
+
editorOutput,
|
|
649
|
+
headSha: editedHeadSha,
|
|
650
|
+
outputs: rereview.outputs,
|
|
651
|
+
posted: rereview.posted,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
436
654
|
export function hasBlockingCiReports(reports) {
|
|
437
655
|
return reports.some((report) => report.scopeInside.length || report.scopeOutsideUnresolved.length);
|
|
438
656
|
}
|
|
@@ -656,6 +874,28 @@ export async function runMerge(input) {
|
|
|
656
874
|
outputs: reportOutputs,
|
|
657
875
|
posted: reportPosted,
|
|
658
876
|
});
|
|
877
|
+
let previousHeadSha = review.headSha;
|
|
878
|
+
const ciReports = [...review.ciReports];
|
|
879
|
+
const threadAttempts = {};
|
|
880
|
+
let dryRunThreads = input.dryRun
|
|
881
|
+
? syntheticReviewThreads(reportOutputs)
|
|
882
|
+
: undefined;
|
|
883
|
+
let conflictRecoveryAttempted = false;
|
|
884
|
+
const applyConflictRecovery = (recovery) => {
|
|
885
|
+
editorOutputs.push({
|
|
886
|
+
...recovery.editorOutput,
|
|
887
|
+
label: "Conflict recovery",
|
|
888
|
+
});
|
|
889
|
+
ciReports.length = 0;
|
|
890
|
+
ciReports.push(...recovery.ciReports);
|
|
891
|
+
reportCiReports = [...recovery.ciReports];
|
|
892
|
+
if (Object.keys(recovery.outputs).length)
|
|
893
|
+
reportOutputs = recovery.outputs;
|
|
894
|
+
if (Object.keys(recovery.posted).length)
|
|
895
|
+
reportPosted = recovery.posted;
|
|
896
|
+
previousHeadSha = recovery.headSha;
|
|
897
|
+
dryRunThreads = undefined;
|
|
898
|
+
};
|
|
659
899
|
if (review.verdict === "SAFETY_BLOCKED") {
|
|
660
900
|
await input.onProgress?.({
|
|
661
901
|
status: "safety_blocked",
|
|
@@ -693,15 +933,45 @@ export async function runMerge(input) {
|
|
|
693
933
|
}
|
|
694
934
|
await input.onProgress?.({ phase: "merging PR", type: "phase" });
|
|
695
935
|
const status = await mergeWithQueue(input, exec, editor.account);
|
|
696
|
-
|
|
697
|
-
|
|
936
|
+
if (status === "dequeued" &&
|
|
937
|
+
input.repository.automation.conflict &&
|
|
938
|
+
input.repository.merge.mergeQueue) {
|
|
939
|
+
if (!review.worktreePath)
|
|
940
|
+
throw new Error("Review worktree is missing");
|
|
941
|
+
conflictRecoveryAttempted = true;
|
|
942
|
+
const recovery = await recoverMergeQueueConflict({
|
|
943
|
+
ciReports,
|
|
944
|
+
cycle: 1,
|
|
945
|
+
exec,
|
|
946
|
+
previousHeadSha,
|
|
947
|
+
run: abortableInput,
|
|
948
|
+
sessionIds: review.sessionIds,
|
|
949
|
+
worktreePath: review.worktreePath,
|
|
950
|
+
});
|
|
951
|
+
if (recovery) {
|
|
952
|
+
applyConflictRecovery(recovery);
|
|
953
|
+
if (recovery.status) {
|
|
954
|
+
await input.onProgress?.({
|
|
955
|
+
status: recovery.status,
|
|
956
|
+
type: "merge_completed",
|
|
957
|
+
});
|
|
958
|
+
return complete({
|
|
959
|
+
cycles: 1,
|
|
960
|
+
pr: input.pr,
|
|
961
|
+
status: recovery.status,
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
await input.onProgress?.({ status, type: "merge_completed" });
|
|
967
|
+
return complete({ cycles: 0, pr: input.pr, status });
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
await input.onProgress?.({ status, type: "merge_completed" });
|
|
972
|
+
return complete({ cycles: 0, pr: input.pr, status });
|
|
973
|
+
}
|
|
698
974
|
}
|
|
699
|
-
let previousHeadSha = review.headSha;
|
|
700
|
-
const ciReports = [...review.ciReports];
|
|
701
|
-
const threadAttempts = {};
|
|
702
|
-
let dryRunThreads = input.dryRun
|
|
703
|
-
? syntheticReviewThreads(reportOutputs)
|
|
704
|
-
: undefined;
|
|
705
975
|
for (let cycle = 1;; cycle += 1) {
|
|
706
976
|
const unresolvedThreads = input.dryRun
|
|
707
977
|
? flattenSyntheticThreads(dryRunThreads ?? {})
|
|
@@ -886,6 +1156,36 @@ export async function runMerge(input) {
|
|
|
886
1156
|
}
|
|
887
1157
|
await input.onProgress?.({ phase: "merging PR", type: "phase" });
|
|
888
1158
|
const status = await mergeWithQueue(input, exec, editor.account);
|
|
1159
|
+
if (status === "dequeued" &&
|
|
1160
|
+
input.repository.automation.conflict &&
|
|
1161
|
+
input.repository.merge.mergeQueue &&
|
|
1162
|
+
!conflictRecoveryAttempted) {
|
|
1163
|
+
conflictRecoveryAttempted = true;
|
|
1164
|
+
const recovery = await recoverMergeQueueConflict({
|
|
1165
|
+
ciReports,
|
|
1166
|
+
cycle: cycle + 1,
|
|
1167
|
+
exec,
|
|
1168
|
+
previousHeadSha,
|
|
1169
|
+
run: abortableInput,
|
|
1170
|
+
sessionIds: review.sessionIds,
|
|
1171
|
+
worktreePath: review.worktreePath,
|
|
1172
|
+
});
|
|
1173
|
+
if (recovery) {
|
|
1174
|
+
applyConflictRecovery(recovery);
|
|
1175
|
+
if (recovery.status) {
|
|
1176
|
+
await input.onProgress?.({
|
|
1177
|
+
status: recovery.status,
|
|
1178
|
+
type: "merge_completed",
|
|
1179
|
+
});
|
|
1180
|
+
return complete({
|
|
1181
|
+
cycles: cycle + 1,
|
|
1182
|
+
pr: input.pr,
|
|
1183
|
+
status: recovery.status,
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
889
1189
|
await input.onProgress?.({ status, type: "merge_completed" });
|
|
890
1190
|
return complete({ cycles: cycle, pr: input.pr, status });
|
|
891
1191
|
}
|
|
@@ -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`,
|