opencode-magi 0.6.1 → 0.8.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 +16 -6
- package/dist/config/resolve.js +40 -3
- package/dist/config/validate.js +231 -66
- package/dist/github/commands.js +32 -0
- package/dist/index.js +21 -50
- package/dist/orchestrator/merge.js +310 -10
- package/dist/orchestrator/report.js +1 -1
- package/dist/orchestrator/review.js +97 -1
- package/dist/orchestrator/run-manager.js +4 -4
- package/dist/orchestrator/triage.js +312 -103
- package/dist/prompts/compose.js +59 -2
- package/dist/prompts/contracts.js +20 -1
- package/dist/prompts/output.js +19 -1
- package/dist/prompts/templates/merge/conflict.md +10 -0
- package/dist/prompts/templates/review/rereview.md +2 -0
- package/dist/prompts/templates/review/review.md +2 -0
- package/dist/prompts/templates/triage/acceptance.md +1 -1
- package/dist/prompts/templates/triage/signal.md +10 -0
- package/package.json +1 -1
- package/schema.json +89 -16
package/dist/github/commands.js
CHANGED
|
@@ -404,6 +404,15 @@ export async function assignIssue(exec, repository, issue, account) {
|
|
|
404
404
|
const token = await ghToken(exec, repository, account);
|
|
405
405
|
return exec(`gh issue edit ${issue} --repo ${shellQuote(repoSpecifier(repository))} --add-assignee ${shellQuote(account)}`, ghTokenEnv(token));
|
|
406
406
|
}
|
|
407
|
+
export async function addIssueLabels(exec, repository, issue, labels, account) {
|
|
408
|
+
const token = await ghToken(exec, repository, account);
|
|
409
|
+
const added = [];
|
|
410
|
+
for (const label of labels) {
|
|
411
|
+
await exec(`gh issue edit ${issue} --repo ${shellQuote(repoSpecifier(repository))} --add-label ${shellQuote(label)}`, ghTokenEnv(token));
|
|
412
|
+
added.push(label);
|
|
413
|
+
}
|
|
414
|
+
return added;
|
|
415
|
+
}
|
|
407
416
|
export async function removeIssueLabels(exec, repository, issue, labels, account) {
|
|
408
417
|
const token = await ghToken(exec, repository, account);
|
|
409
418
|
const removed = [];
|
|
@@ -685,6 +694,29 @@ export async function waitForAutoMerge(exec, repository, pr, intervalMs = 30_000
|
|
|
685
694
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
686
695
|
}
|
|
687
696
|
}
|
|
697
|
+
export async function fetchBaseBranch(exec, repository, meta, worktreePath) {
|
|
698
|
+
await exec(`git fetch --no-tags ${shellQuote(repositoryGitUrl(repository, repository.github.owner, repository.github.repo))} ${shellQuote(`refs/heads/${meta.baseRefName}`)}`, { cwd: worktreePath });
|
|
699
|
+
}
|
|
700
|
+
export async function mergeBaseNoCommit(exec, baseSha, worktreePath) {
|
|
701
|
+
await exec(`git merge --no-commit --no-ff ${shellQuote(baseSha)}`, {
|
|
702
|
+
cwd: worktreePath,
|
|
703
|
+
}).catch(() => undefined);
|
|
704
|
+
}
|
|
705
|
+
export async function listUnmergedFiles(exec, worktreePath) {
|
|
706
|
+
const output = await exec("git diff --name-only --diff-filter=U", {
|
|
707
|
+
cwd: worktreePath,
|
|
708
|
+
});
|
|
709
|
+
return output
|
|
710
|
+
.split("\n")
|
|
711
|
+
.map((line) => line.trim())
|
|
712
|
+
.filter(Boolean);
|
|
713
|
+
}
|
|
714
|
+
export async function abortMerge(exec, worktreePath) {
|
|
715
|
+
await exec("git merge --abort", { cwd: worktreePath }).catch(() => undefined);
|
|
716
|
+
}
|
|
717
|
+
export async function currentHeadSha(exec, worktreePath) {
|
|
718
|
+
return (await exec("git rev-parse HEAD", { cwd: worktreePath })).trim();
|
|
719
|
+
}
|
|
688
720
|
export async function closePullRequest(exec, repository, pr, account) {
|
|
689
721
|
const token = await ghToken(exec, repository, account);
|
|
690
722
|
return exec(`gh pr close ${pr} --repo ${shellQuote(repoSpecifier(repository))}`, ghTokenEnv(token));
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { loadConfig, mergeMagiConfig } from "./config/load";
|
|
|
9
9
|
import { outputBaseDirs } from "./config/output";
|
|
10
10
|
import { worktreeBaseDirs } from "./config/worktree";
|
|
11
11
|
import { resolveRepository } from "./config/resolve";
|
|
12
|
-
import { validateConfig } from "./config/validate";
|
|
12
|
+
import { validateConfig, } from "./config/validate";
|
|
13
13
|
import { withGitHubApiRetry } from "./github/retry";
|
|
14
14
|
import { mapPool } from "./orchestrator/pool";
|
|
15
15
|
import { MagiRunManager } from "./orchestrator/run-manager";
|
|
@@ -286,23 +286,6 @@ function parseOptionalIssue(value) {
|
|
|
286
286
|
function clearFlag(value) {
|
|
287
287
|
return typeof value === "boolean" ? value : undefined;
|
|
288
288
|
}
|
|
289
|
-
function clearToolFlag(value) {
|
|
290
|
-
if (value === true || value === "true")
|
|
291
|
-
return true;
|
|
292
|
-
if (value === "false")
|
|
293
|
-
return false;
|
|
294
|
-
return undefined;
|
|
295
|
-
}
|
|
296
|
-
function hasBlankSelector(args) {
|
|
297
|
-
return !args.runId?.trim() && !args.pr?.trim();
|
|
298
|
-
}
|
|
299
|
-
function hasDefaultedFalseClearFlags(args) {
|
|
300
|
-
return (hasBlankSelector(args) &&
|
|
301
|
-
args.branch === "false" &&
|
|
302
|
-
args.output === "false" &&
|
|
303
|
-
args.session === "false" &&
|
|
304
|
-
args.worktree === "false");
|
|
305
|
-
}
|
|
306
289
|
function parseQuestionAnswers(value) {
|
|
307
290
|
const trimmed = value.trim();
|
|
308
291
|
if (!trimmed)
|
|
@@ -334,6 +317,9 @@ function issueMarkdownLink(repository, issue) {
|
|
|
334
317
|
const url = `https://${host}/${repository.github.owner}/${repository.github.repo}/issues/${issue}`;
|
|
335
318
|
return `[#${issue}](${url})`;
|
|
336
319
|
}
|
|
320
|
+
function validationError(validation) {
|
|
321
|
+
return new Error(JSON.stringify(validation, null, 2));
|
|
322
|
+
}
|
|
337
323
|
function isPlainObject(value) {
|
|
338
324
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
339
325
|
}
|
|
@@ -395,7 +381,8 @@ export async function validateMagiConfigFiles(directory, options = {}) {
|
|
|
395
381
|
? withGitHubApiRetry(options.exec, mergedConfig.github?.apiRetryAttempts ?? 3)
|
|
396
382
|
: undefined,
|
|
397
383
|
modelCatalog: options.modelCatalog,
|
|
398
|
-
requireGithub: hasProjectConfig && Boolean(mergedConfig.review?.
|
|
384
|
+
requireGithub: hasProjectConfig && Boolean(mergedConfig.review?.reviewers),
|
|
385
|
+
requireModelCatalog: true,
|
|
399
386
|
requireWorktreeConfig: true,
|
|
400
387
|
});
|
|
401
388
|
loadedFrom = existing.map((status) => status.path).join(", ");
|
|
@@ -447,7 +434,8 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
447
434
|
.then(extractModelCatalog)
|
|
448
435
|
.catch(() => catalogClient.provider
|
|
449
436
|
?.list({ query: { directory } })
|
|
450
|
-
.then(extractModelCatalog))
|
|
437
|
+
.then(extractModelCatalog))
|
|
438
|
+
.catch(() => undefined);
|
|
451
439
|
return modelCatalogPromise;
|
|
452
440
|
}
|
|
453
441
|
return {
|
|
@@ -492,9 +480,10 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
492
480
|
exec: retryingExec,
|
|
493
481
|
modelCatalog: await modelCatalog(),
|
|
494
482
|
requireEditor: true,
|
|
483
|
+
requireModelCatalog: true,
|
|
495
484
|
});
|
|
496
485
|
if (!validation.ok)
|
|
497
|
-
|
|
486
|
+
throw validationError(validation);
|
|
498
487
|
const repository = resolveRepository(config);
|
|
499
488
|
const sync = parsed.sync || args.sync === true;
|
|
500
489
|
const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startMerge({
|
|
@@ -534,9 +523,10 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
534
523
|
directory,
|
|
535
524
|
exec: retryingExec,
|
|
536
525
|
modelCatalog: await modelCatalog(),
|
|
526
|
+
requireModelCatalog: true,
|
|
537
527
|
});
|
|
538
528
|
if (!validation.ok)
|
|
539
|
-
|
|
529
|
+
throw validationError(validation);
|
|
540
530
|
const repository = resolveRepository(config);
|
|
541
531
|
const sync = parsed.sync || args.sync === true;
|
|
542
532
|
const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startReview({
|
|
@@ -557,7 +547,7 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
557
547
|
},
|
|
558
548
|
}),
|
|
559
549
|
magi_triage: tool({
|
|
560
|
-
description: "Triage one or more GitHub issues with configured Magi triage
|
|
550
|
+
description: "Triage one or more GitHub issues with configured Magi triage voters.",
|
|
561
551
|
args: {
|
|
562
552
|
issues: tool.schema.string(),
|
|
563
553
|
dryRun: tool.schema.boolean().optional(),
|
|
@@ -574,12 +564,13 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
574
564
|
exec: retryingExec,
|
|
575
565
|
modelCatalog: await modelCatalog(),
|
|
576
566
|
requireEditor: config.triage?.automation?.merge === true,
|
|
567
|
+
requireModelCatalog: true,
|
|
577
568
|
requireReview: config.triage?.automation?.review === true ||
|
|
578
569
|
config.triage?.automation?.merge === true,
|
|
579
570
|
requireTriage: true,
|
|
580
571
|
});
|
|
581
572
|
if (!validation.ok)
|
|
582
|
-
|
|
573
|
+
throw validationError(validation);
|
|
583
574
|
const repository = resolveRepository(config);
|
|
584
575
|
if (!repository.triage)
|
|
585
576
|
return JSON.stringify({ errors: ["triage configuration is required"], ok: false }, null, 2);
|
|
@@ -692,41 +683,21 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
692
683
|
}),
|
|
693
684
|
magi_clear: tool({
|
|
694
685
|
description: "Clear all inactive Magi runs by deleting configured sessions, worktrees, branches, and output artifacts.",
|
|
695
|
-
args: {
|
|
696
|
-
|
|
697
|
-
pr: tool.schema.string().optional(),
|
|
698
|
-
issue: tool.schema.string().optional(),
|
|
699
|
-
branch: tool.schema.enum(["true", "false"]).optional(),
|
|
700
|
-
output: tool.schema.enum(["true", "false"]).optional(),
|
|
701
|
-
session: tool.schema.enum(["true", "false"]).optional(),
|
|
702
|
-
worktree: tool.schema.enum(["true", "false"]).optional(),
|
|
703
|
-
},
|
|
704
|
-
async execute(args) {
|
|
686
|
+
args: {},
|
|
687
|
+
async execute() {
|
|
705
688
|
const loaded = await loadConfig(directory).catch(() => undefined);
|
|
706
689
|
const clear = loaded?.config.clear;
|
|
707
|
-
const useConfiguredDefaults = hasDefaultedFalseClearFlags(args);
|
|
708
690
|
const options = {
|
|
709
|
-
branch: (
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
? undefined
|
|
714
|
-
: clearToolFlag(args.output)) ?? clearFlag(clear?.output),
|
|
715
|
-
session: (useConfiguredDefaults
|
|
716
|
-
? undefined
|
|
717
|
-
: clearToolFlag(args.session)) ?? clearFlag(clear?.session),
|
|
718
|
-
worktree: (useConfiguredDefaults
|
|
719
|
-
? undefined
|
|
720
|
-
: clearToolFlag(args.worktree)) ?? clearFlag(clear?.worktree),
|
|
691
|
+
branch: clearFlag(clear?.branch),
|
|
692
|
+
output: clearFlag(clear?.output),
|
|
693
|
+
session: clearFlag(clear?.session),
|
|
694
|
+
worktree: clearFlag(clear?.worktree),
|
|
721
695
|
};
|
|
722
696
|
return runManager.clear({
|
|
723
697
|
options,
|
|
724
|
-
issue: parseOptionalIssue(args.issue),
|
|
725
698
|
outputDir: loaded
|
|
726
699
|
? outputBaseDirs(directory, loaded.config)
|
|
727
700
|
: undefined,
|
|
728
|
-
pr: parseOptionalPr(args.pr),
|
|
729
|
-
runId: args.runId,
|
|
730
701
|
worktreeDir: loaded
|
|
731
702
|
? worktreeBaseDirs(directory, loaded.config)
|
|
732
703
|
: undefined,
|
|
@@ -1,8 +1,8 @@
|
|
|
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, waitForAutoMerge, waitForMergeQueue, } from "../github/commands";
|
|
5
|
-
import { composeEditPrompt, composeRereviewCloseReconsiderationPrompt, composeRereviewPrompt, } from "../prompts/compose";
|
|
4
|
+
import { abortMerge, closePullRequest, configureGitIdentity, currentHeadSha, fetchBaseBranch, fetchMergeQueueRequirement, fetchPullRequest, fetchUnresolvedThreads, listUnmergedFiles, mergePullRequest, mergeBaseNoCommit, postApproval, postChangesRequested, postCloseComment, postReply, pushHead, removeWorktree, resolveThread, waitForAutoMerge, waitForMergeQueue, } from "../github/commands";
|
|
5
|
+
import { composeEditPrompt, composeMergeConflictPrompt, 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";
|
|
@@ -433,6 +433,224 @@ async function mergeWithQueue(input, exec, editorAccount) {
|
|
|
433
433
|
}
|
|
434
434
|
return waitForMergeQueue(exec, input.repository, input.pr);
|
|
435
435
|
}
|
|
436
|
+
async function runConflictEditor(input) {
|
|
437
|
+
const editor = input.run.repository.agents.editor;
|
|
438
|
+
if (!editor)
|
|
439
|
+
throw new Error("merge.editor is required for magi_merge");
|
|
440
|
+
await configureGitIdentity(input.run.exec, input.worktreePath, {
|
|
441
|
+
email: editor.author?.email,
|
|
442
|
+
name: editor.author?.name,
|
|
443
|
+
});
|
|
444
|
+
const artifactDir = outputDir(input.run);
|
|
445
|
+
const prompt = await composeMergeConflictPrompt({
|
|
446
|
+
baseBranch: input.baseBranch,
|
|
447
|
+
baseSha: input.baseSha,
|
|
448
|
+
conflictedFiles: JSON.stringify(input.conflictedFiles, null, 2),
|
|
449
|
+
directory: input.run.directory,
|
|
450
|
+
headSha: input.headSha,
|
|
451
|
+
pr: input.run.pr,
|
|
452
|
+
repository: input.run.repository,
|
|
453
|
+
worktreePath: input.worktreePath,
|
|
454
|
+
});
|
|
455
|
+
await input.run.onProgress?.({ cycle: input.cycle, type: "editor_started" });
|
|
456
|
+
const result = await withEditorFailureProgress({
|
|
457
|
+
cycle: input.cycle,
|
|
458
|
+
onProgress: input.run.onProgress,
|
|
459
|
+
run: () => runModelWithRepair({
|
|
460
|
+
client: input.run.client,
|
|
461
|
+
model: editor.model,
|
|
462
|
+
onProgress: async (progress) => {
|
|
463
|
+
if (progress.type === "session_created") {
|
|
464
|
+
await input.run.onProgress?.({
|
|
465
|
+
cycle: input.cycle,
|
|
466
|
+
options: progress.options,
|
|
467
|
+
sessionId: progress.sessionId,
|
|
468
|
+
type: "editor_session",
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
if (progress.type === "repair") {
|
|
472
|
+
await input.run.onProgress?.({
|
|
473
|
+
cycle: input.cycle,
|
|
474
|
+
type: "editor_repair",
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
if (progress.type === "response") {
|
|
478
|
+
await input.run.onProgress?.({
|
|
479
|
+
cycle: input.cycle,
|
|
480
|
+
sessionId: progress.sessionId,
|
|
481
|
+
type: "editor_response",
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
options: editor.options,
|
|
486
|
+
parentSessionId: input.run.parentSessionId,
|
|
487
|
+
parse: parseEditOutput,
|
|
488
|
+
permission: editor.permission,
|
|
489
|
+
prompt,
|
|
490
|
+
repairAttempts: input.run.config.output?.repairAttempts ?? 3,
|
|
491
|
+
schemaName: "edit",
|
|
492
|
+
signal: input.run.signal,
|
|
493
|
+
title: `magi resolve conflict ${input.run.repository.alias}#${input.run.pr}`,
|
|
494
|
+
}),
|
|
495
|
+
});
|
|
496
|
+
await writeFile(join(artifactDir, "editor.conflict.prompt.txt"), prompt);
|
|
497
|
+
await writeFile(join(artifactDir, "editor.conflict.raw.txt"), result.raw);
|
|
498
|
+
await writeFile(join(artifactDir, "editor.conflict.json"), JSON.stringify(result.value, null, 2));
|
|
499
|
+
await input.run.onProgress?.({ cycle: input.cycle, type: "editor_completed" });
|
|
500
|
+
return result.value;
|
|
501
|
+
}
|
|
502
|
+
async function recoverMergeQueueConflict(input) {
|
|
503
|
+
await input.run.onProgress?.({
|
|
504
|
+
phase: "checking merge queue conflict",
|
|
505
|
+
type: "phase",
|
|
506
|
+
});
|
|
507
|
+
const meta = await fetchPullRequest(input.exec, input.run.repository, input.run.pr);
|
|
508
|
+
if (meta.state && meta.state.toUpperCase() !== "OPEN")
|
|
509
|
+
return undefined;
|
|
510
|
+
await fetchBaseBranch(input.exec, input.run.repository, meta, input.worktreePath);
|
|
511
|
+
await mergeBaseNoCommit(input.exec, meta.baseRefOid, input.worktreePath);
|
|
512
|
+
const conflictedFiles = await listUnmergedFiles(input.exec, input.worktreePath);
|
|
513
|
+
if (!conflictedFiles.length) {
|
|
514
|
+
await abortMerge(input.exec, input.worktreePath);
|
|
515
|
+
return undefined;
|
|
516
|
+
}
|
|
517
|
+
await input.run.onProgress?.({
|
|
518
|
+
phase: "resolving merge conflict",
|
|
519
|
+
type: "phase",
|
|
520
|
+
});
|
|
521
|
+
const editorOutput = await runConflictEditor({
|
|
522
|
+
baseBranch: meta.baseRefName,
|
|
523
|
+
baseSha: meta.baseRefOid,
|
|
524
|
+
conflictedFiles,
|
|
525
|
+
cycle: input.cycle,
|
|
526
|
+
headSha: input.previousHeadSha,
|
|
527
|
+
run: input.run,
|
|
528
|
+
worktreePath: input.worktreePath,
|
|
529
|
+
});
|
|
530
|
+
if (editorOutput.mode !== "EDITED") {
|
|
531
|
+
return {
|
|
532
|
+
ciReports: input.ciReports,
|
|
533
|
+
editorOutput,
|
|
534
|
+
headSha: input.previousHeadSha,
|
|
535
|
+
outputs: {},
|
|
536
|
+
posted: {},
|
|
537
|
+
status: "changes_unresolved",
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
const remainingConflicts = await listUnmergedFiles(input.exec, input.worktreePath);
|
|
541
|
+
const editedHeadSha = await currentHeadSha(input.exec, input.worktreePath);
|
|
542
|
+
if (remainingConflicts.length || editedHeadSha === input.previousHeadSha) {
|
|
543
|
+
return {
|
|
544
|
+
ciReports: input.ciReports,
|
|
545
|
+
editorOutput,
|
|
546
|
+
headSha: input.previousHeadSha,
|
|
547
|
+
outputs: {},
|
|
548
|
+
posted: {},
|
|
549
|
+
status: "changes_unresolved",
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
const editor = input.run.repository.agents.editor;
|
|
553
|
+
if (!editor)
|
|
554
|
+
throw new Error("merge.editor is required for magi_merge");
|
|
555
|
+
const headOwner = meta.headRepositoryOwner?.login;
|
|
556
|
+
const headRepo = meta.headRepository?.name;
|
|
557
|
+
if (!headOwner || !headRepo) {
|
|
558
|
+
throw new Error("Pull request head repository is missing");
|
|
559
|
+
}
|
|
560
|
+
await pushHead(input.exec, input.run.repository, input.worktreePath, editor.account, { owner: headOwner, ref: meta.headRefName, repo: headRepo });
|
|
561
|
+
const ciReports = [...input.ciReports];
|
|
562
|
+
let ciFailureContext = "";
|
|
563
|
+
await input.run.onProgress?.({
|
|
564
|
+
phase: "waiting for checks after conflict resolution",
|
|
565
|
+
type: "phase",
|
|
566
|
+
});
|
|
567
|
+
const checkResult = await waitForChecksWithClassification({
|
|
568
|
+
afterEdit: {
|
|
569
|
+
cycle: input.cycle,
|
|
570
|
+
headSha: editedHeadSha,
|
|
571
|
+
previousHeadSha: input.previousHeadSha,
|
|
572
|
+
worktreePath: input.worktreePath,
|
|
573
|
+
},
|
|
574
|
+
client: input.run.client,
|
|
575
|
+
directory: input.run.directory,
|
|
576
|
+
exec: input.exec,
|
|
577
|
+
headSha: editedHeadSha,
|
|
578
|
+
onProgress: (phase) => input.run.onProgress?.({ phase, type: "phase" }),
|
|
579
|
+
parentSessionId: input.run.parentSessionId,
|
|
580
|
+
pr: input.run.pr,
|
|
581
|
+
repairAttempts: input.run.config.output?.repairAttempts ?? 3,
|
|
582
|
+
repository: input.run.repository,
|
|
583
|
+
signal: input.run.signal,
|
|
584
|
+
wait: input.run.repository.checks.waitAfterEdit,
|
|
585
|
+
});
|
|
586
|
+
ciFailureContext = checkResult?.ciFailureContext ?? "";
|
|
587
|
+
if (checkResult &&
|
|
588
|
+
(checkResult.report.scopeOutsideRecovered.length ||
|
|
589
|
+
checkResult.report.scopeOutsideUnresolved.length ||
|
|
590
|
+
checkResult.report.scopeInside.length)) {
|
|
591
|
+
ciReports.push(checkResult.report);
|
|
592
|
+
await input.run.onProgress?.({
|
|
593
|
+
report: checkResult.report,
|
|
594
|
+
type: "ci_report",
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
await input.run.onProgress?.({
|
|
598
|
+
phase: "rereview after conflict resolution",
|
|
599
|
+
type: "phase",
|
|
600
|
+
});
|
|
601
|
+
const rereview = await runRereview(input.run, input.worktreePath, input.previousHeadSha, input.cycle, input.sessionIds, ciFailureContext);
|
|
602
|
+
if (rereview.verdict === "CLOSE") {
|
|
603
|
+
if (!input.run.repository.automation.close) {
|
|
604
|
+
return {
|
|
605
|
+
ciReports,
|
|
606
|
+
editorOutput,
|
|
607
|
+
headSha: editedHeadSha,
|
|
608
|
+
outputs: rereview.outputs,
|
|
609
|
+
posted: rereview.posted,
|
|
610
|
+
status: "close_requested",
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
await input.run.onProgress?.({ phase: "closing PR", type: "phase" });
|
|
614
|
+
await closePullRequest(input.exec, input.run.repository, input.run.pr, editor.account);
|
|
615
|
+
return {
|
|
616
|
+
ciReports,
|
|
617
|
+
editorOutput,
|
|
618
|
+
headSha: editedHeadSha,
|
|
619
|
+
outputs: rereview.outputs,
|
|
620
|
+
posted: rereview.posted,
|
|
621
|
+
status: "closed",
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
if (rereview.verdict === "MERGE") {
|
|
625
|
+
if (hasBlockingCiReports(ciReports)) {
|
|
626
|
+
return {
|
|
627
|
+
ciReports,
|
|
628
|
+
editorOutput,
|
|
629
|
+
headSha: editedHeadSha,
|
|
630
|
+
outputs: rereview.outputs,
|
|
631
|
+
posted: rereview.posted,
|
|
632
|
+
status: "ci_unresolved",
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
await input.run.onProgress?.({ phase: "re-enqueueing PR", type: "phase" });
|
|
636
|
+
const status = await mergeWithQueue(input.run, input.exec, editor.account);
|
|
637
|
+
return {
|
|
638
|
+
ciReports,
|
|
639
|
+
editorOutput,
|
|
640
|
+
headSha: editedHeadSha,
|
|
641
|
+
outputs: rereview.outputs,
|
|
642
|
+
posted: rereview.posted,
|
|
643
|
+
status,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
ciReports,
|
|
648
|
+
editorOutput,
|
|
649
|
+
headSha: editedHeadSha,
|
|
650
|
+
outputs: rereview.outputs,
|
|
651
|
+
posted: rereview.posted,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
436
654
|
export function hasBlockingCiReports(reports) {
|
|
437
655
|
return reports.some((report) => report.scopeInside.length || report.scopeOutsideUnresolved.length);
|
|
438
656
|
}
|
|
@@ -656,6 +874,28 @@ export async function runMerge(input) {
|
|
|
656
874
|
outputs: reportOutputs,
|
|
657
875
|
posted: reportPosted,
|
|
658
876
|
});
|
|
877
|
+
let previousHeadSha = review.headSha;
|
|
878
|
+
const ciReports = [...review.ciReports];
|
|
879
|
+
const threadAttempts = {};
|
|
880
|
+
let dryRunThreads = input.dryRun
|
|
881
|
+
? syntheticReviewThreads(reportOutputs)
|
|
882
|
+
: undefined;
|
|
883
|
+
let conflictRecoveryAttempted = false;
|
|
884
|
+
const applyConflictRecovery = (recovery) => {
|
|
885
|
+
editorOutputs.push({
|
|
886
|
+
...recovery.editorOutput,
|
|
887
|
+
label: "Conflict recovery",
|
|
888
|
+
});
|
|
889
|
+
ciReports.length = 0;
|
|
890
|
+
ciReports.push(...recovery.ciReports);
|
|
891
|
+
reportCiReports = [...recovery.ciReports];
|
|
892
|
+
if (Object.keys(recovery.outputs).length)
|
|
893
|
+
reportOutputs = recovery.outputs;
|
|
894
|
+
if (Object.keys(recovery.posted).length)
|
|
895
|
+
reportPosted = recovery.posted;
|
|
896
|
+
previousHeadSha = recovery.headSha;
|
|
897
|
+
dryRunThreads = undefined;
|
|
898
|
+
};
|
|
659
899
|
if (review.verdict === "SAFETY_BLOCKED") {
|
|
660
900
|
await input.onProgress?.({
|
|
661
901
|
status: "safety_blocked",
|
|
@@ -693,15 +933,45 @@ export async function runMerge(input) {
|
|
|
693
933
|
}
|
|
694
934
|
await input.onProgress?.({ phase: "merging PR", type: "phase" });
|
|
695
935
|
const status = await mergeWithQueue(input, exec, editor.account);
|
|
696
|
-
|
|
697
|
-
|
|
936
|
+
if (status === "dequeued" &&
|
|
937
|
+
input.repository.automation.conflict &&
|
|
938
|
+
input.repository.merge.mergeQueue) {
|
|
939
|
+
if (!review.worktreePath)
|
|
940
|
+
throw new Error("Review worktree is missing");
|
|
941
|
+
conflictRecoveryAttempted = true;
|
|
942
|
+
const recovery = await recoverMergeQueueConflict({
|
|
943
|
+
ciReports,
|
|
944
|
+
cycle: 1,
|
|
945
|
+
exec,
|
|
946
|
+
previousHeadSha,
|
|
947
|
+
run: abortableInput,
|
|
948
|
+
sessionIds: review.sessionIds,
|
|
949
|
+
worktreePath: review.worktreePath,
|
|
950
|
+
});
|
|
951
|
+
if (recovery) {
|
|
952
|
+
applyConflictRecovery(recovery);
|
|
953
|
+
if (recovery.status) {
|
|
954
|
+
await input.onProgress?.({
|
|
955
|
+
status: recovery.status,
|
|
956
|
+
type: "merge_completed",
|
|
957
|
+
});
|
|
958
|
+
return complete({
|
|
959
|
+
cycles: 1,
|
|
960
|
+
pr: input.pr,
|
|
961
|
+
status: recovery.status,
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
await input.onProgress?.({ status, type: "merge_completed" });
|
|
967
|
+
return complete({ cycles: 0, pr: input.pr, status });
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
await input.onProgress?.({ status, type: "merge_completed" });
|
|
972
|
+
return complete({ cycles: 0, pr: input.pr, status });
|
|
973
|
+
}
|
|
698
974
|
}
|
|
699
|
-
let previousHeadSha = review.headSha;
|
|
700
|
-
const ciReports = [...review.ciReports];
|
|
701
|
-
const threadAttempts = {};
|
|
702
|
-
let dryRunThreads = input.dryRun
|
|
703
|
-
? syntheticReviewThreads(reportOutputs)
|
|
704
|
-
: undefined;
|
|
705
975
|
for (let cycle = 1;; cycle += 1) {
|
|
706
976
|
const unresolvedThreads = input.dryRun
|
|
707
977
|
? flattenSyntheticThreads(dryRunThreads ?? {})
|
|
@@ -886,6 +1156,36 @@ export async function runMerge(input) {
|
|
|
886
1156
|
}
|
|
887
1157
|
await input.onProgress?.({ phase: "merging PR", type: "phase" });
|
|
888
1158
|
const status = await mergeWithQueue(input, exec, editor.account);
|
|
1159
|
+
if (status === "dequeued" &&
|
|
1160
|
+
input.repository.automation.conflict &&
|
|
1161
|
+
input.repository.merge.mergeQueue &&
|
|
1162
|
+
!conflictRecoveryAttempted) {
|
|
1163
|
+
conflictRecoveryAttempted = true;
|
|
1164
|
+
const recovery = await recoverMergeQueueConflict({
|
|
1165
|
+
ciReports,
|
|
1166
|
+
cycle: cycle + 1,
|
|
1167
|
+
exec,
|
|
1168
|
+
previousHeadSha,
|
|
1169
|
+
run: abortableInput,
|
|
1170
|
+
sessionIds: review.sessionIds,
|
|
1171
|
+
worktreePath: review.worktreePath,
|
|
1172
|
+
});
|
|
1173
|
+
if (recovery) {
|
|
1174
|
+
applyConflictRecovery(recovery);
|
|
1175
|
+
if (recovery.status) {
|
|
1176
|
+
await input.onProgress?.({
|
|
1177
|
+
status: recovery.status,
|
|
1178
|
+
type: "merge_completed",
|
|
1179
|
+
});
|
|
1180
|
+
return complete({
|
|
1181
|
+
cycles: cycle + 1,
|
|
1182
|
+
pr: input.pr,
|
|
1183
|
+
status: recovery.status,
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
889
1189
|
await input.onProgress?.({ status, type: "merge_completed" });
|
|
890
1190
|
return complete({ cycles: cycle, pr: input.pr, status });
|
|
891
1191
|
}
|
|
@@ -138,7 +138,7 @@ function editorLines(outputs) {
|
|
|
138
138
|
return [
|
|
139
139
|
"- **Editor**:",
|
|
140
140
|
...outputs.flatMap((output, index) => {
|
|
141
|
-
const label = ` - Cycle ${index + 1}`;
|
|
141
|
+
const label = ` - ${output.label ?? `Cycle ${index + 1}`}`;
|
|
142
142
|
if (output.mode === "REPLIED") {
|
|
143
143
|
return [
|
|
144
144
|
`${label}: replied without code changes`,
|