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/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 { configOverrides, dryRun, prs: parsePrs(prTokens.join(" ")) };
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 { configOverrides, dryRun, issues: parseIssues(issueTokens.join(" ")) };
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) => `Started reviewing ${prMarkdownLink(repository, state.pr)}.`)
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) => `Started reviewing ${prMarkdownLink(repository, state.pr)}.`)
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");
@@ -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
- "Use this as a review hint; still inspect the PR diff before reporting findings.",
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 check = result.value.checks[0];
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.classification = check?.classification;
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
- classification: check?.classification ?? "SCOPE_IN",
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
- return { reviewer: reviewer.key, output: undefined };
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 successfulVotes = votes.filter((vote) => vote.output);
402
- const checkVotes = successfulVotes.map((vote) => {
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.findings.map((finding, findingIndex) => ({
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 findings = output.findings.filter((finding, findingIndex) => {
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
- return true;
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
- findings.length || output.requirementFindings.length
63
- ? { ...output, findings }
64
- : { findings: [], requirementFindings: [], verdict: "MERGE" };
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.reviewer);
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, shellQuote, waitForMergeQueue, } from "../github/commands";
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 { parseRightSideDiffTargets, validateInlineCommentTargets, } from "./inline-comments";
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("agents.editor is required for magi_merge");
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 || output.requirementFindings.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
- ...(finding.line == null ? {} : { line: finding.line }),
155
+ line: finding.line,
155
156
  startLine: finding.startLine,
156
- })), output.requirementFindings);
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: finding.line == null ? "file" : "inline",
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
- ...output.findings.map((finding) => ({
191
- fix: finding.fix,
192
- issue: finding.issue,
193
- path: finding.path,
194
- reviewer,
195
- ...(finding.line == null ? {} : { line: finding.line }),
196
- ...(finding.startLine == null
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 = parseRightSideDiffTargets(await input.exec(`git diff --no-ext-diff --unified=3 ${shellQuote(meta.baseRefOid)} ${shellQuote(headSha)}`, { cwd: worktreePath }));
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: meta.headRefOid,
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
- return "merged";
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("agents.editor is required for magi_merge");
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 = editableThreads.length
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 result = await input.client.session.prompt({
100
- body: {
101
- model: modelBody(input.model),
102
- parts: [{ type: "text", text: input.prompt }],
103
- },
104
- path: { id: input.sessionId },
105
- });
106
- throwIfAborted(input.signal);
107
- return extractText(result, input.allowEmpty);
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
  },