opencode-magi 0.4.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/README.md +53 -45
- 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 +167 -92
- package/dist/index.js +69 -4
- package/dist/orchestrator/ci.js +21 -14
- package/dist/orchestrator/findings.js +28 -7
- package/dist/orchestrator/inline-comments.js +0 -6
- package/dist/orchestrator/majority.js +1 -1
- package/dist/orchestrator/merge.js +46 -47
- package/dist/orchestrator/model.js +23 -9
- package/dist/orchestrator/report.js +7 -18
- package/dist/orchestrator/review-context.js +37 -4
- package/dist/orchestrator/review.js +174 -61
- package/dist/orchestrator/run-manager.js +209 -138
- package/dist/orchestrator/triage.js +243 -201
- package/dist/prompts/compose.js +2 -10
- package/dist/prompts/contracts.js +36 -57
- package/dist/prompts/output.js +28 -56
- package/dist/prompts/templates/merge/edit.md +1 -2
- package/dist/prompts/templates/review/close-reconsideration.md +1 -0
- package/dist/prompts/templates/review/rereview.md +3 -0
- package/dist/prompts/templates/review/review.md +4 -0
- package/package.json +1 -1
- package/schema.json +3 -3
- package/dist/prompts/templates/triage/action.md +0 -5
|
@@ -16,24 +16,17 @@ function pullRequestLine(input) {
|
|
|
16
16
|
return `- **Pull Request**: [#${input.pr}](${url})`;
|
|
17
17
|
}
|
|
18
18
|
function formatFinding(finding) {
|
|
19
|
-
const line = finding.
|
|
20
|
-
? finding.path
|
|
21
|
-
: finding.startLine
|
|
22
|
-
? `${finding.path}:${finding.line}`
|
|
23
|
-
: `${finding.path}:${finding.startLine}-${finding.line}`;
|
|
19
|
+
const line = finding.startLine == null
|
|
20
|
+
? `${finding.path}:${finding.line}`
|
|
21
|
+
: `${finding.path}:${finding.startLine}-${finding.line}`;
|
|
24
22
|
return `\`${line}\`: ${finding.issue}`;
|
|
25
23
|
}
|
|
26
24
|
function formatRereviewFinding(finding) {
|
|
27
|
-
const line = finding.
|
|
28
|
-
? finding.path
|
|
29
|
-
: finding.startLine
|
|
30
|
-
? `${finding.path}:${finding.line}`
|
|
31
|
-
: `${finding.path}:${finding.startLine}-${finding.line}`;
|
|
25
|
+
const line = finding.startLine == null
|
|
26
|
+
? `${finding.path}:${finding.line}`
|
|
27
|
+
: `${finding.path}:${finding.startLine}-${finding.line}`;
|
|
32
28
|
return `\`${line}\`: ${finding.body}`;
|
|
33
29
|
}
|
|
34
|
-
function formatRequirementFinding(finding) {
|
|
35
|
-
return `Issue #${finding.issueNumber}: ${finding.requirement}`;
|
|
36
|
-
}
|
|
37
30
|
function isReviewOutput(output) {
|
|
38
31
|
return "findings" in output;
|
|
39
32
|
}
|
|
@@ -85,10 +78,7 @@ function reviewerDetailLines(output) {
|
|
|
85
78
|
return output.reason ? [output.reason] : [];
|
|
86
79
|
if (output.verdict !== "CHANGES_REQUESTED")
|
|
87
80
|
return [];
|
|
88
|
-
return
|
|
89
|
-
...output.findings.map(formatFinding),
|
|
90
|
-
...output.requirementFindings.map(formatRequirementFinding),
|
|
91
|
-
];
|
|
81
|
+
return output.findings.map(formatFinding);
|
|
92
82
|
}
|
|
93
83
|
if (output.verdict === "CLOSE")
|
|
94
84
|
return output.reason ? [output.reason] : [];
|
|
@@ -96,7 +86,6 @@ function reviewerDetailLines(output) {
|
|
|
96
86
|
return [];
|
|
97
87
|
return [
|
|
98
88
|
...output.newFindings.map(formatRereviewFinding),
|
|
99
|
-
...output.requirementFindings.map(formatRequirementFinding),
|
|
100
89
|
...output.followUps.map((item) => `Comment #${item.commentId}: ${item.body}`),
|
|
101
90
|
];
|
|
102
91
|
}
|
|
@@ -37,6 +37,23 @@ function quoteEvidence(value) {
|
|
|
37
37
|
const compact = value.replaceAll(/\s+/g, " ").trim();
|
|
38
38
|
return compact.length > 120 ? `${compact.slice(0, 117)}...` : compact;
|
|
39
39
|
}
|
|
40
|
+
function errorText(error) {
|
|
41
|
+
if (!error || typeof error !== "object")
|
|
42
|
+
return String(error);
|
|
43
|
+
const value = error;
|
|
44
|
+
return [value.message, value.stderr, value.stdout]
|
|
45
|
+
.filter((item) => typeof item === "string")
|
|
46
|
+
.join("\n");
|
|
47
|
+
}
|
|
48
|
+
function isIssueLookupFailure(error) {
|
|
49
|
+
const text = errorText(error);
|
|
50
|
+
return (/could not resolve to an issue/i.test(text) ||
|
|
51
|
+
/could not fetch issue #\d+/i.test(text) ||
|
|
52
|
+
/not an issue/i.test(text));
|
|
53
|
+
}
|
|
54
|
+
function isIssueUrl(url) {
|
|
55
|
+
return /\/issues\/\d+(?:$|[/?#])/i.test(url);
|
|
56
|
+
}
|
|
40
57
|
function issueReferencePattern(repository) {
|
|
41
58
|
const host = escapeRegExp(repository.github.host || "github.com");
|
|
42
59
|
const owner = escapeRegExp(repository.github.owner);
|
|
@@ -120,6 +137,9 @@ export function collectIssueRelationships(input) {
|
|
|
120
137
|
async function contextIssue(input) {
|
|
121
138
|
const issue = input.issue ??
|
|
122
139
|
(await fetchIssue(input.exec, input.repository, input.relationship.number));
|
|
140
|
+
if (!isIssueUrl(issue.url)) {
|
|
141
|
+
throw new Error(`Reference #${issue.number} resolved to ${issue.url}, not an Issue`);
|
|
142
|
+
}
|
|
123
143
|
const commentPage = await fetchIssueCommentPage(input.exec, input.repository, issue.number, input.limit);
|
|
124
144
|
return {
|
|
125
145
|
author: issue.author,
|
|
@@ -138,6 +158,19 @@ async function contextIssue(input) {
|
|
|
138
158
|
url: issue.url,
|
|
139
159
|
};
|
|
140
160
|
}
|
|
161
|
+
async function contextIssueIfIssue(input) {
|
|
162
|
+
try {
|
|
163
|
+
return await contextIssue(input);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (isIssueLookupFailure(error))
|
|
167
|
+
return undefined;
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function presentIssue(issue) {
|
|
172
|
+
return Boolean(issue);
|
|
173
|
+
}
|
|
141
174
|
function orderReviewThreads(threads) {
|
|
142
175
|
return [...threads]
|
|
143
176
|
.sort((a, b) => {
|
|
@@ -183,13 +216,13 @@ export async function buildReviewContextSnapshot(input) {
|
|
|
183
216
|
const closingRelationships = relationships.filter((relationship) => relationship.relationship === "closing");
|
|
184
217
|
const referencedRelationships = relationships.filter((relationship) => relationship.relationship === "referenced");
|
|
185
218
|
return {
|
|
186
|
-
closingIssues: await Promise.all(closingRelationships.map((relationship) =>
|
|
219
|
+
closingIssues: (await Promise.all(closingRelationships.map((relationship) => contextIssueIfIssue({
|
|
187
220
|
exec: input.exec,
|
|
188
221
|
issue: closingIssueMap.get(relationship.number),
|
|
189
222
|
limit: LIMITS.closingIssueComments,
|
|
190
223
|
relationship,
|
|
191
224
|
repository: input.repository,
|
|
192
|
-
}))),
|
|
225
|
+
})))).filter(presentIssue),
|
|
193
226
|
pullRequest: {
|
|
194
227
|
author: input.pr.author?.login ?? safetyMeta.author,
|
|
195
228
|
baseRef: input.pr.baseRefName,
|
|
@@ -207,12 +240,12 @@ export async function buildReviewContextSnapshot(input) {
|
|
|
207
240
|
title: input.pr.title,
|
|
208
241
|
url: input.pr.url,
|
|
209
242
|
},
|
|
210
|
-
referencedIssues: await Promise.all(referencedRelationships.map((relationship) =>
|
|
243
|
+
referencedIssues: (await Promise.all(referencedRelationships.map((relationship) => contextIssueIfIssue({
|
|
211
244
|
exec: input.exec,
|
|
212
245
|
limit: LIMITS.referencedIssueComments,
|
|
213
246
|
relationship,
|
|
214
247
|
repository: input.repository,
|
|
215
|
-
}))),
|
|
248
|
+
})))).filter(presentIssue),
|
|
216
249
|
reviewDiscussion: {
|
|
217
250
|
prComments: boundedComments(prComments, LIMITS.prComments),
|
|
218
251
|
prCommentsOmitted,
|
|
@@ -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";
|
|
@@ -39,7 +39,7 @@ async function postReviewOutput(input, reviewerKey, output) {
|
|
|
39
39
|
return postApproval(input.exec, input.repository, input.pr, reviewer.account);
|
|
40
40
|
if (output.verdict === "CLOSE")
|
|
41
41
|
return postCloseComment(input.exec, input.repository, input.pr, reviewer.account, output.reason ?? "Close requested.");
|
|
42
|
-
return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.findings
|
|
42
|
+
return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.findings);
|
|
43
43
|
}
|
|
44
44
|
function dryRunReviewPost(key, output) {
|
|
45
45
|
if (output.verdict === "MERGE")
|
|
@@ -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) {
|
|
@@ -147,11 +175,10 @@ function parsePostedFindingLocation(location) {
|
|
|
147
175
|
const line = /^(.*):(\d+)$/.exec(location);
|
|
148
176
|
if (line)
|
|
149
177
|
return { line: Number(line[2]), path: line[1] ?? location };
|
|
150
|
-
return
|
|
178
|
+
return undefined;
|
|
151
179
|
}
|
|
152
180
|
function reviewFindingsFromBody(body) {
|
|
153
181
|
const findings = [];
|
|
154
|
-
const requirementFindings = [];
|
|
155
182
|
const lines = (body ?? "").split(/\r?\n/);
|
|
156
183
|
let section;
|
|
157
184
|
for (let index = 0; index < lines.length; index += 1) {
|
|
@@ -161,7 +188,7 @@ function reviewFindingsFromBody(body) {
|
|
|
161
188
|
continue;
|
|
162
189
|
}
|
|
163
190
|
if (line === "Requirement findings:") {
|
|
164
|
-
section =
|
|
191
|
+
section = undefined;
|
|
165
192
|
continue;
|
|
166
193
|
}
|
|
167
194
|
if (section === "finding") {
|
|
@@ -169,43 +196,63 @@ function reviewFindingsFromBody(body) {
|
|
|
169
196
|
const fix = /^\s+Fix: (.+)$/.exec(lines[index + 1] ?? "");
|
|
170
197
|
if (!match || !fix)
|
|
171
198
|
continue;
|
|
199
|
+
const location = parsePostedFindingLocation(match[1] ?? "");
|
|
200
|
+
if (!location)
|
|
201
|
+
continue;
|
|
172
202
|
findings.push({
|
|
173
|
-
...
|
|
203
|
+
...location,
|
|
174
204
|
fix: fix[1] ?? "Please address this before merging.",
|
|
175
205
|
issue: match[2] ?? "Review finding.",
|
|
176
206
|
});
|
|
177
207
|
index += 1;
|
|
178
208
|
continue;
|
|
179
209
|
}
|
|
180
|
-
if (section !== "requirement")
|
|
181
|
-
continue;
|
|
182
|
-
const match = /^- Missing issue #(\d+) requirement: (.+)$/.exec(line ?? "");
|
|
183
|
-
const evidence = /^\s+Evidence: (.+)$/.exec(lines[index + 1] ?? "");
|
|
184
|
-
const fix = /^\s+Fix: (.+)$/.exec(lines[index + 2] ?? "");
|
|
185
|
-
if (!match || !evidence || !fix)
|
|
186
|
-
continue;
|
|
187
|
-
requirementFindings.push({
|
|
188
|
-
evidence: evidence[1] ?? "See review body.",
|
|
189
|
-
fix: fix[1] ?? "Please address this before merging.",
|
|
190
|
-
issueNumber: Number(match[1]),
|
|
191
|
-
requirement: match[2] ?? "Review requirement.",
|
|
192
|
-
});
|
|
193
|
-
index += 2;
|
|
194
210
|
}
|
|
195
|
-
return { findings
|
|
211
|
+
return { findings };
|
|
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
|
+
};
|
|
196
240
|
}
|
|
197
241
|
export function reviewOutputFromState(review) {
|
|
198
242
|
const verdict = reviewStateToVerdict(review.state);
|
|
199
|
-
if (verdict === "CHANGES_REQUESTED")
|
|
243
|
+
if (verdict === "CHANGES_REQUESTED") {
|
|
244
|
+
const fromComments = reviewFindingsFromComments(review.comments);
|
|
245
|
+
if (fromComments.findings.length)
|
|
246
|
+
return { ...fromComments, verdict };
|
|
200
247
|
return { ...reviewFindingsFromBody(review.body), verdict };
|
|
248
|
+
}
|
|
201
249
|
return verdict === "CLOSE"
|
|
202
250
|
? {
|
|
203
251
|
findings: [],
|
|
204
252
|
reason: review.body || "Close requested.",
|
|
205
|
-
requirementFindings: [],
|
|
206
253
|
verdict,
|
|
207
254
|
}
|
|
208
|
-
: { findings: [],
|
|
255
|
+
: { findings: [], verdict };
|
|
209
256
|
}
|
|
210
257
|
export function hasPendingThreadReply(threads, reviewerAccount) {
|
|
211
258
|
return threads.some((thread) => {
|
|
@@ -229,22 +276,22 @@ async function postRereviewOutput(input, reviewerKey, output) {
|
|
|
229
276
|
return postApproval(input.exec, input.repository, input.pr, reviewer.account);
|
|
230
277
|
if (output.verdict === "CLOSE")
|
|
231
278
|
return postCloseComment(input.exec, input.repository, input.pr, reviewer.account, output.reason ?? "Close requested.");
|
|
232
|
-
if (!output.newFindings.length
|
|
279
|
+
if (!output.newFindings.length)
|
|
233
280
|
return "";
|
|
234
281
|
return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.newFindings.map((finding) => ({
|
|
235
282
|
fix: "Please address this before merging.",
|
|
236
283
|
issue: finding.body,
|
|
237
284
|
path: finding.path,
|
|
238
|
-
|
|
285
|
+
line: finding.line,
|
|
239
286
|
startLine: finding.startLine,
|
|
240
|
-
}))
|
|
287
|
+
})));
|
|
241
288
|
}
|
|
242
289
|
function isReviewOutput(output) {
|
|
243
290
|
return "findings" in output;
|
|
244
291
|
}
|
|
245
292
|
async function runFindingValidation(input) {
|
|
246
|
-
const
|
|
247
|
-
const targets = reviewFindingTargets(
|
|
293
|
+
const outputs = Object.fromEntries(input.entries.map((entry) => [entry.key, entry.value]));
|
|
294
|
+
const targets = reviewFindingTargets(outputs);
|
|
248
295
|
if (!targets.length) {
|
|
249
296
|
return {
|
|
250
297
|
outputs: Object.fromEntries(input.entries.map((entry) => [entry.key, entry.value])),
|
|
@@ -303,6 +350,7 @@ async function runFindingValidation(input) {
|
|
|
303
350
|
}
|
|
304
351
|
},
|
|
305
352
|
options: reviewer.options,
|
|
353
|
+
parentSessionId: input.reviewInput.parentSessionId,
|
|
306
354
|
parse: (text) => {
|
|
307
355
|
const output = parseFindingValidationOutput(text);
|
|
308
356
|
validateFindingVotes({
|
|
@@ -328,7 +376,7 @@ async function runFindingValidation(input) {
|
|
|
328
376
|
return [reviewer.key, result.value];
|
|
329
377
|
}, { signal: input.reviewInput.signal }));
|
|
330
378
|
const filtered = applyFindingValidation({
|
|
331
|
-
outputs
|
|
379
|
+
outputs,
|
|
332
380
|
reviewerKeys: input.reviewInput.repository.agents.reviewers.map((reviewer) => reviewer.key),
|
|
333
381
|
validations,
|
|
334
382
|
});
|
|
@@ -336,7 +384,7 @@ async function runFindingValidation(input) {
|
|
|
336
384
|
await input.reviewInput.onProgress?.({
|
|
337
385
|
discarded: filtered.summary.discarded.length,
|
|
338
386
|
kept: filtered.summary.kept.length,
|
|
339
|
-
reviewersChangedToMerge: Object.entries(
|
|
387
|
+
reviewersChangedToMerge: Object.entries(outputs)
|
|
340
388
|
.filter(([reviewer, output]) => {
|
|
341
389
|
return (output.verdict === "CHANGES_REQUESTED" &&
|
|
342
390
|
filtered.outputs[reviewer]?.verdict === "MERGE");
|
|
@@ -365,27 +413,49 @@ async function runCloseReconsideration(input) {
|
|
|
365
413
|
type: "phase",
|
|
366
414
|
});
|
|
367
415
|
return Promise.all(input.entries.map(async (entry) => {
|
|
368
|
-
if (!targets.includes(entry.key)
|
|
416
|
+
if (!targets.includes(entry.key)) {
|
|
369
417
|
return entry;
|
|
370
418
|
}
|
|
371
419
|
const reviewer = input.reviewInput.repository.agents.reviewers.find((item) => item.key === entry.key);
|
|
372
420
|
if (!reviewer)
|
|
373
421
|
return entry;
|
|
374
422
|
const hasReviewerSession = Boolean(input.sessionIds[reviewer.key]);
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
+
}
|
|
389
459
|
const result = await withReviewerFailureProgress({
|
|
390
460
|
onProgress: input.reviewInput.onProgress,
|
|
391
461
|
reviewer: reviewer.key,
|
|
@@ -416,15 +486,21 @@ async function runCloseReconsideration(input) {
|
|
|
416
486
|
}
|
|
417
487
|
},
|
|
418
488
|
options: reviewer.options,
|
|
489
|
+
parentSessionId: input.reviewInput.parentSessionId,
|
|
419
490
|
parse: (text) => {
|
|
420
|
-
const output =
|
|
421
|
-
|
|
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");
|
|
422
496
|
return output;
|
|
423
497
|
},
|
|
424
498
|
permission: reviewer.permission,
|
|
425
499
|
prompt,
|
|
426
500
|
repairAttempts: input.reviewInput.config.output?.repairAttempts ?? 3,
|
|
427
|
-
schemaName:
|
|
501
|
+
schemaName: isReviewEntry
|
|
502
|
+
? "close reconsideration"
|
|
503
|
+
: "rereview close reconsideration",
|
|
428
504
|
sessionId: input.sessionIds[reviewer.key],
|
|
429
505
|
signal: input.reviewInput.signal,
|
|
430
506
|
title: `magi reconsider close ${input.reviewInput.repository.alias}#${input.reviewInput.pr} ${reviewer.key}`,
|
|
@@ -447,7 +523,9 @@ async function runCloseReconsideration(input) {
|
|
|
447
523
|
});
|
|
448
524
|
input.sessionIds[reviewer.key] = result.sessionId;
|
|
449
525
|
return {
|
|
526
|
+
inlineCommentTargets: entry.inlineCommentTargets,
|
|
450
527
|
key: entry.key,
|
|
528
|
+
previousHeadSha: entry.previousHeadSha,
|
|
451
529
|
raw: result.raw,
|
|
452
530
|
sessionId: result.sessionId,
|
|
453
531
|
value: result.value,
|
|
@@ -527,11 +605,13 @@ export async function runReview(input) {
|
|
|
527
605
|
: preliminaryMode;
|
|
528
606
|
if (mode.type === "already_reviewed" && !input.allowAlreadyReviewed)
|
|
529
607
|
throw new Error("PR has already been reviewed by all configured accounts");
|
|
530
|
-
const
|
|
608
|
+
const runId = input.runId ?? `run-${Date.now().toString(36)}`;
|
|
609
|
+
const outputDir = prRunOutputDir({
|
|
531
610
|
config: input.config,
|
|
532
611
|
directory: input.directory,
|
|
533
612
|
pr: input.pr,
|
|
534
|
-
|
|
613
|
+
runId,
|
|
614
|
+
});
|
|
535
615
|
await mkdir(outputDir, { recursive: true });
|
|
536
616
|
await input.onProgress?.({ phase: "fetching review context", type: "phase" });
|
|
537
617
|
const reviewContextSnapshot = await buildReviewContextSnapshot({
|
|
@@ -580,6 +660,7 @@ export async function runReview(input) {
|
|
|
580
660
|
},
|
|
581
661
|
onProgress: (phase) => input.onProgress?.({ phase, type: "phase" }),
|
|
582
662
|
outputDir,
|
|
663
|
+
parentSessionId: input.parentSessionId,
|
|
583
664
|
pr: input.pr,
|
|
584
665
|
repairAttempts: input.config.output?.repairAttempts ?? 3,
|
|
585
666
|
repository: input.repository,
|
|
@@ -595,10 +676,14 @@ export async function runReview(input) {
|
|
|
595
676
|
checkResult.report.scopeInside.length)) {
|
|
596
677
|
await input.onProgress?.({ report: checkResult.report, type: "ci_report" });
|
|
597
678
|
}
|
|
598
|
-
const
|
|
679
|
+
const worktreePath = prRunWorktreeDir({
|
|
680
|
+
config: input.config,
|
|
681
|
+
directory: input.directory,
|
|
682
|
+
pr: input.pr,
|
|
683
|
+
runId,
|
|
684
|
+
});
|
|
599
685
|
await input.onProgress?.({ phase: "creating worktree", type: "phase" });
|
|
600
|
-
const worktree = await createWorktree(exec, input.repository, input.pr,
|
|
601
|
-
const worktreePath = worktree.path;
|
|
686
|
+
const worktree = await createWorktree(exec, input.repository, input.pr, worktreePath);
|
|
602
687
|
await input.onProgress?.({
|
|
603
688
|
branch: worktree.branch,
|
|
604
689
|
type: "worktree_created",
|
|
@@ -612,7 +697,18 @@ export async function runReview(input) {
|
|
|
612
697
|
return [];
|
|
613
698
|
return [{ assignment, reviewer }];
|
|
614
699
|
});
|
|
615
|
-
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
|
+
});
|
|
616
712
|
for (const reviewer of input.repository.agents.reviewers) {
|
|
617
713
|
const assignment = mode.assignments.get(reviewer.account);
|
|
618
714
|
if (assignment?.type !== "skip")
|
|
@@ -632,6 +728,18 @@ export async function runReview(input) {
|
|
|
632
728
|
const previous = assignment.review;
|
|
633
729
|
if (!previous.commit?.oid)
|
|
634
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
|
+
});
|
|
635
743
|
const unresolved = unresolvedThreadsByAccount.get(reviewer.account) ??
|
|
636
744
|
(await fetchUnresolvedThreads(exec, input.repository, input.pr, reviewer.account));
|
|
637
745
|
const prompt = await composeRereviewPrompt({
|
|
@@ -678,6 +786,7 @@ export async function runReview(input) {
|
|
|
678
786
|
}
|
|
679
787
|
},
|
|
680
788
|
options: reviewer.options,
|
|
789
|
+
parentSessionId: input.parentSessionId,
|
|
681
790
|
parse: (text) => parseRereviewOutputWithInlineTargets(text, inlineCommentTargets),
|
|
682
791
|
permission: reviewer.permission,
|
|
683
792
|
prompt,
|
|
@@ -697,7 +806,9 @@ export async function runReview(input) {
|
|
|
697
806
|
verdict: result.value.verdict,
|
|
698
807
|
});
|
|
699
808
|
return {
|
|
809
|
+
inlineCommentTargets,
|
|
700
810
|
key: reviewer.key,
|
|
811
|
+
previousHeadSha: previous.commit.oid,
|
|
701
812
|
raw: result.raw,
|
|
702
813
|
sessionId: result.sessionId,
|
|
703
814
|
value: result.value,
|
|
@@ -744,7 +855,8 @@ export async function runReview(input) {
|
|
|
744
855
|
}
|
|
745
856
|
},
|
|
746
857
|
options: reviewer.options,
|
|
747
|
-
|
|
858
|
+
parentSessionId: input.parentSessionId,
|
|
859
|
+
parse: (text) => parseReviewOutputWithInlineTargets(text, initialInlineCommentTargets),
|
|
748
860
|
permission: reviewer.permission,
|
|
749
861
|
prompt,
|
|
750
862
|
repairAttempts: input.config.output?.repairAttempts ?? 3,
|
|
@@ -763,6 +875,7 @@ export async function runReview(input) {
|
|
|
763
875
|
verdict: result.value.verdict,
|
|
764
876
|
});
|
|
765
877
|
return {
|
|
878
|
+
inlineCommentTargets: initialInlineCommentTargets,
|
|
766
879
|
key: reviewer.key,
|
|
767
880
|
raw: result.raw,
|
|
768
881
|
sessionId: result.sessionId,
|
|
@@ -796,6 +909,7 @@ export async function runReview(input) {
|
|
|
796
909
|
return [
|
|
797
910
|
{
|
|
798
911
|
key: reviewer.key,
|
|
912
|
+
inlineCommentTargets: initialInlineCommentTargets,
|
|
799
913
|
raw: assignment.review.body ?? "",
|
|
800
914
|
sessionId: "",
|
|
801
915
|
value: reviewOutputFromState(assignment.review),
|
|
@@ -804,7 +918,6 @@ export async function runReview(input) {
|
|
|
804
918
|
});
|
|
805
919
|
entries = await runCloseReconsideration({
|
|
806
920
|
entries: [...entries, ...skippedCloseEntries],
|
|
807
|
-
inlineCommentTargets,
|
|
808
921
|
meta,
|
|
809
922
|
outputDir,
|
|
810
923
|
reviewContext,
|