opencode-magi 0.2.0 → 0.3.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 +19 -0
- package/dist/commands.js +4 -0
- package/dist/config/output.js +11 -2
- package/dist/config/resolve.js +81 -1
- package/dist/config/validate.js +290 -3
- package/dist/config/worktree.js +8 -2
- package/dist/github/commands.js +343 -15
- package/dist/index.js +252 -26
- package/dist/orchestrator/ci.js +1 -1
- package/dist/orchestrator/findings.js +4 -3
- package/dist/orchestrator/inline-comments.js +73 -0
- package/dist/orchestrator/majority.js +14 -0
- package/dist/orchestrator/merge.js +16 -3
- package/dist/orchestrator/report.js +15 -1
- package/dist/orchestrator/review-context.js +309 -0
- package/dist/orchestrator/review.js +49 -9
- package/dist/orchestrator/run-manager.js +408 -17
- package/dist/orchestrator/triage.js +1119 -0
- package/dist/permissions/editor.json +8 -1
- package/dist/prompts/compose.js +162 -1
- package/dist/prompts/contracts.js +119 -12
- package/dist/prompts/output.js +149 -14
- package/dist/prompts/templates/review/review.md +6 -0
- package/dist/prompts/templates/triage/acceptance.md +7 -0
- package/dist/prompts/templates/triage/action.md +5 -0
- package/dist/prompts/templates/triage/category.md +10 -0
- package/dist/prompts/templates/triage/comment-classification.md +7 -0
- package/dist/prompts/templates/triage/comment.md +5 -0
- package/dist/prompts/templates/triage/create.md +7 -0
- package/dist/prompts/templates/triage/duplicate.md +7 -0
- package/dist/prompts/templates/triage/existing-pr.md +7 -0
- package/dist/prompts/templates/triage/question.md +5 -0
- package/dist/prompts/templates/triage/reconsider.md +5 -0
- package/package.json +5 -2
- package/schema.json +127 -2
|
@@ -10,6 +10,11 @@ function reportUrl(value) {
|
|
|
10
10
|
function linkOrText(text, url) {
|
|
11
11
|
return url ? `[${text}](${url})` : text;
|
|
12
12
|
}
|
|
13
|
+
function pullRequestLine(input) {
|
|
14
|
+
const host = input.repository.github.host || "github.com";
|
|
15
|
+
const url = `https://${host}/${input.repository.github.owner}/${input.repository.github.repo}/pull/${input.pr}`;
|
|
16
|
+
return `- **Pull Request**: [#${input.pr}](${url})`;
|
|
17
|
+
}
|
|
13
18
|
function formatFinding(finding) {
|
|
14
19
|
const line = finding.startLine == null
|
|
15
20
|
? `${finding.path}:${finding.line}`
|
|
@@ -22,6 +27,9 @@ function formatRereviewFinding(finding) {
|
|
|
22
27
|
: `${finding.path}:${finding.startLine}-${finding.line}`;
|
|
23
28
|
return `\`${line}\`: ${finding.body}`;
|
|
24
29
|
}
|
|
30
|
+
function formatRequirementFinding(finding) {
|
|
31
|
+
return `Issue #${finding.issueNumber}: ${finding.requirement}`;
|
|
32
|
+
}
|
|
25
33
|
function isReviewOutput(output) {
|
|
26
34
|
return "findings" in output;
|
|
27
35
|
}
|
|
@@ -73,7 +81,10 @@ function reviewerDetailLines(output) {
|
|
|
73
81
|
return output.reason ? [output.reason] : [];
|
|
74
82
|
if (output.verdict !== "CHANGES_REQUESTED")
|
|
75
83
|
return [];
|
|
76
|
-
return
|
|
84
|
+
return [
|
|
85
|
+
...output.findings.map(formatFinding),
|
|
86
|
+
...output.requirementFindings.map(formatRequirementFinding),
|
|
87
|
+
];
|
|
77
88
|
}
|
|
78
89
|
if (output.verdict === "CLOSE")
|
|
79
90
|
return output.reason ? [output.reason] : [];
|
|
@@ -81,6 +92,7 @@ function reviewerDetailLines(output) {
|
|
|
81
92
|
return [];
|
|
82
93
|
return [
|
|
83
94
|
...output.newFindings.map(formatRereviewFinding),
|
|
95
|
+
...output.requirementFindings.map(formatRequirementFinding),
|
|
84
96
|
...output.followUps.map((item) => `Comment #${item.commentId}: ${item.body}`),
|
|
85
97
|
];
|
|
86
98
|
}
|
|
@@ -150,6 +162,7 @@ function editorLines(outputs) {
|
|
|
150
162
|
}
|
|
151
163
|
export function formatReviewReport(input) {
|
|
152
164
|
return [
|
|
165
|
+
pullRequestLine(input),
|
|
153
166
|
...dryRunLines(input.dryRun),
|
|
154
167
|
...safetyLines(input.safety),
|
|
155
168
|
...checkLines(input.ciReports),
|
|
@@ -158,6 +171,7 @@ export function formatReviewReport(input) {
|
|
|
158
171
|
}
|
|
159
172
|
export function formatMergeReport(input) {
|
|
160
173
|
return [
|
|
174
|
+
pullRequestLine(input),
|
|
161
175
|
...mergeStatusLines(input.status),
|
|
162
176
|
...dryRunLines(input.dryRun),
|
|
163
177
|
...safetyLines(input.safety),
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { fetchIssue, fetchIssueCommentPage, fetchPullRequestClosingIssues, fetchPullRequestCommentPage, fetchPullRequestReviewThreadPage, fetchPullRequestSafetyMeta, } from "../github/commands";
|
|
2
|
+
const LIMITS = {
|
|
3
|
+
closingIssueComments: 20,
|
|
4
|
+
commentBody: 4000,
|
|
5
|
+
prComments: 20,
|
|
6
|
+
referencedIssueComments: 10,
|
|
7
|
+
reviewThreadComments: 20,
|
|
8
|
+
reviewThreads: 50,
|
|
9
|
+
};
|
|
10
|
+
function escapeRegExp(value) {
|
|
11
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
12
|
+
}
|
|
13
|
+
function truncateBody(body) {
|
|
14
|
+
if (body.length <= LIMITS.commentBody)
|
|
15
|
+
return { body };
|
|
16
|
+
return {
|
|
17
|
+
body: `${body.slice(0, LIMITS.commentBody)}\n[truncated after ${LIMITS.commentBody} characters]`,
|
|
18
|
+
truncated: true,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function boundedComments(comments, limit) {
|
|
22
|
+
return [...comments]
|
|
23
|
+
.sort((a, b) => a.createdAt.localeCompare(b.createdAt))
|
|
24
|
+
.slice(-limit)
|
|
25
|
+
.map((comment) => ({
|
|
26
|
+
author: comment.author,
|
|
27
|
+
createdAt: comment.createdAt,
|
|
28
|
+
id: comment.id,
|
|
29
|
+
url: comment.url,
|
|
30
|
+
...truncateBody(comment.body),
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
function omittedCommentCount(input) {
|
|
34
|
+
return input.omitted + Math.max(0, input.comments.length - input.limit);
|
|
35
|
+
}
|
|
36
|
+
function quoteEvidence(value) {
|
|
37
|
+
const compact = value.replaceAll(/\s+/g, " ").trim();
|
|
38
|
+
return compact.length > 120 ? `${compact.slice(0, 117)}...` : compact;
|
|
39
|
+
}
|
|
40
|
+
function issueReferencePattern(repository) {
|
|
41
|
+
const host = escapeRegExp(repository.github.host || "github.com");
|
|
42
|
+
const owner = escapeRegExp(repository.github.owner);
|
|
43
|
+
const repo = escapeRegExp(repository.github.repo);
|
|
44
|
+
return new RegExp(`(?:https?://${host}/${owner}/${repo}/issues/(\\d+)|#(\\d+))`, "gi");
|
|
45
|
+
}
|
|
46
|
+
function issueNumberFromMatch(match) {
|
|
47
|
+
return Number(match[1] ?? match[2]);
|
|
48
|
+
}
|
|
49
|
+
function addRelationship(relationships, number, relationship, source) {
|
|
50
|
+
const current = relationships.get(number);
|
|
51
|
+
const nextRelationship = current?.relationship === "closing" || relationship === "closing"
|
|
52
|
+
? "closing"
|
|
53
|
+
: "referenced";
|
|
54
|
+
const sources = current?.sources ?? [];
|
|
55
|
+
relationships.set(number, {
|
|
56
|
+
number,
|
|
57
|
+
relationship: nextRelationship,
|
|
58
|
+
sources: sources.includes(source) ? sources : [...sources, source],
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function scanRelationshipText(input) {
|
|
62
|
+
const referencePattern = issueReferencePattern(input.repository);
|
|
63
|
+
const closingPattern = new RegExp(`\\b(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\\b[\\s\\S]{0,80}?${referencePattern.source}`, "gi");
|
|
64
|
+
for (const match of input.text.matchAll(referencePattern)) {
|
|
65
|
+
const number = issueNumberFromMatch(match);
|
|
66
|
+
if (number === input.currentPr)
|
|
67
|
+
continue;
|
|
68
|
+
addRelationship(input.relationships, number, "referenced", `${input.label} "${quoteEvidence(match[0])}"`);
|
|
69
|
+
}
|
|
70
|
+
for (const match of input.text.matchAll(closingPattern)) {
|
|
71
|
+
const number = Number(match[1] ?? match[2]);
|
|
72
|
+
if (!number || number === input.currentPr)
|
|
73
|
+
continue;
|
|
74
|
+
addRelationship(input.relationships, number, "closing", `${input.label} "${quoteEvidence(match[0])}"`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function collectIssueRelationships(input) {
|
|
78
|
+
const relationships = new Map();
|
|
79
|
+
for (const issue of input.closingIssues) {
|
|
80
|
+
if (issue.number === input.pr.number)
|
|
81
|
+
continue;
|
|
82
|
+
addRelationship(relationships, issue.number, "closing", "GitHub closingIssuesReferences");
|
|
83
|
+
}
|
|
84
|
+
scanRelationshipText({
|
|
85
|
+
currentPr: input.pr.number,
|
|
86
|
+
label: "PR title",
|
|
87
|
+
relationships,
|
|
88
|
+
repository: input.repository,
|
|
89
|
+
text: input.pr.title,
|
|
90
|
+
});
|
|
91
|
+
scanRelationshipText({
|
|
92
|
+
currentPr: input.pr.number,
|
|
93
|
+
label: "PR body",
|
|
94
|
+
relationships,
|
|
95
|
+
repository: input.repository,
|
|
96
|
+
text: input.pr.body ?? "",
|
|
97
|
+
});
|
|
98
|
+
for (const comment of input.prComments) {
|
|
99
|
+
scanRelationshipText({
|
|
100
|
+
currentPr: input.pr.number,
|
|
101
|
+
label: `PR comment ${comment.id}`,
|
|
102
|
+
relationships,
|
|
103
|
+
repository: input.repository,
|
|
104
|
+
text: comment.body,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
for (const thread of input.reviewThreads) {
|
|
108
|
+
for (const comment of thread.comments) {
|
|
109
|
+
scanRelationshipText({
|
|
110
|
+
currentPr: input.pr.number,
|
|
111
|
+
label: `review thread ${thread.threadId} comment ${comment.commentId}`,
|
|
112
|
+
relationships,
|
|
113
|
+
repository: input.repository,
|
|
114
|
+
text: comment.body,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return [...relationships.values()].sort((a, b) => a.number - b.number);
|
|
119
|
+
}
|
|
120
|
+
async function contextIssue(input) {
|
|
121
|
+
const issue = input.issue ??
|
|
122
|
+
(await fetchIssue(input.exec, input.repository, input.relationship.number));
|
|
123
|
+
const commentPage = await fetchIssueCommentPage(input.exec, input.repository, issue.number, input.limit);
|
|
124
|
+
return {
|
|
125
|
+
author: issue.author,
|
|
126
|
+
body: issue.body,
|
|
127
|
+
comments: boundedComments(commentPage.comments, input.limit),
|
|
128
|
+
commentsOmitted: omittedCommentCount({
|
|
129
|
+
comments: commentPage.comments,
|
|
130
|
+
limit: input.limit,
|
|
131
|
+
omitted: commentPage.omitted,
|
|
132
|
+
}),
|
|
133
|
+
number: issue.number,
|
|
134
|
+
relationship: input.relationship.relationship,
|
|
135
|
+
source: input.relationship.sources.join("; "),
|
|
136
|
+
state: issue.state,
|
|
137
|
+
title: issue.title,
|
|
138
|
+
url: issue.url,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function orderReviewThreads(threads) {
|
|
142
|
+
return [...threads]
|
|
143
|
+
.sort((a, b) => {
|
|
144
|
+
if (a.isResolved !== b.isResolved)
|
|
145
|
+
return a.isResolved ? 1 : -1;
|
|
146
|
+
const aLatest = a.comments.at(-1)?.createdAt ?? "";
|
|
147
|
+
const bLatest = b.comments.at(-1)?.createdAt ?? "";
|
|
148
|
+
return bLatest.localeCompare(aLatest);
|
|
149
|
+
})
|
|
150
|
+
.slice(0, LIMITS.reviewThreads)
|
|
151
|
+
.map((thread) => ({
|
|
152
|
+
...thread,
|
|
153
|
+
comments: thread.comments
|
|
154
|
+
.slice(-LIMITS.reviewThreadComments)
|
|
155
|
+
.map((comment) => ({
|
|
156
|
+
...comment,
|
|
157
|
+
...truncateBody(comment.body),
|
|
158
|
+
})),
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
export async function buildReviewContextSnapshot(input) {
|
|
162
|
+
const [prCommentPage, reviewThreadPage, safetyMeta, closingIssues] = await Promise.all([
|
|
163
|
+
fetchPullRequestCommentPage(input.exec, input.repository, input.pr.number, LIMITS.prComments),
|
|
164
|
+
fetchPullRequestReviewThreadPage(input.exec, input.repository, input.pr.number, LIMITS.reviewThreads, LIMITS.reviewThreadComments),
|
|
165
|
+
fetchPullRequestSafetyMeta(input.exec, input.repository, input.pr.number),
|
|
166
|
+
fetchPullRequestClosingIssues(input.exec, input.repository, input.pr.number).catch(() => []),
|
|
167
|
+
]);
|
|
168
|
+
const prComments = prCommentPage.comments;
|
|
169
|
+
const orderedReviewThreads = orderReviewThreads(reviewThreadPage.threads);
|
|
170
|
+
const prCommentsOmitted = omittedCommentCount({
|
|
171
|
+
comments: prComments,
|
|
172
|
+
limit: LIMITS.prComments,
|
|
173
|
+
omitted: prCommentPage.omitted,
|
|
174
|
+
});
|
|
175
|
+
const relationships = collectIssueRelationships({
|
|
176
|
+
closingIssues,
|
|
177
|
+
pr: input.pr,
|
|
178
|
+
prComments,
|
|
179
|
+
repository: input.repository,
|
|
180
|
+
reviewThreads: orderedReviewThreads,
|
|
181
|
+
});
|
|
182
|
+
const closingIssueMap = new Map(closingIssues.map((issue) => [issue.number, issue]));
|
|
183
|
+
const closingRelationships = relationships.filter((relationship) => relationship.relationship === "closing");
|
|
184
|
+
const referencedRelationships = relationships.filter((relationship) => relationship.relationship === "referenced");
|
|
185
|
+
return {
|
|
186
|
+
closingIssues: await Promise.all(closingRelationships.map((relationship) => contextIssue({
|
|
187
|
+
exec: input.exec,
|
|
188
|
+
issue: closingIssueMap.get(relationship.number),
|
|
189
|
+
limit: LIMITS.closingIssueComments,
|
|
190
|
+
relationship,
|
|
191
|
+
repository: input.repository,
|
|
192
|
+
}))),
|
|
193
|
+
pullRequest: {
|
|
194
|
+
author: input.pr.author?.login ?? safetyMeta.author,
|
|
195
|
+
baseRef: input.pr.baseRefName,
|
|
196
|
+
baseSha: input.pr.baseRefOid,
|
|
197
|
+
body: input.pr.body ?? "",
|
|
198
|
+
changedFiles: safetyMeta.files,
|
|
199
|
+
comments: boundedComments(prComments, LIMITS.prComments),
|
|
200
|
+
commentsOmitted: prCommentsOmitted,
|
|
201
|
+
headRef: input.pr.headRefName,
|
|
202
|
+
headSha: input.pr.headRefOid,
|
|
203
|
+
number: input.pr.number,
|
|
204
|
+
relationship: "target",
|
|
205
|
+
source: "/magi:review input",
|
|
206
|
+
state: input.pr.state ?? "",
|
|
207
|
+
title: input.pr.title,
|
|
208
|
+
url: input.pr.url,
|
|
209
|
+
},
|
|
210
|
+
referencedIssues: await Promise.all(referencedRelationships.map((relationship) => contextIssue({
|
|
211
|
+
exec: input.exec,
|
|
212
|
+
limit: LIMITS.referencedIssueComments,
|
|
213
|
+
relationship,
|
|
214
|
+
repository: input.repository,
|
|
215
|
+
}))),
|
|
216
|
+
reviewDiscussion: {
|
|
217
|
+
prComments: boundedComments(prComments, LIMITS.prComments),
|
|
218
|
+
prCommentsOmitted,
|
|
219
|
+
reviewThreads: orderedReviewThreads,
|
|
220
|
+
reviewThreadsOmitted: reviewThreadPage.omitted,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function indented(value) {
|
|
225
|
+
return value.trim() ? value : "(empty)";
|
|
226
|
+
}
|
|
227
|
+
function renderOmissionNote(omitted, label, limit) {
|
|
228
|
+
return omitted > 0
|
|
229
|
+
? `\n[omitted ${omitted} older ${label} due to limit ${limit}]`
|
|
230
|
+
: "";
|
|
231
|
+
}
|
|
232
|
+
function renderComments(comments, omitted = 0, limit = comments.length) {
|
|
233
|
+
if (!comments.length)
|
|
234
|
+
return `(none)${renderOmissionNote(omitted, "comments", limit)}`;
|
|
235
|
+
return (comments
|
|
236
|
+
.map((comment) => {
|
|
237
|
+
const suffix = comment.truncated ? " [truncated]" : "";
|
|
238
|
+
return `- ${comment.createdAt} @${comment.author} (${comment.id})${suffix}\n${indented(comment.body)}`;
|
|
239
|
+
})
|
|
240
|
+
.join("\n") + renderOmissionNote(omitted, "comments", limit));
|
|
241
|
+
}
|
|
242
|
+
function renderIssue(issue) {
|
|
243
|
+
return `<issue>
|
|
244
|
+
number: ${issue.number}
|
|
245
|
+
title: ${issue.title}
|
|
246
|
+
url: ${issue.url}
|
|
247
|
+
state: ${issue.state}
|
|
248
|
+
author: ${issue.author}
|
|
249
|
+
relationship: ${issue.relationship}
|
|
250
|
+
source: ${issue.source}
|
|
251
|
+
body:
|
|
252
|
+
${indented(issue.body)}
|
|
253
|
+
comments:
|
|
254
|
+
${renderComments(issue.comments, issue.commentsOmitted, issue.relationship === "closing" ? LIMITS.closingIssueComments : LIMITS.referencedIssueComments)}
|
|
255
|
+
</issue>`;
|
|
256
|
+
}
|
|
257
|
+
function renderThreads(threads, omitted = 0) {
|
|
258
|
+
if (!threads.length) {
|
|
259
|
+
return `(none)${renderOmissionNote(omitted, "review threads", LIMITS.reviewThreads)}`;
|
|
260
|
+
}
|
|
261
|
+
return (threads
|
|
262
|
+
.map((thread) => {
|
|
263
|
+
const comments = thread.comments
|
|
264
|
+
.map((comment) => {
|
|
265
|
+
const suffix = comment.truncated ? " [truncated]" : "";
|
|
266
|
+
return ` - ${comment.createdAt} @${comment.author} (${comment.commentId})${suffix}\n${indented(comment.body)}`;
|
|
267
|
+
})
|
|
268
|
+
.join("\n") +
|
|
269
|
+
renderOmissionNote(thread.omittedComments ?? 0, "thread comments", LIMITS.reviewThreadComments);
|
|
270
|
+
return `- threadId: ${thread.threadId}\n resolved: ${Boolean(thread.isResolved)}\n path: ${thread.path}:${thread.line}\n comments:\n${comments}`;
|
|
271
|
+
})
|
|
272
|
+
.join("\n") +
|
|
273
|
+
renderOmissionNote(omitted, "review threads", LIMITS.reviewThreads));
|
|
274
|
+
}
|
|
275
|
+
export function renderReviewContext(snapshot) {
|
|
276
|
+
return [
|
|
277
|
+
`<pull_request_context>
|
|
278
|
+
number: ${snapshot.pullRequest.number}
|
|
279
|
+
title: ${snapshot.pullRequest.title}
|
|
280
|
+
url: ${snapshot.pullRequest.url}
|
|
281
|
+
state: ${snapshot.pullRequest.state}
|
|
282
|
+
author: ${snapshot.pullRequest.author}
|
|
283
|
+
relationship: ${snapshot.pullRequest.relationship}
|
|
284
|
+
source: ${snapshot.pullRequest.source}
|
|
285
|
+
baseRef: ${snapshot.pullRequest.baseRef}
|
|
286
|
+
headRef: ${snapshot.pullRequest.headRef}
|
|
287
|
+
baseSha: ${snapshot.pullRequest.baseSha}
|
|
288
|
+
headSha: ${snapshot.pullRequest.headSha}
|
|
289
|
+
body:
|
|
290
|
+
${indented(snapshot.pullRequest.body)}
|
|
291
|
+
comments:
|
|
292
|
+
${renderComments(snapshot.pullRequest.comments, snapshot.pullRequest.commentsOmitted, LIMITS.prComments)}
|
|
293
|
+
changedFiles:
|
|
294
|
+
${snapshot.pullRequest.changedFiles.length ? snapshot.pullRequest.changedFiles.map((file) => `- ${file}`).join("\n") : "(none)"}
|
|
295
|
+
</pull_request_context>`,
|
|
296
|
+
`<closing_issues>
|
|
297
|
+
${snapshot.closingIssues.length ? snapshot.closingIssues.map(renderIssue).join("\n") : "(none)"}
|
|
298
|
+
</closing_issues>`,
|
|
299
|
+
`<referenced_issues>
|
|
300
|
+
${snapshot.referencedIssues.length ? snapshot.referencedIssues.map(renderIssue).join("\n") : "(none)"}
|
|
301
|
+
</referenced_issues>`,
|
|
302
|
+
`<review_discussion>
|
|
303
|
+
prComments:
|
|
304
|
+
${renderComments(snapshot.reviewDiscussion.prComments, snapshot.reviewDiscussion.prCommentsOmitted, LIMITS.prComments)}
|
|
305
|
+
reviewThreads:
|
|
306
|
+
${renderThreads(snapshot.reviewDiscussion.reviewThreads, snapshot.reviewDiscussion.reviewThreadsOmitted)}
|
|
307
|
+
</review_discussion>`,
|
|
308
|
+
].join("\n\n");
|
|
309
|
+
}
|
|
@@ -1,17 +1,19 @@
|
|
|
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, } from "../github/commands";
|
|
3
|
+
import { createWorktree, fetchPullRequest, fetchPullRequestCommits, fetchPullRequestReviews, fetchUnresolvedThreads, closePullRequest, mergePullRequest, postApproval, postChangesRequested, postCloseComment, postReply, removeWorktree, resolveThread, shellQuote, } from "../github/commands";
|
|
4
4
|
import { composeFindingValidationPrompt, composeCloseReconsiderationPrompt, composeRereviewPrompt, composeReviewPrompt, } from "../prompts/compose";
|
|
5
5
|
import { prRunOutputDir } from "../config/output";
|
|
6
6
|
import { worktreeBaseDir } from "../config/worktree";
|
|
7
7
|
import { parseCloseReconsiderationOutput, parseFindingValidationOutput, parseRereviewOutput, parseReviewOutput, } from "../prompts/output";
|
|
8
8
|
import { throwIfAborted, withAbortSignal } from "./abort";
|
|
9
9
|
import { waitForChecksWithClassification } from "./ci";
|
|
10
|
+
import { parseRightSideDiffTargets, validateInlineCommentTargets, } from "./inline-comments";
|
|
10
11
|
import { applyFindingValidation, reviewFindingTargets, validateFindingVotes, } from "./findings";
|
|
11
12
|
import { closeMinorityReviewers, mergeVerdictForPolicy, } from "./majority";
|
|
12
13
|
import { runModelWithRepair } from "./model";
|
|
13
14
|
import { mapPool } from "./pool";
|
|
14
15
|
import { formatReviewReport } from "./report";
|
|
16
|
+
import { buildReviewContextSnapshot, renderReviewContext, } from "./review-context";
|
|
15
17
|
import { checkSafetyGate, hasSafetyGate } from "./safety";
|
|
16
18
|
function errorMessage(error) {
|
|
17
19
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -37,7 +39,7 @@ async function postReviewOutput(input, reviewerKey, output) {
|
|
|
37
39
|
return postApproval(input.exec, input.repository, input.pr, reviewer.account);
|
|
38
40
|
if (output.verdict === "CLOSE")
|
|
39
41
|
return postCloseComment(input.exec, input.repository, input.pr, reviewer.account, output.reason ?? "Close requested.");
|
|
40
|
-
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, output.requirementFindings);
|
|
41
43
|
}
|
|
42
44
|
function dryRunReviewPost(key, output) {
|
|
43
45
|
if (output.verdict === "MERGE")
|
|
@@ -123,11 +125,26 @@ function previousReviewText(review) {
|
|
|
123
125
|
submittedAt: review.submittedAt,
|
|
124
126
|
}, null, 2);
|
|
125
127
|
}
|
|
128
|
+
function parseReviewOutputWithInlineTargets(text, targets) {
|
|
129
|
+
const output = parseReviewOutput(text);
|
|
130
|
+
validateInlineCommentTargets(output.findings, targets);
|
|
131
|
+
return output;
|
|
132
|
+
}
|
|
133
|
+
function parseRereviewOutputWithInlineTargets(text, targets) {
|
|
134
|
+
const output = parseRereviewOutput(text);
|
|
135
|
+
validateInlineCommentTargets(output.newFindings, targets, "newFindings");
|
|
136
|
+
return output;
|
|
137
|
+
}
|
|
126
138
|
function reviewOutputFromState(review) {
|
|
127
139
|
const verdict = reviewStateToVerdict(review.state);
|
|
128
140
|
return verdict === "CLOSE"
|
|
129
|
-
? {
|
|
130
|
-
|
|
141
|
+
? {
|
|
142
|
+
findings: [],
|
|
143
|
+
reason: review.body || "Close requested.",
|
|
144
|
+
requirementFindings: [],
|
|
145
|
+
verdict,
|
|
146
|
+
}
|
|
147
|
+
: { findings: [], requirementFindings: [], verdict };
|
|
131
148
|
}
|
|
132
149
|
export function hasPendingThreadReply(threads, reviewerAccount) {
|
|
133
150
|
return threads.some((thread) => {
|
|
@@ -151,7 +168,7 @@ async function postRereviewOutput(input, reviewerKey, output) {
|
|
|
151
168
|
return postApproval(input.exec, input.repository, input.pr, reviewer.account);
|
|
152
169
|
if (output.verdict === "CLOSE")
|
|
153
170
|
return postCloseComment(input.exec, input.repository, input.pr, reviewer.account, output.reason ?? "Close requested.");
|
|
154
|
-
if (!output.newFindings.length)
|
|
171
|
+
if (!output.newFindings.length && !output.requirementFindings.length)
|
|
155
172
|
return "";
|
|
156
173
|
return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.newFindings.map((finding) => ({
|
|
157
174
|
fix: "Please address this before merging.",
|
|
@@ -159,7 +176,7 @@ async function postRereviewOutput(input, reviewerKey, output) {
|
|
|
159
176
|
line: finding.line,
|
|
160
177
|
path: finding.path,
|
|
161
178
|
startLine: finding.startLine,
|
|
162
|
-
})));
|
|
179
|
+
})), output.requirementFindings);
|
|
163
180
|
}
|
|
164
181
|
function isReviewOutput(output) {
|
|
165
182
|
return "findings" in output;
|
|
@@ -191,6 +208,7 @@ async function runFindingValidation(input) {
|
|
|
191
208
|
includeSessionContext: !hasReviewerSession,
|
|
192
209
|
pr: input.reviewInput.pr,
|
|
193
210
|
repository: input.reviewInput.repository,
|
|
211
|
+
reviewContext: input.reviewContext,
|
|
194
212
|
reviewer,
|
|
195
213
|
worktreePath: input.worktreePath,
|
|
196
214
|
});
|
|
@@ -303,6 +321,7 @@ async function runCloseReconsideration(input) {
|
|
|
303
321
|
includeSessionContext: !hasReviewerSession,
|
|
304
322
|
pr: input.reviewInput.pr,
|
|
305
323
|
repository: input.reviewInput.repository,
|
|
324
|
+
reviewContext: input.reviewContext,
|
|
306
325
|
reviewer,
|
|
307
326
|
worktreePath: input.worktreePath,
|
|
308
327
|
});
|
|
@@ -336,7 +355,11 @@ async function runCloseReconsideration(input) {
|
|
|
336
355
|
}
|
|
337
356
|
},
|
|
338
357
|
options: reviewer.options,
|
|
339
|
-
parse:
|
|
358
|
+
parse: (text) => {
|
|
359
|
+
const output = parseCloseReconsiderationOutput(text);
|
|
360
|
+
validateInlineCommentTargets(output.findings, input.inlineCommentTargets);
|
|
361
|
+
return output;
|
|
362
|
+
},
|
|
340
363
|
permission: reviewer.permission,
|
|
341
364
|
prompt,
|
|
342
365
|
repairAttempts: input.reviewInput.config.output?.repairAttempts ?? 3,
|
|
@@ -397,6 +420,7 @@ export async function runReview(input) {
|
|
|
397
420
|
dryRun: input.dryRun,
|
|
398
421
|
outputs: {},
|
|
399
422
|
posted: {},
|
|
423
|
+
pr: input.pr,
|
|
400
424
|
repository: input.repository,
|
|
401
425
|
safety,
|
|
402
426
|
});
|
|
@@ -448,6 +472,15 @@ export async function runReview(input) {
|
|
|
448
472
|
pr: input.pr,
|
|
449
473
|
}), ...(input.runId ? [input.runId] : []));
|
|
450
474
|
await mkdir(outputDir, { recursive: true });
|
|
475
|
+
await input.onProgress?.({ phase: "fetching review context", type: "phase" });
|
|
476
|
+
const reviewContextSnapshot = await buildReviewContextSnapshot({
|
|
477
|
+
exec,
|
|
478
|
+
pr: meta,
|
|
479
|
+
repository: input.repository,
|
|
480
|
+
});
|
|
481
|
+
const reviewContext = renderReviewContext(reviewContextSnapshot);
|
|
482
|
+
await writeFile(join(outputDir, "review-context.json"), JSON.stringify(reviewContextSnapshot, null, 2));
|
|
483
|
+
await writeFile(join(outputDir, "review-context.md"), `${reviewContext}\n`);
|
|
451
484
|
await input.onProgress?.({ phase: "waiting for checks", type: "phase" });
|
|
452
485
|
const checkResult = await waitForChecksWithClassification({
|
|
453
486
|
client: input.client,
|
|
@@ -518,6 +551,7 @@ export async function runReview(input) {
|
|
|
518
551
|
return [];
|
|
519
552
|
return [{ assignment, reviewer }];
|
|
520
553
|
});
|
|
554
|
+
const inlineCommentTargets = parseRightSideDiffTargets(await exec(`git diff --no-ext-diff --unified=3 ${shellQuote(meta.baseRefOid)} ${shellQuote(meta.headRefOid)}`, { cwd: worktreePath }));
|
|
521
555
|
for (const reviewer of input.repository.agents.reviewers) {
|
|
522
556
|
const assignment = mode.assignments.get(reviewer.account);
|
|
523
557
|
if (assignment?.type !== "skip")
|
|
@@ -548,6 +582,7 @@ export async function runReview(input) {
|
|
|
548
582
|
previousReview: previousReviewText(previous),
|
|
549
583
|
previousHeadSha: previous.commit.oid,
|
|
550
584
|
repository: input.repository,
|
|
585
|
+
reviewContext,
|
|
551
586
|
reviewer,
|
|
552
587
|
unresolvedThreads: JSON.stringify(unresolved, null, 2),
|
|
553
588
|
worktreePath,
|
|
@@ -582,7 +617,7 @@ export async function runReview(input) {
|
|
|
582
617
|
}
|
|
583
618
|
},
|
|
584
619
|
options: reviewer.options,
|
|
585
|
-
parse:
|
|
620
|
+
parse: (text) => parseRereviewOutputWithInlineTargets(text, inlineCommentTargets),
|
|
586
621
|
permission: reviewer.permission,
|
|
587
622
|
prompt,
|
|
588
623
|
repairAttempts: input.config.output?.repairAttempts ?? 3,
|
|
@@ -614,6 +649,7 @@ export async function runReview(input) {
|
|
|
614
649
|
headSha: meta.headRefOid,
|
|
615
650
|
pr: input.pr,
|
|
616
651
|
repository: input.repository,
|
|
652
|
+
reviewContext,
|
|
617
653
|
reviewer,
|
|
618
654
|
worktreePath,
|
|
619
655
|
});
|
|
@@ -647,7 +683,7 @@ export async function runReview(input) {
|
|
|
647
683
|
}
|
|
648
684
|
},
|
|
649
685
|
options: reviewer.options,
|
|
650
|
-
parse:
|
|
686
|
+
parse: (text) => parseReviewOutputWithInlineTargets(text, inlineCommentTargets),
|
|
651
687
|
permission: reviewer.permission,
|
|
652
688
|
prompt,
|
|
653
689
|
repairAttempts: input.config.output?.repairAttempts ?? 3,
|
|
@@ -707,8 +743,10 @@ export async function runReview(input) {
|
|
|
707
743
|
});
|
|
708
744
|
entries = await runCloseReconsideration({
|
|
709
745
|
entries: [...entries, ...skippedCloseEntries],
|
|
746
|
+
inlineCommentTargets,
|
|
710
747
|
meta,
|
|
711
748
|
outputDir,
|
|
749
|
+
reviewContext,
|
|
712
750
|
reviewInput: { ...input, exec },
|
|
713
751
|
sessionIds,
|
|
714
752
|
targets: closeTargets,
|
|
@@ -718,6 +756,7 @@ export async function runReview(input) {
|
|
|
718
756
|
entries,
|
|
719
757
|
meta,
|
|
720
758
|
outputDir,
|
|
759
|
+
reviewContext,
|
|
721
760
|
reviewInput: { ...input, exec },
|
|
722
761
|
sessionIds,
|
|
723
762
|
worktreePath,
|
|
@@ -793,6 +832,7 @@ export async function runReview(input) {
|
|
|
793
832
|
dryRun: input.dryRun,
|
|
794
833
|
outputs,
|
|
795
834
|
posted,
|
|
835
|
+
pr: input.pr,
|
|
796
836
|
repository: input.repository,
|
|
797
837
|
});
|
|
798
838
|
await writeFile(join(outputDir, "report.md"), `${report}\n`);
|