opencode-magi 0.10.0 → 0.12.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.ja.md +254 -0
- package/README.md +31 -26
- package/dist/commands.js +11 -9
- package/dist/config/index.js +2 -0
- package/dist/config/resolve.js +120 -246
- package/dist/config/validate.js +98 -1110
- package/dist/constant.js +131 -0
- package/dist/graphql/index.generated.js +125 -0
- package/dist/graphql/index.js +49 -0
- package/dist/index.js +12 -800
- package/dist/magi.js +357 -0
- package/dist/permissions/common.json +22 -22
- package/dist/permissions/editor.json +12 -12
- package/dist/prompts/index.js +98 -0
- package/dist/prompts/merge/ci-classification/output-contract.md +22 -0
- package/dist/prompts/merge/ci-classification/task.md +10 -0
- package/dist/prompts/merge/ci-classification/validate.json +20 -0
- package/dist/prompts/merge/conflict/output-contract.md +12 -0
- package/dist/prompts/merge/conflict/task.md +9 -0
- package/dist/prompts/merge/conflict/validate.json +4 -0
- package/dist/prompts/merge/edit/output-contract.md +26 -0
- package/dist/prompts/merge/edit/task.md +10 -0
- package/dist/prompts/merge/edit/validate.json +60 -0
- package/dist/prompts/review/ci-classification/output-contract.md +21 -0
- package/dist/prompts/review/ci-classification/task.md +7 -0
- package/dist/prompts/review/ci-classification/validate.json +20 -0
- package/dist/prompts/review/close-reconsideration/output-contract.md +21 -0
- package/dist/prompts/review/close-reconsideration/task.md +6 -0
- package/dist/prompts/review/close-reconsideration/validate.json +44 -0
- package/dist/prompts/review/comment/output-contract.md +11 -0
- package/dist/prompts/review/comment/task.md +3 -0
- package/dist/prompts/review/comment/validate.json +8 -0
- package/dist/prompts/review/finding-validation/output-contract.md +25 -0
- package/dist/prompts/{templates/review/finding-validation.md → review/finding-validation/task.md} +2 -3
- package/dist/prompts/review/finding-validation/validate.json +21 -0
- package/dist/prompts/review/rereview/output-contract.md +30 -0
- package/dist/prompts/{templates/review/rereview.md → review/rereview/task.md} +8 -11
- package/dist/prompts/review/rereview/validate.json +87 -0
- package/dist/prompts/review/review/output-contract.md +25 -0
- package/dist/prompts/{templates/review/review.md → review/review/task.md} +2 -5
- package/dist/prompts/review/review/validate.json +54 -0
- package/dist/prompts/triage/acceptance/output-contract.md +16 -0
- package/dist/prompts/triage/acceptance/validate.json +23 -0
- package/dist/prompts/triage/category/output-contract.md +16 -0
- package/dist/prompts/triage/category/validate.json +23 -0
- package/dist/prompts/triage/comment-classification/output-contract.md +15 -0
- package/dist/prompts/triage/comment-classification/validate.json +28 -0
- package/dist/prompts/triage/create/output-contract.md +28 -0
- package/dist/prompts/triage/create/validate.json +73 -0
- package/dist/prompts/triage/deplicate/output-contract.md +16 -0
- package/dist/prompts/triage/deplicate/validate.json +20 -0
- package/dist/prompts/triage/existing/output-contract.md +16 -0
- package/dist/prompts/triage/existing/validate.json +13 -0
- package/dist/prompts/triage/reconsider/output-contract.md +16 -0
- package/dist/prompts/triage/reconsider/validate.json +23 -0
- package/dist/prompts/triage/signal/output-contract.md +19 -0
- package/dist/prompts/triage/signal/validate.json +18 -0
- package/dist/tools/clear/index.js +21 -0
- package/dist/tools/index.js +12 -0
- package/dist/tools/merge/action.js +47 -0
- package/dist/tools/merge/context.js +116 -0
- package/dist/tools/merge/editor.js +256 -0
- package/dist/tools/merge/index.js +151 -0
- package/dist/tools/merge/index.type.js +1 -0
- package/dist/tools/merge/merge.js +42 -0
- package/dist/tools/merge/report.js +73 -0
- package/dist/tools/review/action.js +459 -0
- package/dist/tools/review/check.js +335 -0
- package/dist/tools/review/context.js +254 -0
- package/dist/tools/review/index.js +104 -0
- package/dist/tools/review/index.type.js +1 -0
- package/dist/tools/review/report.js +143 -0
- package/dist/tools/review/review.js +146 -0
- package/dist/tools/review/reviewer.js +409 -0
- package/dist/tools/triage/index.js +16 -0
- package/dist/tools/validate/index.js +28 -0
- package/dist/utils/array.js +6 -0
- package/dist/utils/assertion.js +38 -0
- package/dist/utils/exec.js +14 -0
- package/dist/utils/fs.js +24 -0
- package/dist/utils/function.js +46 -0
- package/dist/utils/github.js +63 -0
- package/dist/utils/index.js +10 -0
- package/dist/utils/index.type.js +1 -0
- package/dist/utils/object.js +21 -0
- package/dist/utils/opencode.js +25 -0
- package/dist/utils/string.js +38 -0
- package/dist/utils/worker.js +30 -0
- package/package.json +36 -23
- package/schema.json +116 -20
- package/dist/config/load.js +0 -62
- package/dist/config/output.js +0 -25
- package/dist/config/worktree.js +0 -25
- package/dist/github/commands.js +0 -905
- package/dist/github/retry.js +0 -44
- package/dist/orchestrator/abort.js +0 -9
- package/dist/orchestrator/ci.js +0 -579
- package/dist/orchestrator/findings.js +0 -88
- package/dist/orchestrator/inline-comments.js +0 -73
- package/dist/orchestrator/majority.js +0 -62
- package/dist/orchestrator/merge.js +0 -1260
- package/dist/orchestrator/model.js +0 -216
- package/dist/orchestrator/pool.js +0 -15
- package/dist/orchestrator/report.js +0 -175
- package/dist/orchestrator/review-context.js +0 -342
- package/dist/orchestrator/review.js +0 -1480
- package/dist/orchestrator/run-manager.js +0 -2132
- package/dist/orchestrator/safety.js +0 -44
- package/dist/orchestrator/triage.js +0 -1370
- package/dist/prompts/compose.js +0 -473
- package/dist/prompts/contracts.js +0 -300
- package/dist/prompts/output.js +0 -401
- package/dist/prompts/templates/merge/ci-classification.md +0 -16
- package/dist/prompts/templates/merge/conflict.md +0 -10
- package/dist/prompts/templates/merge/edit.md +0 -15
- package/dist/prompts/templates/review/ci-classification.md +0 -9
- package/dist/prompts/templates/review/close-reconsideration.md +0 -6
- /package/dist/{types.js → config/index.type.js} +0 -0
- /package/dist/prompts/{templates/triage/acceptance.md → triage/acceptance/task.md} +0 -0
- /package/dist/prompts/{templates/triage/category.md → triage/category/task.md} +0 -0
- /package/dist/prompts/{templates/triage/comment-classification.md → triage/comment-classification/task.md} +0 -0
- /package/dist/prompts/{templates/triage/create.md → triage/create/task.md} +0 -0
- /package/dist/prompts/{templates/triage/duplicate.md → triage/deplicate/task.md} +0 -0
- /package/dist/prompts/{templates/triage/existing-pr.md → triage/existing/task.md} +0 -0
- /package/dist/prompts/{templates/triage/reconsider.md → triage/reconsider/task.md} +0 -0
- /package/dist/prompts/{templates/triage/signal.md → triage/signal/task.md} +0 -0
|
@@ -1,1260 +0,0 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { prRunOutputDir } from "../config/output";
|
|
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
|
-
import { parseEditOutput, parseRereviewCloseReconsiderationOutput, parseRereviewOutput, } from "../prompts/output";
|
|
7
|
-
import { throwIfAborted, withAbortSignal } from "./abort";
|
|
8
|
-
import { waitForChecksWithClassification } from "./ci";
|
|
9
|
-
import { validateInlineCommentTargets, } from "./inline-comments";
|
|
10
|
-
import { closeMinorityReviewers, mergeVerdictForPolicy } from "./majority";
|
|
11
|
-
import { runModelWithRepair } from "./model";
|
|
12
|
-
import { mapPool } from "./pool";
|
|
13
|
-
import { formatMergeReport } from "./report";
|
|
14
|
-
import { inlineCommentTargetsForDiff, assignThreadsByReviewFindingMarker, formatReviewMarker, postSingleConsensusReview, runReview, reviewPostingAccount, } from "./review";
|
|
15
|
-
import { checkSafetyGate, hasSafetyGate } from "./safety";
|
|
16
|
-
function outputDir(input) {
|
|
17
|
-
return prRunOutputDir({
|
|
18
|
-
config: input.config,
|
|
19
|
-
directory: input.directory,
|
|
20
|
-
pr: input.pr,
|
|
21
|
-
runId: input.runId,
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
function errorMessage(error) {
|
|
25
|
-
return error instanceof Error ? error.message : String(error);
|
|
26
|
-
}
|
|
27
|
-
async function withEditorFailureProgress(input) {
|
|
28
|
-
try {
|
|
29
|
-
return await input.run();
|
|
30
|
-
}
|
|
31
|
-
catch (error) {
|
|
32
|
-
await input.onProgress?.({
|
|
33
|
-
cycle: input.cycle,
|
|
34
|
-
error: errorMessage(error),
|
|
35
|
-
type: "editor_failed",
|
|
36
|
-
});
|
|
37
|
-
throw error;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
async function withReviewerFailureProgress(input) {
|
|
41
|
-
try {
|
|
42
|
-
return await input.run();
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
await input.onProgress?.({
|
|
46
|
-
error: errorMessage(error),
|
|
47
|
-
reviewer: input.reviewer,
|
|
48
|
-
type: "reviewer_failed",
|
|
49
|
-
});
|
|
50
|
-
throw error;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
async function runEditor(input, worktreePath, cycle, reviewFindings, unresolvedThreads) {
|
|
54
|
-
const editor = input.repository.agents.editor;
|
|
55
|
-
if (!editor)
|
|
56
|
-
throw new Error("merge.editor is required for magi_merge");
|
|
57
|
-
throwIfAborted(input.signal);
|
|
58
|
-
await configureGitIdentity(input.exec, worktreePath, {
|
|
59
|
-
email: editor.author?.email,
|
|
60
|
-
name: editor.author?.name,
|
|
61
|
-
});
|
|
62
|
-
const artifactDir = outputDir(input);
|
|
63
|
-
const prompt = await composeEditPrompt({
|
|
64
|
-
directory: input.directory,
|
|
65
|
-
pr: input.pr,
|
|
66
|
-
repository: input.repository,
|
|
67
|
-
reviewFindings: JSON.stringify(reviewFindings, null, 2),
|
|
68
|
-
unresolvedThreads: JSON.stringify(unresolvedThreads, null, 2),
|
|
69
|
-
worktreePath,
|
|
70
|
-
});
|
|
71
|
-
await input.onProgress?.({ cycle, type: "editor_started" });
|
|
72
|
-
const result = await withEditorFailureProgress({
|
|
73
|
-
cycle,
|
|
74
|
-
onProgress: input.onProgress,
|
|
75
|
-
run: () => runModelWithRepair({
|
|
76
|
-
client: input.client,
|
|
77
|
-
model: editor.model,
|
|
78
|
-
onProgress: async (progress) => {
|
|
79
|
-
if (progress.type === "session_created") {
|
|
80
|
-
await input.onProgress?.({
|
|
81
|
-
cycle,
|
|
82
|
-
options: progress.options,
|
|
83
|
-
sessionId: progress.sessionId,
|
|
84
|
-
type: "editor_session",
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
if (progress.type === "repair") {
|
|
88
|
-
await input.onProgress?.({ cycle, type: "editor_repair" });
|
|
89
|
-
}
|
|
90
|
-
if (progress.type === "response") {
|
|
91
|
-
await input.onProgress?.({
|
|
92
|
-
cycle,
|
|
93
|
-
sessionId: progress.sessionId,
|
|
94
|
-
type: "editor_response",
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
options: editor.options,
|
|
99
|
-
parentSessionId: input.parentSessionId,
|
|
100
|
-
parse: parseEditOutput,
|
|
101
|
-
permission: editor.permission,
|
|
102
|
-
prompt,
|
|
103
|
-
repairAttempts: input.config.output?.repairAttempts ?? 3,
|
|
104
|
-
schemaName: "edit",
|
|
105
|
-
signal: input.signal,
|
|
106
|
-
title: `magi edit ${input.repository.alias}#${input.pr} cycle ${cycle}`,
|
|
107
|
-
}),
|
|
108
|
-
});
|
|
109
|
-
await writeFile(join(artifactDir, `editor.cycle-${cycle}.prompt.txt`), prompt);
|
|
110
|
-
await writeFile(join(artifactDir, `editor.cycle-${cycle}.raw.txt`), result.raw);
|
|
111
|
-
await writeFile(join(artifactDir, `editor.cycle-${cycle}.json`), JSON.stringify(result.value, null, 2));
|
|
112
|
-
await input.onProgress?.({ cycle, type: "editor_completed" });
|
|
113
|
-
if (!input.dryRun) {
|
|
114
|
-
if (result.value.mode === "EDITED") {
|
|
115
|
-
const meta = await fetchPullRequest(input.exec, input.repository, input.pr);
|
|
116
|
-
const headOwner = meta.headRepositoryOwner?.login;
|
|
117
|
-
const headRepo = meta.headRepository?.name;
|
|
118
|
-
if (!headOwner || !headRepo) {
|
|
119
|
-
throw new Error("Pull request head repository is missing");
|
|
120
|
-
}
|
|
121
|
-
await pushHead(input.exec, input.repository, worktreePath, editor.account, { owner: headOwner, ref: meta.headRefName, repo: headRepo });
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
throwIfAborted(input.signal);
|
|
125
|
-
if (!input.dryRun) {
|
|
126
|
-
await Promise.all(result.value.responses.map((reply) => postReply(input.exec, input.repository, input.pr, editor.account, reply.commentId, reply.body)));
|
|
127
|
-
}
|
|
128
|
-
return result.value;
|
|
129
|
-
}
|
|
130
|
-
async function postRereviewOutput(input, reviewerKey, output) {
|
|
131
|
-
const reviewer = input.repository.agents.reviewers.find((item) => item.key === reviewerKey);
|
|
132
|
-
if (!reviewer)
|
|
133
|
-
throw new Error(`Unknown reviewer: ${reviewerKey}`);
|
|
134
|
-
const account = reviewPostingAccount(input.repository, reviewer);
|
|
135
|
-
if (input.dryRun) {
|
|
136
|
-
if (output.verdict === "MERGE")
|
|
137
|
-
return `dry-run:would-approve:${reviewerKey}`;
|
|
138
|
-
if (output.verdict === "CLOSE") {
|
|
139
|
-
return `dry-run:would-comment-close:${reviewerKey}`;
|
|
140
|
-
}
|
|
141
|
-
return `dry-run:would-request-changes:${reviewerKey}`;
|
|
142
|
-
}
|
|
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)));
|
|
145
|
-
if (output.verdict === "MERGE") {
|
|
146
|
-
return postApproval(input.exec, input.repository, input.pr, account);
|
|
147
|
-
}
|
|
148
|
-
if (output.verdict === "CLOSE") {
|
|
149
|
-
return postCloseComment(input.exec, input.repository, input.pr, account, output.reason ?? "Close requested.");
|
|
150
|
-
}
|
|
151
|
-
if (output.newFindings.length) {
|
|
152
|
-
return postChangesRequested(input.exec, input.repository, input.pr, account, output.newFindings.map((finding) => ({
|
|
153
|
-
fix: "Please address this before merging.",
|
|
154
|
-
issue: finding.body,
|
|
155
|
-
path: finding.path,
|
|
156
|
-
line: finding.line,
|
|
157
|
-
startLine: finding.startLine,
|
|
158
|
-
})));
|
|
159
|
-
}
|
|
160
|
-
return replies[0] ?? "";
|
|
161
|
-
}
|
|
162
|
-
function parseRereviewOutputWithInlineTargets(text, targets) {
|
|
163
|
-
const output = parseRereviewOutput(text);
|
|
164
|
-
validateInlineCommentTargets(output.newFindings, targets, "newFindings");
|
|
165
|
-
return output;
|
|
166
|
-
}
|
|
167
|
-
function newFindingToEditorFinding(reviewer, finding) {
|
|
168
|
-
return {
|
|
169
|
-
body: finding.body,
|
|
170
|
-
fix: "Please address this before merging.",
|
|
171
|
-
line: finding.line,
|
|
172
|
-
path: finding.path,
|
|
173
|
-
reviewer,
|
|
174
|
-
...(finding.startLine == null ? {} : { startLine: finding.startLine }),
|
|
175
|
-
type: "inline",
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
export function blockingReviewFindings(outputs) {
|
|
179
|
-
return Object.entries(outputs).flatMap(([reviewer, output]) => {
|
|
180
|
-
if (output.verdict !== "CHANGES_REQUESTED")
|
|
181
|
-
return [];
|
|
182
|
-
if ("findings" in output) {
|
|
183
|
-
return output.findings.map((finding) => ({
|
|
184
|
-
fix: finding.fix,
|
|
185
|
-
issue: finding.issue,
|
|
186
|
-
line: finding.line,
|
|
187
|
-
path: finding.path,
|
|
188
|
-
reviewer,
|
|
189
|
-
...(finding.startLine == null ? {} : { startLine: finding.startLine }),
|
|
190
|
-
type: "inline",
|
|
191
|
-
}));
|
|
192
|
-
}
|
|
193
|
-
return output.newFindings.map((finding) => newFindingToEditorFinding(reviewer, finding));
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionIds, ciFailureContext, options = {}) {
|
|
197
|
-
throwIfAborted(input.signal);
|
|
198
|
-
const meta = await fetchPullRequest(input.exec, input.repository, input.pr);
|
|
199
|
-
const headSha = options.dryRunHeadSha ?? meta.headRefOid;
|
|
200
|
-
const inlineCommentTargets = await inlineCommentTargetsForDiff({
|
|
201
|
-
ensure: options.dryRunHeadSha
|
|
202
|
-
? undefined
|
|
203
|
-
: {
|
|
204
|
-
fromSource: "base",
|
|
205
|
-
meta,
|
|
206
|
-
repository: input.repository,
|
|
207
|
-
toSource: "head",
|
|
208
|
-
},
|
|
209
|
-
exec: input.exec,
|
|
210
|
-
fromSha: meta.baseRefOid,
|
|
211
|
-
range: "direct",
|
|
212
|
-
toSha: headSha,
|
|
213
|
-
worktreePath,
|
|
214
|
-
});
|
|
215
|
-
const artifactDir = outputDir(input);
|
|
216
|
-
const singleReviewMode = input.repository.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.account ?? "")
|
|
225
|
-
: Object.values(options.dryRunThreads).flat(),
|
|
226
|
-
})
|
|
227
|
-
: undefined;
|
|
228
|
-
let entries = await mapPool(input.repository.agents.reviewers, input.repository.concurrency.reviewers, async (reviewer) => {
|
|
229
|
-
throwIfAborted(input.signal);
|
|
230
|
-
const unresolved = singleModeThreads?.[reviewer.key] ??
|
|
231
|
-
options.dryRunThreads?.[reviewer.key] ??
|
|
232
|
-
(await fetchUnresolvedThreads(input.exec, input.repository, input.pr, reviewPostingAccount(input.repository, reviewer)));
|
|
233
|
-
const hasReviewerSession = Boolean(sessionIds[reviewer.key]);
|
|
234
|
-
const prompt = await composeRereviewPrompt({
|
|
235
|
-
baseSha: meta.baseRefOid,
|
|
236
|
-
ciFailureContext,
|
|
237
|
-
directory: input.directory,
|
|
238
|
-
headSha,
|
|
239
|
-
includeReviewGuidelines: !hasReviewerSession,
|
|
240
|
-
includeSessionContext: !hasReviewerSession,
|
|
241
|
-
pr: input.pr,
|
|
242
|
-
previousHeadSha,
|
|
243
|
-
repository: input.repository,
|
|
244
|
-
reviewer,
|
|
245
|
-
unresolvedThreads: JSON.stringify(unresolved, null, 2),
|
|
246
|
-
worktreePath,
|
|
247
|
-
});
|
|
248
|
-
await input.onProgress?.({
|
|
249
|
-
reviewer: reviewer.key,
|
|
250
|
-
type: "reviewer_started",
|
|
251
|
-
});
|
|
252
|
-
const result = await withReviewerFailureProgress({
|
|
253
|
-
onProgress: input.onProgress,
|
|
254
|
-
reviewer: reviewer.key,
|
|
255
|
-
run: () => runModelWithRepair({
|
|
256
|
-
client: input.client,
|
|
257
|
-
model: reviewer.model,
|
|
258
|
-
onProgress: async (progress) => {
|
|
259
|
-
if (progress.type === "session_created") {
|
|
260
|
-
await input.onProgress?.({
|
|
261
|
-
reviewer: reviewer.key,
|
|
262
|
-
options: progress.options,
|
|
263
|
-
sessionId: progress.sessionId,
|
|
264
|
-
type: "reviewer_session",
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
if (progress.type === "repair") {
|
|
268
|
-
await input.onProgress?.({
|
|
269
|
-
reviewer: reviewer.key,
|
|
270
|
-
type: "reviewer_repair",
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
if (progress.type === "response") {
|
|
274
|
-
await input.onProgress?.({
|
|
275
|
-
reviewer: reviewer.key,
|
|
276
|
-
sessionId: progress.sessionId,
|
|
277
|
-
type: "reviewer_response",
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
},
|
|
281
|
-
options: reviewer.options,
|
|
282
|
-
parentSessionId: input.parentSessionId,
|
|
283
|
-
parse: (text) => parseRereviewOutputWithInlineTargets(text, inlineCommentTargets),
|
|
284
|
-
permission: reviewer.permission,
|
|
285
|
-
prompt,
|
|
286
|
-
repairAttempts: input.config.output?.repairAttempts ?? 3,
|
|
287
|
-
schemaName: "rereview",
|
|
288
|
-
signal: input.signal,
|
|
289
|
-
sessionId: sessionIds[reviewer.key],
|
|
290
|
-
title: `magi rereview ${input.repository.alias}#${input.pr} ${reviewer.key} cycle ${cycle}`,
|
|
291
|
-
}),
|
|
292
|
-
});
|
|
293
|
-
sessionIds[reviewer.key] = result.sessionId;
|
|
294
|
-
await writeFile(join(artifactDir, `${reviewer.key}.rereview.cycle-${cycle}.prompt.txt`), prompt);
|
|
295
|
-
await writeFile(join(artifactDir, `${reviewer.key}.rereview.cycle-${cycle}.raw.txt`), result.raw);
|
|
296
|
-
await writeFile(join(artifactDir, `${reviewer.key}.rereview.cycle-${cycle}.json`), JSON.stringify(result.value, null, 2));
|
|
297
|
-
await input.onProgress?.({
|
|
298
|
-
reviewer: reviewer.key,
|
|
299
|
-
sessionId: result.sessionId,
|
|
300
|
-
type: "reviewer_completed",
|
|
301
|
-
verdict: result.value.verdict,
|
|
302
|
-
});
|
|
303
|
-
return {
|
|
304
|
-
output: result.value,
|
|
305
|
-
reviewer: reviewer.key,
|
|
306
|
-
sessionId: result.sessionId,
|
|
307
|
-
verdict: result.value.verdict,
|
|
308
|
-
};
|
|
309
|
-
}, { signal: input.signal });
|
|
310
|
-
const targets = closeMinorityReviewers(entries);
|
|
311
|
-
if (targets.length) {
|
|
312
|
-
await input.onProgress?.({
|
|
313
|
-
phase: `reconsidering close verdicts cycle ${cycle}`,
|
|
314
|
-
type: "phase",
|
|
315
|
-
});
|
|
316
|
-
entries = await Promise.all(entries.map(async (entry) => {
|
|
317
|
-
if (!targets.includes(entry.reviewer))
|
|
318
|
-
return entry;
|
|
319
|
-
const reviewer = input.repository.agents.reviewers.find((item) => item.key === entry.reviewer);
|
|
320
|
-
if (!reviewer)
|
|
321
|
-
return entry;
|
|
322
|
-
const hasReviewerSession = Boolean(sessionIds[reviewer.key]);
|
|
323
|
-
const prompt = await composeRereviewCloseReconsiderationPrompt({
|
|
324
|
-
baseSha: meta.baseRefOid,
|
|
325
|
-
closeReason: entry.output.reason,
|
|
326
|
-
directory: input.directory,
|
|
327
|
-
headSha,
|
|
328
|
-
includeReviewGuidelines: !hasReviewerSession,
|
|
329
|
-
includeSessionContext: !hasReviewerSession,
|
|
330
|
-
pr: input.pr,
|
|
331
|
-
previousHeadSha,
|
|
332
|
-
repository: input.repository,
|
|
333
|
-
reviewer,
|
|
334
|
-
worktreePath,
|
|
335
|
-
});
|
|
336
|
-
const result = await withReviewerFailureProgress({
|
|
337
|
-
onProgress: input.onProgress,
|
|
338
|
-
reviewer: reviewer.key,
|
|
339
|
-
run: () => runModelWithRepair({
|
|
340
|
-
client: input.client,
|
|
341
|
-
model: reviewer.model,
|
|
342
|
-
onProgress: async (progress) => {
|
|
343
|
-
if (progress.type === "session_created") {
|
|
344
|
-
await input.onProgress?.({
|
|
345
|
-
reviewer: reviewer.key,
|
|
346
|
-
options: progress.options,
|
|
347
|
-
sessionId: progress.sessionId,
|
|
348
|
-
type: "reviewer_session",
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
if (progress.type === "repair") {
|
|
352
|
-
await input.onProgress?.({
|
|
353
|
-
reviewer: reviewer.key,
|
|
354
|
-
type: "reviewer_repair",
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
if (progress.type === "response") {
|
|
358
|
-
await input.onProgress?.({
|
|
359
|
-
reviewer: reviewer.key,
|
|
360
|
-
sessionId: progress.sessionId,
|
|
361
|
-
type: "reviewer_response",
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
},
|
|
365
|
-
options: reviewer.options,
|
|
366
|
-
parentSessionId: input.parentSessionId,
|
|
367
|
-
parse: (text) => {
|
|
368
|
-
const output = parseRereviewCloseReconsiderationOutput(text);
|
|
369
|
-
validateInlineCommentTargets(output.newFindings, inlineCommentTargets, "newFindings");
|
|
370
|
-
return output;
|
|
371
|
-
},
|
|
372
|
-
permission: reviewer.permission,
|
|
373
|
-
prompt,
|
|
374
|
-
repairAttempts: input.config.output?.repairAttempts ?? 3,
|
|
375
|
-
schemaName: "rereview close reconsideration",
|
|
376
|
-
sessionId: sessionIds[reviewer.key],
|
|
377
|
-
signal: input.signal,
|
|
378
|
-
title: `magi reconsider close ${input.repository.alias}#${input.pr} ${reviewer.key} cycle ${cycle}`,
|
|
379
|
-
}),
|
|
380
|
-
});
|
|
381
|
-
sessionIds[reviewer.key] = result.sessionId;
|
|
382
|
-
await writeFile(join(artifactDir, `${reviewer.key}.close-reconsideration.cycle-${cycle}.prompt.txt`), prompt);
|
|
383
|
-
await writeFile(join(artifactDir, `${reviewer.key}.close-reconsideration.cycle-${cycle}.raw.txt`), result.raw);
|
|
384
|
-
await writeFile(join(artifactDir, `${reviewer.key}.close-reconsideration.cycle-${cycle}.json`), JSON.stringify(result.value, null, 2));
|
|
385
|
-
await input.onProgress?.({
|
|
386
|
-
from: "CLOSE",
|
|
387
|
-
reviewer: reviewer.key,
|
|
388
|
-
to: result.value.verdict,
|
|
389
|
-
type: "reviewer_reconsidered",
|
|
390
|
-
});
|
|
391
|
-
await input.onProgress?.({
|
|
392
|
-
reviewer: reviewer.key,
|
|
393
|
-
sessionId: result.sessionId,
|
|
394
|
-
type: "reviewer_completed",
|
|
395
|
-
verdict: result.value.verdict,
|
|
396
|
-
});
|
|
397
|
-
return {
|
|
398
|
-
output: result.value,
|
|
399
|
-
reviewer: reviewer.key,
|
|
400
|
-
sessionId: result.sessionId,
|
|
401
|
-
verdict: result.value.verdict,
|
|
402
|
-
};
|
|
403
|
-
}));
|
|
404
|
-
}
|
|
405
|
-
const verdict = mergeVerdictForPolicy(entries.map((entry) => ({
|
|
406
|
-
reviewer: entry.reviewer,
|
|
407
|
-
verdict: entry.verdict,
|
|
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.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
|
-
])));
|
|
443
|
-
await writeFile(join(artifactDir, `rereview-majority.cycle-${cycle}.json`), JSON.stringify({
|
|
444
|
-
approvalPolicy: input.repository.merge.approvalPolicy,
|
|
445
|
-
verdict,
|
|
446
|
-
verdicts: entries.map((entry) => ({
|
|
447
|
-
reviewer: entry.reviewer,
|
|
448
|
-
verdict: entry.verdict,
|
|
449
|
-
})),
|
|
450
|
-
}, null, 2));
|
|
451
|
-
return {
|
|
452
|
-
outputs,
|
|
453
|
-
posted,
|
|
454
|
-
verdict,
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
async function finishMergeRun(input, result, reportInput) {
|
|
458
|
-
const report = formatMergeReport({
|
|
459
|
-
ciReports: reportInput.ciReports,
|
|
460
|
-
dryRun: input.dryRun,
|
|
461
|
-
editorOutputs: reportInput.editorOutputs,
|
|
462
|
-
outputs: reportInput.outputs,
|
|
463
|
-
posted: reportInput.posted,
|
|
464
|
-
pr: input.pr,
|
|
465
|
-
repository: input.repository,
|
|
466
|
-
status: result.status,
|
|
467
|
-
});
|
|
468
|
-
await writeFile(join(outputDir(input), "report.md"), `${report}\n`);
|
|
469
|
-
return { ...result, report };
|
|
470
|
-
}
|
|
471
|
-
async function mergeWithQueue(input, exec, editorAccount) {
|
|
472
|
-
await mergePullRequest(exec, input.repository, input.pr, editorAccount);
|
|
473
|
-
if (!input.repository.merge.mergeQueue) {
|
|
474
|
-
if (!input.repository.merge.auto)
|
|
475
|
-
return "merged";
|
|
476
|
-
return waitForAutoMerge(exec, input.repository, input.pr);
|
|
477
|
-
}
|
|
478
|
-
return waitForMergeQueue(exec, input.repository, input.pr);
|
|
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
|
-
}
|
|
698
|
-
export function hasBlockingCiReports(reports) {
|
|
699
|
-
return reports.some((report) => report.scopeInside.length || report.scopeOutsideUnresolved.length);
|
|
700
|
-
}
|
|
701
|
-
function copyThreadAttempts(attempts) {
|
|
702
|
-
return Object.fromEntries(Object.entries(attempts).map(([key, value]) => [key, { ...value }]));
|
|
703
|
-
}
|
|
704
|
-
export function reviewThreadKey(thread) {
|
|
705
|
-
return thread.threadId || `comment:${thread.commentId}`;
|
|
706
|
-
}
|
|
707
|
-
export function recordReviewThreads(input) {
|
|
708
|
-
for (const thread of input.threads) {
|
|
709
|
-
const key = reviewThreadKey(thread);
|
|
710
|
-
const current = input.attempts[key];
|
|
711
|
-
input.attempts[key] = current
|
|
712
|
-
? { ...current, lastSeenCycle: input.cycle }
|
|
713
|
-
: {
|
|
714
|
-
attempts: 0,
|
|
715
|
-
firstSeenCycle: input.cycle,
|
|
716
|
-
lastSeenCycle: input.cycle,
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
export function editableReviewThreads(input) {
|
|
721
|
-
if (input.maxThreadResolutionCycles === 0)
|
|
722
|
-
return input.threads;
|
|
723
|
-
return input.threads.filter((thread) => {
|
|
724
|
-
const attempt = input.attempts[reviewThreadKey(thread)];
|
|
725
|
-
return !attempt || attempt.attempts < input.maxThreadResolutionCycles;
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
export function exhaustedReviewThreads(input) {
|
|
729
|
-
if (input.maxThreadResolutionCycles === 0)
|
|
730
|
-
return [];
|
|
731
|
-
return input.threads.filter((thread) => {
|
|
732
|
-
const attempt = input.attempts[reviewThreadKey(thread)];
|
|
733
|
-
return !!attempt && attempt.attempts >= input.maxThreadResolutionCycles;
|
|
734
|
-
});
|
|
735
|
-
}
|
|
736
|
-
export function incrementReviewThreadAttempts(input) {
|
|
737
|
-
const newlyExhausted = [];
|
|
738
|
-
for (const thread of input.threads) {
|
|
739
|
-
const key = reviewThreadKey(thread);
|
|
740
|
-
const current = input.attempts[key] ?? {
|
|
741
|
-
attempts: 0,
|
|
742
|
-
firstSeenCycle: input.cycle,
|
|
743
|
-
lastSeenCycle: input.cycle,
|
|
744
|
-
};
|
|
745
|
-
const attempts = current.attempts + 1;
|
|
746
|
-
const exhaustedAtCycle = input.maxThreadResolutionCycles !== 0 &&
|
|
747
|
-
attempts >= input.maxThreadResolutionCycles &&
|
|
748
|
-
current.exhaustedAtCycle == null
|
|
749
|
-
? input.cycle
|
|
750
|
-
: current.exhaustedAtCycle;
|
|
751
|
-
if (exhaustedAtCycle === input.cycle)
|
|
752
|
-
newlyExhausted.push(thread);
|
|
753
|
-
input.attempts[key] = {
|
|
754
|
-
...current,
|
|
755
|
-
attempts,
|
|
756
|
-
exhaustedAtCycle,
|
|
757
|
-
lastAttemptedCycle: input.cycle,
|
|
758
|
-
lastSeenCycle: input.cycle,
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
return newlyExhausted;
|
|
762
|
-
}
|
|
763
|
-
export function reviewThreadNotification(repository, pr, thread) {
|
|
764
|
-
const host = repository.github.host || "github.com";
|
|
765
|
-
return {
|
|
766
|
-
label: "GitHub thread",
|
|
767
|
-
url: `https://${host}/${repository.github.owner}/${repository.github.repo}/pull/${pr}#discussion_r${thread.commentId}`,
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
|
-
function syntheticReviewThreads(outputs) {
|
|
771
|
-
let nextCommentId = -1;
|
|
772
|
-
const threads = {};
|
|
773
|
-
for (const [reviewer, output] of Object.entries(outputs)) {
|
|
774
|
-
if ("findings" in output) {
|
|
775
|
-
threads[reviewer] = output.findings.flatMap((finding) => {
|
|
776
|
-
const commentId = nextCommentId--;
|
|
777
|
-
return [
|
|
778
|
-
{
|
|
779
|
-
body: `Issue: ${finding.issue}\n\nFix: ${finding.fix}`,
|
|
780
|
-
commentId,
|
|
781
|
-
comments: [
|
|
782
|
-
{
|
|
783
|
-
author: reviewer,
|
|
784
|
-
body: `Issue: ${finding.issue}\n\nFix: ${finding.fix}`,
|
|
785
|
-
commentId,
|
|
786
|
-
createdAt: new Date(0).toISOString(),
|
|
787
|
-
},
|
|
788
|
-
],
|
|
789
|
-
line: finding.line,
|
|
790
|
-
path: finding.path,
|
|
791
|
-
threadId: `dry-run:${reviewer}:${Math.abs(commentId)}`,
|
|
792
|
-
},
|
|
793
|
-
];
|
|
794
|
-
});
|
|
795
|
-
continue;
|
|
796
|
-
}
|
|
797
|
-
threads[reviewer] = output.newFindings.flatMap((finding) => {
|
|
798
|
-
const commentId = nextCommentId--;
|
|
799
|
-
return [
|
|
800
|
-
{
|
|
801
|
-
body: finding.body,
|
|
802
|
-
commentId,
|
|
803
|
-
comments: [
|
|
804
|
-
{
|
|
805
|
-
author: reviewer,
|
|
806
|
-
body: finding.body,
|
|
807
|
-
commentId,
|
|
808
|
-
createdAt: new Date(0).toISOString(),
|
|
809
|
-
},
|
|
810
|
-
],
|
|
811
|
-
line: finding.line,
|
|
812
|
-
path: finding.path,
|
|
813
|
-
threadId: `dry-run:${reviewer}:${Math.abs(commentId)}`,
|
|
814
|
-
},
|
|
815
|
-
];
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
|
-
return threads;
|
|
819
|
-
}
|
|
820
|
-
function flattenSyntheticThreads(threads) {
|
|
821
|
-
return Object.values(threads).flat();
|
|
822
|
-
}
|
|
823
|
-
function appendDryRunEditorResponses(input) {
|
|
824
|
-
if (!input.threads || !input.output.responses.length)
|
|
825
|
-
return input.threads;
|
|
826
|
-
let nextCommentId = -10_000;
|
|
827
|
-
const responses = [...input.output.responses];
|
|
828
|
-
return Object.fromEntries(Object.entries(input.threads).map(([reviewer, threads]) => [
|
|
829
|
-
reviewer,
|
|
830
|
-
threads.map((thread) => {
|
|
831
|
-
const matched = responses.filter((response) => response.commentId === thread.commentId ||
|
|
832
|
-
thread.comments.some((comment) => comment.commentId === response.commentId));
|
|
833
|
-
if (!matched.length)
|
|
834
|
-
return thread;
|
|
835
|
-
return {
|
|
836
|
-
...thread,
|
|
837
|
-
comments: [
|
|
838
|
-
...thread.comments,
|
|
839
|
-
...matched.map((response, index) => ({
|
|
840
|
-
author: input.author,
|
|
841
|
-
body: response.body,
|
|
842
|
-
commentId: nextCommentId--,
|
|
843
|
-
createdAt: `9999-01-01T00:00:${String(index).padStart(2, "0")}Z`,
|
|
844
|
-
})),
|
|
845
|
-
],
|
|
846
|
-
};
|
|
847
|
-
}),
|
|
848
|
-
]));
|
|
849
|
-
}
|
|
850
|
-
export async function runMerge(input) {
|
|
851
|
-
const exec = withAbortSignal(input.exec, input.signal);
|
|
852
|
-
const abortableInput = { ...input, exec };
|
|
853
|
-
const editor = input.repository.agents.editor;
|
|
854
|
-
if (!editor)
|
|
855
|
-
throw new Error("merge.editor is required for magi_merge");
|
|
856
|
-
throwIfAborted(input.signal);
|
|
857
|
-
const artifactDir = outputDir(input);
|
|
858
|
-
await mkdir(artifactDir, { recursive: true });
|
|
859
|
-
if (hasSafetyGate(input.repository)) {
|
|
860
|
-
await input.onProgress?.({ phase: "checking safety", type: "phase" });
|
|
861
|
-
const safety = await checkSafetyGate({
|
|
862
|
-
exec,
|
|
863
|
-
pr: input.pr,
|
|
864
|
-
repository: input.repository,
|
|
865
|
-
});
|
|
866
|
-
if (!safety.ok) {
|
|
867
|
-
const report = formatMergeReport({
|
|
868
|
-
ciReports: [],
|
|
869
|
-
dryRun: input.dryRun,
|
|
870
|
-
editorOutputs: [],
|
|
871
|
-
outputs: {},
|
|
872
|
-
posted: {},
|
|
873
|
-
pr: input.pr,
|
|
874
|
-
repository: input.repository,
|
|
875
|
-
safety,
|
|
876
|
-
status: "safety_blocked",
|
|
877
|
-
});
|
|
878
|
-
await writeFile(join(artifactDir, "report.md"), `${report}\n`);
|
|
879
|
-
await input.onProgress?.({
|
|
880
|
-
status: "safety_blocked",
|
|
881
|
-
type: "merge_completed",
|
|
882
|
-
});
|
|
883
|
-
return { cycles: 0, pr: input.pr, report, status: "safety_blocked" };
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
if (input.repository.merge.mergeQueue) {
|
|
887
|
-
const meta = await fetchPullRequest(exec, input.repository, input.pr);
|
|
888
|
-
const requiresMergeQueue = await fetchMergeQueueRequirement(exec, input.repository, meta.baseRefName).catch(() => undefined);
|
|
889
|
-
if (requiresMergeQueue !== true) {
|
|
890
|
-
await input.onProgress?.({
|
|
891
|
-
message: requiresMergeQueue === false
|
|
892
|
-
? `Merge queue is not enabled for base branch ${meta.baseRefName}.`
|
|
893
|
-
: `Could not verify merge queue for base branch ${meta.baseRefName}.`,
|
|
894
|
-
type: "warning",
|
|
895
|
-
});
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
await input.onProgress?.({ phase: "initial review", type: "phase" });
|
|
899
|
-
const review = await runReview({
|
|
900
|
-
...abortableInput,
|
|
901
|
-
allowAlreadyReviewed: true,
|
|
902
|
-
approvalPolicy: input.repository.merge.approvalPolicy,
|
|
903
|
-
enableReviewAutomation: false,
|
|
904
|
-
onProgress: (progress) => input.onProgress?.(progress),
|
|
905
|
-
runId: input.runId,
|
|
906
|
-
dryRun: input.dryRun,
|
|
907
|
-
skipSafety: true,
|
|
908
|
-
});
|
|
909
|
-
try {
|
|
910
|
-
throwIfAborted(input.signal);
|
|
911
|
-
let reportOutputs = review.outputs;
|
|
912
|
-
let reportPosted = review.posted;
|
|
913
|
-
let reportCiReports = review.ciReports;
|
|
914
|
-
const editorOutputs = [];
|
|
915
|
-
const complete = (result) => finishMergeRun(input, result, {
|
|
916
|
-
ciReports: reportCiReports,
|
|
917
|
-
editorOutputs,
|
|
918
|
-
outputs: reportOutputs,
|
|
919
|
-
posted: reportPosted,
|
|
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
|
-
};
|
|
943
|
-
if (review.verdict === "SAFETY_BLOCKED") {
|
|
944
|
-
await input.onProgress?.({
|
|
945
|
-
status: "safety_blocked",
|
|
946
|
-
type: "merge_completed",
|
|
947
|
-
});
|
|
948
|
-
return complete({ cycles: 0, pr: input.pr, status: "safety_blocked" });
|
|
949
|
-
}
|
|
950
|
-
if (review.verdict === "CLOSE") {
|
|
951
|
-
if (!input.repository.automation.close || input.dryRun) {
|
|
952
|
-
await input.onProgress?.({
|
|
953
|
-
status: "close_requested",
|
|
954
|
-
type: "merge_completed",
|
|
955
|
-
});
|
|
956
|
-
return complete({ cycles: 0, pr: input.pr, status: "close_requested" });
|
|
957
|
-
}
|
|
958
|
-
await input.onProgress?.({ phase: "closing PR", type: "phase" });
|
|
959
|
-
await closePullRequest(exec, input.repository, input.pr, editor.account);
|
|
960
|
-
await input.onProgress?.({ status: "closed", type: "merge_completed" });
|
|
961
|
-
return complete({ cycles: 0, pr: input.pr, status: "closed" });
|
|
962
|
-
}
|
|
963
|
-
if (review.verdict === "MERGE") {
|
|
964
|
-
if (hasBlockingCiReports(review.ciReports)) {
|
|
965
|
-
await input.onProgress?.({
|
|
966
|
-
status: "ci_unresolved",
|
|
967
|
-
type: "merge_completed",
|
|
968
|
-
});
|
|
969
|
-
return complete({ cycles: 0, pr: input.pr, status: "ci_unresolved" });
|
|
970
|
-
}
|
|
971
|
-
if (!input.repository.automation.merge || input.dryRun) {
|
|
972
|
-
await input.onProgress?.({
|
|
973
|
-
status: "approved",
|
|
974
|
-
type: "merge_completed",
|
|
975
|
-
});
|
|
976
|
-
return complete({ cycles: 0, pr: input.pr, status: "approved" });
|
|
977
|
-
}
|
|
978
|
-
await input.onProgress?.({ phase: "merging PR", type: "phase" });
|
|
979
|
-
const status = await mergeWithQueue(input, exec, editor.account);
|
|
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
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
for (let cycle = 1;; cycle += 1) {
|
|
1020
|
-
const unresolvedThreads = input.dryRun
|
|
1021
|
-
? flattenSyntheticThreads(dryRunThreads ?? {})
|
|
1022
|
-
: await fetchUnresolvedThreads(exec, input.repository, input.pr);
|
|
1023
|
-
recordReviewThreads({
|
|
1024
|
-
attempts: threadAttempts,
|
|
1025
|
-
cycle,
|
|
1026
|
-
threads: unresolvedThreads,
|
|
1027
|
-
});
|
|
1028
|
-
await input.onProgress?.({
|
|
1029
|
-
attempts: copyThreadAttempts(threadAttempts),
|
|
1030
|
-
type: "thread_attempts",
|
|
1031
|
-
});
|
|
1032
|
-
const editableThreads = editableReviewThreads({
|
|
1033
|
-
attempts: threadAttempts,
|
|
1034
|
-
maxThreadResolutionCycles: input.repository.merge.maxThreadResolutionCycles,
|
|
1035
|
-
threads: unresolvedThreads,
|
|
1036
|
-
});
|
|
1037
|
-
const editorFindings = blockingReviewFindings(reportOutputs);
|
|
1038
|
-
const editableFindings = editorFindings;
|
|
1039
|
-
const findingAttemptsExhausted = input.repository.merge.maxThreadResolutionCycles !== 0 &&
|
|
1040
|
-
cycle > input.repository.merge.maxThreadResolutionCycles;
|
|
1041
|
-
if (!editableThreads.length &&
|
|
1042
|
-
(!editableFindings.length || findingAttemptsExhausted)) {
|
|
1043
|
-
await input.onProgress?.({
|
|
1044
|
-
status: "changes_unresolved",
|
|
1045
|
-
type: "merge_completed",
|
|
1046
|
-
});
|
|
1047
|
-
return complete({
|
|
1048
|
-
cycles: cycle - 1,
|
|
1049
|
-
pr: input.pr,
|
|
1050
|
-
status: "changes_unresolved",
|
|
1051
|
-
});
|
|
1052
|
-
}
|
|
1053
|
-
await input.onProgress?.({
|
|
1054
|
-
phase: `editing cycle ${cycle}`,
|
|
1055
|
-
type: "phase",
|
|
1056
|
-
});
|
|
1057
|
-
const newlyExhausted = incrementReviewThreadAttempts({
|
|
1058
|
-
attempts: threadAttempts,
|
|
1059
|
-
cycle,
|
|
1060
|
-
maxThreadResolutionCycles: input.repository.merge.maxThreadResolutionCycles,
|
|
1061
|
-
threads: editableThreads,
|
|
1062
|
-
});
|
|
1063
|
-
await input.onProgress?.({
|
|
1064
|
-
attempts: copyThreadAttempts(threadAttempts),
|
|
1065
|
-
type: "thread_attempts",
|
|
1066
|
-
});
|
|
1067
|
-
if (!review.worktreePath)
|
|
1068
|
-
throw new Error("Review worktree is missing");
|
|
1069
|
-
const editorOutput = await runEditor(abortableInput, review.worktreePath, cycle, editableFindings, editableThreads);
|
|
1070
|
-
editorOutputs.push(editorOutput);
|
|
1071
|
-
dryRunThreads = input.dryRun
|
|
1072
|
-
? appendDryRunEditorResponses({
|
|
1073
|
-
author: editor.account,
|
|
1074
|
-
output: editorOutput,
|
|
1075
|
-
threads: dryRunThreads,
|
|
1076
|
-
})
|
|
1077
|
-
: dryRunThreads;
|
|
1078
|
-
if (newlyExhausted.length) {
|
|
1079
|
-
await input.onProgress?.({
|
|
1080
|
-
threads: newlyExhausted.map((thread) => reviewThreadNotification(input.repository, input.pr, thread)),
|
|
1081
|
-
type: "thread_limit_reached",
|
|
1082
|
-
});
|
|
1083
|
-
}
|
|
1084
|
-
let ciFailureContext = "";
|
|
1085
|
-
let reviewHeadSha = previousHeadSha;
|
|
1086
|
-
if (editorOutput.mode === "EDITED") {
|
|
1087
|
-
ciReports.length = 0;
|
|
1088
|
-
const editedHeadSha = input.dryRun
|
|
1089
|
-
? editorOutput.commitSha
|
|
1090
|
-
: (await fetchPullRequest(exec, input.repository, input.pr))
|
|
1091
|
-
.headRefOid;
|
|
1092
|
-
if (!editedHeadSha)
|
|
1093
|
-
throw new Error("Editor output did not include commitSha");
|
|
1094
|
-
reviewHeadSha = editedHeadSha;
|
|
1095
|
-
if (input.dryRun) {
|
|
1096
|
-
await input.onProgress?.({
|
|
1097
|
-
message: "Dry run skipped post-edit CI because editor changes were not pushed.",
|
|
1098
|
-
type: "warning",
|
|
1099
|
-
});
|
|
1100
|
-
}
|
|
1101
|
-
else {
|
|
1102
|
-
await input.onProgress?.({
|
|
1103
|
-
phase: `waiting for checks after edit cycle ${cycle}`,
|
|
1104
|
-
type: "phase",
|
|
1105
|
-
});
|
|
1106
|
-
const checkResult = await waitForChecksWithClassification({
|
|
1107
|
-
afterEdit: {
|
|
1108
|
-
cycle,
|
|
1109
|
-
headSha: editedHeadSha,
|
|
1110
|
-
previousHeadSha,
|
|
1111
|
-
worktreePath: review.worktreePath,
|
|
1112
|
-
},
|
|
1113
|
-
client: input.client,
|
|
1114
|
-
directory: input.directory,
|
|
1115
|
-
exec,
|
|
1116
|
-
headSha: editedHeadSha,
|
|
1117
|
-
onProgress: (phase) => input.onProgress?.({ phase, type: "phase" }),
|
|
1118
|
-
parentSessionId: input.parentSessionId,
|
|
1119
|
-
pr: input.pr,
|
|
1120
|
-
repairAttempts: input.config.output?.repairAttempts ?? 3,
|
|
1121
|
-
repository: input.repository,
|
|
1122
|
-
signal: input.signal,
|
|
1123
|
-
wait: input.repository.checks.waitAfterEdit,
|
|
1124
|
-
});
|
|
1125
|
-
ciFailureContext = checkResult?.ciFailureContext ?? "";
|
|
1126
|
-
if (checkResult &&
|
|
1127
|
-
(checkResult.report.scopeOutsideRecovered.length ||
|
|
1128
|
-
checkResult.report.scopeOutsideUnresolved.length ||
|
|
1129
|
-
checkResult.report.scopeInside.length)) {
|
|
1130
|
-
ciReports.push(checkResult.report);
|
|
1131
|
-
await input.onProgress?.({
|
|
1132
|
-
report: checkResult.report,
|
|
1133
|
-
type: "ci_report",
|
|
1134
|
-
});
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
await input.onProgress?.({
|
|
1139
|
-
phase: `rereview cycle ${cycle}`,
|
|
1140
|
-
type: "phase",
|
|
1141
|
-
});
|
|
1142
|
-
reportCiReports = [...ciReports];
|
|
1143
|
-
const rereview = await runRereview(abortableInput, review.worktreePath, previousHeadSha, cycle, review.sessionIds, ciFailureContext, {
|
|
1144
|
-
dryRunHeadSha: input.dryRun ? reviewHeadSha : undefined,
|
|
1145
|
-
dryRunThreads,
|
|
1146
|
-
});
|
|
1147
|
-
reportOutputs = rereview.outputs;
|
|
1148
|
-
reportPosted = rereview.posted;
|
|
1149
|
-
dryRunThreads = input.dryRun
|
|
1150
|
-
? syntheticReviewThreads(reportOutputs)
|
|
1151
|
-
: undefined;
|
|
1152
|
-
previousHeadSha = input.dryRun
|
|
1153
|
-
? reviewHeadSha
|
|
1154
|
-
: (await fetchPullRequest(exec, input.repository, input.pr)).headRefOid;
|
|
1155
|
-
if (rereview.verdict === "MERGE") {
|
|
1156
|
-
const remainingThreads = input.dryRun
|
|
1157
|
-
? flattenSyntheticThreads(dryRunThreads ?? {})
|
|
1158
|
-
: await fetchUnresolvedThreads(exec, input.repository, input.pr);
|
|
1159
|
-
recordReviewThreads({
|
|
1160
|
-
attempts: threadAttempts,
|
|
1161
|
-
cycle,
|
|
1162
|
-
threads: remainingThreads,
|
|
1163
|
-
});
|
|
1164
|
-
await input.onProgress?.({
|
|
1165
|
-
attempts: copyThreadAttempts(threadAttempts),
|
|
1166
|
-
type: "thread_attempts",
|
|
1167
|
-
});
|
|
1168
|
-
if (exhaustedReviewThreads({
|
|
1169
|
-
attempts: threadAttempts,
|
|
1170
|
-
maxThreadResolutionCycles: input.repository.merge.maxThreadResolutionCycles,
|
|
1171
|
-
threads: remainingThreads,
|
|
1172
|
-
}).length) {
|
|
1173
|
-
await input.onProgress?.({
|
|
1174
|
-
status: "changes_unresolved",
|
|
1175
|
-
type: "merge_completed",
|
|
1176
|
-
});
|
|
1177
|
-
return complete({
|
|
1178
|
-
cycles: cycle,
|
|
1179
|
-
pr: input.pr,
|
|
1180
|
-
status: "changes_unresolved",
|
|
1181
|
-
});
|
|
1182
|
-
}
|
|
1183
|
-
if (hasBlockingCiReports(ciReports)) {
|
|
1184
|
-
await input.onProgress?.({
|
|
1185
|
-
status: "ci_unresolved",
|
|
1186
|
-
type: "merge_completed",
|
|
1187
|
-
});
|
|
1188
|
-
return complete({
|
|
1189
|
-
cycles: cycle,
|
|
1190
|
-
pr: input.pr,
|
|
1191
|
-
status: "ci_unresolved",
|
|
1192
|
-
});
|
|
1193
|
-
}
|
|
1194
|
-
if (!input.repository.automation.merge || input.dryRun) {
|
|
1195
|
-
await input.onProgress?.({
|
|
1196
|
-
status: "approved",
|
|
1197
|
-
type: "merge_completed",
|
|
1198
|
-
});
|
|
1199
|
-
return complete({ cycles: cycle, pr: input.pr, status: "approved" });
|
|
1200
|
-
}
|
|
1201
|
-
await input.onProgress?.({ phase: "merging PR", type: "phase" });
|
|
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
|
-
}
|
|
1233
|
-
await input.onProgress?.({ status, type: "merge_completed" });
|
|
1234
|
-
return complete({ cycles: cycle, pr: input.pr, status });
|
|
1235
|
-
}
|
|
1236
|
-
if (rereview.verdict === "CLOSE") {
|
|
1237
|
-
if (!input.repository.automation.close || input.dryRun) {
|
|
1238
|
-
await input.onProgress?.({
|
|
1239
|
-
status: "close_requested",
|
|
1240
|
-
type: "merge_completed",
|
|
1241
|
-
});
|
|
1242
|
-
return complete({
|
|
1243
|
-
cycles: cycle,
|
|
1244
|
-
pr: input.pr,
|
|
1245
|
-
status: "close_requested",
|
|
1246
|
-
});
|
|
1247
|
-
}
|
|
1248
|
-
await input.onProgress?.({ phase: "closing PR", type: "phase" });
|
|
1249
|
-
await closePullRequest(exec, input.repository, input.pr, editor.account);
|
|
1250
|
-
await input.onProgress?.({ status: "closed", type: "merge_completed" });
|
|
1251
|
-
return complete({ cycles: cycle, pr: input.pr, status: "closed" });
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
finally {
|
|
1256
|
-
if (review.worktreePath) {
|
|
1257
|
-
await removeWorktree(input.exec, review.worktreePath).catch(() => undefined);
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
}
|