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
package/dist/index.js
CHANGED
|
@@ -95,16 +95,27 @@ export function parseRunArguments(value, dryRun = false, command = "review") {
|
|
|
95
95
|
const tokens = value.split(/[\s,]+/).filter(Boolean);
|
|
96
96
|
const configOverrides = {};
|
|
97
97
|
const prTokens = [];
|
|
98
|
+
let sync = false;
|
|
99
|
+
let timeoutMs;
|
|
98
100
|
for (let index = 0; index < tokens.length; index++) {
|
|
99
101
|
const token = tokens[index];
|
|
100
102
|
if (token === "--dry-run") {
|
|
101
103
|
dryRun = true;
|
|
102
104
|
continue;
|
|
103
105
|
}
|
|
106
|
+
if (token === "--sync") {
|
|
107
|
+
sync = true;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
104
110
|
switch (token) {
|
|
105
111
|
case "--language":
|
|
106
112
|
setConfigOverride(configOverrides, ["language"], nextFlagValue(tokens, ++index, token));
|
|
107
113
|
break;
|
|
114
|
+
case "--timeout":
|
|
115
|
+
timeoutMs =
|
|
116
|
+
parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 0) *
|
|
117
|
+
1_000;
|
|
118
|
+
break;
|
|
108
119
|
case "--merge":
|
|
109
120
|
case "--no-merge":
|
|
110
121
|
setConfigOverride(configOverrides, [command, "automation", "merge"], token === "--merge");
|
|
@@ -146,22 +157,39 @@ export function parseRunArguments(value, dryRun = false, command = "review") {
|
|
|
146
157
|
prTokens.push(token);
|
|
147
158
|
}
|
|
148
159
|
}
|
|
149
|
-
return {
|
|
160
|
+
return {
|
|
161
|
+
configOverrides,
|
|
162
|
+
dryRun,
|
|
163
|
+
prs: parsePrs(prTokens.join(" ")),
|
|
164
|
+
sync,
|
|
165
|
+
timeoutMs,
|
|
166
|
+
};
|
|
150
167
|
}
|
|
151
168
|
export function parseIssueRunArguments(value, dryRun = false) {
|
|
152
169
|
const tokens = value.split(/[\s,]+/).filter(Boolean);
|
|
153
170
|
const configOverrides = {};
|
|
154
171
|
const issueTokens = [];
|
|
172
|
+
let sync = false;
|
|
173
|
+
let timeoutMs;
|
|
155
174
|
for (let index = 0; index < tokens.length; index++) {
|
|
156
175
|
const token = tokens[index];
|
|
157
176
|
if (token === "--dry-run") {
|
|
158
177
|
dryRun = true;
|
|
159
178
|
continue;
|
|
160
179
|
}
|
|
180
|
+
if (token === "--sync") {
|
|
181
|
+
sync = true;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
161
184
|
switch (token) {
|
|
162
185
|
case "--language":
|
|
163
186
|
setConfigOverride(configOverrides, ["language"], nextFlagValue(tokens, ++index, token));
|
|
164
187
|
break;
|
|
188
|
+
case "--timeout":
|
|
189
|
+
timeoutMs =
|
|
190
|
+
parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 0) *
|
|
191
|
+
1_000;
|
|
192
|
+
break;
|
|
165
193
|
case "--close":
|
|
166
194
|
case "--no-close":
|
|
167
195
|
setConfigOverride(configOverrides, ["triage", "automation", "close"], token === "--close");
|
|
@@ -195,7 +223,13 @@ export function parseIssueRunArguments(value, dryRun = false) {
|
|
|
195
223
|
issueTokens.push(token);
|
|
196
224
|
}
|
|
197
225
|
}
|
|
198
|
-
return {
|
|
226
|
+
return {
|
|
227
|
+
configOverrides,
|
|
228
|
+
dryRun,
|
|
229
|
+
issues: parseIssues(issueTokens.join(" ")),
|
|
230
|
+
sync,
|
|
231
|
+
timeoutMs,
|
|
232
|
+
};
|
|
199
233
|
}
|
|
200
234
|
function nextFlagValue(tokens, index, flag) {
|
|
201
235
|
const value = tokens[index];
|
|
@@ -203,6 +237,15 @@ function nextFlagValue(tokens, index, flag) {
|
|
|
203
237
|
throw new Error(`${flag} requires a value.`);
|
|
204
238
|
return value;
|
|
205
239
|
}
|
|
240
|
+
async function syncResult(runManager, states) {
|
|
241
|
+
const output = await runManager.formatStatesWithReports(states, {
|
|
242
|
+
verbose: true,
|
|
243
|
+
});
|
|
244
|
+
const failed = states.filter((state) => state.status !== "completed");
|
|
245
|
+
if (failed.length)
|
|
246
|
+
throw new Error(output);
|
|
247
|
+
return output;
|
|
248
|
+
}
|
|
206
249
|
function parseIntegerFlag(value, flag, minimum) {
|
|
207
250
|
const parsed = Number.parseInt(value, 10);
|
|
208
251
|
if (!Number.isInteger(parsed) ||
|
|
@@ -282,6 +325,10 @@ function prMarkdownLink(repository, pr) {
|
|
|
282
325
|
const url = `https://${host}/${repository.github.owner}/${repository.github.repo}/pull/${pr}`;
|
|
283
326
|
return `[#${pr}](${url})`;
|
|
284
327
|
}
|
|
328
|
+
export function formatRunStartMessage(command, repository, pr) {
|
|
329
|
+
const action = command === "merge" ? "merge flow" : "reviewing";
|
|
330
|
+
return `Started ${action} ${prMarkdownLink(repository, pr)}.`;
|
|
331
|
+
}
|
|
285
332
|
function issueMarkdownLink(repository, issue) {
|
|
286
333
|
const host = repository.github.host || "github.com";
|
|
287
334
|
const url = `https://${host}/${repository.github.owner}/${repository.github.repo}/issues/${issue}`;
|
|
@@ -432,6 +479,7 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
432
479
|
args: {
|
|
433
480
|
prs: tool.schema.string(),
|
|
434
481
|
dryRun: tool.schema.boolean().optional(),
|
|
482
|
+
sync: tool.schema.boolean().optional(),
|
|
435
483
|
},
|
|
436
484
|
async execute(args, context) {
|
|
437
485
|
const parsed = parseRunArguments(args.prs, args.dryRun ?? false, "merge");
|
|
@@ -448,6 +496,7 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
448
496
|
if (!validation.ok)
|
|
449
497
|
return JSON.stringify(validation, null, 2);
|
|
450
498
|
const repository = resolveRepository(config);
|
|
499
|
+
const sync = parsed.sync || args.sync === true;
|
|
451
500
|
const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startMerge({
|
|
452
501
|
config,
|
|
453
502
|
dryRun: parsed.dryRun,
|
|
@@ -455,9 +504,13 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
455
504
|
pr,
|
|
456
505
|
parentSessionId: context.sessionID,
|
|
457
506
|
signal: context.abort,
|
|
507
|
+
sync,
|
|
508
|
+
timeoutMs: parsed.timeoutMs,
|
|
458
509
|
}), { signal: context.abort });
|
|
510
|
+
if (sync)
|
|
511
|
+
return syncResult(runManager, states);
|
|
459
512
|
return states
|
|
460
|
-
.map((state) =>
|
|
513
|
+
.map((state) => formatRunStartMessage("merge", repository, state.pr))
|
|
461
514
|
.join("\n");
|
|
462
515
|
},
|
|
463
516
|
}),
|
|
@@ -469,6 +522,7 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
469
522
|
args: {
|
|
470
523
|
prs: tool.schema.string(),
|
|
471
524
|
dryRun: tool.schema.boolean().optional(),
|
|
525
|
+
sync: tool.schema.boolean().optional(),
|
|
472
526
|
},
|
|
473
527
|
async execute(args, context) {
|
|
474
528
|
const parsed = parseRunArguments(args.prs, args.dryRun ?? false);
|
|
@@ -484,6 +538,7 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
484
538
|
if (!validation.ok)
|
|
485
539
|
return JSON.stringify(validation, null, 2);
|
|
486
540
|
const repository = resolveRepository(config);
|
|
541
|
+
const sync = parsed.sync || args.sync === true;
|
|
487
542
|
const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startReview({
|
|
488
543
|
config,
|
|
489
544
|
dryRun: parsed.dryRun,
|
|
@@ -491,9 +546,13 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
491
546
|
pr,
|
|
492
547
|
parentSessionId: context.sessionID,
|
|
493
548
|
signal: context.abort,
|
|
549
|
+
sync,
|
|
550
|
+
timeoutMs: parsed.timeoutMs,
|
|
494
551
|
}), { signal: context.abort });
|
|
552
|
+
if (sync)
|
|
553
|
+
return syncResult(runManager, states);
|
|
495
554
|
return states
|
|
496
|
-
.map((state) =>
|
|
555
|
+
.map((state) => formatRunStartMessage("review", repository, state.pr))
|
|
497
556
|
.join("\n");
|
|
498
557
|
},
|
|
499
558
|
}),
|
|
@@ -502,6 +561,7 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
502
561
|
args: {
|
|
503
562
|
issues: tool.schema.string(),
|
|
504
563
|
dryRun: tool.schema.boolean().optional(),
|
|
564
|
+
sync: tool.schema.boolean().optional(),
|
|
505
565
|
},
|
|
506
566
|
async execute(args, context) {
|
|
507
567
|
const parsed = parseIssueRunArguments(args.issues, args.dryRun ?? false);
|
|
@@ -523,6 +583,7 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
523
583
|
const repository = resolveRepository(config);
|
|
524
584
|
if (!repository.triage)
|
|
525
585
|
return JSON.stringify({ errors: ["triage configuration is required"], ok: false }, null, 2);
|
|
586
|
+
const sync = parsed.sync || args.sync === true;
|
|
526
587
|
const states = await mapPool(parsed.issues, repository.triage.concurrency.runs, (issue) => runManager.startTriage({
|
|
527
588
|
config,
|
|
528
589
|
dryRun: parsed.dryRun,
|
|
@@ -530,7 +591,11 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
530
591
|
parentSessionId: context.sessionID,
|
|
531
592
|
repository,
|
|
532
593
|
signal: context.abort,
|
|
594
|
+
sync,
|
|
595
|
+
timeoutMs: parsed.timeoutMs,
|
|
533
596
|
}), { signal: context.abort });
|
|
597
|
+
if (sync)
|
|
598
|
+
return syncResult(runManager, states);
|
|
534
599
|
return states
|
|
535
600
|
.map((state) => `Started triaging ${issueMarkdownLink(repository, state.issue)}.`)
|
|
536
601
|
.join("\n");
|
package/dist/orchestrator/ci.js
CHANGED
|
@@ -176,7 +176,15 @@ function ciFailureContextForClassified(items, classified) {
|
|
|
176
176
|
return "";
|
|
177
177
|
return [
|
|
178
178
|
"CI has scope-in failures that may be caused by this PR.",
|
|
179
|
-
"
|
|
179
|
+
"Treat these failures as blocking review issues until the checks pass.",
|
|
180
|
+
[
|
|
181
|
+
"Do not approve this PR while this ci_failure_context is present.",
|
|
182
|
+
"Return CHANGES_REQUESTED and include a finding for each failing CI check.",
|
|
183
|
+
].join(" "),
|
|
184
|
+
[
|
|
185
|
+
"Still inspect the PR diff before reporting findings.",
|
|
186
|
+
"If a CI failure does not map to an exact changed line, anchor the finding to the nearest responsible or first relevant changed line.",
|
|
187
|
+
].join(" "),
|
|
180
188
|
"",
|
|
181
189
|
...sections,
|
|
182
190
|
].join("\n\n");
|
|
@@ -343,6 +351,7 @@ async function classifyChecks(input) {
|
|
|
343
351
|
}
|
|
344
352
|
},
|
|
345
353
|
options: reviewer.options,
|
|
354
|
+
parentSessionId: input.parentSessionId,
|
|
346
355
|
parse: (text) => {
|
|
347
356
|
const output = parseCiClassificationOutput(text);
|
|
348
357
|
for (const check of output.checks) {
|
|
@@ -366,18 +375,20 @@ async function classifyChecks(input) {
|
|
|
366
375
|
const rawPath = input.outputDir
|
|
367
376
|
? join(input.outputDir, `${reviewer.key}.ci-classification.raw.txt`)
|
|
368
377
|
: undefined;
|
|
369
|
-
const
|
|
378
|
+
const checks = result.value.checks.map((check) => ({
|
|
379
|
+
classification: check.classification,
|
|
380
|
+
name: check.name,
|
|
381
|
+
reason: check.reason,
|
|
382
|
+
}));
|
|
370
383
|
if (rawPath)
|
|
371
384
|
await writeFile(rawPath, result.raw);
|
|
372
|
-
run.
|
|
385
|
+
run.checks = checks;
|
|
373
386
|
run.rawPath = rawPath;
|
|
374
|
-
run.reason = check?.reason;
|
|
375
387
|
run.sessionId = result.sessionId;
|
|
376
388
|
run.status = "completed";
|
|
377
389
|
await input.onClassifierProgress?.({
|
|
378
|
-
|
|
390
|
+
checks,
|
|
379
391
|
rawPath,
|
|
380
|
-
reason: check?.reason ?? "No classification reason was provided.",
|
|
381
392
|
reviewer: reviewer.key,
|
|
382
393
|
sessionId: result.sessionId,
|
|
383
394
|
type: "classifier_completed",
|
|
@@ -392,22 +403,20 @@ async function classifyChecks(input) {
|
|
|
392
403
|
reviewer: reviewer.key,
|
|
393
404
|
type: "classifier_failed",
|
|
394
405
|
});
|
|
395
|
-
|
|
406
|
+
throw error;
|
|
396
407
|
}
|
|
397
408
|
}, { signal: input.signal });
|
|
398
409
|
const threshold = majorityThreshold(reviewers.length);
|
|
399
410
|
return {
|
|
400
411
|
classified: input.checks.map((item) => {
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
const check = vote.output?.checks.find((output) => output.name === item.check.name);
|
|
412
|
+
const checkVotes = votes.map((vote) => {
|
|
413
|
+
const check = vote.output.checks.find((output) => output.name === item.check.name);
|
|
404
414
|
return {
|
|
405
415
|
classification: check?.classification ?? "SCOPE_IN",
|
|
406
416
|
reason: check?.reason ?? "Missing classification; treated as scope-in.",
|
|
407
417
|
reviewer: vote.reviewer,
|
|
408
418
|
};
|
|
409
419
|
});
|
|
410
|
-
const failures = votes.filter((vote) => !vote.output);
|
|
411
420
|
const scopeIn = checkVotes.filter((vote) => vote.classification === "SCOPE_IN");
|
|
412
421
|
const scopeOut = checkVotes.filter((vote) => vote.classification === "SCOPE_OUT");
|
|
413
422
|
const classification = scopeOut.length >= threshold
|
|
@@ -421,9 +430,6 @@ async function classifyChecks(input) {
|
|
|
421
430
|
const reasons = checkVotes
|
|
422
431
|
.filter((vote) => vote.classification === classification)
|
|
423
432
|
.map((vote) => `${vote.reviewer}: ${vote.reason}`);
|
|
424
|
-
for (const failure of failures) {
|
|
425
|
-
reasons.push(`${failure.reviewer}: classifier failed; vote ignored`);
|
|
426
|
-
}
|
|
427
433
|
return {
|
|
428
434
|
check: item.check,
|
|
429
435
|
classification,
|
|
@@ -505,6 +511,7 @@ export async function waitForChecksWithClassification(input) {
|
|
|
505
511
|
directory: input.directory,
|
|
506
512
|
onClassifierProgress: input.onClassifierProgress,
|
|
507
513
|
outputDir: input.outputDir,
|
|
514
|
+
parentSessionId: input.parentSessionId,
|
|
508
515
|
pr: input.pr,
|
|
509
516
|
repairAttempts: input.repairAttempts,
|
|
510
517
|
repository: input.repository,
|
|
@@ -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,15 +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
|
});
|
|
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));
|
|
61
82
|
next[reviewer] =
|
|
62
|
-
|
|
63
|
-
? { ...output,
|
|
64
|
-
: {
|
|
83
|
+
newFindings.length || output.followUps.length
|
|
84
|
+
? { ...output, newFindings }
|
|
85
|
+
: { ...output, newFindings, verdict: "MERGE" };
|
|
65
86
|
}
|
|
66
87
|
return { outputs: next, summary: { discarded, kept } };
|
|
67
88
|
}
|
|
@@ -52,12 +52,6 @@ function assertPositiveInteger(value, name) {
|
|
|
52
52
|
export function validateInlineCommentTargets(findings, targets, label = "findings") {
|
|
53
53
|
for (const [index, finding] of findings.entries()) {
|
|
54
54
|
const name = `${label}[${index}]`;
|
|
55
|
-
if (finding.line == null) {
|
|
56
|
-
if (finding.startLine != null) {
|
|
57
|
-
throw new Error(`${name}.startLine requires line`);
|
|
58
|
-
}
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
55
|
assertPositiveInteger(finding.line, `${name}.line`);
|
|
62
56
|
if (finding.startLine != null) {
|
|
63
57
|
assertPositiveInteger(finding.startLine, `${name}.startLine`);
|
|
@@ -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,
|
|
@@ -146,14 +147,14 @@ async function postRereviewOutput(input, reviewerKey, output) {
|
|
|
146
147
|
if (output.verdict === "CLOSE") {
|
|
147
148
|
return postCloseComment(input.exec, input.repository, input.pr, reviewer.account, output.reason ?? "Close requested.");
|
|
148
149
|
}
|
|
149
|
-
if (output.newFindings.length
|
|
150
|
+
if (output.newFindings.length) {
|
|
150
151
|
return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.newFindings.map((finding) => ({
|
|
151
152
|
fix: "Please address this before merging.",
|
|
152
153
|
issue: finding.body,
|
|
153
154
|
path: finding.path,
|
|
154
|
-
|
|
155
|
+
line: finding.line,
|
|
155
156
|
startLine: finding.startLine,
|
|
156
|
-
}))
|
|
157
|
+
})));
|
|
157
158
|
}
|
|
158
159
|
return replies[0] ?? "";
|
|
159
160
|
}
|
|
@@ -166,52 +167,50 @@ function newFindingToEditorFinding(reviewer, finding) {
|
|
|
166
167
|
return {
|
|
167
168
|
body: finding.body,
|
|
168
169
|
fix: "Please address this before merging.",
|
|
170
|
+
line: finding.line,
|
|
169
171
|
path: finding.path,
|
|
170
172
|
reviewer,
|
|
171
|
-
...(finding.line == null ? {} : { line: finding.line }),
|
|
172
173
|
...(finding.startLine == null ? {} : { startLine: finding.startLine }),
|
|
173
|
-
type:
|
|
174
|
+
type: "inline",
|
|
174
175
|
};
|
|
175
176
|
}
|
|
176
177
|
export function blockingReviewFindings(outputs) {
|
|
177
178
|
return Object.entries(outputs).flatMap(([reviewer, output]) => {
|
|
178
179
|
if (output.verdict !== "CHANGES_REQUESTED")
|
|
179
180
|
return [];
|
|
180
|
-
const requirementFindings = output.requirementFindings.map((finding) => ({
|
|
181
|
-
evidence: finding.evidence,
|
|
182
|
-
fix: finding.fix,
|
|
183
|
-
issueNumber: finding.issueNumber,
|
|
184
|
-
requirement: finding.requirement,
|
|
185
|
-
reviewer,
|
|
186
|
-
type: "requirement",
|
|
187
|
-
}));
|
|
188
181
|
if ("findings" in output) {
|
|
189
|
-
return
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
: { startLine: finding.startLine }),
|
|
199
|
-
type: finding.line == null ? "file" : "inline",
|
|
200
|
-
})),
|
|
201
|
-
...requirementFindings,
|
|
202
|
-
];
|
|
182
|
+
return output.findings.map((finding) => ({
|
|
183
|
+
fix: finding.fix,
|
|
184
|
+
issue: finding.issue,
|
|
185
|
+
line: finding.line,
|
|
186
|
+
path: finding.path,
|
|
187
|
+
reviewer,
|
|
188
|
+
...(finding.startLine == null ? {} : { startLine: finding.startLine }),
|
|
189
|
+
type: "inline",
|
|
190
|
+
}));
|
|
203
191
|
}
|
|
204
|
-
return
|
|
205
|
-
...output.newFindings.map((finding) => newFindingToEditorFinding(reviewer, finding)),
|
|
206
|
-
...requirementFindings,
|
|
207
|
-
];
|
|
192
|
+
return output.newFindings.map((finding) => newFindingToEditorFinding(reviewer, finding));
|
|
208
193
|
});
|
|
209
194
|
}
|
|
210
195
|
async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionIds, ciFailureContext, options = {}) {
|
|
211
196
|
throwIfAborted(input.signal);
|
|
212
197
|
const meta = await fetchPullRequest(input.exec, input.repository, input.pr);
|
|
213
198
|
const headSha = options.dryRunHeadSha ?? meta.headRefOid;
|
|
214
|
-
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
|
+
});
|
|
215
214
|
const artifactDir = outputDir(input);
|
|
216
215
|
let entries = await mapPool(input.repository.agents.reviewers, input.repository.concurrency.reviewers, async (reviewer) => {
|
|
217
216
|
throwIfAborted(input.signal);
|
|
@@ -266,6 +265,7 @@ async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionI
|
|
|
266
265
|
}
|
|
267
266
|
},
|
|
268
267
|
options: reviewer.options,
|
|
268
|
+
parentSessionId: input.parentSessionId,
|
|
269
269
|
parse: (text) => parseRereviewOutputWithInlineTargets(text, inlineCommentTargets),
|
|
270
270
|
permission: reviewer.permission,
|
|
271
271
|
prompt,
|
|
@@ -310,7 +310,7 @@ async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionI
|
|
|
310
310
|
baseSha: meta.baseRefOid,
|
|
311
311
|
closeReason: entry.output.reason,
|
|
312
312
|
directory: input.directory,
|
|
313
|
-
headSha
|
|
313
|
+
headSha,
|
|
314
314
|
includeReviewGuidelines: !hasReviewerSession,
|
|
315
315
|
includeSessionContext: !hasReviewerSession,
|
|
316
316
|
pr: input.pr,
|
|
@@ -349,6 +349,7 @@ async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionI
|
|
|
349
349
|
}
|
|
350
350
|
},
|
|
351
351
|
options: reviewer.options,
|
|
352
|
+
parentSessionId: input.parentSessionId,
|
|
352
353
|
parse: (text) => {
|
|
353
354
|
const output = parseRereviewCloseReconsiderationOutput(text);
|
|
354
355
|
validateInlineCommentTargets(output.newFindings, inlineCommentTargets, "newFindings");
|
|
@@ -425,8 +426,11 @@ async function finishMergeRun(input, result, reportInput) {
|
|
|
425
426
|
}
|
|
426
427
|
async function mergeWithQueue(input, exec, editorAccount) {
|
|
427
428
|
await mergePullRequest(exec, input.repository, input.pr, editorAccount);
|
|
428
|
-
if (!input.repository.merge.mergeQueue)
|
|
429
|
-
|
|
429
|
+
if (!input.repository.merge.mergeQueue) {
|
|
430
|
+
if (!input.repository.merge.auto)
|
|
431
|
+
return "merged";
|
|
432
|
+
return waitForAutoMerge(exec, input.repository, input.pr);
|
|
433
|
+
}
|
|
430
434
|
return waitForMergeQueue(exec, input.repository, input.pr);
|
|
431
435
|
}
|
|
432
436
|
export function hasBlockingCiReports(reports) {
|
|
@@ -507,8 +511,6 @@ function syntheticReviewThreads(outputs) {
|
|
|
507
511
|
for (const [reviewer, output] of Object.entries(outputs)) {
|
|
508
512
|
if ("findings" in output) {
|
|
509
513
|
threads[reviewer] = output.findings.flatMap((finding) => {
|
|
510
|
-
if (finding.line == null)
|
|
511
|
-
return [];
|
|
512
514
|
const commentId = nextCommentId--;
|
|
513
515
|
return [
|
|
514
516
|
{
|
|
@@ -531,8 +533,6 @@ function syntheticReviewThreads(outputs) {
|
|
|
531
533
|
continue;
|
|
532
534
|
}
|
|
533
535
|
threads[reviewer] = output.newFindings.flatMap((finding) => {
|
|
534
|
-
if (finding.line == null)
|
|
535
|
-
return [];
|
|
536
536
|
const commentId = nextCommentId--;
|
|
537
537
|
return [
|
|
538
538
|
{
|
|
@@ -590,7 +590,7 @@ export async function runMerge(input) {
|
|
|
590
590
|
const abortableInput = { ...input, exec };
|
|
591
591
|
const editor = input.repository.agents.editor;
|
|
592
592
|
if (!editor)
|
|
593
|
-
throw new Error("
|
|
593
|
+
throw new Error("merge.editor is required for magi_merge");
|
|
594
594
|
throwIfAborted(input.signal);
|
|
595
595
|
const artifactDir = outputDir(input);
|
|
596
596
|
await mkdir(artifactDir, { recursive: true });
|
|
@@ -721,9 +721,7 @@ export async function runMerge(input) {
|
|
|
721
721
|
threads: unresolvedThreads,
|
|
722
722
|
});
|
|
723
723
|
const editorFindings = blockingReviewFindings(reportOutputs);
|
|
724
|
-
const editableFindings =
|
|
725
|
-
? editorFindings
|
|
726
|
-
: editorFindings.filter((finding) => finding.type !== "inline");
|
|
724
|
+
const editableFindings = editorFindings;
|
|
727
725
|
const findingAttemptsExhausted = input.repository.merge.maxThreadResolutionCycles !== 0 &&
|
|
728
726
|
cycle > input.repository.merge.maxThreadResolutionCycles;
|
|
729
727
|
if (!editableThreads.length &&
|
|
@@ -803,6 +801,7 @@ export async function runMerge(input) {
|
|
|
803
801
|
exec,
|
|
804
802
|
headSha: editedHeadSha,
|
|
805
803
|
onProgress: (phase) => input.onProgress?.({ phase, type: "phase" }),
|
|
804
|
+
parentSessionId: input.parentSessionId,
|
|
806
805
|
pr: input.pr,
|
|
807
806
|
repairAttempts: input.config.output?.repairAttempts ?? 3,
|
|
808
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
|
},
|