opencode-magi 0.2.0 → 0.4.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 +341 -3
- package/dist/config/worktree.js +8 -2
- package/dist/github/commands.js +381 -19
- 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 +79 -0
- package/dist/orchestrator/majority.js +14 -0
- package/dist/orchestrator/merge.js +108 -34
- package/dist/orchestrator/report.js +25 -7
- package/dist/orchestrator/review-context.js +309 -0
- package/dist/orchestrator/review.js +122 -14
- 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 +163 -1
- package/dist/prompts/contracts.js +131 -18
- package/dist/prompts/output.js +173 -22
- package/dist/prompts/templates/merge/edit.md +12 -5
- 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 +162 -5
|
@@ -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,87 @@ function previousReviewText(review) {
|
|
|
123
125
|
submittedAt: review.submittedAt,
|
|
124
126
|
}, null, 2);
|
|
125
127
|
}
|
|
126
|
-
function
|
|
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
|
+
}
|
|
138
|
+
function parsePostedFindingLocation(location) {
|
|
139
|
+
const range = /^(.*):(\d+)-(\d+)$/.exec(location);
|
|
140
|
+
if (range) {
|
|
141
|
+
return {
|
|
142
|
+
line: Number(range[3]),
|
|
143
|
+
path: range[1] ?? location,
|
|
144
|
+
startLine: Number(range[2]),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const line = /^(.*):(\d+)$/.exec(location);
|
|
148
|
+
if (line)
|
|
149
|
+
return { line: Number(line[2]), path: line[1] ?? location };
|
|
150
|
+
return { path: location };
|
|
151
|
+
}
|
|
152
|
+
function reviewFindingsFromBody(body) {
|
|
153
|
+
const findings = [];
|
|
154
|
+
const requirementFindings = [];
|
|
155
|
+
const lines = (body ?? "").split(/\r?\n/);
|
|
156
|
+
let section;
|
|
157
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
158
|
+
const line = lines[index];
|
|
159
|
+
if (line === "Inline findings:" || line === "File-level findings:") {
|
|
160
|
+
section = "finding";
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (line === "Requirement findings:") {
|
|
164
|
+
section = "requirement";
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (section === "finding") {
|
|
168
|
+
const match = /^- (.*): (.+)$/.exec(line ?? "");
|
|
169
|
+
const fix = /^\s+Fix: (.+)$/.exec(lines[index + 1] ?? "");
|
|
170
|
+
if (!match || !fix)
|
|
171
|
+
continue;
|
|
172
|
+
findings.push({
|
|
173
|
+
...parsePostedFindingLocation(match[1] ?? ""),
|
|
174
|
+
fix: fix[1] ?? "Please address this before merging.",
|
|
175
|
+
issue: match[2] ?? "Review finding.",
|
|
176
|
+
});
|
|
177
|
+
index += 1;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
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
|
+
}
|
|
195
|
+
return { findings, requirementFindings };
|
|
196
|
+
}
|
|
197
|
+
export function reviewOutputFromState(review) {
|
|
127
198
|
const verdict = reviewStateToVerdict(review.state);
|
|
199
|
+
if (verdict === "CHANGES_REQUESTED")
|
|
200
|
+
return { ...reviewFindingsFromBody(review.body), verdict };
|
|
128
201
|
return verdict === "CLOSE"
|
|
129
|
-
? {
|
|
130
|
-
|
|
202
|
+
? {
|
|
203
|
+
findings: [],
|
|
204
|
+
reason: review.body || "Close requested.",
|
|
205
|
+
requirementFindings: [],
|
|
206
|
+
verdict,
|
|
207
|
+
}
|
|
208
|
+
: { findings: [], requirementFindings: [], verdict };
|
|
131
209
|
}
|
|
132
210
|
export function hasPendingThreadReply(threads, reviewerAccount) {
|
|
133
211
|
return threads.some((thread) => {
|
|
@@ -151,15 +229,15 @@ async function postRereviewOutput(input, reviewerKey, output) {
|
|
|
151
229
|
return postApproval(input.exec, input.repository, input.pr, reviewer.account);
|
|
152
230
|
if (output.verdict === "CLOSE")
|
|
153
231
|
return postCloseComment(input.exec, input.repository, input.pr, reviewer.account, output.reason ?? "Close requested.");
|
|
154
|
-
if (!output.newFindings.length)
|
|
232
|
+
if (!output.newFindings.length && !output.requirementFindings.length)
|
|
155
233
|
return "";
|
|
156
234
|
return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.newFindings.map((finding) => ({
|
|
157
235
|
fix: "Please address this before merging.",
|
|
158
236
|
issue: finding.body,
|
|
159
|
-
line: finding.line,
|
|
160
237
|
path: finding.path,
|
|
238
|
+
...(finding.line == null ? {} : { line: finding.line }),
|
|
161
239
|
startLine: finding.startLine,
|
|
162
|
-
})));
|
|
240
|
+
})), output.requirementFindings);
|
|
163
241
|
}
|
|
164
242
|
function isReviewOutput(output) {
|
|
165
243
|
return "findings" in output;
|
|
@@ -191,6 +269,7 @@ async function runFindingValidation(input) {
|
|
|
191
269
|
includeSessionContext: !hasReviewerSession,
|
|
192
270
|
pr: input.reviewInput.pr,
|
|
193
271
|
repository: input.reviewInput.repository,
|
|
272
|
+
reviewContext: input.reviewContext,
|
|
194
273
|
reviewer,
|
|
195
274
|
worktreePath: input.worktreePath,
|
|
196
275
|
});
|
|
@@ -303,6 +382,7 @@ async function runCloseReconsideration(input) {
|
|
|
303
382
|
includeSessionContext: !hasReviewerSession,
|
|
304
383
|
pr: input.reviewInput.pr,
|
|
305
384
|
repository: input.reviewInput.repository,
|
|
385
|
+
reviewContext: input.reviewContext,
|
|
306
386
|
reviewer,
|
|
307
387
|
worktreePath: input.worktreePath,
|
|
308
388
|
});
|
|
@@ -336,7 +416,11 @@ async function runCloseReconsideration(input) {
|
|
|
336
416
|
}
|
|
337
417
|
},
|
|
338
418
|
options: reviewer.options,
|
|
339
|
-
parse:
|
|
419
|
+
parse: (text) => {
|
|
420
|
+
const output = parseCloseReconsiderationOutput(text);
|
|
421
|
+
validateInlineCommentTargets(output.findings, input.inlineCommentTargets);
|
|
422
|
+
return output;
|
|
423
|
+
},
|
|
340
424
|
permission: reviewer.permission,
|
|
341
425
|
prompt,
|
|
342
426
|
repairAttempts: input.reviewInput.config.output?.repairAttempts ?? 3,
|
|
@@ -397,6 +481,7 @@ export async function runReview(input) {
|
|
|
397
481
|
dryRun: input.dryRun,
|
|
398
482
|
outputs: {},
|
|
399
483
|
posted: {},
|
|
484
|
+
pr: input.pr,
|
|
400
485
|
repository: input.repository,
|
|
401
486
|
safety,
|
|
402
487
|
});
|
|
@@ -448,6 +533,15 @@ export async function runReview(input) {
|
|
|
448
533
|
pr: input.pr,
|
|
449
534
|
}), ...(input.runId ? [input.runId] : []));
|
|
450
535
|
await mkdir(outputDir, { recursive: true });
|
|
536
|
+
await input.onProgress?.({ phase: "fetching review context", type: "phase" });
|
|
537
|
+
const reviewContextSnapshot = await buildReviewContextSnapshot({
|
|
538
|
+
exec,
|
|
539
|
+
pr: meta,
|
|
540
|
+
repository: input.repository,
|
|
541
|
+
});
|
|
542
|
+
const reviewContext = renderReviewContext(reviewContextSnapshot);
|
|
543
|
+
await writeFile(join(outputDir, "review-context.json"), JSON.stringify(reviewContextSnapshot, null, 2));
|
|
544
|
+
await writeFile(join(outputDir, "review-context.md"), `${reviewContext}\n`);
|
|
451
545
|
await input.onProgress?.({ phase: "waiting for checks", type: "phase" });
|
|
452
546
|
const checkResult = await waitForChecksWithClassification({
|
|
453
547
|
client: input.client,
|
|
@@ -518,6 +612,7 @@ export async function runReview(input) {
|
|
|
518
612
|
return [];
|
|
519
613
|
return [{ assignment, reviewer }];
|
|
520
614
|
});
|
|
615
|
+
const inlineCommentTargets = parseRightSideDiffTargets(await exec(`git diff --no-ext-diff --unified=3 ${shellQuote(meta.baseRefOid)} ${shellQuote(meta.headRefOid)}`, { cwd: worktreePath }));
|
|
521
616
|
for (const reviewer of input.repository.agents.reviewers) {
|
|
522
617
|
const assignment = mode.assignments.get(reviewer.account);
|
|
523
618
|
if (assignment?.type !== "skip")
|
|
@@ -548,6 +643,7 @@ export async function runReview(input) {
|
|
|
548
643
|
previousReview: previousReviewText(previous),
|
|
549
644
|
previousHeadSha: previous.commit.oid,
|
|
550
645
|
repository: input.repository,
|
|
646
|
+
reviewContext,
|
|
551
647
|
reviewer,
|
|
552
648
|
unresolvedThreads: JSON.stringify(unresolved, null, 2),
|
|
553
649
|
worktreePath,
|
|
@@ -582,7 +678,7 @@ export async function runReview(input) {
|
|
|
582
678
|
}
|
|
583
679
|
},
|
|
584
680
|
options: reviewer.options,
|
|
585
|
-
parse:
|
|
681
|
+
parse: (text) => parseRereviewOutputWithInlineTargets(text, inlineCommentTargets),
|
|
586
682
|
permission: reviewer.permission,
|
|
587
683
|
prompt,
|
|
588
684
|
repairAttempts: input.config.output?.repairAttempts ?? 3,
|
|
@@ -614,6 +710,7 @@ export async function runReview(input) {
|
|
|
614
710
|
headSha: meta.headRefOid,
|
|
615
711
|
pr: input.pr,
|
|
616
712
|
repository: input.repository,
|
|
713
|
+
reviewContext,
|
|
617
714
|
reviewer,
|
|
618
715
|
worktreePath,
|
|
619
716
|
});
|
|
@@ -647,7 +744,7 @@ export async function runReview(input) {
|
|
|
647
744
|
}
|
|
648
745
|
},
|
|
649
746
|
options: reviewer.options,
|
|
650
|
-
parse:
|
|
747
|
+
parse: (text) => parseReviewOutputWithInlineTargets(text, inlineCommentTargets),
|
|
651
748
|
permission: reviewer.permission,
|
|
652
749
|
prompt,
|
|
653
750
|
repairAttempts: input.config.output?.repairAttempts ?? 3,
|
|
@@ -707,8 +804,10 @@ export async function runReview(input) {
|
|
|
707
804
|
});
|
|
708
805
|
entries = await runCloseReconsideration({
|
|
709
806
|
entries: [...entries, ...skippedCloseEntries],
|
|
807
|
+
inlineCommentTargets,
|
|
710
808
|
meta,
|
|
711
809
|
outputDir,
|
|
810
|
+
reviewContext,
|
|
712
811
|
reviewInput: { ...input, exec },
|
|
713
812
|
sessionIds,
|
|
714
813
|
targets: closeTargets,
|
|
@@ -718,11 +817,19 @@ export async function runReview(input) {
|
|
|
718
817
|
entries,
|
|
719
818
|
meta,
|
|
720
819
|
outputDir,
|
|
820
|
+
reviewContext,
|
|
721
821
|
reviewInput: { ...input, exec },
|
|
722
822
|
sessionIds,
|
|
723
823
|
worktreePath,
|
|
724
824
|
});
|
|
725
|
-
const
|
|
825
|
+
const activeOutputs = validation.outputs;
|
|
826
|
+
const skippedOutputs = Object.fromEntries(input.repository.agents.reviewers.flatMap((reviewer) => {
|
|
827
|
+
const assignment = mode.assignments.get(reviewer.account);
|
|
828
|
+
return assignment?.type === "skip"
|
|
829
|
+
? [[reviewer.key, reviewOutputFromState(assignment.review)]]
|
|
830
|
+
: [];
|
|
831
|
+
}));
|
|
832
|
+
const outputs = { ...skippedOutputs, ...activeOutputs };
|
|
726
833
|
const remainingSkippedVerdicts = input.repository.agents.reviewers.flatMap((reviewer) => {
|
|
727
834
|
const assignment = mode.assignments.get(reviewer.account);
|
|
728
835
|
if (assignment?.type !== "skip" || closeTargets.includes(reviewer.key))
|
|
@@ -734,7 +841,7 @@ export async function runReview(input) {
|
|
|
734
841
|
},
|
|
735
842
|
];
|
|
736
843
|
});
|
|
737
|
-
const activeVerdicts = Object.entries(
|
|
844
|
+
const activeVerdicts = Object.entries(activeOutputs).map(([reviewer, output]) => ({
|
|
738
845
|
reviewer,
|
|
739
846
|
verdict: output.verdict,
|
|
740
847
|
}));
|
|
@@ -747,7 +854,7 @@ export async function runReview(input) {
|
|
|
747
854
|
? [[reviewer.key, "skipped: already reviewed current head"]]
|
|
748
855
|
: [];
|
|
749
856
|
})),
|
|
750
|
-
...Object.fromEntries(await Promise.all(Object.entries(
|
|
857
|
+
...Object.fromEntries(await Promise.all(Object.entries(activeOutputs).map(async ([key, output]) => [
|
|
751
858
|
key,
|
|
752
859
|
input.dryRun
|
|
753
860
|
? dryRunReviewPost(key, output)
|
|
@@ -793,6 +900,7 @@ export async function runReview(input) {
|
|
|
793
900
|
dryRun: input.dryRun,
|
|
794
901
|
outputs,
|
|
795
902
|
posted,
|
|
903
|
+
pr: input.pr,
|
|
796
904
|
repository: input.repository,
|
|
797
905
|
});
|
|
798
906
|
await writeFile(join(outputDir, "report.md"), `${report}\n`);
|