opencode-magi 0.5.0 → 0.6.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 +3 -3
- package/dist/config/validate.js +31 -18
- package/dist/config/worktree.js +6 -0
- package/dist/github/commands.js +153 -45
- package/dist/index.js +29 -3
- package/dist/orchestrator/ci.js +21 -14
- package/dist/orchestrator/findings.js +29 -7
- package/dist/orchestrator/majority.js +1 -1
- package/dist/orchestrator/merge.js +31 -10
- package/dist/orchestrator/model.js +23 -9
- package/dist/orchestrator/review.js +162 -36
- package/dist/orchestrator/run-manager.js +171 -146
- package/dist/orchestrator/triage.js +243 -201
- package/dist/prompts/compose.js +2 -10
- package/dist/prompts/contracts.js +6 -20
- package/dist/prompts/output.js +6 -16
- package/package.json +1 -1
- package/schema.json +3 -3
- package/dist/prompts/templates/triage/action.md +0 -5
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { majorityThreshold } from "./majority";
|
|
2
|
+
function validationFindings(output) {
|
|
3
|
+
if ("findings" in output)
|
|
4
|
+
return output.findings;
|
|
5
|
+
return output.newFindings.map((finding) => ({
|
|
6
|
+
fix: "Please address this before merging.",
|
|
7
|
+
issue: finding.body,
|
|
8
|
+
line: finding.line,
|
|
9
|
+
path: finding.path,
|
|
10
|
+
startLine: finding.startLine,
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
2
13
|
export function reviewFindingTargets(outputs) {
|
|
3
14
|
return Object.entries(outputs).flatMap(([reviewer, output]) => {
|
|
4
15
|
if (output.verdict !== "CHANGES_REQUESTED")
|
|
5
16
|
return [];
|
|
6
|
-
return output.
|
|
17
|
+
return validationFindings(output).map((finding, findingIndex) => ({
|
|
7
18
|
finding,
|
|
8
19
|
findingIndex,
|
|
9
20
|
reviewer,
|
|
@@ -41,7 +52,9 @@ export function applyFindingValidation(input) {
|
|
|
41
52
|
next[reviewer] = output;
|
|
42
53
|
continue;
|
|
43
54
|
}
|
|
44
|
-
const
|
|
55
|
+
const keptIndexes = new Set();
|
|
56
|
+
const findings = validationFindings(output);
|
|
57
|
+
findings.forEach((finding, findingIndex) => {
|
|
45
58
|
let agrees = 1;
|
|
46
59
|
for (const validator of input.reviewerKeys) {
|
|
47
60
|
if (validator === reviewer)
|
|
@@ -53,14 +66,23 @@ export function applyFindingValidation(input) {
|
|
|
53
66
|
const target = { finding, findingIndex, reviewer };
|
|
54
67
|
if (agrees >= threshold) {
|
|
55
68
|
kept.push(target);
|
|
56
|
-
|
|
69
|
+
keptIndexes.add(findingIndex);
|
|
70
|
+
return;
|
|
57
71
|
}
|
|
58
72
|
discarded.push(target);
|
|
59
|
-
return false;
|
|
60
73
|
});
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
if ("findings" in output) {
|
|
75
|
+
const keptFindings = output.findings.filter((_finding, index) => keptIndexes.has(index));
|
|
76
|
+
next[reviewer] = keptFindings.length
|
|
77
|
+
? { ...output, findings: keptFindings }
|
|
78
|
+
: { findings: [], verdict: "MERGE" };
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const newFindings = output.newFindings.filter((_finding, index) => keptIndexes.has(index));
|
|
82
|
+
next[reviewer] =
|
|
83
|
+
newFindings.length || output.followUps.length
|
|
84
|
+
? { ...output, newFindings }
|
|
85
|
+
: { ...output, newFindings, verdict: "MERGE" };
|
|
64
86
|
}
|
|
65
87
|
return { outputs: next, summary: { discarded, kept } };
|
|
66
88
|
}
|
|
@@ -10,7 +10,7 @@ export function aggregateStringMajority(results, votes) {
|
|
|
10
10
|
const voters = Object.fromEntries(votes.map((vote) => [vote, []]));
|
|
11
11
|
for (const result of results) {
|
|
12
12
|
counts[result.vote] += 1;
|
|
13
|
-
voters[result.vote].push(result.
|
|
13
|
+
voters[result.vote].push(result.voter);
|
|
14
14
|
}
|
|
15
15
|
const threshold = majorityThreshold(results.length);
|
|
16
16
|
const vote = votes.find((item) => counts[item] >= threshold);
|
|
@@ -1,17 +1,17 @@
|
|
|
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,
|
|
4
|
+
import { closePullRequest, configureGitIdentity, fetchMergeQueueRequirement, fetchPullRequest, fetchUnresolvedThreads, mergePullRequest, postApproval, postChangesRequested, postCloseComment, postReply, pushHead, removeWorktree, resolveThread, waitForAutoMerge, waitForMergeQueue, } from "../github/commands";
|
|
5
5
|
import { composeEditPrompt, 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";
|
|
9
|
-
import {
|
|
9
|
+
import { validateInlineCommentTargets, } from "./inline-comments";
|
|
10
10
|
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 { runReview } from "./review";
|
|
14
|
+
import { inlineCommentTargetsForDiff, runReview, } from "./review";
|
|
15
15
|
import { checkSafetyGate, hasSafetyGate } from "./safety";
|
|
16
16
|
function outputDir(input) {
|
|
17
17
|
return prRunOutputDir({
|
|
@@ -53,7 +53,7 @@ async function withReviewerFailureProgress(input) {
|
|
|
53
53
|
async function runEditor(input, worktreePath, cycle, reviewFindings, unresolvedThreads) {
|
|
54
54
|
const editor = input.repository.agents.editor;
|
|
55
55
|
if (!editor)
|
|
56
|
-
throw new Error("
|
|
56
|
+
throw new Error("merge.editor is required for magi_merge");
|
|
57
57
|
throwIfAborted(input.signal);
|
|
58
58
|
await configureGitIdentity(input.exec, worktreePath, {
|
|
59
59
|
email: editor.author?.email,
|
|
@@ -96,6 +96,7 @@ async function runEditor(input, worktreePath, cycle, reviewFindings, unresolvedT
|
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
options: editor.options,
|
|
99
|
+
parentSessionId: input.parentSessionId,
|
|
99
100
|
parse: parseEditOutput,
|
|
100
101
|
permission: editor.permission,
|
|
101
102
|
prompt,
|
|
@@ -195,7 +196,21 @@ async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionI
|
|
|
195
196
|
throwIfAborted(input.signal);
|
|
196
197
|
const meta = await fetchPullRequest(input.exec, input.repository, input.pr);
|
|
197
198
|
const headSha = options.dryRunHeadSha ?? meta.headRefOid;
|
|
198
|
-
const inlineCommentTargets =
|
|
199
|
+
const inlineCommentTargets = await inlineCommentTargetsForDiff({
|
|
200
|
+
ensure: options.dryRunHeadSha
|
|
201
|
+
? undefined
|
|
202
|
+
: {
|
|
203
|
+
fromSource: "base",
|
|
204
|
+
meta,
|
|
205
|
+
repository: input.repository,
|
|
206
|
+
toSource: "head",
|
|
207
|
+
},
|
|
208
|
+
exec: input.exec,
|
|
209
|
+
fromSha: meta.baseRefOid,
|
|
210
|
+
range: "direct",
|
|
211
|
+
toSha: headSha,
|
|
212
|
+
worktreePath,
|
|
213
|
+
});
|
|
199
214
|
const artifactDir = outputDir(input);
|
|
200
215
|
let entries = await mapPool(input.repository.agents.reviewers, input.repository.concurrency.reviewers, async (reviewer) => {
|
|
201
216
|
throwIfAborted(input.signal);
|
|
@@ -250,6 +265,7 @@ async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionI
|
|
|
250
265
|
}
|
|
251
266
|
},
|
|
252
267
|
options: reviewer.options,
|
|
268
|
+
parentSessionId: input.parentSessionId,
|
|
253
269
|
parse: (text) => parseRereviewOutputWithInlineTargets(text, inlineCommentTargets),
|
|
254
270
|
permission: reviewer.permission,
|
|
255
271
|
prompt,
|
|
@@ -294,7 +310,7 @@ async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionI
|
|
|
294
310
|
baseSha: meta.baseRefOid,
|
|
295
311
|
closeReason: entry.output.reason,
|
|
296
312
|
directory: input.directory,
|
|
297
|
-
headSha
|
|
313
|
+
headSha,
|
|
298
314
|
includeReviewGuidelines: !hasReviewerSession,
|
|
299
315
|
includeSessionContext: !hasReviewerSession,
|
|
300
316
|
pr: input.pr,
|
|
@@ -333,6 +349,7 @@ async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionI
|
|
|
333
349
|
}
|
|
334
350
|
},
|
|
335
351
|
options: reviewer.options,
|
|
352
|
+
parentSessionId: input.parentSessionId,
|
|
336
353
|
parse: (text) => {
|
|
337
354
|
const output = parseRereviewCloseReconsiderationOutput(text);
|
|
338
355
|
validateInlineCommentTargets(output.newFindings, inlineCommentTargets, "newFindings");
|
|
@@ -409,8 +426,11 @@ async function finishMergeRun(input, result, reportInput) {
|
|
|
409
426
|
}
|
|
410
427
|
async function mergeWithQueue(input, exec, editorAccount) {
|
|
411
428
|
await mergePullRequest(exec, input.repository, input.pr, editorAccount);
|
|
412
|
-
if (!input.repository.merge.mergeQueue)
|
|
413
|
-
|
|
429
|
+
if (!input.repository.merge.mergeQueue) {
|
|
430
|
+
if (!input.repository.merge.auto)
|
|
431
|
+
return "merged";
|
|
432
|
+
return waitForAutoMerge(exec, input.repository, input.pr);
|
|
433
|
+
}
|
|
414
434
|
return waitForMergeQueue(exec, input.repository, input.pr);
|
|
415
435
|
}
|
|
416
436
|
export function hasBlockingCiReports(reports) {
|
|
@@ -570,7 +590,7 @@ export async function runMerge(input) {
|
|
|
570
590
|
const abortableInput = { ...input, exec };
|
|
571
591
|
const editor = input.repository.agents.editor;
|
|
572
592
|
if (!editor)
|
|
573
|
-
throw new Error("
|
|
593
|
+
throw new Error("merge.editor is required for magi_merge");
|
|
574
594
|
throwIfAborted(input.signal);
|
|
575
595
|
const artifactDir = outputDir(input);
|
|
576
596
|
await mkdir(artifactDir, { recursive: true });
|
|
@@ -701,7 +721,7 @@ export async function runMerge(input) {
|
|
|
701
721
|
threads: unresolvedThreads,
|
|
702
722
|
});
|
|
703
723
|
const editorFindings = blockingReviewFindings(reportOutputs);
|
|
704
|
-
const editableFindings =
|
|
724
|
+
const editableFindings = editorFindings;
|
|
705
725
|
const findingAttemptsExhausted = input.repository.merge.maxThreadResolutionCycles !== 0 &&
|
|
706
726
|
cycle > input.repository.merge.maxThreadResolutionCycles;
|
|
707
727
|
if (!editableThreads.length &&
|
|
@@ -781,6 +801,7 @@ export async function runMerge(input) {
|
|
|
781
801
|
exec,
|
|
782
802
|
headSha: editedHeadSha,
|
|
783
803
|
onProgress: (phase) => input.onProgress?.({ phase, type: "phase" }),
|
|
804
|
+
parentSessionId: input.parentSessionId,
|
|
784
805
|
pr: input.pr,
|
|
785
806
|
repairAttempts: input.config.output?.repairAttempts ?? 3,
|
|
786
807
|
repository: input.repository,
|
|
@@ -89,6 +89,7 @@ function extractText(result, allowEmpty = false) {
|
|
|
89
89
|
export async function createModelSession(input) {
|
|
90
90
|
return extractSessionId(await input.client.session.create({
|
|
91
91
|
body: {
|
|
92
|
+
parentID: input.parentSessionId,
|
|
92
93
|
permission: toOpenCodePermissionRules(input.permission),
|
|
93
94
|
title: input.title,
|
|
94
95
|
},
|
|
@@ -96,15 +97,26 @@ export async function createModelSession(input) {
|
|
|
96
97
|
}
|
|
97
98
|
export async function promptModelText(input) {
|
|
98
99
|
throwIfAborted(input.signal);
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
100
|
+
const abort = () => {
|
|
101
|
+
void input.client.session
|
|
102
|
+
.abort?.({ path: { id: input.sessionId } })
|
|
103
|
+
.catch(() => undefined);
|
|
104
|
+
};
|
|
105
|
+
input.signal?.addEventListener("abort", abort, { once: true });
|
|
106
|
+
try {
|
|
107
|
+
const result = await input.client.session.prompt({
|
|
108
|
+
body: {
|
|
109
|
+
model: modelBody(input.model),
|
|
110
|
+
parts: [{ type: "text", text: input.prompt }],
|
|
111
|
+
},
|
|
112
|
+
path: { id: input.sessionId },
|
|
113
|
+
});
|
|
114
|
+
throwIfAborted(input.signal);
|
|
115
|
+
return extractText(result, input.allowEmpty);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
input.signal?.removeEventListener("abort", abort);
|
|
119
|
+
}
|
|
108
120
|
}
|
|
109
121
|
async function sendPrompt(client, sessionId, model, prompt, signal) {
|
|
110
122
|
return promptModelText({ client, model, prompt, sessionId, signal });
|
|
@@ -113,6 +125,7 @@ export async function runModelText(input) {
|
|
|
113
125
|
throwIfAborted(input.signal);
|
|
114
126
|
const sessionId = await createModelSession({
|
|
115
127
|
client: input.client,
|
|
128
|
+
parentSessionId: input.parentSessionId,
|
|
116
129
|
permission: input.permission,
|
|
117
130
|
title: input.title,
|
|
118
131
|
});
|
|
@@ -152,6 +165,7 @@ export async function runModelWithRepair(input) {
|
|
|
152
165
|
? input.sessionId
|
|
153
166
|
: extractSessionId(await input.client.session.create({
|
|
154
167
|
body: {
|
|
168
|
+
parentID: input.parentSessionId,
|
|
155
169
|
permission: toOpenCodePermissionRules(input.permission),
|
|
156
170
|
title: input.title,
|
|
157
171
|
},
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { createWorktree, fetchPullRequest, fetchPullRequestCommits, fetchPullRequestReviews, fetchUnresolvedThreads, closePullRequest, mergePullRequest, postApproval, postChangesRequested, postCloseComment, postReply, removeWorktree, resolveThread, shellQuote, } from "../github/commands";
|
|
4
|
-
import { composeFindingValidationPrompt, composeCloseReconsiderationPrompt, composeRereviewPrompt, composeReviewPrompt, } from "../prompts/compose";
|
|
3
|
+
import { createWorktree, fetchPullRequest, fetchPullRequestCommits, fetchPullRequestReviews, fetchUnresolvedThreads, closePullRequest, mergePullRequest, ensurePullRequestCommits, postApproval, postChangesRequested, postCloseComment, postReply, removeWorktree, resolveThread, shellQuote, } from "../github/commands";
|
|
4
|
+
import { composeFindingValidationPrompt, composeCloseReconsiderationPrompt, composeRereviewCloseReconsiderationPrompt, composeRereviewPrompt, composeReviewPrompt, } from "../prompts/compose";
|
|
5
5
|
import { prRunOutputDir } from "../config/output";
|
|
6
|
-
import {
|
|
7
|
-
import { parseCloseReconsiderationOutput, parseFindingValidationOutput, parseRereviewOutput, parseReviewOutput, } from "../prompts/output";
|
|
6
|
+
import { prRunWorktreeDir } from "../config/worktree";
|
|
7
|
+
import { parseCloseReconsiderationOutput, parseFindingValidationOutput, parseRereviewCloseReconsiderationOutput, parseRereviewOutput, parseReviewOutput, } from "../prompts/output";
|
|
8
8
|
import { throwIfAborted, withAbortSignal } from "./abort";
|
|
9
9
|
import { waitForChecksWithClassification } from "./ci";
|
|
10
10
|
import { parseRightSideDiffTargets, validateInlineCommentTargets, } from "./inline-comments";
|
|
@@ -112,7 +112,7 @@ function reviewStateToVerdict(state) {
|
|
|
112
112
|
return "MERGE";
|
|
113
113
|
if (state === "CHANGES_REQUESTED")
|
|
114
114
|
return "CHANGES_REQUESTED";
|
|
115
|
-
|
|
115
|
+
throw new Error(`Unsupported GitHub review state: ${state}`);
|
|
116
116
|
}
|
|
117
117
|
function hasBlockingCiReports(reports) {
|
|
118
118
|
return reports.some((report) => report.scopeInside.length || report.scopeOutsideUnresolved.length);
|
|
@@ -135,6 +135,34 @@ function parseRereviewOutputWithInlineTargets(text, targets) {
|
|
|
135
135
|
validateInlineCommentTargets(output.newFindings, targets, "newFindings");
|
|
136
136
|
return output;
|
|
137
137
|
}
|
|
138
|
+
export async function inlineCommentTargetsForDiff(input) {
|
|
139
|
+
if (input.ensure) {
|
|
140
|
+
await ensurePullRequestCommits({
|
|
141
|
+
commits: [
|
|
142
|
+
{
|
|
143
|
+
label: "base",
|
|
144
|
+
sha: input.fromSha,
|
|
145
|
+
source: input.ensure.fromSource,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
label: "head",
|
|
149
|
+
sha: input.toSha,
|
|
150
|
+
source: input.ensure.toSource,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
exec: input.exec,
|
|
154
|
+
meta: input.ensure.meta,
|
|
155
|
+
repository: input.ensure.repository,
|
|
156
|
+
worktreePath: input.worktreePath,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const diffRange = input.range === "direct"
|
|
160
|
+
? `${shellQuote(input.fromSha)} ${shellQuote(input.toSha)}`
|
|
161
|
+
: `${shellQuote(input.fromSha)}...${shellQuote(input.toSha)}`;
|
|
162
|
+
return parseRightSideDiffTargets(await input.exec(`git diff --no-ext-diff --unified=3 ${diffRange}`, {
|
|
163
|
+
cwd: input.worktreePath,
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
138
166
|
function parsePostedFindingLocation(location) {
|
|
139
167
|
const range = /^(.*):(\d+)-(\d+)$/.exec(location);
|
|
140
168
|
if (range) {
|
|
@@ -182,10 +210,42 @@ function reviewFindingsFromBody(body) {
|
|
|
182
210
|
}
|
|
183
211
|
return { findings };
|
|
184
212
|
}
|
|
213
|
+
function parsePostedFindingComment(body) {
|
|
214
|
+
const match = /^\*\*Issue:\*\*\s*([\s\S]*?)\s*\r?\n\r?\n\*\*Fix:\*\*\s*([\s\S]+?)\s*$/.exec(body);
|
|
215
|
+
if (!match)
|
|
216
|
+
return undefined;
|
|
217
|
+
return {
|
|
218
|
+
fix: match[2]?.trim() || "Please address this before merging.",
|
|
219
|
+
issue: match[1]?.trim() || "Review finding.",
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function reviewFindingsFromComments(comments) {
|
|
223
|
+
return {
|
|
224
|
+
findings: (comments ?? []).flatMap((comment) => {
|
|
225
|
+
if (comment.line == null)
|
|
226
|
+
return [];
|
|
227
|
+
const parsed = parsePostedFindingComment(comment.body);
|
|
228
|
+
if (!parsed)
|
|
229
|
+
return [];
|
|
230
|
+
return [
|
|
231
|
+
{
|
|
232
|
+
...parsed,
|
|
233
|
+
line: comment.line,
|
|
234
|
+
path: comment.path,
|
|
235
|
+
startLine: comment.startLine ?? undefined,
|
|
236
|
+
},
|
|
237
|
+
];
|
|
238
|
+
}),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
185
241
|
export function reviewOutputFromState(review) {
|
|
186
242
|
const verdict = reviewStateToVerdict(review.state);
|
|
187
|
-
if (verdict === "CHANGES_REQUESTED")
|
|
243
|
+
if (verdict === "CHANGES_REQUESTED") {
|
|
244
|
+
const fromComments = reviewFindingsFromComments(review.comments);
|
|
245
|
+
if (fromComments.findings.length)
|
|
246
|
+
return { ...fromComments, verdict };
|
|
188
247
|
return { ...reviewFindingsFromBody(review.body), verdict };
|
|
248
|
+
}
|
|
189
249
|
return verdict === "CLOSE"
|
|
190
250
|
? {
|
|
191
251
|
findings: [],
|
|
@@ -230,8 +290,8 @@ function isReviewOutput(output) {
|
|
|
230
290
|
return "findings" in output;
|
|
231
291
|
}
|
|
232
292
|
async function runFindingValidation(input) {
|
|
233
|
-
const
|
|
234
|
-
const targets = reviewFindingTargets(
|
|
293
|
+
const outputs = Object.fromEntries(input.entries.map((entry) => [entry.key, entry.value]));
|
|
294
|
+
const targets = reviewFindingTargets(outputs);
|
|
235
295
|
if (!targets.length) {
|
|
236
296
|
return {
|
|
237
297
|
outputs: Object.fromEntries(input.entries.map((entry) => [entry.key, entry.value])),
|
|
@@ -290,6 +350,7 @@ async function runFindingValidation(input) {
|
|
|
290
350
|
}
|
|
291
351
|
},
|
|
292
352
|
options: reviewer.options,
|
|
353
|
+
parentSessionId: input.reviewInput.parentSessionId,
|
|
293
354
|
parse: (text) => {
|
|
294
355
|
const output = parseFindingValidationOutput(text);
|
|
295
356
|
validateFindingVotes({
|
|
@@ -315,7 +376,7 @@ async function runFindingValidation(input) {
|
|
|
315
376
|
return [reviewer.key, result.value];
|
|
316
377
|
}, { signal: input.reviewInput.signal }));
|
|
317
378
|
const filtered = applyFindingValidation({
|
|
318
|
-
outputs
|
|
379
|
+
outputs,
|
|
319
380
|
reviewerKeys: input.reviewInput.repository.agents.reviewers.map((reviewer) => reviewer.key),
|
|
320
381
|
validations,
|
|
321
382
|
});
|
|
@@ -323,7 +384,7 @@ async function runFindingValidation(input) {
|
|
|
323
384
|
await input.reviewInput.onProgress?.({
|
|
324
385
|
discarded: filtered.summary.discarded.length,
|
|
325
386
|
kept: filtered.summary.kept.length,
|
|
326
|
-
reviewersChangedToMerge: Object.entries(
|
|
387
|
+
reviewersChangedToMerge: Object.entries(outputs)
|
|
327
388
|
.filter(([reviewer, output]) => {
|
|
328
389
|
return (output.verdict === "CHANGES_REQUESTED" &&
|
|
329
390
|
filtered.outputs[reviewer]?.verdict === "MERGE");
|
|
@@ -352,27 +413,49 @@ async function runCloseReconsideration(input) {
|
|
|
352
413
|
type: "phase",
|
|
353
414
|
});
|
|
354
415
|
return Promise.all(input.entries.map(async (entry) => {
|
|
355
|
-
if (!targets.includes(entry.key)
|
|
416
|
+
if (!targets.includes(entry.key)) {
|
|
356
417
|
return entry;
|
|
357
418
|
}
|
|
358
419
|
const reviewer = input.reviewInput.repository.agents.reviewers.find((item) => item.key === entry.key);
|
|
359
420
|
if (!reviewer)
|
|
360
421
|
return entry;
|
|
361
422
|
const hasReviewerSession = Boolean(input.sessionIds[reviewer.key]);
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
423
|
+
const isReviewEntry = isReviewOutput(entry.value);
|
|
424
|
+
let prompt;
|
|
425
|
+
if (isReviewEntry) {
|
|
426
|
+
prompt = await composeCloseReconsiderationPrompt({
|
|
427
|
+
baseSha: input.meta.baseRefOid,
|
|
428
|
+
ciFailureContext: undefined,
|
|
429
|
+
closeReason: entry.value.reason,
|
|
430
|
+
directory: input.reviewInput.directory,
|
|
431
|
+
headSha: input.meta.headRefOid,
|
|
432
|
+
includeReviewGuidelines: !hasReviewerSession,
|
|
433
|
+
includeSessionContext: !hasReviewerSession,
|
|
434
|
+
pr: input.reviewInput.pr,
|
|
435
|
+
repository: input.reviewInput.repository,
|
|
436
|
+
reviewContext: input.reviewContext,
|
|
437
|
+
reviewer,
|
|
438
|
+
worktreePath: input.worktreePath,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
if (!entry.previousHeadSha) {
|
|
443
|
+
throw new Error(`Missing previous review commit for ${reviewer.account}`);
|
|
444
|
+
}
|
|
445
|
+
prompt = await composeRereviewCloseReconsiderationPrompt({
|
|
446
|
+
baseSha: input.meta.baseRefOid,
|
|
447
|
+
closeReason: entry.value.reason,
|
|
448
|
+
directory: input.reviewInput.directory,
|
|
449
|
+
headSha: input.meta.headRefOid,
|
|
450
|
+
includeReviewGuidelines: !hasReviewerSession,
|
|
451
|
+
includeSessionContext: !hasReviewerSession,
|
|
452
|
+
pr: input.reviewInput.pr,
|
|
453
|
+
previousHeadSha: entry.previousHeadSha,
|
|
454
|
+
repository: input.reviewInput.repository,
|
|
455
|
+
reviewer,
|
|
456
|
+
worktreePath: input.worktreePath,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
376
459
|
const result = await withReviewerFailureProgress({
|
|
377
460
|
onProgress: input.reviewInput.onProgress,
|
|
378
461
|
reviewer: reviewer.key,
|
|
@@ -403,15 +486,21 @@ async function runCloseReconsideration(input) {
|
|
|
403
486
|
}
|
|
404
487
|
},
|
|
405
488
|
options: reviewer.options,
|
|
489
|
+
parentSessionId: input.reviewInput.parentSessionId,
|
|
406
490
|
parse: (text) => {
|
|
407
|
-
const output =
|
|
408
|
-
|
|
491
|
+
const output = isReviewEntry
|
|
492
|
+
? parseCloseReconsiderationOutput(text)
|
|
493
|
+
: parseRereviewCloseReconsiderationOutput(text);
|
|
494
|
+
const findings = "newFindings" in output ? output.newFindings : output.findings;
|
|
495
|
+
validateInlineCommentTargets(findings, entry.inlineCommentTargets, "newFindings" in output ? "newFindings" : "findings");
|
|
409
496
|
return output;
|
|
410
497
|
},
|
|
411
498
|
permission: reviewer.permission,
|
|
412
499
|
prompt,
|
|
413
500
|
repairAttempts: input.reviewInput.config.output?.repairAttempts ?? 3,
|
|
414
|
-
schemaName:
|
|
501
|
+
schemaName: isReviewEntry
|
|
502
|
+
? "close reconsideration"
|
|
503
|
+
: "rereview close reconsideration",
|
|
415
504
|
sessionId: input.sessionIds[reviewer.key],
|
|
416
505
|
signal: input.reviewInput.signal,
|
|
417
506
|
title: `magi reconsider close ${input.reviewInput.repository.alias}#${input.reviewInput.pr} ${reviewer.key}`,
|
|
@@ -434,7 +523,9 @@ async function runCloseReconsideration(input) {
|
|
|
434
523
|
});
|
|
435
524
|
input.sessionIds[reviewer.key] = result.sessionId;
|
|
436
525
|
return {
|
|
526
|
+
inlineCommentTargets: entry.inlineCommentTargets,
|
|
437
527
|
key: entry.key,
|
|
528
|
+
previousHeadSha: entry.previousHeadSha,
|
|
438
529
|
raw: result.raw,
|
|
439
530
|
sessionId: result.sessionId,
|
|
440
531
|
value: result.value,
|
|
@@ -514,11 +605,13 @@ export async function runReview(input) {
|
|
|
514
605
|
: preliminaryMode;
|
|
515
606
|
if (mode.type === "already_reviewed" && !input.allowAlreadyReviewed)
|
|
516
607
|
throw new Error("PR has already been reviewed by all configured accounts");
|
|
517
|
-
const
|
|
608
|
+
const runId = input.runId ?? `run-${Date.now().toString(36)}`;
|
|
609
|
+
const outputDir = prRunOutputDir({
|
|
518
610
|
config: input.config,
|
|
519
611
|
directory: input.directory,
|
|
520
612
|
pr: input.pr,
|
|
521
|
-
|
|
613
|
+
runId,
|
|
614
|
+
});
|
|
522
615
|
await mkdir(outputDir, { recursive: true });
|
|
523
616
|
await input.onProgress?.({ phase: "fetching review context", type: "phase" });
|
|
524
617
|
const reviewContextSnapshot = await buildReviewContextSnapshot({
|
|
@@ -567,6 +660,7 @@ export async function runReview(input) {
|
|
|
567
660
|
},
|
|
568
661
|
onProgress: (phase) => input.onProgress?.({ phase, type: "phase" }),
|
|
569
662
|
outputDir,
|
|
663
|
+
parentSessionId: input.parentSessionId,
|
|
570
664
|
pr: input.pr,
|
|
571
665
|
repairAttempts: input.config.output?.repairAttempts ?? 3,
|
|
572
666
|
repository: input.repository,
|
|
@@ -582,10 +676,14 @@ export async function runReview(input) {
|
|
|
582
676
|
checkResult.report.scopeInside.length)) {
|
|
583
677
|
await input.onProgress?.({ report: checkResult.report, type: "ci_report" });
|
|
584
678
|
}
|
|
585
|
-
const
|
|
679
|
+
const worktreePath = prRunWorktreeDir({
|
|
680
|
+
config: input.config,
|
|
681
|
+
directory: input.directory,
|
|
682
|
+
pr: input.pr,
|
|
683
|
+
runId,
|
|
684
|
+
});
|
|
586
685
|
await input.onProgress?.({ phase: "creating worktree", type: "phase" });
|
|
587
|
-
const worktree = await createWorktree(exec, input.repository, input.pr,
|
|
588
|
-
const worktreePath = worktree.path;
|
|
686
|
+
const worktree = await createWorktree(exec, input.repository, input.pr, worktreePath);
|
|
589
687
|
await input.onProgress?.({
|
|
590
688
|
branch: worktree.branch,
|
|
591
689
|
type: "worktree_created",
|
|
@@ -599,7 +697,18 @@ export async function runReview(input) {
|
|
|
599
697
|
return [];
|
|
600
698
|
return [{ assignment, reviewer }];
|
|
601
699
|
});
|
|
602
|
-
const
|
|
700
|
+
const initialInlineCommentTargets = await inlineCommentTargetsForDiff({
|
|
701
|
+
ensure: {
|
|
702
|
+
fromSource: "base",
|
|
703
|
+
meta,
|
|
704
|
+
repository: input.repository,
|
|
705
|
+
toSource: "head",
|
|
706
|
+
},
|
|
707
|
+
exec,
|
|
708
|
+
fromSha: meta.baseRefOid,
|
|
709
|
+
toSha: meta.headRefOid,
|
|
710
|
+
worktreePath,
|
|
711
|
+
});
|
|
603
712
|
for (const reviewer of input.repository.agents.reviewers) {
|
|
604
713
|
const assignment = mode.assignments.get(reviewer.account);
|
|
605
714
|
if (assignment?.type !== "skip")
|
|
@@ -619,6 +728,18 @@ export async function runReview(input) {
|
|
|
619
728
|
const previous = assignment.review;
|
|
620
729
|
if (!previous.commit?.oid)
|
|
621
730
|
throw new Error(`Missing previous review commit for ${reviewer.account}`);
|
|
731
|
+
const inlineCommentTargets = await inlineCommentTargetsForDiff({
|
|
732
|
+
ensure: {
|
|
733
|
+
fromSource: "head",
|
|
734
|
+
meta,
|
|
735
|
+
repository: input.repository,
|
|
736
|
+
toSource: "head",
|
|
737
|
+
},
|
|
738
|
+
exec,
|
|
739
|
+
fromSha: previous.commit.oid,
|
|
740
|
+
toSha: meta.headRefOid,
|
|
741
|
+
worktreePath,
|
|
742
|
+
});
|
|
622
743
|
const unresolved = unresolvedThreadsByAccount.get(reviewer.account) ??
|
|
623
744
|
(await fetchUnresolvedThreads(exec, input.repository, input.pr, reviewer.account));
|
|
624
745
|
const prompt = await composeRereviewPrompt({
|
|
@@ -665,6 +786,7 @@ export async function runReview(input) {
|
|
|
665
786
|
}
|
|
666
787
|
},
|
|
667
788
|
options: reviewer.options,
|
|
789
|
+
parentSessionId: input.parentSessionId,
|
|
668
790
|
parse: (text) => parseRereviewOutputWithInlineTargets(text, inlineCommentTargets),
|
|
669
791
|
permission: reviewer.permission,
|
|
670
792
|
prompt,
|
|
@@ -684,7 +806,9 @@ export async function runReview(input) {
|
|
|
684
806
|
verdict: result.value.verdict,
|
|
685
807
|
});
|
|
686
808
|
return {
|
|
809
|
+
inlineCommentTargets,
|
|
687
810
|
key: reviewer.key,
|
|
811
|
+
previousHeadSha: previous.commit.oid,
|
|
688
812
|
raw: result.raw,
|
|
689
813
|
sessionId: result.sessionId,
|
|
690
814
|
value: result.value,
|
|
@@ -731,7 +855,8 @@ export async function runReview(input) {
|
|
|
731
855
|
}
|
|
732
856
|
},
|
|
733
857
|
options: reviewer.options,
|
|
734
|
-
|
|
858
|
+
parentSessionId: input.parentSessionId,
|
|
859
|
+
parse: (text) => parseReviewOutputWithInlineTargets(text, initialInlineCommentTargets),
|
|
735
860
|
permission: reviewer.permission,
|
|
736
861
|
prompt,
|
|
737
862
|
repairAttempts: input.config.output?.repairAttempts ?? 3,
|
|
@@ -750,6 +875,7 @@ export async function runReview(input) {
|
|
|
750
875
|
verdict: result.value.verdict,
|
|
751
876
|
});
|
|
752
877
|
return {
|
|
878
|
+
inlineCommentTargets: initialInlineCommentTargets,
|
|
753
879
|
key: reviewer.key,
|
|
754
880
|
raw: result.raw,
|
|
755
881
|
sessionId: result.sessionId,
|
|
@@ -783,6 +909,7 @@ export async function runReview(input) {
|
|
|
783
909
|
return [
|
|
784
910
|
{
|
|
785
911
|
key: reviewer.key,
|
|
912
|
+
inlineCommentTargets: initialInlineCommentTargets,
|
|
786
913
|
raw: assignment.review.body ?? "",
|
|
787
914
|
sessionId: "",
|
|
788
915
|
value: reviewOutputFromState(assignment.review),
|
|
@@ -791,7 +918,6 @@ export async function runReview(input) {
|
|
|
791
918
|
});
|
|
792
919
|
entries = await runCloseReconsideration({
|
|
793
920
|
entries: [...entries, ...skippedCloseEntries],
|
|
794
|
-
inlineCommentTargets,
|
|
795
921
|
meta,
|
|
796
922
|
outputDir,
|
|
797
923
|
reviewContext,
|