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.
@@ -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?.agents),
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
- return JSON.stringify(validation, null, 2);
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
- return JSON.stringify(validation, null, 2);
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 agents.",
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
- return JSON.stringify(validation, null, 2);
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
- runId: tool.schema.string().optional(),
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: (useConfiguredDefaults
710
- ? undefined
711
- : clearToolFlag(args.branch)) ?? clearFlag(clear?.branch),
712
- output: (useConfiguredDefaults
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
- await input.onProgress?.({ status, type: "merge_completed" });
697
- return complete({ cycles: 0, pr: input.pr, status });
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`,